diff --git a/apps/admin/src/pages/Users/index.tsx b/apps/admin/src/pages/Users/index.tsx index 4de83a2..ec9037e 100644 --- a/apps/admin/src/pages/Users/index.tsx +++ b/apps/admin/src/pages/Users/index.tsx @@ -10,8 +10,9 @@ import { Tag, Popconfirm, message, + TreeSelect, } from 'antd'; -import { PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined, IdcardOutlined, CloudDownloadOutlined } from '@ant-design/icons'; +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'; @@ -31,6 +32,25 @@ const UsersPage: React.FC = () => { 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); @@ -62,7 +82,7 @@ const UsersPage: React.FC = () => { setLoading(true); try { const [users, rolesRes] = await Promise.all([ - api.get('/rbac/users') as Promise, + api.get(`/rbac/users?isArchived=${showArchived}`) as Promise, api.get('/rbac/roles') as Promise, ]); setData(users); @@ -71,17 +91,22 @@ const UsersPage: React.FC = () => { console.error(e); } setLoading(false); - }, []); + }, [showArchived]); const handleSyncDingTalk = async () => { setSyncing(true); try { - const res: any = await api.post('/sync/trigger?platform=dingtalk'); + 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: any) { - message.error(e?.message || '同步失败'); + } catch (e: unknown) { + const err = e as { message?: string }; + message.error(err?.message || '同步失败'); } finally { setSyncing(false); } @@ -137,14 +162,36 @@ const UsersPage: React.FC = () => { 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: any) { - message.error(e.message || '删除失败'); + } 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 || '操作失败'); } }; @@ -161,8 +208,9 @@ const UsersPage: React.FC = () => { await api.put(`/rbac/users/${resetTarget.id}/password`, { password: values.password }); message.success('密码已重置'); setPwdModalOpen(false); - } catch (e: any) { - message.error(e.message || '操作失败'); + } catch (e: unknown) { + const err = e as { message?: string }; + message.error(err?.message || '操作失败'); } finally { setSaving(false); } @@ -207,9 +255,9 @@ const UsersPage: React.FC = () => { }, { title: '操作', - width: 280, + width: 320, fixed: 'right' as const, - render: (_: any, record: any) => ( + render: (_: unknown, record: any) => ( { > 重置密码 + {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)}> + handleDelete(record.id)}> }> 删除 @@ -261,24 +328,45 @@ const UsersPage: React.FC = () => { flexWrap: 'wrap', gap: 8, }} - >

账号管理

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