This commit is contained in:
2025-08-28 00:55:35 +08:00
commit 410f54a65e
93 changed files with 9863 additions and 0 deletions

View File

@ -0,0 +1,482 @@
import React, { useEffect, useMemo, useState } from 'react'
import { Button, Form, Input, Modal, Popconfirm, Select, Space, Table, Tag, Typography, message, TreeSelect } from 'antd'
import api from '../utils/axios'
import type { ColumnsType } from 'antd/es/table'
import { formatDateTime } from '../utils/datetime'
import { EditOutlined, DeleteOutlined, KeyOutlined, UserOutlined } from '@ant-design/icons'
import { usePermission } from '../utils/permission'
import PageHeader from '../components/PageHeader'
interface UserItem {
id: number
username: string
nickname?: string
status?: number
created_at?: string
}
// 简单的 {id, name} 类型守卫,便于从未知数组安全映射
type IdName = { id: number; name: string }
const isIdName = (o: unknown): o is IdName => {
if (typeof o !== 'object' || o === null) return false
const rec = o as Record<string, unknown>
return typeof rec.id === 'number' && typeof rec.name === 'string'
}
// 部门基础类型与守卫(包含 parent_id 便于构建树)
type DeptBasic = { id: number; name: string; parent_id?: number }
const isDeptBasic = (o: unknown): o is DeptBasic => {
if (typeof o !== 'object' || o === null) return false
const rec = o as Record<string, unknown>
const okId = typeof rec.id === 'number'
const okName = typeof rec.name === 'string'
const okPid = rec.parent_id === undefined || typeof rec.parent_id === 'number'
return okId && okName && okPid
}
export default function Users(){
const [loading, setLoading] = useState(false)
const [data, setData] = useState([] as UserItem[])
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [total, setTotal] = useState(0)
const [keyword, setKeyword] = useState('')
const [createOpen, setCreateOpen] = useState(false)
const [editOpen, setEditOpen] = useState(false)
const [pwdOpen, setPwdOpen] = useState(false)
const [positionsOpen, setPositionsOpen] = useState(false)
const [current, setCurrent] = useState(null as UserItem | null)
const [currentUserId, setCurrentUserId] = useState(null as number | null)
const [form] = Form.useForm()
const [editForm] = Form.useForm()
const [pwdForm] = Form.useForm()
const [searchForm] = Form.useForm()
// 分配角色(移到编辑弹窗内)
const [selectedRoleIds, setSelectedRoleIds] = useState([] as number[])
const [allRoles, setAllRoles] = useState([] as { id: number; name: string }[])
// 分配部门(移到编辑弹窗内)
const [selectedDeptIds, setSelectedDeptIds] = useState([] as number[])
const [allDepts, setAllDepts] = useState([] as DeptBasic[])
// 岗位分配相关状态
const [positionOptions, setPositionOptions] = useState([] as { label: string; value: number }[])
const [userPositions, setUserPositions] = useState([] as number[])
// 新增/编辑弹窗内的岗位选择
const [createPositionIds, setCreatePositionIds] = useState([] as number[])
const [editPositionIds, setEditPositionIds] = useState([] as number[])
// 权限判断
const { has } = usePermission()
// 根据 allDepts 构建部门树(用于选择多个部门)
type DeptTreeNode = { title: string; value: number; key: number; children?: DeptTreeNode[] }
const deptTreeData: DeptTreeNode[] = useMemo(() => {
const map = new Map<number, DeptTreeNode & { children: DeptTreeNode[] }>()
allDepts.forEach((d: DeptBasic) => map.set(d.id, { title: d.name, value: d.id, key: d.id, children: [] }))
const roots: (DeptTreeNode & { children: DeptTreeNode[] })[] = []
allDepts.forEach((d: DeptBasic) => {
const node = map.get(d.id)!
if (d.parent_id && map.has(d.parent_id)) {
const parent = map.get(d.parent_id)
if (parent) parent.children.push(node)
} else {
roots.push(node)
}
})
return roots
}, [allDepts])
// 获取岗位选项
const fetchPositionOptions = async () => {
try {
const { data } = await api.get('/positions', { params: { page: 1, page_size: 1000 } })
if (data?.code === 0) {
setPositionOptions((data.data.items || []).map((it: any) => ({ label: it.name, value: it.id })))
}
} catch (e) {
console.error('获取岗位列表失败:', e)
}
}
useEffect(() => {
fetchPositionOptions()
}, [])
const fetchUsers = async (p: number = page, ps: number = pageSize, kw: string = keyword) => {
setLoading(true)
try{
const { data } = await api.get(`/users`, { params: { page: p, page_size: ps, keyword: kw } })
if(data?.code === 0){
const resp = data.data
setData(resp.items || [])
setTotal(resp.total || 0)
setPage(resp.page || p)
setPageSize(resp.page_size || ps)
}else{
throw new Error(data?.message || '获取用户失败')
}
}catch(e: any){
message.error(e.message || '获取用户失败')
}finally{
setLoading(false)
}
}
useEffect(() => { fetchUsers(1, pageSize, keyword) }, [])
const onCreate = async () => {
form.resetFields()
// 新增用户:预置清空角色与部门选择,并加载候选数据
setSelectedRoleIds([])
setSelectedDeptIds([])
setCreatePositionIds([])
setCreateOpen(true)
try {
await fetchPositionOptions()
const [rolesRes, deptsRes] = await Promise.all([
api.get('/roles', { params: { page: 1, page_size: 1000 } }),
api.get('/departments', { params: { keyword: '' } })
])
if (rolesRes.data?.code === 0) {
const rolesItemsSrc = Array.isArray(rolesRes.data?.data?.items) ? (rolesRes.data.data.items as unknown[]) : []
const roleItems = rolesItemsSrc.filter(isIdName).map(({ id, name }) => ({ id, name }))
setAllRoles(roleItems)
}
if (deptsRes.data?.code === 0) {
const deptsSrc = Array.isArray(deptsRes.data?.data) ? (deptsRes.data.data as unknown[]) : []
const deptBasics = deptsSrc.filter(isDeptBasic)
setAllDepts(deptBasics)
}
} catch (e: any) {
message.error(e.message || '加载角色/部门失败')
}
}
const handleCreate = async () => {
try{
const values = await form.validateFields()
const { data } = await api.post('/users', values)
if(data?.code !== 0){ throw new Error(data?.message || '创建失败') }
const uid = typeof data?.data?.id === 'number' ? data.data.id : undefined
if (uid) {
const [rolesSave, deptsSave, posSave] = await Promise.all([
api.put(`/users/${uid}/roles`, { ids: selectedRoleIds }),
api.put(`/users/${uid}/departments`, { ids: selectedDeptIds }),
api.put(`/users/${uid}/positions`, { ids: createPositionIds })
])
if (rolesSave.data?.code !== 0) throw new Error(rolesSave.data?.message || '保存角色失败')
if (deptsSave.data?.code !== 0) throw new Error(deptsSave.data?.message || '保存部门失败')
if (posSave.data?.code !== 0) throw new Error(posSave.data?.message || '保存岗位失败')
} else {
message.warning('创建成功但未获取到用户ID未能分配角色/部门/岗位')
}
message.success('创建成功')
setCreateOpen(false)
fetchUsers(1, pageSize)
}catch(e: any){ if(e?.errorFields) return; message.error(e.message || '创建失败') }
}
const onEdit = async (record: UserItem) => {
setCurrent(record)
editForm.setFieldsValue({ nickname: record.nickname, status: record.status })
setEditOpen(true)
// 加载该用户已分配的角色/部门/岗位及候选列表
try{
await fetchPositionOptions()
const [roleIdsRes, rolesRes, deptIdsRes, deptsRes, posIdsRes] = await Promise.all([
api.get(`/users/${record.id}/roles`),
api.get('/roles', { params: { page: 1, page_size: 1000 } }),
api.get(`/users/${record.id}/departments`),
api.get('/departments', { params: { keyword: '' } }),
api.get(`/users/${record.id}/positions`)
])
if(roleIdsRes.data?.code !== 0) throw new Error(roleIdsRes.data?.message || '获取用户角色失败')
if(rolesRes.data?.code !== 0) throw new Error(rolesRes.data?.message || '获取角色列表失败')
if(deptIdsRes.data?.code !== 0) throw new Error(deptIdsRes.data?.message || '获取用户部门失败')
if(deptsRes.data?.code !== 0) throw new Error(deptsRes.data?.message || '获取部门列表失败')
if(posIdsRes.data?.code !== 0) throw new Error(posIdsRes.data?.message || '获取用户岗位失败')
const roleIds = Array.isArray(roleIdsRes.data?.data) ? (roleIdsRes.data.data as unknown[]).filter((n): n is number => typeof n === 'number') : []
setSelectedRoleIds(roleIds)
const rolesItemsSrc = Array.isArray(rolesRes.data?.data?.items) ? rolesRes.data.data.items as unknown[] : []
const roleItems = rolesItemsSrc.filter(isIdName).map(({ id, name }) => ({ id, name }))
setAllRoles(roleItems)
const deptIds = Array.isArray(deptIdsRes.data?.data) ? (deptIdsRes.data.data as unknown[]).filter((n): n is number => typeof n === 'number') : []
setSelectedDeptIds(deptIds)
const deptsSrc = Array.isArray(deptsRes.data?.data) ? (deptsRes.data.data as unknown[]) : []
const deptBasics = deptsSrc.filter(isDeptBasic)
setAllDepts(deptBasics)
const posIds = Array.isArray(posIdsRes.data?.data) ? (posIdsRes.data.data as unknown[]).filter((n): n is number => typeof n === 'number') : []
setEditPositionIds(posIds)
}catch(e: any){ message.error(e.message || '加载编辑数据失败') }
}
const handleEdit = async () => {
if(!current) return
try{
const values = await editForm.validateFields()
// 先保存基础信息
const { data: upd } = await api.put(`/users/${current!.id}`, values)
if(upd?.code !== 0) throw new Error(upd?.message || '更新失败')
// 再保存角色与部门
const [rolesSave, deptsSave] = await Promise.all([
api.put(`/users/${current.id}/roles`, { ids: selectedRoleIds }),
api.put(`/users/${current.id}/departments`, { ids: selectedDeptIds }),
api.put(`/users/${current.id}/positions`, { ids: editPositionIds })
])
if(rolesSave.data?.code !== 0) throw new Error(rolesSave.data?.message || '保存角色失败')
if(deptsSave.data?.code !== 0) throw new Error(deptsSave.data?.message || '保存部门失败')
message.success('更新成功')
setEditOpen(false)
fetchUsers(page, pageSize)
}catch(e: any){ if(e?.errorFields) return; message.error(e.message || '更新失败') }
}
const onResetPwd = (record: UserItem) => {
setCurrent(record)
pwdForm.resetFields()
setPwdOpen(true)
}
const handleResetPwd = async () => {
try{
const values = await pwdForm.validateFields()
const { data } = await api.post(`/users/${current!.id}/reset_password`, values)
if(data?.code === 0){
message.success('密码已重置')
setPwdOpen(false)
}else{ throw new Error(data?.message || '重置失败') }
}catch(e: any){ if(e?.errorFields) return; message.error(e.message || '重置失败') }
}
// 岗位分配相关方法
const openPositions = async (userId: number) => {
setCurrentUserId(userId)
try {
const { data } = await api.get(`/users/${userId}/positions`)
if (data?.code === 0) {
setUserPositions(data.data || [])
} else {
throw new Error(data?.message || '获取用户岗位失败')
}
} catch (e: any) {
message.error(e.message || '获取用户岗位失败')
setUserPositions([])
}
setPositionsOpen(true)
}
const savePositions = async () => {
if (!currentUserId) return
try {
const { data } = await api.put(`/users/${currentUserId}/positions`, { ids: userPositions })
if (data?.code === 0) {
message.success('岗位分配成功')
setPositionsOpen(false)
} else {
throw new Error(data?.message || '保存岗位失败')
}
} catch (e: any) {
message.error(e.message || '保存岗位失败')
}
}
const columns: ColumnsType<UserItem> = useMemo(() => [
{ title: 'ID', dataIndex: 'id', width: 80 },
{ title: '用户名', dataIndex: 'username' },
{ title: '昵称', dataIndex: 'nickname' },
{ title: '状态', dataIndex: 'status', render: (v: number) => v === 1 ? <Tag color="green"></Tag> : <Tag></Tag> },
{ title: '创建时间', dataIndex: 'created_at', render: (v: any) => formatDateTime(v) },
{ title: '操作', key: 'actions', width: 360, render: (_: any, r: UserItem) => (
<Space>
{has('system:user:update') && (
<a className="action-link" onClick={() => onEdit(r)}>
<EditOutlined />
<span></span>
</a>
)}
{has('system:user:reset') && (
<a className="action-link" onClick={() => onResetPwd(r)}>
<KeyOutlined />
<span></span>
</a>
)}
{has('system:user:assignPosition') && (
<a className="action-link" onClick={() => openPositions(r.id)}>
<UserOutlined />
<span></span>
</a>
)}
{has('system:user:delete') && (
<Popconfirm title={`确认删除用户「${r.username}」?`} onConfirm={async ()=>{ try{ const { data } = await api.delete(`/users/${r.id}`); if(data?.code===0){ message.success('删除成功'); fetchUsers(page, pageSize, keyword) } else { throw new Error(data?.message||'删除失败') } }catch(e:any){ message.error(e.message||'删除失败') } }}>
<a className="action-link action-danger">
<DeleteOutlined />
<span></span>
</a>
</Popconfirm>
)}
</Space>
)},
], [page, pageSize, keyword, has])
return (
<div>
<PageHeader items={["系统管理","用户管理"]} title="" />
<Form form={searchForm} layout="inline" onFinish={(vals: any)=>{ const kw = String(vals?.keyword ?? '').trim(); setKeyword(kw); fetchUsers(1, pageSize, kw) }} style={{ marginBottom: 12 }}>
<Form.Item name="keyword">
<Input allowClear placeholder="搜索用户名/昵称" style={{ width: 320 }} />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit"></Button>
<Button onClick={()=>{ searchForm.resetFields(); setKeyword(''); fetchUsers(1, pageSize, '') }}></Button>
{has('system:user:create') && (
<Button type="primary" onClick={onCreate}></Button>
)}
</Space>
</Form.Item>
</Form>
<Table<UserItem>
rowKey="id"
loading={loading}
dataSource={data}
columns={columns}
pagination={{ current: page, pageSize, total, showSizeChanger: true, onChange: (p: number, ps?: number) => fetchUsers(p, ps ?? pageSize, keyword) }}
/>
<Typography.Paragraph type="secondary" style={{ marginTop: 12 }}>
</Typography.Paragraph>
<Modal title="新增用户" open={createOpen} onOk={handleCreate} onCancel={() => setCreateOpen(false)} okText="创建" width={840}>
<Form form={form} layout="horizontal" labelCol={{ span: 2 }} wrapperCol={{ span: 22 }} labelAlign="right">
<Form.Item name="username" label="用户名" rules={[{ required: true }]}>
<Input placeholder="请输入用户名" />
</Form.Item>
<Form.Item name="password" label="密码" rules={[{ required: true }]}>
<Input.Password placeholder="请输入密码" />
</Form.Item>
<Form.Item name="nickname" label="昵称">
<Input placeholder="请输入昵称" />
</Form.Item>
<Form.Item name="status" label="状态" initialValue={1}>
<Select options={[{label:'启用', value:1},{label:'禁用', value:0}]} />
</Form.Item>
<Form.Item label="角色">
<Select
mode="multiple"
style={{ width: '100%' }}
placeholder="选择角色"
value={selectedRoleIds}
options={allRoles.map((r: { id: number; name: string }) => ({ label: r.name, value: r.id }))}
onChange={(vals: number[]) => setSelectedRoleIds(vals)}
/>
</Form.Item>
<Form.Item label="部门">
<TreeSelect
allowClear
multiple
treeCheckable
placeholder="选择部门(支持多选)"
style={{ width: '100%' }}
treeData={deptTreeData}
treeDefaultExpandAll
showSearch
filterTreeNode={(input: string, node: any) => String(node?.title ?? '').toLowerCase().includes(String(input).toLowerCase())}
value={selectedDeptIds}
onChange={(vals) => setSelectedDeptIds(Array.isArray(vals) ? (vals as (string | number)[]).map(v => Number(v)) : [])}
/>
</Form.Item>
{has('system:user:assignPosition') && (
<Form.Item label="岗位">
<Select
mode="multiple"
allowClear
placeholder="选择岗位"
value={createPositionIds}
options={positionOptions}
style={{ width: '100%' }}
onChange={setCreatePositionIds}
/>
</Form.Item>
)}
</Form>
</Modal>
<Modal title="编辑用户" open={editOpen} onOk={handleEdit} onCancel={() => setEditOpen(false)} okText="保存" width={840}>
<Form form={editForm} layout="horizontal" labelCol={{ span: 2 }} wrapperCol={{ span: 22 }} labelAlign="right">
<Form.Item name="nickname" label="昵称">
<Input placeholder="请输入昵称" />
</Form.Item>
<Form.Item name="status" label="状态">
<Select options={[{label:'启用', value:1},{label:'禁用', value:0}]} />
</Form.Item>
<Form.Item label="角色">
<Select
mode="multiple"
style={{ width: '100%' }}
placeholder="选择角色"
value={selectedRoleIds}
options={allRoles.map((r: { id: number; name: string }) => ({ label: r.name, value: r.id }))}
onChange={(vals: number[]) => setSelectedRoleIds(vals)}
/>
</Form.Item>
<Form.Item label="部门">
<TreeSelect
allowClear
multiple
treeCheckable
placeholder="选择部门(支持多选)"
style={{ width: '100%' }}
treeData={deptTreeData}
treeDefaultExpandAll
showSearch
filterTreeNode={(input: string, node: any) => String(node?.title ?? '').toLowerCase().includes(String(input).toLowerCase())}
value={selectedDeptIds}
onChange={(vals) => setSelectedDeptIds(Array.isArray(vals) ? (vals as (string | number)[]).map(v => Number(v)) : [])}
/>
</Form.Item>
{has('system:user:assignPosition') && (
<Form.Item label="岗位">
<Select
mode="multiple"
allowClear
placeholder="选择岗位"
value={editPositionIds}
options={positionOptions}
style={{ width: '100%' }}
onChange={setEditPositionIds}
/>
</Form.Item>
)}
</Form>
</Modal>
<Modal title={`重置密码${current ? `${current.username}` : ''}`} open={pwdOpen} onOk={handleResetPwd} onCancel={() => setPwdOpen(false)} okText="重置">
<Form form={pwdForm} layout="vertical">
<Form.Item name="password" label="新密码" rules={[{ required: true }]}>
<Input.Password placeholder="请输入新密码" />
</Form.Item>
</Form>
</Modal>
<Modal title="分配岗位" open={positionsOpen} onOk={savePositions} onCancel={() => setPositionsOpen(false)} okText="保存">
<Form layout="vertical">
<Form.Item label="选择岗位">
<Select
mode="multiple"
allowClear
value={userPositions}
onChange={setUserPositions}
options={positionOptions}
style={{ width: '100%' }}
placeholder="选择岗位"
/>
</Form.Item>
</Form>
</Modal>
</div>
)
}