diff --git a/frontend/src/pages/usr/UserDetailPage.tsx b/frontend/src/pages/usr/UserDetailPage.tsx index 63478f1..f6b61eb 100644 --- a/frontend/src/pages/usr/UserDetailPage.tsx +++ b/frontend/src/pages/usr/UserDetailPage.tsx @@ -1,3 +1,201 @@ +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() { - return null + 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([]) + const [permGroups, setPermGroups] = useState([]) + const [selectedPermIds, setSelectedPermIds] = useState([]) + 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(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 ( +
+ {/* Dark toolbar */} +
+ {[ + { 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 => ( + + ))} + + +
+ + {/* 3-column form grid */} +
+ {/* Row 1: 创建时间 / 制单人 / 员工名 */} +
+ 创建时间: +
{isNew ? '' : rowData?.tCreateDate}
+
+
+ 制单人: +
{isNew ? '保存后自动生成' : rowData?.sCreatorUsername}
+
+
+ *员工名: + +
+ {/* Row 2: 用户名 / 类型 / 语言 */} +
+ {isNew ? '*用户名:' : '用户名:'} + {isNew + ? setUsername(e.target.value)} placeholder="请输入用户名" style={inputStyle} /> + :
{rowData?.sUsername}
} +
+
+ *类型: + +
+
+ *语言: + +
+ {/* Row 3: 用户号 / (empty) / 单据修改权限 */} +
+ {isNew ? '*用户号:' : '用户号:'} + {isNew + ? setUserCode(e.target.value)} placeholder="请输入用户号" style={inputStyle} /> + :
{rowData?.sUserCode}
} +
+
+
+ 单据修改权限: + setCanEditDoc(e.target.checked)} style={{ width: 14, height: 14 }} /> +
+
+ + {/* Permission tabs row */} +
+ {PERM_TABS.map((tab, i) => ( +
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} +
+ ))} +
+ + {/* Permission list */} +
+
+ + 权限分类 +
+ {activePermTab === 0 + ? permGroups.map(pg => ( +
+ setSelectedPermIds(prev => e.target.checked ? [...prev, pg.sId] : prev.filter(x => x !== pg.sId))} + style={{ width: 14, height: 14, flexShrink: 0 }} + /> + {pg.sGroupName} +
+ )) + :
功能待实现
} +
+
+ ) } diff --git a/frontend/src/test/UserDetailPage.test.tsx b/frontend/src/test/UserDetailPage.test.tsx new file mode 100644 index 0000000..c990bf4 --- /dev/null +++ b/frontend/src/test/UserDetailPage.test.tsx @@ -0,0 +1,135 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { Provider } from 'react-redux' +import { MemoryRouter, Routes, Route } from 'react-router-dom' +import { configureStore } from '@reduxjs/toolkit' +import authReducer from '../store/slices/authSlice' +import tabsReducer from '../store/slices/tabsSlice' +import UserDetailPage from '../pages/usr/UserDetailPage' + +vi.mock('../api/usr', () => ({ + getStaffs: vi.fn().mockResolvedValue([{ sId: 's1', sStaffName: '张三' }]), + getPermissionGroups: vi.fn().mockResolvedValue([{ sId: 'pg1', sGroupCode: 'usr:create', sGroupName: '新增用户', sCategory: '用户管理' }]), + createUser: vi.fn().mockResolvedValue({ userId: 'u2', userCode: 'UC002', username: 'bob' }), + updateUser: vi.fn().mockResolvedValue({ userId: 'u1', username: 'alice', updatedAt: '2026-05-08T10:00:00' }), + getUserList: vi.fn().mockResolvedValue({ total: 0, page: 1, pageSize: 20, list: [] }), +})) + +vi.mock('../api/request', () => ({ + default: { get: vi.fn(), post: vi.fn(), put: vi.fn() }, +})) + +function makeStore() { + const store = configureStore({ reducer: { auth: authReducer, tabs: tabsReducer } }) + store.dispatch({ + type: 'auth/setCredentials', + payload: { accessToken: 'tok', refreshToken: 'ref', userInfo: { userId: 'u0', username: 'admin', userType: '超级管理员', language: '中文', brandId: 'b1' } }, + }) + return store +} + +const rowData = { + sId: 'u1', sUsername: 'alice', sUserCode: 'UC001', sUserType: '普通用户', + sLanguage: '中文', bCanEditDoc: 0, bIsDisabled: 0, tLastLoginDate: null, + sCreatorUsername: 'admin', tCreateDate: '2026-01-01 00:00:00', + sStaffName: null, sDepartment: null, +} + +function renderNew() { + return render( + + + + } /> + UserList
} /> + + + + ) +} + +function renderEdit(state = rowData) { + return render( + + + + } /> + UserList} /> + + + + ) +} + +describe('UserDetailPage', () => { + beforeEach(() => { vi.clearAllMocks() }) + + it('newMode_showsSaveButton', () => { + renderNew() + expect(screen.getByRole('button', { name: /保存/ })).toBeInTheDocument() + }) + + it('newMode_showsPermissionGroupTab', async () => { + renderNew() + await waitFor(() => expect(screen.getByText('新增用户')).toBeInTheDocument()) + }) + + it('editMode_showsUsernameReadonly', async () => { + renderEdit() + await waitFor(() => expect(screen.getByText('alice')).toBeInTheDocument()) + }) + + it('editMode_noState_redirectsToList', async () => { + render( + + + + } /> + UserList} /> + + + + ) + await waitFor(() => expect(screen.getByText('UserList')).toBeInTheDocument()) + }) + + it('newMode_save_callsCreateUser', async () => { + const { createUser } = await import('../api/usr') + renderNew() + await userEvent.type(screen.getByPlaceholderText('请输入用户号'), 'UC002') + await userEvent.type(screen.getByPlaceholderText('请输入用户名'), 'bob') + await userEvent.click(screen.getByRole('button', { name: /保存/ })) + await waitFor(() => expect(vi.mocked(createUser)).toHaveBeenCalledWith( + expect.objectContaining({ userCode: 'UC002', username: 'bob' }) + )) + }) + + it('editMode_save_callsUpdateUser', async () => { + const { updateUser } = await import('../api/usr') + renderEdit() + await waitFor(() => expect(screen.getByText('alice')).toBeInTheDocument()) + await userEvent.click(screen.getByRole('button', { name: /保存/ })) + await waitFor(() => expect(vi.mocked(updateUser)).toHaveBeenCalledWith( + 'u1', + expect.objectContaining({ userType: '普通用户' }) + )) + }) + + it('permCheckbox_togglesSelection', async () => { + renderNew() + await waitFor(() => expect(screen.getByText('新增用户')).toBeInTheDocument()) + const cb = screen.getByRole('checkbox') + expect(cb).not.toBeChecked() + await userEvent.click(cb) + expect(cb).toBeChecked() + await userEvent.click(cb) + expect(cb).not.toBeChecked() + }) + + it('cancelButton_navigatesToList', async () => { + renderNew() + await userEvent.click(screen.getByRole('button', { name: /取消/ })) + await waitFor(() => expect(screen.getByText('UserList')).toBeInTheDocument()) + }) +})