Commit 11778b8c72a92be15810a3af9f79c1758771e137

Authored by zichun
1 parent 4a9044d7

feat(usr): rewrite UserListPage visual + URL filter sync REQ-USR-003

frontend/src/pages/usr/UserListPage.tsx
1 -import { useState, useEffect } from 'react'  
2 -import { Table, Select, Input, Button, Space } from 'antd'  
3 -import type { ColumnsType } from 'antd/es/table' 1 +import { useEffect, useState } from 'react'
  2 +import { useNavigate, useSearchParams, useLocation } from 'react-router-dom'
  3 +import { useAppDispatch } from '../../store/hooks'
  4 +import { openTab, updateTabPath } from '../../store/slices/tabsSlice'
4 import { PermButton } from '../../components/PermButton' 5 import { PermButton } from '../../components/PermButton'
5 -import UserFormDrawer from './UserFormDrawer'  
6 import { getUserList } from '../../api/usr' 6 import { getUserList } from '../../api/usr'
7 import type { PageVO, UserListItemVO } from '../../api/usr' 7 import type { PageVO, UserListItemVO } from '../../api/usr'
8 8
@@ -24,110 +24,136 @@ const MATCH_TYPES = [ @@ -24,110 +24,136 @@ const MATCH_TYPES = [
24 ] 24 ]
25 25
26 // REQ-USR-003: 查询用户 26 // REQ-USR-003: 查询用户
27 -// REQ-USR-002: 修改用户 27 +// REQ-USR-001: 新增入口 (navigate to /usr/users/new)
  28 +// REQ-USR-002: 修改入口 (double-click row → /usr/users/:id)
