diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index fdbe5e1..e91309e 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,6 +1,7 @@
import { Navigate, Route, Routes } from 'react-router-dom'
import { useAppSelector } from './store/hooks'
import LoginPage from './pages/usr/LoginPage'
+import UserListPage from './pages/usr/UserListPage'
function PrivateRoute({ children }: { children: React.ReactNode }) {
const accessToken = useAppSelector(s => s.auth.accessToken)
@@ -12,6 +13,7 @@ export default function App() {
} />
主页(待实现)
} />
+ } />
} />
)
diff --git a/frontend/src/api/usr.ts b/frontend/src/api/usr.ts
new file mode 100644
index 0000000..e2a309a
--- /dev/null
+++ b/frontend/src/api/usr.ts
@@ -0,0 +1,41 @@
+import request from './request'
+
+export interface StaffVO {
+ sId: string
+ sStaffName: string
+}
+
+export interface PermissionGroupVO {
+ sId: string
+ sGroupCode: string
+ sGroupName: string
+ sCategory: string | null
+}
+
+export interface UserCreateReq {
+ userCode: string
+ username: string
+ userType: '普通用户' | '超级管理员'
+ language: '中文' | '英文' | '繁体'
+ canEditDoc?: boolean
+ employeeId?: string | null
+ permGroupIds?: string[]
+}
+
+export interface UserCreateResp {
+ userId: string
+ userCode: string
+ username: string
+}
+
+export function getStaffs(): Promise {
+ return request.get('/usr/users/staffs')
+}
+
+export function getPermissionGroups(): Promise {
+ return request.get('/usr/users/permission-groups')
+}
+
+export function createUser(req: UserCreateReq): Promise {
+ return request.post('/usr/users', req)
+}
diff --git a/frontend/src/components/PermButton.tsx b/frontend/src/components/PermButton.tsx
new file mode 100644
index 0000000..f6e4e8a
--- /dev/null
+++ b/frontend/src/components/PermButton.tsx
@@ -0,0 +1,12 @@
+import { Button, ButtonProps } from 'antd'
+import { useAppSelector } from '../store/hooks'
+
+interface PermButtonProps extends ButtonProps {
+ permission: string
+}
+
+export function PermButton({ permission: _permission, children, ...props }: PermButtonProps) {
+ const userType = useAppSelector(s => s.auth.userInfo?.userType)
+ if (userType !== '超级管理员') return null
+ return
+}
diff --git a/frontend/src/pages/usr/UserFormDrawer.tsx b/frontend/src/pages/usr/UserFormDrawer.tsx
new file mode 100644
index 0000000..9d2b503
--- /dev/null
+++ b/frontend/src/pages/usr/UserFormDrawer.tsx
@@ -0,0 +1,123 @@
+import { useEffect, useState } from 'react'
+import {
+ Drawer, Form, Input, Select, Checkbox, Button, message, Table
+} from 'antd'
+import type { ColumnsType } from 'antd/es/table'
+import { getStaffs, getPermissionGroups, createUser, StaffVO, PermissionGroupVO, UserCreateReq } from '../../api/usr'
+
+interface Props {
+ open: boolean
+ onClose: () => void
+ onSuccess: () => void
+}
+
+export default function UserFormDrawer({ open, onClose, onSuccess }: Props) {
+ const [form] = Form.useForm()
+ const [staffs, setStaffs] = useState([])
+ const [permGroups, setPermGroups] = useState([])
+ const [selectedPermIds, setSelectedPermIds] = useState([])
+ const [submitting, setSubmitting] = useState(false)
+
+ useEffect(() => {
+ if (open) {
+ getStaffs().then(setStaffs).catch(() => {})
+ getPermissionGroups().then(setPermGroups).catch(() => {})
+ }
+ }, [open])
+
+ const permColumns: ColumnsType = [
+ {
+ title: '',
+ key: 'select',
+ width: 40,
+ render: (_, record) => (
+ {
+ setSelectedPermIds(prev =>
+ e.target.checked ? [...prev, record.sId] : prev.filter(id => id !== record.sId)
+ )
+ }}
+ />
+ )
+ },
+ { title: '权限组', dataIndex: 'sGroupName' },
+ { title: '分类', dataIndex: 'sCategory' }
+ ]
+
+ async function handleSubmit() {
+ try {
+ const values = await form.validateFields()
+ setSubmitting(true)
+ const req: UserCreateReq = {
+ userCode: values.userCode,
+ username: values.username,
+ userType: values.userType,
+ language: values.language,
+ canEditDoc: values.canEditDoc ?? false,
+ employeeId: values.employeeId ?? null,
+ permGroupIds: selectedPermIds
+ }
+ await createUser(req)
+ message.success('新增用户成功')
+ form.resetFields()
+ setSelectedPermIds([])
+ onSuccess()
+ } catch (e: unknown) {
+ if (e instanceof Error) {
+ message.error(e.message)
+ }
+ } finally {
+ setSubmitting(false)
+ }
+ }
+
+ return (
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 可编辑文档
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/pages/usr/UserListPage.tsx b/frontend/src/pages/usr/UserListPage.tsx
new file mode 100644
index 0000000..94c3963
--- /dev/null
+++ b/frontend/src/pages/usr/UserListPage.tsx
@@ -0,0 +1,28 @@
+import { useState } from 'react'
+import { Table } from 'antd'
+import { PermButton } from '../../components/PermButton'
+import UserFormDrawer from './UserFormDrawer'
+
+export default function UserListPage() {
+ const [drawerOpen, setDrawerOpen] = useState(false)
+
+ return (
+
+
+
setDrawerOpen(true)}
+ >
+ 新增
+
+
+
+
setDrawerOpen(false)}
+ onSuccess={() => setDrawerOpen(false)}
+ />
+
+ )
+}
diff --git a/frontend/src/test/UserListPage.test.tsx b/frontend/src/test/UserListPage.test.tsx
new file mode 100644
index 0000000..2a26bfa
--- /dev/null
+++ b/frontend/src/test/UserListPage.test.tsx
@@ -0,0 +1,64 @@
+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 } from 'react-router-dom'
+import { configureStore } from '@reduxjs/toolkit'
+import authReducer from '../store/slices/authSlice'
+import UserListPage from '../pages/usr/UserListPage'
+
+vi.mock('../api/usr', () => ({
+ getStaffs: vi.fn().mockResolvedValue([]),
+ getPermissionGroups: vi.fn().mockResolvedValue([]),
+ createUser: vi.fn()
+}))
+
+vi.mock('../api/request', () => ({
+ default: { get: vi.fn(), post: vi.fn() }
+}))
+
+function makeStore(userType: string) {
+ const store = configureStore({ reducer: { auth: authReducer } })
+ store.dispatch({
+ type: 'auth/setCredentials',
+ payload: {
+ accessToken: 'test-token',
+ refreshToken: 'test-refresh',
+ userInfo: { userId: 'u1', username: 'admin', userType, language: '中文', brandId: 'b1' }
+ }
+ })
+ return store
+}
+
+function renderPage(userType: string) {
+ const store = makeStore(userType)
+ return render(
+
+
+
+
+
+ )
+}
+
+describe('UserListPage', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('superAdmin_seesNewButton', () => {
+ renderPage('超级管理员')
+ expect(screen.getByRole('button', { name: /新\s*增/ })).toBeInTheDocument()
+ })
+
+ it('normalUser_doesNotSeeNewButton', () => {
+ renderPage('普通用户')
+ expect(screen.queryByRole('button', { name: /新\s*增/ })).not.toBeInTheDocument()
+ })
+
+ it('clickNewButton_opensDrawer', async () => {
+ renderPage('超级管理员')
+ await userEvent.click(screen.getByRole('button', { name: /新\s*增/ }))
+ await waitFor(() => expect(screen.getByText('新增用户')).toBeInTheDocument())
+ })
+})