UserListPage.tsx
10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
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 { getUserList } from '../../api/usr'
import type { PageVO, UserListItemVO } from '../../api/usr'
const QUERY_FIELDS = [
{ value: 'username', label: '用户名' },
{ value: 'staffName', label: '员工名' },
{ value: 'userCode', label: '用户号' },
{ value: 'department', label: '部门' },
{ value: 'userType', label: '用户类型' },
{ value: 'disabled', label: '作废' },
{ value: 'lastLoginDate', label: '登录日期' },
{ value: 'creator', label: '制单人' },
]
const MATCH_TYPES = [
{ value: 'contains', label: '包含' },
{ value: 'notContains', label: '不包含' },
{ value: 'equals', label: '等于' },
]
// REQ-USR-003: 查询用户
// REQ-USR-001: 新增入口 (navigate to /usr/users/new)
// REQ-USR-002: 修改入口 (double-click row → /usr/users/:id)
export default function UserListPage() {
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<PageVO<UserListItemVO> | null>(null)
const [selectedId, setSelectedId] = useState<string | null>(null)
useEffect(() => {
dispatch(openTab({ id: 'userlist', title: '用户列表', path: location.pathname + location.search, closable: true }))
doLoad(currentPage, queryField, matchType, queryValue)
}, [])
function doLoad(pg: number, field: string, match: string, val: string) {
setCurrentPage(pg)
getUserList({ queryField: field, matchType: match, queryValue: val, page: pg, pageSize: 20 }).then(setData)
}
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)
}
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 (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{/* Toolbar */}
<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 }}>
<button onClick={() => doLoad(currentPage, queryField, matchType, queryValue)} style={tbBtn}>
<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>
刷新
</button>
<PermButton permission="usr:create" onClick={() => navigate('/usr/users/new')} style={tbBtn}>
<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>
新增
</PermButton>
<button style={tbBtn}>
<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>
导出Excel
</button>
<span style={{ flex: 1 }} />
<span style={{ padding: '6px 8px', cursor: 'pointer', color: '#cfd2d8' }}>⚙</span>
</div>
{/* Filter bar */}
<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 }}>
<select value={queryField} onChange={e => setQueryField(e.target.value)} style={fSel}>
{QUERY_FIELDS.map(f => <option key={f.value} value={f.value}>{f.label}</option>)}
</select>
<select value={matchType} onChange={e => setMatchType(e.target.value)} style={fSel}>
{MATCH_TYPES.map(m => <option key={m.value} value={m.value}>{m.label}</option>)}
</select>
<input value={queryValue} onChange={e => setQueryValue(e.target.value)} style={fIn} />
<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>
<button onClick={handleSearch} style={fBtn}>
<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>
搜索
</button>
<button onClick={handleClear} style={{ ...fBtn, background: '#fff', color: '#444', borderColor: '#cfd3da' }}>⊗ 清空</button>
</div>
{/* Table */}
<div style={{ flex: 1, overflow: 'auto', background: '#fff', border: '1px solid var(--color-table-border)', borderTop: 'none' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
<thead>
<tr>
{['', '序号', '用户名', '员工名', '用户号', '部门', '用户类型', '语言', '作废', '登录日期', '制单人', '制单日期'].map((h, i) => (
<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 }}>
{h}{i > 1 && <span style={{ color: '#aaa', marginLeft: 4 }}>⇅ ⌕</span>}
</th>
))}
</tr>
</thead>
<tbody>
{(data?.list ?? []).map((row, idx) => (
<tr
key={row.sId}
onClick={() => 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' }}
>
<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>
<td style={td}>{idx + 1}</td>
<td style={td}>{row.sUsername}</td>
<td style={td}>{row.sStaffName ?? ''}</td>
<td style={td}>{row.sUserCode}</td>
<td style={td}>{row.sDepartment ?? ''}</td>
<td style={td}>{row.sUserType}</td>
<td style={td}>{row.sLanguage}</td>
<td style={td}>{row.bIsDisabled ? '是' : ''}</td>
<td style={td}>{row.tLastLoginDate ?? ''}</td>
<td style={td}>{row.sCreatorUsername ?? ''}</td>
<td style={td}>{row.tCreateDate}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pager */}
<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 }}>
<span>当前显示 共{data?.total ?? 0}条记录</span>
<button onClick={() => currentPage > 1 && doLoad(currentPage - 1, queryField, matchType, queryValue)} style={pgBtn}>‹</button>
<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>
<button onClick={() => currentPage < totalPages && doLoad(currentPage + 1, queryField, matchType, queryValue)} style={pgBtn}>›</button>
<select defaultValue="10000" style={{ height: 28, border: '1px solid #d5d8de', borderRadius: 2, padding: '0 8px' }}>
<option value="10000">10000 条/页</option>
</select>
</div>
</div>
)
}
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' }