28 export default function UserListPage() { 29 export default function UserListPage() {
29 - const [drawerOpen, setDrawerOpen] = useState(false)  
30 - const [editingUser, setEditingUser] = useState<UserListItemVO | null>(null) 30 + const navigate = useNavigate()
  31 + const dispatch = useAppDispatch()
  32 + const location = useLocation()
  33 + const [searchParams, setSearchParams] = useSearchParams()
  34 +
  35 + const [queryField, setQueryField] = useState(searchParams.get('queryField') ?? 'username')
  36 + const [matchType, setMatchType] = useState(searchParams.get('matchType') ?? 'contains')
  37 + const [queryValue, setQueryValue] = useState(searchParams.get('queryValue') ?? '')
  38 + const [currentPage, setCurrentPage] = useState(Number(searchParams.get('page') ?? '1'))
31 const [data, setData] = useState<PageVO<UserListItemVO> | null>(null) 39 const [data, setData] = useState<PageVO<UserListItemVO> | null>(null)
32 - const [queryField, setQueryField] = useState('username')  
33 - const [matchType, setMatchType] = useState('contains')  
34 - const [queryValue, setQueryValue] = useState('')  
35 - const [currentPage, setCurrentPage] = useState(1) 40 + const [selectedId, setSelectedId] = useState<string | null>(null)
  41 +
  42 + useEffect(() => {
  43 + dispatch(openTab({ id: 'userlist', title: '用户列表', path: location.pathname + location.search, closable: true }))
  44 + doLoad(currentPage, queryField, matchType, queryValue)
  45 + }, [])
36 46
37 - const load = (pg = 1) => { 47 + function doLoad(pg: number, field: string, match: string, val: string) {
38 setCurrentPage(pg) 48 setCurrentPage(pg)
39 - getUserList({ queryField, matchType, queryValue, page: pg, pageSize: 20 }).then(setData) 49 + getUserList({ queryField: field, matchType: match, queryValue: val, page: pg, pageSize: 20 }).then(setData)
40 } 50 }
41 51
42 - useEffect(() => {  
43 - load()  
44 - }, []) 52 + function handleSearch() {
  53 + const newPath = `/usr/users?queryField=${queryField}&matchType=${matchType}&queryValue=${encodeURIComponent(queryValue)}`
  54 + setSearchParams({ queryField, matchType, queryValue, page: '1' })
  55 + dispatch(updateTabPath({ id: 'userlist', path: newPath }))
  56 + doLoad(1, queryField, matchType, queryValue)
  57 + }
45 58
46 - const columns: ColumnsType<UserListItemVO> = [  
47 - { title: '用户名', dataIndex: 'sUsername' },  
48 - { title: '员工名', dataIndex: 'sStaffName' },  
49 - { title: '用户号', dataIndex: 'sUserCode' },  
50 - { title: '部门', dataIndex: 'sDepartment' },  
51 - { title: '用户类型', dataIndex: 'sUserType' },  
52 - { title: '语言', dataIndex: 'sLanguage' },  
53 - { title: '作废', dataIndex: 'bIsDisabled', render: (v: number) => v ? '是' : '否' },  
54 - { title: '登录日期', dataIndex: 'tLastLoginDate' },  
55 - { title: '制单人', dataIndex: 'sCreatorUsername' },  
56 - { title: '制单日期', dataIndex: 'tCreateDate' },  
57 - {  
58 - title: '操作',  
59 - key: 'action',  
60 - render: (_, record) => (  
61 - <PermButton  
62 - permission="usr:edit"  
63 - type="link"  
64 - onClick={() => setEditingUser(record)}  
65 - >  
66 - 修改  
67 - </PermButton>  
68 - )  
69 - },  
70 - ] 59 + function handleClear() {
  60 + setQueryField('username'); setMatchType('contains'); setQueryValue('')
  61 + setSearchParams({})
  62 + dispatch(updateTabPath({ id: 'userlist', path: '/usr/users' }))
  63 + doLoad(1, 'username', 'contains', '')
  64 + }
  65 +
  66 + const totalPages = data ? Math.ceil(data.total / 20) : 1
71 67
72 return ( 68 return (
73 - <div>  
74 - <Space style={{ marginBottom: 16 }} wrap>  
75 - <Select  
76 - value={queryField}  
77 - onChange={setQueryField}  
78 - options={QUERY_FIELDS}  
79 - style={{ width: 120 }}  
80 - />  
81 - <Select  
82 - value={matchType}  
83 - onChange={setMatchType}  
84 - options={MATCH_TYPES}  
85 - style={{ width: 100 }}  
86 - />  
87 - <Input  
88 - value={queryValue}  
89 - onChange={e => setQueryValue(e.target.value)}  
90 - placeholder="查询值"  
91 - style={{ width: 160 }}  
92 - />  
93 - <Button type="primary" onClick={() => load(1)}>搜索</Button>  
94 - <PermButton  
95 - permission="usr:create"  
96 - type="primary"  
97 - onClick={() => setDrawerOpen(true)}  
98 - > 69 + <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
  70 + {/* Toolbar */}
  71 + <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 }}>
  72 + <button onClick={() => doLoad(currentPage, queryField, matchType, queryValue)} style={tbBtn}>
  73 + <svg width={14} height={14} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}><path d="M21 12a9 9 0 1 1-3-6.7"/><path d="M21 4v5h-5"/></svg>
  74 + 刷新
  75 + </button>
  76 + <PermButton permission="usr:create" onClick={() => navigate('/usr/users/new')} style={tbBtn}>
  77 + <svg width={14} height={14} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}><circle cx={12} cy={12} r={9}/><path d="M12 8v8M8 12h8"/></svg>
