Commit 3090964b21361d39e3b63aa18667095020c7d01f

Authored by zichun
1 parent ff5af471

feat(usr): 前端 UserListPage + UserFormDrawer + usr.ts + PermButton REQ-USR-001

frontend/src/App.tsx
1 1 import { Navigate, Route, Routes } from 'react-router-dom'
2 2 import { useAppSelector } from './store/hooks'
3 3 import LoginPage from './pages/usr/LoginPage'
  4 +import UserListPage from './pages/usr/UserListPage'
4 5  
5 6 function PrivateRoute({ children }: { children: React.ReactNode }) {
6 7 const accessToken = useAppSelector(s => s.auth.accessToken)
... ... @@ -12,6 +13,7 @@ export default function App() {
12 13 <Routes>
13 14 <Route path="/login" element={<LoginPage />} />
14 15 <Route path="/" element={<PrivateRoute><div>主页(待实现)</div></PrivateRoute>} />
  16 + <Route path="/usr/users" element={<PrivateRoute><UserListPage /></PrivateRoute>} />
15 17 <Route path="*" element={<Navigate to="/" replace />} />
16 18 </Routes>
17 19 )
... ...
frontend/src/api/usr.ts 0 → 100644
  1 +import request from './request'
  2 +
  3 +export interface StaffVO {
  4 + sId: string
  5 + sStaffName: string
  6 +}
  7 +
  8 +export interface PermissionGroupVO {
  9 + sId: string
  10 + sGroupCode: string
  11 + sGroupName: string
  12 + sCategory: string | null
  13 +}
  14 +
  15 +export interface UserCreateReq {
  16 + userCode: string
  17 + username: string
  18 + userType: '普通用户' | '超级管理员'
  19 + language: '中文' | '英文' | '繁体'
  20 + canEditDoc?: boolean
  21 + employeeId?: string | null
  22 + permGroupIds?: string[]
  23 +}
  24 +
  25 +export interface UserCreateResp {
  26 + userId: string
  27 + userCode: string
  28 + username: string
  29 +}
  30 +
  31 +export function getStaffs(): Promise<StaffVO[]> {
  32 + return request.get('/usr/users/staffs')
  33 +}
  34 +
  35 +export function getPermissionGroups(): Promise<PermissionGroupVO[]> {
  36 + return request.get('/usr/users/permission-groups')
  37 +}
  38 +
  39 +export function createUser(req: UserCreateReq): Promise<UserCreateResp> {
  40 + return request.post('/usr/users', req)
  41 +}
... ...
frontend/src/components/PermButton.tsx 0 → 100644
  1 +import { Button, ButtonProps } from 'antd'
  2 +import { useAppSelector } from '../store/hooks'
  3 +
  4 +interface PermButtonProps extends ButtonProps {
  5 + permission: string
  6 +}
  7 +
  8 +export function PermButton({ permission: _permission, children, ...props }: PermButtonProps) {
  9 + const userType = useAppSelector(s => s.auth.userInfo?.userType)
  10 + if (userType !== '超级管理员') return null
  11 + return <Button {...props}>{children}</Button>
  12 +}
... ...
frontend/src/pages/usr/UserFormDrawer.tsx 0 → 100644
  1 +import { useEffect, useState } from 'react'
  2 +import {
  3 + Drawer, Form, Input, Select, Checkbox, Button, message, Table
  4 +} from 'antd'
  5 +import type { ColumnsType } from 'antd/es/table'
  6 +import { getStaffs, getPermissionGroups, createUser, StaffVO, PermissionGroupVO, UserCreateReq } from '../../api/usr'
  7 +
  8 +interface Props {
  9 + open: boolean
  10 + onClose: () => void
  11 + onSuccess: () => void
  12 +}
  13 +
  14 +export default function UserFormDrawer({ open, onClose, onSuccess }: Props) {
  15 + const [form] = Form.useForm()
  16 + const [staffs, setStaffs] = useState<StaffVO[]>([])
  17 + const [permGroups, setPermGroups] = useState<PermissionGroupVO[]>([])
  18 + const [selectedPermIds, setSelectedPermIds] = useState<string[]>([])
  19 + const [submitting, setSubmitting] = useState(false)
  20 +
  21 + useEffect(() => {
  22 + if (open) {
  23 + getStaffs().then(setStaffs).catch(() => {})
  24 + getPermissionGroups().then(setPermGroups).catch(() => {})
  25 + }
  26 + }, [open])
  27 +
  28 + const permColumns: ColumnsType<PermissionGroupVO> = [
  29 + {
  30 + title: '',
  31 + key: 'select',
  32 + width: 40,
  33 + render: (_, record) => (
  34 + <Checkbox
  35 + checked={selectedPermIds.includes(record.sId)}
  36 + onChange={e => {
  37 + setSelectedPermIds(prev =>
  38 + e.target.checked ? [...prev, record.sId] : prev.filter(id => id !== record.sId)
  39 + )
  40 + }}
  41 + />
  42 + )
  43 + },
  44 + { title: '权限组', dataIndex: 'sGroupName' },
  45 + { title: '分类', dataIndex: 'sCategory' }
  46 + ]
  47 +
  48 + async function handleSubmit() {
  49 + try {
  50 + const values = await form.validateFields()
  51 + setSubmitting(true)
  52 + const req: UserCreateReq = {
  53 + userCode: values.userCode,
  54 + username: values.username,
  55 + userType: values.userType,
  56 + language: values.language,
  57 + canEditDoc: values.canEditDoc ?? false,
  58 + employeeId: values.employeeId ?? null,
  59 + permGroupIds: selectedPermIds
  60 + }
  61 + await createUser(req)
  62 + message.success('新增用户成功')
  63 + form.resetFields()
  64 + setSelectedPermIds([])
  65 + onSuccess()
  66 + } catch (e: unknown) {
  67 + if (e instanceof Error) {
  68 + message.error(e.message)
  69 + }
  70 + } finally {
  71 + setSubmitting(false)
  72 + }
  73 + }
  74 +
  75 + return (
  76 + <Drawer
  77 + title="新增用户"
  78 + open={open}
  79 + onClose={onClose}
  80 + width={520}
  81 + footer={
  82 + <div style={{ textAlign: 'right' }}>
  83 + <Button onClick={onClose} style={{ marginRight: 8 }}>取消</Button>
  84 + <Button type="primary" onClick={handleSubmit} loading={submitting}>确认</Button>
  85 + </div>
  86 + }
  87 + >
  88 + <Form form={form} layout="vertical">
  89 + <Form.Item name="userCode" label="用户号" rules={[{ required: true, message: '请输入用户号' }]}>
  90 + <Input placeholder="请输入用户号" />
  91 + </Form.Item>
  92 + <Form.Item name="username" label="用户名" rules={[{ required: true, message: '请输入用户名' }]}>
  93 + <Input placeholder="请输入用户名" />
  94 + </Form.Item>
  95 + <Form.Item name="userType" label="用户类型" rules={[{ required: true }]} initialValue="普通用户">
  96 + <Select options={[{ value: '普通用户', label: '普通用户' }, { value: '超级管理员', label: '超级管理员' }]} />
  97 + </Form.Item>
  98 + <Form.Item name="language" label="语言" rules={[{ required: true }]} initialValue="中文">
  99 + <Select options={[{ value: '中文', label: '中文' }, { value: '英文', label: 'English' }, { value: '繁体', label: '繁體' }]} />
  100 + </Form.Item>
  101 + <Form.Item name="canEditDoc" valuePropName="checked">
  102 + <Checkbox>可编辑文档</Checkbox>
  103 + </Form.Item>
  104 + <Form.Item name="employeeId" label="关联员工">
  105 + <Select
  106 + allowClear
  107 + placeholder="选择员工(可选)"
  108 + options={staffs.map(s => ({ value: s.sId, label: s.sStaffName }))}
  109 + />
  110 + </Form.Item>
  111 + <Form.Item label="权限组">
  112 + <Table
  113 + size="small"
  114 + columns={permColumns}
  115 + dataSource={permGroups}
  116 + rowKey="sId"
  117 + pagination={false}
  118 + />
  119 + </Form.Item>
  120 + </Form>
  121 + </Drawer>
  122 + )
  123 +}
... ...
frontend/src/pages/usr/UserListPage.tsx 0 → 100644
  1 +import { useState } from 'react'
  2 +import { Table } from 'antd'
  3 +import { PermButton } from '../../components/PermButton'
  4 +import UserFormDrawer from './UserFormDrawer'
  5 +
  6 +export default function UserListPage() {
  7 + const [drawerOpen, setDrawerOpen] = useState(false)
  8 +
  9 + return (
  10 + <div>
  11 + <div style={{ marginBottom: 16 }}>
  12 + <PermButton
  13 + permission="usr:create"
  14 + type="primary"
  15 + onClick={() => setDrawerOpen(true)}
  16 + >
  17 + 新增
  18 + </PermButton>
  19 + </div>
  20 + <Table dataSource={[]} columns={[]} rowKey="sId" />
  21 + <UserFormDrawer
  22 + open={drawerOpen}
  23 + onClose={() => setDrawerOpen(false)}
  24 + onSuccess={() => setDrawerOpen(false)}
  25 + />
  26 + </div>
  27 + )
  28 +}
... ...
frontend/src/test/UserListPage.test.tsx 0 → 100644
  1 +import { describe, it, expect, vi, beforeEach } from 'vitest'
  2 +import { render, screen, waitFor } from '@testing-library/react'
  3 +import userEvent from '@testing-library/user-event'
  4 +import { Provider } from 'react-redux'
  5 +import { MemoryRouter } from 'react-router-dom'
  6 +import { configureStore } from '@reduxjs/toolkit'
  7 +import authReducer from '../store/slices/authSlice'
  8 +import UserListPage from '../pages/usr/UserListPage'
  9 +
  10 +vi.mock('../api/usr', () => ({
  11 + getStaffs: vi.fn().mockResolvedValue([]),
  12 + getPermissionGroups: vi.fn().mockResolvedValue([]),
  13 + createUser: vi.fn()
  14 +}))
  15 +
  16 +vi.mock('../api/request', () => ({
  17 + default: { get: vi.fn(), post: vi.fn() }
  18 +}))
  19 +
  20 +function makeStore(userType: string) {
  21 + const store = configureStore({ reducer: { auth: authReducer } })
  22 + store.dispatch({
  23 + type: 'auth/setCredentials',
  24 + payload: {
  25 + accessToken: 'test-token',
  26 + refreshToken: 'test-refresh',
  27 + userInfo: { userId: 'u1', username: 'admin', userType, language: '中文', brandId: 'b1' }
  28 + }
  29 + })
  30 + return store
  31 +}
  32 +
  33 +function renderPage(userType: string) {
  34 + const store = makeStore(userType)
  35 + return render(
  36 + <Provider store={store}>
  37 + <MemoryRouter>
  38 + <UserListPage />
  39 + </MemoryRouter>
  40 + </Provider>
  41 + )
  42 +}
  43 +
  44 +describe('UserListPage', () => {
  45 + beforeEach(() => {
  46 + vi.clearAllMocks()
  47 + })
  48 +
  49 + it('superAdmin_seesNewButton', () => {
  50 + renderPage('超级管理员')
  51 + expect(screen.getByRole('button', { name: /新\s*增/ })).toBeInTheDocument()
  52 + })
  53 +
  54 + it('normalUser_doesNotSeeNewButton', () => {
  55 + renderPage('普通用户')
  56 + expect(screen.queryByRole('button', { name: /新\s*增/ })).not.toBeInTheDocument()
  57 + })
  58 +
  59 + it('clickNewButton_opensDrawer', async () => {
  60 + renderPage('超级管理员')
  61 + await userEvent.click(screen.getByRole('button', { name: /新\s*增/ }))
  62 + await waitFor(() => expect(screen.getByText('新增用户')).toBeInTheDocument())
  63 + })
  64 +})
... ...