UserDetailPage.tsx
11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import { useEffect, useState } from 'react'
import { useNavigate, useLocation, useParams } from 'react-router-dom'
import { message } from 'antd'
import { useAppDispatch } from '../../store/hooks'
import { openTab } from '../../store/slices/tabsSlice'
import { getStaffs, getPermissionGroups, createUser, updateUser } from '../../api/usr'
import type { StaffVO, PermissionGroupVO, UserListItemVO, UserCreateReq, UserUpdateReq } from '../../api/usr'
// REQ-USR-001: 增加用户
// REQ-USR-002: 修改用户
export default function UserDetailPage() {
const { id } = useParams()
const navigate = useNavigate()
const location = useLocation()
const dispatch = useAppDispatch()
const isNew = !id
const rowData = location.state as UserListItemVO | null
const [staffs, setStaffs] = useState<StaffVO[]>([])
const [permGroups, setPermGroups] = useState<PermissionGroupVO[]>([])
const [selectedPermIds, setSelectedPermIds] = useState<string[]>([])
const [submitting, setSubmitting] = useState(false)
const [activePermTab, setActivePermTab] = useState(0)
const [userCode, setUserCode] = useState('')
const [username, setUsername] = useState('')
const [userType, setUserType] = useState<'普通用户' | '超级管理员'>('普通用户')
const [language, setLanguage] = useState<'中文' | '英文' | '繁体'>('中文')
const [canEditDoc, setCanEditDoc] = useState(false)
const [isDisabled, setIsDisabled] = useState(false)
const [employeeId, setEmployeeId] = useState<string | null>(null)
useEffect(() => {
if (!isNew && !rowData) {
navigate('/usr/users', { replace: true })
return
}
dispatch(openTab({ id: 'userdetail', title: '用户信息单据', path: location.pathname, closable: true }))
getStaffs().then(setStaffs).catch(() => {})
getPermissionGroups().then(setPermGroups).catch(() => {})
if (!isNew && rowData) {
setUserType(rowData.sUserType as '普通用户' | '超级管理员')
setLanguage(rowData.sLanguage as '中文' | '英文' | '繁体')
setCanEditDoc(rowData.bCanEditDoc === 1)
setIsDisabled(rowData.bIsDisabled === 1)
}
}, []) // eslint-disable-line react-hooks/exhaustive-deps
async function handleSave() {
if (isNew) {
if (!userCode.trim()) { message.error('请输入用户号'); return }
if (!username.trim()) { message.error('请输入用户名'); return }
}
setSubmitting(true)
try {
if (isNew) {
const req: UserCreateReq = { userCode, username, userType, language, canEditDoc, employeeId, permGroupIds: selectedPermIds }
await createUser(req)
message.success('新增用户成功')
} else {
const req: UserUpdateReq = { userType, language, canEditDoc, isDisabled, employeeId, permGroupIds: selectedPermIds }
await updateUser(id!, req)
message.success('修改用户成功')
}
navigate('/usr/users')
} catch (e: unknown) {
if (e instanceof Error) message.error(e.message)
} finally {
setSubmitting(false)
}
}
const PERM_TABS = ['权限组', '客户查看权限', '供应商查看权限', '人员查看权限', '工序查看权限', '司机查看权限']
const fieldBg = 'var(--color-field-bg-edit)'
const readonlyBg = 'var(--color-form-bg-readonly)'
const inputStyle: React.CSSProperties = { flex: 1, height: 28, border: '1px solid #d5d8de', borderRadius: 2, padding: '0 10px', background: fieldBg, minWidth: 0, fontSize: 13, fontFamily: 'inherit' }
const roStyle: React.CSSProperties = { flex: 1, height: 28, border: '1px solid #d5d8de', borderRadius: 2, padding: '0 10px', background: readonlyBg, color: '#444', display: 'flex', alignItems: 'center', fontSize: 13, minWidth: 0, overflow: 'hidden' }
const selectStyle: React.CSSProperties = { ...inputStyle, padding: '0 8px' }
const cellStyle: React.CSSProperties = { display: 'flex', alignItems: 'center', gap: 6, padding: '8px 10px' }
const lblStyle: React.CSSProperties = { minWidth: 88, color: '#333', fontSize: 13, textAlign: 'right', flexShrink: 0 }
const reqLblStyle: React.CSSProperties = { ...lblStyle, color: 'var(--color-field-label-req)' }
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
{/* Dark toolbar */}
<div style={{ background: 'var(--color-toolbar-bg)', color: 'var(--color-toolbar-text)', display: 'flex', alignItems: 'center', gap: 6, padding: '0 8px', height: 38, flexShrink: 0 }}>
{[
{ label: '⊕ 新增', onClick: () => navigate('/usr/users/new'), disabled: false },
{ label: '✎ 修改', onClick: undefined, disabled: true },
{ label: '🗑 删除', onClick: undefined, disabled: true, title: '功能待实现' },
{ label: '💾 保存', onClick: handleSave, disabled: submitting },
{ label: '✕ 取消', onClick: () => navigate('/usr/users'), disabled: false },
{ label: '作废', onClick: undefined, disabled: true, title: '功能待实现' },
{ label: '重置密码', onClick: undefined, disabled: true, title: '功能待实现' },
].map(btn => (
<button
key={btn.label}
onClick={btn.onClick}
disabled={btn.disabled}
title={btn.title}
style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '6px 12px', color: btn.disabled ? '#666' : 'var(--color-toolbar-text)', cursor: btn.disabled ? 'not-allowed' : 'pointer', fontSize: 13, borderRadius: 2, border: 'none', background: 'transparent', fontFamily: 'inherit', opacity: btn.disabled ? 0.5 : 1 }}
>
{btn.label}
</button>
))}
<span style={{ flex: 1 }} />
<span style={{ padding: '6px 8px', color: '#cfd2d8', cursor: 'pointer' }}>⚙</span>
</div>
{/* 3-column form grid */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', background: '#fff', padding: '10px 14px', borderBottom: '1px solid var(--color-table-border)', flexShrink: 0 }}>
{/* Row 1: 创建时间 / 制单人 / 员工名 */}
<div style={cellStyle}>
<span style={lblStyle}>创建时间:</span>
<div style={roStyle}>{isNew ? '' : rowData?.tCreateDate}</div>
</div>
<div style={cellStyle}>
<span style={lblStyle}>制单人:</span>
<div style={roStyle}>{isNew ? '保存后自动生成' : rowData?.sCreatorUsername}</div>
</div>
<div style={cellStyle}>
<span style={reqLblStyle}>*员工名:</span>
<select value={employeeId ?? ''} onChange={e => setEmployeeId(e.target.value || null)} style={{ ...selectStyle, backgroundImage: 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'10\' height=\'10\' viewBox=\'0 0 10 10\'><path d=\'M2 3l3 4 3-4z\' fill=\'%23888\'/></svg>")', backgroundRepeat: 'no-repeat', backgroundPosition: 'right 8px center', paddingRight: 24 }}>
<option value="">— 不关联 —</option>
{staffs.map(s => <option key={s.sId} value={s.sId}>{s.sStaffName}</option>)}
</select>
</div>
{/* Row 2: 用户名 / 类型 / 语言 */}
<div style={cellStyle}>
<span style={isNew ? reqLblStyle : lblStyle}>{isNew ? '*用户名:' : '用户名:'}</span>
{isNew
? <input type="text" value={username} onChange={e => setUsername(e.target.value)} placeholder="请输入用户名" style={inputStyle} />
: <div style={roStyle}>{rowData?.sUsername}</div>}
</div>
<div style={cellStyle}>
<span style={reqLblStyle}>*类型:</span>
<select value={userType} onChange={e => setUserType(e.target.value as typeof userType)} style={selectStyle}>
<option value="普通用户">普通用户</option>
<option value="超级管理员">超级管理员</option>
</select>
</div>
<div style={cellStyle}>
<span style={reqLblStyle}>*语言:</span>
<select value={language} onChange={e => setLanguage(e.target.value as typeof language)} style={selectStyle}>
<option value="中文">中文</option>
<option value="英文">English</option>
<option value="繁体">繁體</option>
</select>
</div>
{/* Row 3: 用户号 / (empty) / 单据修改权限 */}
<div style={cellStyle}>
<span style={isNew ? reqLblStyle : lblStyle}>{isNew ? '*用户号:' : '用户号:'}</span>
{isNew
? <input type="text" value={userCode} onChange={e => setUserCode(e.target.value)} placeholder="请输入用户号" style={inputStyle} />
: <div style={roStyle}>{rowData?.sUserCode}</div>}
</div>
<div style={cellStyle} />
<div style={cellStyle}>
<span style={lblStyle}>单据修改权限:</span>
<input type="checkbox" aria-hidden="true" checked={canEditDoc} onChange={e => setCanEditDoc(e.target.checked)} style={{ width: 14, height: 14 }} />
</div>
</div>
{/* Permission tabs row */}
<div style={{ display: 'flex', background: '#fff', borderBottom: '1px solid var(--color-table-border)', padding: '0 6px', flexShrink: 0 }}>
{PERM_TABS.map((tab, i) => (
<div
key={tab}
onClick={() => setActivePermTab(i)}
style={{ padding: '11px 18px', fontSize: 14, color: i === activePermTab ? 'var(--color-tab-active)' : '#444', cursor: 'pointer', borderBottom: i === activePermTab ? `2px solid var(--color-tab-active)` : 'none', marginRight: 4 }}
>
{tab}
</div>
))}
</div>
{/* Permission list
KNOWN LIMITATION (edit mode): UserListItemVO does not include permGroupIds (backend list API
does not return them). In edit mode, selectedPermIds starts empty — saving will overwrite
existing permissions with whatever the user selects here. A future backend GET /usr/users/:id
endpoint should return permGroupIds so they can be pre-populated. */}
{!isNew && activePermTab === 0 && (
<div style={{ background: '#fffbe6', borderBottom: '1px solid #ffe58f', padding: '8px 14px', fontSize: 12, color: '#ad6800' }}>
⚠ 编辑模式下权限组未预加载(后端列表接口不返回 permGroupIds)。保存将以当前勾选为准,请重新勾选所需权限组。
</div>
)}
<div style={{ flex: 1, background: '#fff', overflow: 'auto' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '10px 14px', borderBottom: '1px solid var(--color-table-border)', background: 'var(--color-table-header-bg)', fontWeight: 500, color: '#222', fontSize: 13 }}>
<span style={{ width: 14, height: 14, border: '1px solid #b8bcc3', display: 'inline-block', flexShrink: 0 }} />
<span>权限分类</span>
</div>
{activePermTab === 0
? permGroups.map(pg => (
<div key={pg.sId} style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '10px 14px', borderBottom: '1px solid var(--color-table-border)', fontSize: 13, color: '#333' }}>
<input
type="checkbox"
checked={selectedPermIds.includes(pg.sId)}
onChange={e => setSelectedPermIds(prev => e.target.checked ? [...prev, pg.sId] : prev.filter(x => x !== pg.sId))}
style={{ width: 14, height: 14, flexShrink: 0 }}
/>
<span>{pg.sGroupName}</span>
</div>
))
: <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: 100, color: '#aaa' }}>功能待实现</div>}
</div>
</div>
)
}