diff --git a/frontend/src/pages/usr/UserListPage.tsx b/frontend/src/pages/usr/UserListPage.tsx index 1444727..8e7a045 100644 --- a/frontend/src/pages/usr/UserListPage.tsx +++ b/frontend/src/pages/usr/UserListPage.tsx @@ -1,8 +1,8 @@ -import { useState, useEffect } from 'react' -import { Table, Select, Input, Button, Space } from 'antd' -import type { ColumnsType } from 'antd/es/table' +import { useEffect, useState } from 'react' +import { useNavigate, useSearchParams, useLocation } from 'react-router-dom' +import { useAppDispatch } from '../../store/hooks' +import { openTab, updateTabPath } from '../../store/slices/tabsSlice' import { PermButton } from '../../components/PermButton' -import UserFormDrawer from './UserFormDrawer' import { getUserList } from '../../api/usr' import type { PageVO, UserListItemVO } from '../../api/usr' @@ -24,110 +24,136 @@ const MATCH_TYPES = [ ] // REQ-USR-003: 查询用户 -// REQ-USR-002: 修改用户 +// REQ-USR-001: 新增入口 (navigate to /usr/users/new) +// REQ-USR-002: 修改入口 (double-click row → /usr/users/:id) export default function UserListPage() { - const [drawerOpen, setDrawerOpen] = useState(false) - const [editingUser, setEditingUser] = useState(null) + const navigate = useNavigate() + const dispatch = useAppDispatch() + const location = useLocation() + const [searchParams, setSearchParams] = useSearchParams() + + const [queryField, setQueryField] = useState(searchParams.get('queryField') ?? 'username') + const [matchType, setMatchType] = useState(searchParams.get('matchType') ?? 'contains') + const [queryValue, setQueryValue] = useState(searchParams.get('queryValue') ?? '') + const [currentPage, setCurrentPage] = useState(Number(searchParams.get('page') ?? '1')) const [data, setData] = useState | null>(null) - const [queryField, setQueryField] = useState('username') - const [matchType, setMatchType] = useState('contains') - const [queryValue, setQueryValue] = useState('') - const [currentPage, setCurrentPage] = useState(1) + const [selectedId, setSelectedId] = useState(null) + + useEffect(() => { + dispatch(openTab({ id: 'userlist', title: '用户列表', path: location.pathname + location.search, closable: true })) + doLoad(currentPage, queryField, matchType, queryValue) + }, []) - const load = (pg = 1) => { + function doLoad(pg: number, field: string, match: string, val: string) { setCurrentPage(pg) - getUserList({ queryField, matchType, queryValue, page: pg, pageSize: 20 }).then(setData) + getUserList({ queryField: field, matchType: match, queryValue: val, page: pg, pageSize: 20 }).then(setData) } - useEffect(() => { - load() - }, []) + function handleSearch() { + const newPath = `/usr/users?queryField=${queryField}&matchType=${matchType}&queryValue=${encodeURIComponent(queryValue)}` + setSearchParams({ queryField, matchType, queryValue, page: '1' }) + dispatch(updateTabPath({ id: 'userlist', path: newPath })) + doLoad(1, queryField, matchType, queryValue) + } - const columns: ColumnsType = [ - { title: '用户名', dataIndex: 'sUsername' }, - { title: '员工名', dataIndex: 'sStaffName' }, - { title: '用户号', dataIndex: 'sUserCode' }, - { title: '部门', dataIndex: 'sDepartment' }, - { title: '用户类型', dataIndex: 'sUserType' }, - { title: '语言', dataIndex: 'sLanguage' }, - { title: '作废', dataIndex: 'bIsDisabled', render: (v: number) => v ? '是' : '否' }, - { title: '登录日期', dataIndex: 'tLastLoginDate' }, - { title: '制单人', dataIndex: 'sCreatorUsername' }, - { title: '制单日期', dataIndex: 'tCreateDate' }, - { - title: '操作', - key: 'action', - render: (_, record) => ( - setEditingUser(record)} - > - 修改 - - ) - }, - ] + function handleClear() { + setQueryField('username'); setMatchType('contains'); setQueryValue('') + setSearchParams({}) + dispatch(updateTabPath({ id: 'userlist', path: '/usr/users' })) + doLoad(1, 'username', 'contains', '') + } + + const totalPages = data ? Math.ceil(data.total / 20) : 1 return ( -
- - - setQueryValue(e.target.value)} - placeholder="查询值" - style={{ width: 160 }} - /> - - setDrawerOpen(true)} - > +
+ {/* Toolbar */} +
+ + navigate('/usr/users/new')} style={tbBtn}> + 新增 - - - setDrawerOpen(false)} - onSuccess={() => { setDrawerOpen(false); load(1) }} - /> - setEditingUser(null)} - onSuccess={() => { setEditingUser(null); load(1) }} - /> + + + + + {/* Filter bar */} +
+ + + setQueryValue(e.target.value)} style={fIn} /> + + + +
+ {/* Table */} +
+
+ + + {['', '序号', '用户名', '员工名', '用户号', '部门', '用户类型', '语言', '作废', '登录日期', '制单人', '制单日期'].map((h, i) => ( + + ))} + + + + {(data?.list ?? []).map((row, idx) => ( + setSelectedId(row.sId)} + onDoubleClick={() => navigate(`/usr/users/${row.sId}`, { state: row })} + style={{ background: row.sId === selectedId ? 'var(--color-table-row-bg-selected)' : idx % 2 === 1 ? 'var(--color-table-row-alt)' : '#fff', cursor: 'pointer' }} + > + + + + + + + + + + + + + + ))} + +
+ {h}{i > 1 && ⇅ ⌕} +
{idx + 1}{row.sUsername}{row.sStaffName ?? ''}{row.sUserCode}{row.sDepartment ?? ''}{row.sUserType}{row.sLanguage}{row.bIsDisabled ? '是' : ''}{row.tLastLoginDate ?? ''}{row.sCreatorUsername ?? ''}{row.tCreateDate}
+
+ {/* Pager */} +
+ 当前显示 共{data?.total ?? 0}条记录 + + {currentPage} + + +
) } + +const tbBtn: React.CSSProperties = { display: 'inline-flex', alignItems: 'center', gap: 6, padding: '6px 12px', color: 'var(--color-toolbar-text)', cursor: 'pointer', fontSize: 13, borderRadius: 2, border: 'none', background: 'transparent', fontFamily: 'inherit' } +const fSel: React.CSSProperties = { height: 30, border: '1px solid #d5d8de', borderRadius: 2, padding: '0 28px 0 10px', background: '#fff', minWidth: 100 } +const fIn: React.CSSProperties = { height: 30, border: '1px solid #d5d8de', borderRadius: 2, padding: '0 10px', minWidth: 140, fontSize: 13, fontFamily: 'inherit' } +const fBtn: React.CSSProperties = { height: 30, padding: '0 14px', borderRadius: 2, border: '1px solid var(--color-primary)', background: 'var(--color-primary)', color: '#fff', display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 13, cursor: 'pointer', fontFamily: 'inherit' } +const td: React.CSSProperties = { padding: '7px 10px', textAlign: 'left', whiteSpace: 'nowrap', border: '1px solid var(--color-table-border)' } +const pgBtn: React.CSSProperties = { width: 28, height: 28, border: '1px solid #d5d8de', background: '#fff', borderRadius: 2, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', color: '#666', fontFamily: 'inherit' } diff --git a/frontend/src/test/UserListPage.test.tsx b/frontend/src/test/UserListPage.test.tsx index 75b502d..ebb79e0 100644 --- a/frontend/src/test/UserListPage.test.tsx +++ b/frontend/src/test/UserListPage.test.tsx @@ -2,9 +2,10 @@ 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 { 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 UserListPage from '../pages/usr/UserListPage' vi.mock('../api/usr', () => ({ @@ -12,41 +13,50 @@ vi.mock('../api/usr', () => ({ getPermissionGroups: vi.fn().mockResolvedValue([]), createUser: vi.fn(), getUserList: vi.fn().mockResolvedValue({ total: 0, page: 1, pageSize: 20, list: [] }), - updateUser: vi.fn().mockResolvedValue({ userId: 'u1', username: 'alice', updatedAt: '2026-05-08T10:00:00' }) + updateUser: vi.fn().mockResolvedValue({ userId: 'u1', username: 'alice', updatedAt: '2026-05-08T10:00:00' }), })) vi.mock('../api/request', () => ({ - default: { get: vi.fn(), post: vi.fn() } + default: { get: vi.fn(), post: vi.fn(), put: vi.fn() }, })) function makeStore(userType: string) { - const store = configureStore({ reducer: { auth: authReducer } }) + const store = configureStore({ reducer: { auth: authReducer, tabs: tabsReducer } }) store.dispatch({ type: 'auth/setCredentials', - payload: { - accessToken: 'test-token', - refreshToken: 'test-refresh', - userInfo: { userId: 'u1', username: 'admin', userType, language: '中文', brandId: 'b1' } - } + payload: { accessToken: 'test-token', refreshToken: 'test-refresh', userInfo: { userId: 'u1', username: 'admin', userType, language: '中文', brandId: 'b1' } }, }) return store } -function renderPage(userType: string) { +const sampleRow = { + sId: 'u1', sUsername: 'alice', sUserCode: 'UC001', sUserType: '普通用户', + sLanguage: '中文', bCanEditDoc: 0, bIsDisabled: 0, tLastLoginDate: null, + sCreatorUsername: 'admin', tCreateDate: '2026-01-01T00:00:00', + sStaffName: '张三', sDepartment: '研发部', +} + +function renderPage(userType: string, extraRoutes = false) { const store = makeStore(userType) return render( - - + + {extraRoutes ? ( + + } /> + NewUserPage
} /> + DetailPage} /> + + ) : ( + + )} ) } describe('UserListPage', () => { - beforeEach(() => { - vi.clearAllMocks() - }) + beforeEach(() => { vi.clearAllMocks() }) it('superAdmin_seesNewButton', () => { renderPage('超级管理员') @@ -58,23 +68,15 @@ describe('UserListPage', () => { expect(screen.queryByRole('button', { name: /新\s*增/ })).not.toBeInTheDocument() }) - it('clickNewButton_opensDrawer', async () => { - renderPage('超级管理员') + it('clickNewButton_navigatesToNewPage', async () => { + renderPage('超级管理员', true) await userEvent.click(screen.getByRole('button', { name: /新\s*增/ })) - await waitFor(() => expect(screen.getByText('新增用户')).toBeInTheDocument()) + await waitFor(() => expect(screen.getByText('NewUserPage')).toBeInTheDocument()) }) it('initialLoad_rendersTableRows', async () => { const { getUserList } = await import('../api/usr') - vi.mocked(getUserList).mockResolvedValueOnce({ - total: 1, page: 1, pageSize: 20, - list: [{ - sId: 'u1', sUsername: 'alice', sUserCode: 'UC001', sUserType: '普通用户', - sLanguage: '中文', bCanEditDoc: 0, bIsDisabled: 0, tLastLoginDate: null, - sCreatorUsername: 'admin', tCreateDate: '2026-01-01T00:00:00', - sStaffName: '张三', sDepartment: '研发部' - }] - }) + vi.mocked(getUserList).mockResolvedValueOnce({ total: 1, page: 1, pageSize: 20, list: [sampleRow] }) renderPage('超级管理员') await waitFor(() => expect(screen.getByText('alice')).toBeInTheDocument()) expect(screen.getByText('张三')).toBeInTheDocument() @@ -87,22 +89,12 @@ describe('UserListPage', () => { await waitFor(() => expect(vi.mocked(getUserList)).toHaveBeenCalledTimes(2)) }) - it('editMode_submit_callsUpdateUser', async () => { - const { getUserList, updateUser } = await import('../api/usr') - vi.mocked(getUserList).mockResolvedValue({ - total: 1, page: 1, pageSize: 20, - list: [{ - sId: 'u1', sUsername: 'alice', sUserCode: 'UC001', sUserType: '普通用户', - sLanguage: '中文', bCanEditDoc: 0, bIsDisabled: 0, tLastLoginDate: null, - sCreatorUsername: 'admin', tCreateDate: '2026-01-01T00:00:00', - sStaffName: null, sDepartment: null - }] - }) - renderPage('超级管理员') + it('doubleClickRow_navigatesToDetailPage', async () => { + const { getUserList } = await import('../api/usr') + vi.mocked(getUserList).mockResolvedValueOnce({ total: 1, page: 1, pageSize: 20, list: [sampleRow] }) + renderPage('超级管理员', true) await waitFor(() => expect(screen.getByText('alice')).toBeInTheDocument()) - await userEvent.click(screen.getByRole('button', { name: /修改/ })) - await waitFor(() => expect(screen.getByText('修改用户')).toBeInTheDocument()) - await userEvent.click(screen.getByRole('button', { name: /确\s*认/ })) - await waitFor(() => expect(vi.mocked(updateUser)).toHaveBeenCalledTimes(1)) + await userEvent.dblClick(screen.getByText('alice').closest('tr')!) + await waitFor(() => expect(screen.getByText('DetailPage')).toBeInTheDocument()) }) })