import React, { useEffect, useState, useMemo, useCallback } from 'react'; import { Table, Modal, Form, Input, Select, Switch, Space, Tag, Popconfirm, message, TreeSelect, } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined, IdcardOutlined, CloudDownloadOutlined, InboxOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import api from '../../api'; import PermissionButton from '../../components/PermissionButton'; const UsersPage: React.FC = () => { const [data, setData] = useState([]); const [roles, setRoles] = useState([]); const [loading, setLoading] = useState(false); const [modalOpen, setModalOpen] = useState(false); const [pwdModalOpen, setPwdModalOpen] = useState(false); const [editing, setEditing] = useState(null); const [resetTarget, setResetTarget] = useState(null); const [profileModalOpen, setProfileModalOpen] = useState(false); const [profileUser, setProfileUser] = useState(null); const [profileForm] = Form.useForm(); const [form] = Form.useForm(); const [pwdForm] = Form.useForm(); const [syncing, setSyncing] = useState(false); const [saving, setSaving] = useState(false); const [showArchived, setShowArchived] = useState(false); // 组织树同步 const [syncDeptId, setSyncDeptId] = useState(undefined); const [orgTree, setOrgTree] = useState }>>([]); const loadOrgTree = async () => { try { const res = await api.get<{ success: boolean; data: Array<{ id: number; name: string; children: Array<{ id: number; name: string; children: unknown[] }> }> }>('/sync/dingtalk/org-tree'); const toTreeNode = (nodes: Array<{ id: number; name: string; children: unknown[] }>): Array<{ title: string; value: number; children?: unknown[] }> => nodes.map((n) => ({ title: n.name, value: n.id, children: n.children?.length > 0 ? toTreeNode(n.children as Array<{ id: number; name: string; children: unknown[] }>) : undefined, })); setOrgTree(toTreeNode(res.data)); } catch { // org tree may not be available } }; const handleOpenProfile = async (record: any) => { setProfileUser(record); try { const res: any = await api.get(`/rbac/users/${record.id}/profile`); profileForm.setFieldsValue(res); } catch { profileForm.setFieldsValue({}); } setProfileModalOpen(true); }; const handleProfileSubmit = async () => { setSaving(true); const values = await profileForm.validateFields(); try { await api.put(`/rbac/users/${profileUser.id}/profile`, values); message.success('档案更新成功'); setProfileModalOpen(false); fetchData(); } catch (e: any) { message.error(e.message || '操作失败'); } finally { setSaving(false); } }; const fetchData = useCallback(async () => { setLoading(true); try { const [users, rolesRes] = await Promise.all([ api.get(`/rbac/users?isArchived=${showArchived}`) as Promise, api.get('/rbac/roles') as Promise, ]); setData(users); setRoles(rolesRes); } catch (e) { console.error(e); } setLoading(false); }, [showArchived]); const handleSyncDingTalk = async () => { setSyncing(true); try { const params: Record = {}; if (syncDeptId) params.rootDeptId = String(syncDeptId); const res = await api.post('/sync/trigger?platform=dingtalk', undefined as unknown as null, { params }) as { synced: number; logs: Array<{ recordsCount?: number }>; }; const log = res.logs?.[0]; message.success(`同步完成:${log?.recordsCount || 0} 条记录`); fetchData(); } catch (e: unknown) { const err = e as { message?: string }; message.error(err?.message || '同步失败'); } finally { setSyncing(false); } }; useEffect(() => { fetchData(); }, [fetchData]); const handleAdd = () => { setEditing(null); form.resetFields(); setModalOpen(true); }; const handleEdit = (record: any) => { setEditing(record); form.setFieldsValue({ username: record.username, name: record.name, isActive: record.isActive, roleIds: record.roles?.map((r: any) => r.id) || [], }); setModalOpen(true); }; const handleSubmit = async () => { setSaving(true); const values = await form.validateFields(); try { if (editing) { await api.put(`/rbac/users/${editing.id}`, { username: values.username, name: values.name, isActive: values.isActive, roleIds: values.roleIds || [], }); message.success('更新成功'); } else { await api.post('/rbac/users', { username: values.username, password: values.password, name: values.name, roleIds: values.roleIds || [], }); message.success('创建成功'); } setModalOpen(false); fetchData(); } catch (e: any) { message.error(e.message || '操作失败'); } finally { setSaving(false); } }; const handleArchive = async (id: number, archive: boolean) => { try { await api.put(`/rbac/users/${id}/${archive ? 'archive' : 'restore'}`); message.success(archive ? '已归档' : '已恢复'); fetchData(); } catch (e: unknown) { const err = e as { message?: string }; message.error(err?.message || '操作失败'); } }; const handleDelete = async (id: number) => { try { await api.delete(`/rbac/users/${id}`); message.success('已删除'); fetchData(); } catch (e: unknown) { const err = e as { message?: string }; message.error(err?.message || '删除失败'); } }; const handleMarkStaff = async (id: number, toStaff: boolean) => { try { await api.put(`/rbac/users/${id}/${toStaff ? 'mark-staff' : 'mark-student'}`); message.success(toStaff ? '已标记为教职工' : '已恢复为学员'); fetchData(); } catch (e: unknown) { const err = e as { message?: string }; message.error(err?.message || '操作失败'); } }; const handleResetPwd = (record: any) => { setResetTarget(record); pwdForm.resetFields(); setPwdModalOpen(true); }; const handlePwdSubmit = async () => { setSaving(true); const values = await pwdForm.validateFields(); try { await api.put(`/rbac/users/${resetTarget.id}/password`, { password: values.password }); message.success('密码已重置'); setPwdModalOpen(false); } catch (e: unknown) { const err = e as { message?: string }; message.error(err?.message || '操作失败'); } finally { setSaving(false); } }; const columns = useMemo(() => [ { title: 'ID', dataIndex: 'id', width: 60 }, { title: '用户名', dataIndex: 'username', width: 120 }, { title: '姓名', dataIndex: 'name', width: 120 }, { title: '角色', dataIndex: 'roles', width: 200, render: (v: any[]) => v && v.length > 0 ? ( v.map((r: any) => ( {r.name} )) ) : ( 无角色 ), }, { title: '状态', dataIndex: 'isActive', width: 80, render: (v: boolean) => {v ? '启用' : '禁用'}, }, { title: '最后登录', dataIndex: 'lastLoginAt', width: 170, render: (v: string) => (v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : '-'), }, { title: '创建时间', dataIndex: 'createdAt', width: 170, render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm:ss'), }, { title: '操作', width: 320, fixed: 'right' as const, render: (_: unknown, record: any) => ( } onClick={() => handleOpenProfile(record)} > 档案 } onClick={() => handleEdit(record)} > 编辑 } onClick={() => handleResetPwd(record)} > 重置密码 {record.isArchived ? ( handleArchive(record.id, false)}> 恢复 ) : ( handleArchive(record.id, true)}> 归档 )} {record.studentStatus === 'active' && ( handleMarkStaff(record.id, true)}> 标记教职工 )} {record.studentStatus === 'staff' && ( handleMarkStaff(record.id, false)}> 恢复学员 )} {record.username !== 'admin' && ( handleDelete(record.id)}> }> 删除 )} ), }, ], []); return (

账号管理

setSyncDeptId(v)} placeholder="选择同步部门(不选=全部)" allowClear treeDefaultExpandAll style={{ minWidth: 260 }} onDropdownVisibleChange={(open) => { if (open) loadOrgTree(); }} /> } onClick={handleAdd} > 新增账号 } loading={syncing} onClick={handleSyncDingTalk} > 同步钉钉用户 归档
setModalOpen(false)} destroyOnHidden confirmLoading={saving} >
{!editing && ( )} {editing && ( )}