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 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 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() 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 = useMemo(() => [ { title: 'ID', dataIndex: 'id', width: 80 }, { title: '用户名', dataIndex: 'username' }, { title: '昵称', dataIndex: 'nickname' }, { title: '状态', dataIndex: 'status', render: (v: number) => v === 1 ? 启用 : 禁用 }, { title: '创建时间', dataIndex: 'created_at', render: (v: any) => formatDateTime(v) }, { title: '操作', key: 'actions', width: 360, render: (_: any, r: UserItem) => ( {has('system:user:update') && ( onEdit(r)}> 编辑 )} {has('system:user:reset') && ( onResetPwd(r)}> 重置密码 )} {has('system:user:assignPosition') && ( openPositions(r.id)}> 分配岗位 )} {has('system:user:delete') && ( { 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||'删除失败') } }}> 删除 )} )}, ], [page, pageSize, keyword, has]) return (
{ const kw = String(vals?.keyword ?? '').trim(); setKeyword(kw); fetchUsers(1, pageSize, kw) }} style={{ marginBottom: 12 }}> {has('system:user:create') && ( )}
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) }} /> 提示:此页面已支持分页、创建、编辑与重置密码。 setCreateOpen(false)} okText="创建" width={840}>
({ label: r.name, value: r.id }))} onChange={(vals: number[]) => setSelectedRoleIds(vals)} /> String(node?.title ?? '').toLowerCase().includes(String(input).toLowerCase())} value={selectedDeptIds} onChange={(vals) => setSelectedDeptIds(Array.isArray(vals) ? (vals as (string | number)[]).map(v => Number(v)) : [])} /> {has('system:user:assignPosition') && ( ({ label: r.name, value: r.id }))} onChange={(vals: number[]) => setSelectedRoleIds(vals)} /> String(node?.title ?? '').toLowerCase().includes(String(input).toLowerCase())} value={selectedDeptIds} onChange={(vals) => setSelectedDeptIds(Array.isArray(vals) ? (vals as (string | number)[]).map(v => Number(v)) : [])} /> {has('system:user:assignPosition') && (
) }