99 新增 78 新增
100 </PermButton> 79 </PermButton>
101 - </Space>  
102 - <Table  
103 - dataSource={data?.list ?? []}  
104 - columns={columns}  
105 - rowKey="sId"  
106 - pagination={{  
107 - total: data?.total ?? 0,  
108 - pageSize: 20,  
109 - current: currentPage,  
110 - onChange: load,  
111 - }}  
112 - />  
113 - <UserFormDrawer  
114 - open={drawerOpen}  
115 - onClose={() => setDrawerOpen(false)}  
116 - onSuccess={() => { setDrawerOpen(false); load(1) }}  
117 - />  
118 - <UserFormDrawer  
119 - open={editingUser !== null}  
120 - userId={editingUser?.sId}  
121 - initialData={editingUser ? {  
122 - userType: editingUser.sUserType,  
123 - language: editingUser.sLanguage,  
124 - canEditDoc: editingUser.bCanEditDoc === 1,  
125 - isDisabled: editingUser.bIsDisabled === 1,  
126 - employeeId: null,  
127 - } : undefined}  
128 - onClose={() => setEditingUser(null)}  
129 - onSuccess={() => { setEditingUser(null); load(1) }}  
130 - /> 80 + <button style={tbBtn}>
  81 + <svg width={14} height={14} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}><path d="M4 4h12l4 4v12H4z"/><path d="M16 4v4h4M8 12h8M8 16h8"/></svg>
  82 + 导出Excel
  83 + </button>
  84 + <span style={{ flex: 1 }} />
  85 + <span style={{ padding: '6px 8px', cursor: 'pointer', color: '#cfd2d8' }}>⚙</span>
  86 + </div>
  87 + {/* Filter bar */}
  88 + <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 12px', background: 'var(--color-filterbar-bg)', borderBottom: '1px solid var(--color-table-border)', flexShrink: 0 }}>
  89 + <select value={queryField} onChange={e => setQueryField(e.target.value)} style={fSel}>
  90 + {QUERY_FIELDS.map(f => <option key={f.value} value={f.value}>{f.label}</option>)}
  91 + </select>
  92 + <select value={matchType} onChange={e => setMatchType(e.target.value)} style={fSel}>
  93 + {MATCH_TYPES.map(m => <option key={m.value} value={m.value}>{m.label}</option>)}
  94 + </select>
  95 + <input value={queryValue} onChange={e => setQueryValue(e.target.value)} style={fIn} />
  96 + <span style={{ width: 34, height: 30, background: '#dfe5ee', border: '1px solid #d5d8de', display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 2, cursor: 'pointer', color: '#3776c8' }}>▾</span>
  97 + <button onClick={handleSearch} style={fBtn}>
  98 + <svg width={13} height={13} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}><circle cx={11} cy={11} r={7}/><line x1={21} y1={21} x2={16.5} y2={16.5}/></svg>
  99 + 搜索
  100 + </button>
  101 + <button onClick={handleClear} style={{ ...fBtn, background: '#fff', color: '#444', borderColor: '#cfd3da' }}>⊗ 清空</button>
  102 + </div>
  103 + {/* Table */}
  104 + <div style={{ flex: 1, overflow: 'auto', background: '#fff', border: '1px solid var(--color-table-border)', borderTop: 'none' }}>
  105 + <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
  106 + <thead>
  107 + <tr>
  108 + {['', '序号', '用户名', '员工名', '用户号', '部门', '用户类型', '语言', '作废', '登录日期', '制单人', '制单日期'].map((h, i) => (
  109 + <th key={i} style={{ background: 'var(--color-table-header-bg)', fontWeight: 500, color: '#333', padding: '7px 10px', textAlign: 'left', whiteSpace: 'nowrap', border: '1px solid var(--color-table-border)', position: 'sticky', top: 0, zIndex: 1, width: i === 0 ? 32 : i === 1 ? 60 : undefined }}>
  110 + {h}{i > 1 && <span style={{ color: '#aaa', marginLeft: 4 }}>⇅ ⌕</span>}
  111 + </th>
  112 + ))}
  113 + </tr>
  114 + </thead>
  115 + <tbody>
  116 + {(data?.list ?? []).map((row, idx) => (
  117 + <tr
  118 + key={row.sId}
  119 + onClick={() => setSelectedId(row.sId)}
  120 + onDoubleClick={() => navigate(`/usr/users/${row.sId}`, { state: row })}
  121 + style={{ background: row.sId === selectedId ? 'var(--color-table-row-bg-selected)' : idx % 2 === 1 ? 'var(--color-table-row-alt)' : '#fff', cursor: 'pointer' }}
  122 + >
  123 + <td style={td}><span style={{ width: 14, height: 14, border: '1px solid #b8bcc3', borderRadius: '50%', display: 'inline-block', background: row.sId === selectedId ? 'var(--color-primary)' : '#fff' }} /></td>
  124 + <td style={td}>{idx + 1}</td>
  125 + <td style={td}>{row.sUsername}</td>
  126 + <td style={td}>{row.sStaffName ?? ''}</td>
  127 + <td style={td}>{row.sUserCode}</td>
  128 + <td style={td}>{row.sDepartment ?? ''}</td>
  129 + <td style={td}>{row.sUserType}</td>
  130 + <td style={td}>{row.sLanguage}</td>
  131 + <td style={td}>{row.bIsDisabled ? '是' : ''}</td>
  132 + <td style={td}>{row.tLastLoginDate ?? ''}</td>
  133 + <td style={td}>{row.sCreatorUsername ?? ''}</td>
  134 + <td style={td}>{row.tCreateDate}</td>
  135 + </tr>
  136 + ))}
  137 + </tbody>
  138 + </table>
  139 + </div>
  140 + {/* Pager */}
  141 + <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 14px', background: '#fff', borderTop: '1px solid var(--color-table-border)', justifyContent: 'flex-end', fontSize: 13, color: '#555', flexShrink: 0 }}>
  142 + <span>当前显示 共{data?.total ?? 0}条记录</span>
  143 + <button onClick={() => currentPage > 1 && doLoad(currentPage - 1, queryField, matchType, queryValue)} style={pgBtn}>‹</button>
  144 + <span style={{ width: 28, height: 28, border: '1px solid var(--color-primary)', color: 'var(--color-primary)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', borderRadius: 2 }}>{currentPage}</span>
  145 + <button onClick={() => currentPage < totalPages && doLoad(currentPage + 1, queryField, matchType, queryValue)} style={pgBtn}>›</button>
  146 + <select defaultValue="10000" style={{ height: 28, border: '1px solid #d5d8de', borderRadius: 2, padding: '0 8px' }}>
  147 + <option value="10000">10000 条/页</option>
  148 + </select>
  149 + </div>
