feat(admin): add class marking modal in DingTalk org import drawer

This commit is contained in:
2026-07-09 12:41:29 +08:00
parent 0f3fec6b44
commit 7f42c12392

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState, useMemo } from 'react';
import React, { useEffect, useState, useMemo, useCallback } from 'react';
import {
Card, Form, Input, Button, Space, message, Spin, Switch, Alert, Descriptions, Tag,
Tabs, Drawer, Tree, Checkbox, Select, TreeSelect,
Tabs, Drawer, Tree, Checkbox, Select, TreeSelect, Modal, DatePicker, InputNumber,
} from 'antd';
import {
SaveOutlined, ApiOutlined, CheckCircleOutlined, CloseCircleOutlined,
@@ -22,7 +22,7 @@ interface DingOrgTreeNodeExt {
name: string;
parentId: number;
children: DingOrgTreeNodeExt[];
users: Array<{ userid: string; name: string; mobile: string }>;
users: Array<{ userid: string; name: string; mobile: string; deptIds: number[] }>;
}
interface RoleItem {
@@ -53,6 +53,17 @@ interface ImportUsersResponse {
skipped: number;
}
interface ClassMarkForm {
deptId: number;
name: string;
code: string;
classType: string;
startDate?: string;
endDate?: string;
maxStudents?: number;
notes?: string;
}
const UserTreeNode: React.FC<{
u: { userid: string; name: string; mobile: string };
isTeacher: boolean;
@@ -99,6 +110,10 @@ const IntegrationConfigPage: React.FC = () => {
const [roles, setRoles] = useState<Array<{ id: number; name: string }>>([]);
const [defaultTeacherRoleId, setDefaultTeacherRoleId] = useState<number | null>(null);
const [deptPickerTree, setDeptPickerTree] = useState<Array<{ title: string; value: number; children?: Array<{ title: string; value: number; children?: unknown[] }> }>>([]);
const [classMarks, setClassMarks] = useState<Record<number, ClassMarkForm>>({});
const [classModalOpen, setClassModalOpen] = useState(false);
const [classModalDept, setClassModalDept] = useState<{ id: number; name: string } | null>(null);
const [classForm] = Form.useForm<ClassMarkForm>();
const fetchConfig = async () => {
setLoading(true);
@@ -206,6 +221,37 @@ const IntegrationConfigPage: React.FC = () => {
}
};
const openClassModal = (deptId: number, deptName: string) => {
const existing = classMarks[deptId];
if (existing) {
classForm.setFieldsValue(existing);
} else {
classForm.setFieldsValue({
deptId,
name: deptName,
code: '',
classType: 'culture',
});
}
setClassModalDept({ id: deptId, name: deptName });
setClassModalOpen(true);
};
const handleClassModalOk = async () => {
const values = await classForm.validateFields();
setClassMarks((prev) => ({
...prev,
[values.deptId]: values,
}));
setClassModalOpen(false);
setClassModalDept(null);
};
const handleClassModalCancel = () => {
setClassModalOpen(false);
setClassModalDept(null);
};
const handleImportUsers = async () => {
setImporting(true);
try {
@@ -213,24 +259,34 @@ const IntegrationConfigPage: React.FC = () => {
dingUserId: string;
name: string;
mobile: string;
deptIds: number[];
}> = [];
const flatten = (nodes: DingOrgTreeNodeExt[]) => {
for (const node of nodes) {
allUsers.push(...node.users);
allUsers.push(
...node.users.map((u) => ({
dingUserId: u.userid,
name: u.name,
mobile: u.mobile,
deptIds: u.deptIds || [],
})),
);
flatten(node.children);
}
};
flatten(orgTree);
const payload = {
classes: Object.values(classMarks),
users: allUsers.map((u) => ({
dingUserId: u.userid,
dingUserId: u.dingUserId,
name: u.name,
mobile: u.mobile,
roleId: teacherChecks[u.userid]
? (teacherRoles[u.userid] || defaultTeacherRoleId)
roleId: teacherChecks[u.dingUserId]
? (teacherRoles[u.dingUserId] || defaultTeacherRoleId)
: null,
dingDeptIds: u.deptIds,
})),
};
@@ -248,9 +304,31 @@ const IntegrationConfigPage: React.FC = () => {
}
};
const buildTreeData = (nodes: DingOrgTreeNodeExt[]): DataNode[] => {
const buildTreeData = useCallback((nodes: DingOrgTreeNodeExt[]): DataNode[] => {
return nodes.map((node) => ({
title: node.name,
title: (
<Space size="small">
<span>{node.name}</span>
{classMarks[node.id] ? (
<Tag
color="blue"
style={{ cursor: 'pointer' }}
onClick={() => openClassModal(node.id, node.name)}
>
: {classMarks[node.id].name} []
</Tag>
) : (
<Button
size="small"
type="link"
icon={<span>🏫</span>}
onClick={() => openClassModal(node.id, node.name)}
>
</Button>
)}
</Space>
),
key: `dept-${node.id}`,
children: [
...buildTreeData(node.children),
@@ -258,7 +336,7 @@ const IntegrationConfigPage: React.FC = () => {
title: (
<UserTreeNode
key={u.userid}
u={u}
u={{ userid: u.userid, name: u.name, mobile: u.mobile }}
isTeacher={!!teacherChecks[u.userid]}
onToggle={() => {
setTeacherChecks((prev) => ({
@@ -286,9 +364,9 @@ const IntegrationConfigPage: React.FC = () => {
})),
],
}));
};
}, [classMarks, teacherChecks, teacherRoles, defaultTeacherRoleId, roles]);
const treeData = useMemo(() => buildTreeData(orgTree), [orgTree, teacherChecks, teacherRoles, defaultTeacherRoleId, roles]);
const treeData = useMemo(() => buildTreeData(orgTree), [orgTree, buildTreeData]);
const syncTabItems = config
? [
@@ -323,15 +401,55 @@ const IntegrationConfigPage: React.FC = () => {
</Button>
</Space>
<Modal
title={classMarks[classModalDept?.id ?? -1] ? '修改班级信息' : '标记为班级'}
open={classModalOpen}
onOk={handleClassModalOk}
onCancel={handleClassModalCancel}
destroyOnClose
>
<Form form={classForm} layout="vertical">
<Form.Item name="deptId" hidden><Input /></Form.Item>
<Form.Item name="name" label="班级名称" rules={[{ required: true, message: '请输入班级名称' }]}>
<Input />
</Form.Item>
<Form.Item name="code" label="班级编码" rules={[{ required: true, message: '请输入班级编码' }]}>
<Input placeholder="如 CS2024-01" />
</Form.Item>
<Form.Item name="classType" label="班型" rules={[{ required: true }]}>
<Select
options={[
{ value: 'culture', label: '文化课' },
{ value: 'professional', label: '专业课' },
{ value: 'bootcamp', label: '集训营' },
{ value: 'sprint', label: '冲刺班' },
]}
/>
</Form.Item>
<Form.Item name="startDate" label="开班日期">
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="endDate" label="结束日期">
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="maxStudents" label="最大人数">
<InputNumber min={0} style={{ width: '100%' }} placeholder="0 表示不限制" />
</Form.Item>
<Form.Item name="notes" label="备注">
<Input.TextArea rows={2} />
</Form.Item>
</Form>
</Modal>
{drawerOpen && (
<Drawer
title="钉钉组织架构 — 勾选老师"
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
onClose={() => { setDrawerOpen(false); setClassMarks({}); }}
width={520}
footer={
<Space>
<Button onClick={() => setDrawerOpen(false)}></Button>
<Button onClick={() => { setDrawerOpen(false); setClassMarks({}); }}></Button>
<Button
type="primary"
icon={<ReloadOutlined />}