feat: add mark-staff/restore-student buttons in user management
This commit is contained in:
@@ -10,8 +10,9 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
message,
|
message,
|
||||||
|
TreeSelect,
|
||||||
} from 'antd';
|
} 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 dayjs from 'dayjs';
|
||||||
import api from '../../api';
|
import api from '../../api';
|
||||||
import PermissionButton from '../../components/PermissionButton';
|
import PermissionButton from '../../components/PermissionButton';
|
||||||
@@ -31,6 +32,25 @@ const UsersPage: React.FC = () => {
|
|||||||
const [pwdForm] = Form.useForm();
|
const [pwdForm] = Form.useForm();
|
||||||
const [syncing, setSyncing] = useState(false);
|
const [syncing, setSyncing] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [showArchived, setShowArchived] = useState(false);
|
||||||
|
// 组织树同步
|
||||||
|
const [syncDeptId, setSyncDeptId] = useState<number | undefined>(undefined);
|
||||||
|
const [orgTree, setOrgTree] = useState<Array<{ title: string; value: number; children?: Array<{ title: string; value: number; children?: unknown[] }> }>>([]);
|
||||||
|
|
||||||
|
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) => {
|
const handleOpenProfile = async (record: any) => {
|
||||||
setProfileUser(record);
|
setProfileUser(record);
|
||||||
@@ -62,7 +82,7 @@ const UsersPage: React.FC = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const [users, rolesRes] = await Promise.all([
|
const [users, rolesRes] = await Promise.all([
|
||||||
api.get('/rbac/users') as Promise<any[]>,
|
api.get(`/rbac/users?isArchived=${showArchived}`) as Promise<any[]>,
|
||||||
api.get('/rbac/roles') as Promise<any[]>,
|
api.get('/rbac/roles') as Promise<any[]>,
|
||||||
]);
|
]);
|
||||||
setData(users);
|
setData(users);
|
||||||
@@ -71,17 +91,22 @@ const UsersPage: React.FC = () => {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, []);
|
}, [showArchived]);
|
||||||
|
|
||||||
const handleSyncDingTalk = async () => {
|
const handleSyncDingTalk = async () => {
|
||||||
setSyncing(true);
|
setSyncing(true);
|
||||||
try {
|
try {
|
||||||
const res: any = await api.post('/sync/trigger?platform=dingtalk');
|
const params: Record<string, string> = {};
|
||||||
|
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];
|
const log = res.logs?.[0];
|
||||||
message.success(`同步完成:${log?.recordsCount || 0} 条记录`);
|
message.success(`同步完成:${log?.recordsCount || 0} 条记录`);
|
||||||
fetchData();
|
fetchData();
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
message.error(e?.message || '同步失败');
|
const err = e as { message?: string };
|
||||||
|
message.error(err?.message || '同步失败');
|
||||||
} finally {
|
} finally {
|
||||||
setSyncing(false);
|
setSyncing(false);
|
||||||
}
|
}
|
||||||
@@ -137,14 +162,36 @@ const UsersPage: React.FC = () => {
|
|||||||
setSaving(false);
|
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) => {
|
const handleDelete = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
await api.delete(`/rbac/users/${id}`);
|
await api.delete(`/rbac/users/${id}`);
|
||||||
message.success('已删除');
|
message.success('已删除');
|
||||||
fetchData();
|
fetchData();
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
message.error(e.message || '删除失败');
|
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 });
|
await api.put(`/rbac/users/${resetTarget.id}/password`, { password: values.password });
|
||||||
message.success('密码已重置');
|
message.success('密码已重置');
|
||||||
setPwdModalOpen(false);
|
setPwdModalOpen(false);
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
message.error(e.message || '操作失败');
|
const err = e as { message?: string };
|
||||||
|
message.error(err?.message || '操作失败');
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -207,9 +255,9 @@ const UsersPage: React.FC = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
width: 280,
|
width: 320,
|
||||||
fixed: 'right' as const,
|
fixed: 'right' as const,
|
||||||
render: (_: any, record: any) => (
|
render: (_: unknown, record: any) => (
|
||||||
<Space>
|
<Space>
|
||||||
<PermissionButton
|
<PermissionButton
|
||||||
permission="user:edit"
|
permission="user:edit"
|
||||||
@@ -238,8 +286,27 @@ const UsersPage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
重置密码
|
重置密码
|
||||||
</PermissionButton>
|
</PermissionButton>
|
||||||
|
{record.isArchived ? (
|
||||||
|
<Popconfirm title="确认恢复?" onConfirm={() => handleArchive(record.id, false)}>
|
||||||
|
<PermissionButton permission="user:edit" type="link" size="small">恢复</PermissionButton>
|
||||||
|
</Popconfirm>
|
||||||
|
) : (
|
||||||
|
<Popconfirm title="归档后可恢复,确认归档?" onConfirm={() => handleArchive(record.id, true)}>
|
||||||
|
<PermissionButton permission="user:edit" type="link" size="small">归档</PermissionButton>
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
{record.studentStatus === 'active' && (
|
||||||
|
<Popconfirm title="确认标记为教职工?" onConfirm={() => handleMarkStaff(record.id, true)}>
|
||||||
|
<PermissionButton permission="user:edit" type="link" size="small">标记教职工</PermissionButton>
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
{record.studentStatus === 'staff' && (
|
||||||
|
<Popconfirm title="确认恢复为学员?" onConfirm={() => handleMarkStaff(record.id, false)}>
|
||||||
|
<PermissionButton permission="user:edit" type="link" size="small">恢复学员</PermissionButton>
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
{record.username !== 'admin' && (
|
{record.username !== 'admin' && (
|
||||||
<Popconfirm title="确认删除该用户?" onConfirm={() => handleDelete(record.id)}>
|
<Popconfirm title="确认删除?需先归档" onConfirm={() => handleDelete(record.id)}>
|
||||||
<PermissionButton permission="user:delete" type="link" size="small" danger icon={<DeleteOutlined />}>
|
<PermissionButton permission="user:delete" type="link" size="small" danger icon={<DeleteOutlined />}>
|
||||||
删除
|
删除
|
||||||
</PermissionButton>
|
</PermissionButton>
|
||||||
@@ -261,24 +328,45 @@ const UsersPage: React.FC = () => {
|
|||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
gap: 8,
|
gap: 8,
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<h2 style={{ margin: 0 }}>账号管理</h2>
|
<h2 style={{ margin: 0 }}>账号管理</h2>
|
||||||
<PermissionButton
|
<Space wrap>
|
||||||
permission="user:create"
|
<TreeSelect
|
||||||
type="primary"
|
treeData={orgTree}
|
||||||
icon={<PlusOutlined />}
|
value={syncDeptId}
|
||||||
onClick={handleAdd}
|
onChange={(v) => setSyncDeptId(v)}
|
||||||
>
|
placeholder="选择同步部门(不选=全部)"
|
||||||
新增账号
|
allowClear
|
||||||
</PermissionButton>
|
treeDefaultExpandAll
|
||||||
<PermissionButton
|
style={{ minWidth: 260 }}
|
||||||
permission="sync:trigger"
|
onDropdownVisibleChange={(open) => { if (open) loadOrgTree(); }}
|
||||||
icon={<CloudDownloadOutlined />}
|
/>
|
||||||
loading={syncing}
|
<PermissionButton
|
||||||
onClick={handleSyncDingTalk}
|
permission="user:create"
|
||||||
>
|
type="primary"
|
||||||
同步钉钉用户
|
icon={<PlusOutlined />}
|
||||||
</PermissionButton>
|
onClick={handleAdd}
|
||||||
|
>
|
||||||
|
新增账号
|
||||||
|
</PermissionButton>
|
||||||
|
<PermissionButton
|
||||||
|
permission="sync:trigger"
|
||||||
|
icon={<CloudDownloadOutlined />}
|
||||||
|
loading={syncing}
|
||||||
|
onClick={handleSyncDingTalk}
|
||||||
|
>
|
||||||
|
同步钉钉用户
|
||||||
|
</PermissionButton>
|
||||||
|
<span style={{ marginLeft: 8 }}>
|
||||||
|
<InboxOutlined style={{ marginRight: 4 }} />
|
||||||
|
归档
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
style={{ marginLeft: 4 }}
|
||||||
|
checked={showArchived}
|
||||||
|
onChange={setShowArchived}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
Reference in New Issue
Block a user