131 </div> 150 </div>
132 ) 151 )
133 } 152 }
  153 +
  154 +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' }
  155 +const fSel: React.CSSProperties = { height: 30, border: '1px solid #d5d8de', borderRadius: 2, padding: '0 28px 0 10px', background: '#fff', minWidth: 100 }
  156 +const fIn: React.CSSProperties = { height: 30, border: '1px solid #d5d8de', borderRadius: 2, padding: '0 10px', minWidth: 140, fontSize: 13, fontFamily: 'inherit' }
  157 +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' }
  158 +const td: React.CSSProperties = { padding: '7px 10px', textAlign: 'left', whiteSpace: 'nowrap', border: '1px solid var(--color-table-border)' }
  159 +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' }
frontend/src/test/UserListPage.test.tsx
@@ -2,9 +2,10 @@ import { describe, it, expect, vi, beforeEach } from &#39;vitest&#39; @@ -2,9 +2,10 @@ import { describe, it, expect, vi, beforeEach } from &#39;vitest&#39;
2 import { render, screen, waitFor } from '@testing-library/react' 2 import { render, screen, waitFor } from '@testing-library/react'
3 import userEvent from '@testing-library/user-event' 3 import userEvent from '@testing-library/user-event'
4 import { Provider } from 'react-redux' 4 import { Provider } from 'react-redux'
5 -import { MemoryRouter } from 'react-router-dom' 5 +import { MemoryRouter, Routes, Route } from 'react-router-dom'
6 import { configureStore } from '@reduxjs/toolkit' 6 import { configureStore } from '@reduxjs/toolkit'
7 import authReducer from '../store/slices/authSlice' 7 import authReducer from '../store/slices/authSlice'
  8 +import tabsReducer from '../store/slices/tabsSlice'
8 import UserListPage from '../pages/usr/UserListPage' 9 import UserListPage from '../pages/usr/UserListPage'
9 10
10 vi.mock('../api/usr', () => ({ 11 vi.mock('../api/usr', () => ({
@@ -12,41 +13,50 @@ vi.mock(&#39;../api/usr&#39;, () =&gt; ({ @@ -12,41 +13,50 @@ vi.mock(&#39;../api/usr&#39;, () =&gt; ({
12 getPermissionGroups: vi.fn().mockResolvedValue([]), 13 getPermissionGroups: vi.fn().mockResolvedValue([]),
13 createUser: vi.fn(), 14 createUser: vi.fn(),
14 getUserList: vi.fn().mockResolvedValue({ total: 0, page: 1, pageSize: 20, list: [] }), 15 getUserList: vi.fn().mockResolvedValue({ total: 0, page: 1, pageSize: 20, list: [] }),
15 - updateUser: vi.fn().mockResolvedValue({ userId: 'u1', username: 'alice', updatedAt: '2026-05-08T10:00:00' }) 16 + updateUser: vi.fn().mockResolvedValue({ userId: 'u1', username: 'alice', updatedAt: '2026-05-08T10:00:00' }),
16 })) 17 }))
17 18
18 vi.mock('../api/request', () => ({ 19 vi.mock('../api/request', () => ({
19 - default: { get: vi.fn(), post: vi.fn() } 20 + default: { get: vi.fn(), post: vi.fn(), put: vi.fn() },
20 })) 21 }))
21 22
22 function makeStore(userType: string) { 23 function makeStore(userType: string) {
23 - const store = configureStore({ reducer: { auth: authReducer } }) 24 + const store = configureStore({ reducer: { auth: authReducer, tabs: tabsReducer } })
24 store.dispatch({ 25 store.dispatch({
25 type: 'auth/setCredentials', 26 type: 'auth/setCredentials',
26 - payload: {  
27 - accessToken: 'test-token',  
28 - refreshToken: 'test-refresh',  
29 - userInfo: { userId: 'u1', username: 'admin', userType, language: '中文', brandId: 'b1' }  
30 - } 27 + payload: { accessToken: 'test-token', refreshToken: 'test-refresh', userInfo: { userId: 'u1', username: 'admin', userType, language: '中文', brandId: 'b1' } },
31 }) 28 })
32 return store 29 return store
33 } 30 }
34 31
35 -function renderPage(userType: string) { 32 +const sampleRow = {
  33 + sId: 'u1', sUsername: 'alice', sUserCode: 'UC001', sUserType: '普通用户',
  34 + sLanguage: '中文', bCanEditDoc: 0, bIsDisabled: 0, tLastLoginDate: null,
  35 + sCreatorUsername: 'admin', tCreateDate: '2026-01-01T00:00:00',
  36 + sStaffName: '张三', sDepartment: '研发部',
  37 +}
  38 +
  39 +function renderPage(userType: string, extraRoutes = false) {
36 const store = makeStore(userType) 40 const store = makeStore(userType)
37 return render( 41 return render(
38 <Provider store={store}> 42 <Provider store={store}>
39 - <MemoryRouter>  
40 - <UserListPage /> 43 + <MemoryRouter initialEntries={['/usr/users']}>
  44 + {extraRoutes ? (
  45 + <Routes>
  46 + <Route path="/usr/users" element={<UserListPage />} />
  47 + <Route path="/usr/users/new" element={<div>NewUserPage</div>} />
  48 + <Route path="/usr/users/:id" element={<div>DetailPage</div>} />
  49 + </Routes>
  50 + ) : (
  51 + <UserListPage />
  52 + )}
41 </MemoryRouter> 53 </MemoryRouter>
42 </Provider> 54 </Provider>
43 ) 55 )
44 } 56 }
45 57
46 describe('UserListPage', () => { 58 describe('UserListPage', () => {
47 - beforeEach(() => {  
48 - vi.clearAllMocks()  
49 - }) 59 + beforeEach(() => { vi.clearAllMocks() })
50 60
51 it('superAdmin_seesNewButton', () => { 61 it('superAdmin_seesNewButton', () => {
52 renderPage('超级管理员') 62 renderPage('超级管理员')
@@ -58,23 +68,15 @@ describe(&#39;UserListPage&#39;, () =&gt; { @@ -58,23 +68,15 @@ describe(&#39;UserListPage&#39;, () =&gt; {
58 expect(screen.queryByRole('button', { name: /新\s*增/ })).not.toBeInTheDocument() 68 expect(screen.queryByRole('button', { name: /新\s*增/ })).not.toBeInTheDocument()
59 }) 69 })
60 70
61 - it('clickNewButton_opensDrawer', async () => {  
62 - renderPage('超级管理员') 71 + it('clickNewButton_navigatesToNewPage', async () => {
  72 + renderPage('超级管理员', true)
63 await userEvent.click(screen.getByRole('button', { name: /新\s*增/ })) 73 await userEvent.click(screen.getByRole('button', { name: /新\s*增/ }))
64 - await waitFor(() => expect(screen.getByText('新增用户')).toBeInTheDocument()) 74 + await waitFor(() => expect(screen.getByText('NewUserPage')).toBeInTheDocument())
65 }) 75 })
66 76
67 it('initialLoad_rendersTableRows', async () => { 77 it('initialLoad_rendersTableRows', async () => {
68 const { getUserList } = await import('../api/usr') 78 const { getUserList } = await import('../api/usr')
69 - vi.mocked(getUserList).mockResolvedValueOnce({  
70 - total: 1, page: 1, pageSize: 20,  
71 - list: [{  
72 - sId: 'u1', sUsername: 'alice', sUserCode: 'UC001', sUserType: '普通用户',  
73 - sLanguage: '中文', bCanEditDoc: 0, bIsDisabled: 0, tLastLoginDate: null,  
74 - sCreatorUsername: 'admin', tCreateDate: '2026-01-01T00:00:00',  
75 - sStaffName: '张三', sDepartment: '研发部'  
76 - }]  
77 - }) 79 + vi.mocked(getUserList).mockResolvedValueOnce({ total: 1, page: 1, pageSize: 20, list: [sampleRow] })
78 renderPage('超级管理员') 80 renderPage('超级管理员')
79 await waitFor(() => expect(screen.getByText('alice')).toBeInTheDocument()) 81 await waitFor(() => expect(screen.getByText('alice')).toBeInTheDocument())
80 expect(screen.getByText('张三')).toBeInTheDocument() 82 expect(screen.getByText('张三')).toBeInTheDocument()
@@ -87,22 +89,12 @@ describe(&#39;UserListPage&#39;, () =&gt; { @@ -87,22 +89,12 @@ describe(&#39;UserListPage&#39;, () =&gt; {
87 await waitFor(() => expect(vi.mocked(getUserList)).toHaveBeenCalledTimes(2)) 89 await waitFor(() => expect(vi.mocked(getUserList)).toHaveBeenCalledTimes(2))
88 }) 90 })
89 91
90 - it('editMode_submit_callsUpdateUser', async () => {  
91 - const { getUserList, updateUser } = await import('../api/usr')  
92 - vi.mocked(getUserList).mockResolvedValue({  
93 - total: 1, page: 1, pageSize: 20,  
94 - list: [{  
95 - sId: 'u1', sUsername: 'alice', sUserCode: 'UC001', sUserType: '普通用户',  
96 - sLanguage: '中文', bCanEditDoc: 0, bIsDisabled: 0, tLastLoginDate: null,  
97 - sCreatorUsername: 'admin', tCreateDate: '2026-01-01T00:00:00',  
98 - sStaffName: null, sDepartment: null  
99 - }]  
100 - })  
101 - renderPage('超级管理员') 92 + it('doubleClickRow_navigatesToDetailPage', async () => {
  93 + const { getUserList } = await import('../api/usr')
  94 + vi.mocked(getUserList).mockResolvedValueOnce({ total: 1, page: 1, pageSize: 20, list: [sampleRow] })
  95 + renderPage('超级管理员', true)
102 await waitFor(() => expect(screen.getByText('alice')).toBeInTheDocument()) 96 await waitFor(() => expect(screen.getByText('alice')).toBeInTheDocument())
103 - await userEvent.click(screen.getByRole('button', { name: /修改/ }))  
104 - await waitFor(() => expect(screen.getByText('修改用户')).toBeInTheDocument())  
105 - await userEvent.click(screen.getByRole('button', { name: /确\s*认/ }))  
106 - await waitFor(() => expect(vi.mocked(updateUser)).toHaveBeenCalledTimes(1)) 97 + await userEvent.dblClick(screen.getByText('alice').closest('tr')!)
  98 + await waitFor(() => expect(screen.getByText('DetailPage')).toBeInTheDocument())
107 }) 99 })
108 }) 100 })