forked from wangziqi/gongxue-base
feat(admin): add class marking modal in DingTalk org import drawer
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState, useMemo } from 'react';
|
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Card, Form, Input, Button, Space, message, Spin, Switch, Alert, Descriptions, Tag,
|
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';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
SaveOutlined, ApiOutlined, CheckCircleOutlined, CloseCircleOutlined,
|
SaveOutlined, ApiOutlined, CheckCircleOutlined, CloseCircleOutlined,
|
||||||
@@ -22,7 +22,7 @@ interface DingOrgTreeNodeExt {
|
|||||||
name: string;
|
name: string;
|
||||||
parentId: number;
|
parentId: number;
|
||||||
children: DingOrgTreeNodeExt[];
|
children: DingOrgTreeNodeExt[];
|
||||||
users: Array<{ userid: string; name: string; mobile: string }>;
|
users: Array<{ userid: string; name: string; mobile: string; deptIds: number[] }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RoleItem {
|
interface RoleItem {
|
||||||
@@ -53,6 +53,17 @@ interface ImportUsersResponse {
|
|||||||
skipped: number;
|
skipped: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ClassMarkForm {
|
||||||
|
deptId: number;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
classType: string;
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
maxStudents?: number;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const UserTreeNode: React.FC<{
|
const UserTreeNode: React.FC<{
|
||||||
u: { userid: string; name: string; mobile: string };
|
u: { userid: string; name: string; mobile: string };
|
||||||
isTeacher: boolean;
|
isTeacher: boolean;
|
||||||
@@ -99,6 +110,10 @@ const IntegrationConfigPage: React.FC = () => {
|
|||||||
const [roles, setRoles] = useState<Array<{ id: number; name: string }>>([]);
|
const [roles, setRoles] = useState<Array<{ id: number; name: string }>>([]);
|
||||||
const [defaultTeacherRoleId, setDefaultTeacherRoleId] = useState<number | null>(null);
|
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 [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 () => {
|
const fetchConfig = async () => {
|
||||||
setLoading(true);
|
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 () => {
|
const handleImportUsers = async () => {
|
||||||
setImporting(true);
|
setImporting(true);
|
||||||
try {
|
try {
|
||||||
@@ -213,24 +259,34 @@ const IntegrationConfigPage: React.FC = () => {
|
|||||||
dingUserId: string;
|
dingUserId: string;
|
||||||
name: string;
|
name: string;
|
||||||
mobile: string;
|
mobile: string;
|
||||||
|
deptIds: number[];
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
const flatten = (nodes: DingOrgTreeNodeExt[]) => {
|
const flatten = (nodes: DingOrgTreeNodeExt[]) => {
|
||||||
for (const node of nodes) {
|
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(node.children);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
flatten(orgTree);
|
flatten(orgTree);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
|
classes: Object.values(classMarks),
|
||||||
users: allUsers.map((u) => ({
|
users: allUsers.map((u) => ({
|
||||||
dingUserId: u.userid,
|
dingUserId: u.dingUserId,
|
||||||
name: u.name,
|
name: u.name,
|
||||||
mobile: u.mobile,
|
mobile: u.mobile,
|
||||||
roleId: teacherChecks[u.userid]
|
roleId: teacherChecks[u.dingUserId]
|
||||||
? (teacherRoles[u.userid] || defaultTeacherRoleId)
|
? (teacherRoles[u.dingUserId] || defaultTeacherRoleId)
|
||||||
: null,
|
: 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) => ({
|
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}`,
|
key: `dept-${node.id}`,
|
||||||
children: [
|
children: [
|
||||||
...buildTreeData(node.children),
|
...buildTreeData(node.children),
|
||||||
@@ -258,7 +336,7 @@ const IntegrationConfigPage: React.FC = () => {
|
|||||||
title: (
|
title: (
|
||||||
<UserTreeNode
|
<UserTreeNode
|
||||||
key={u.userid}
|
key={u.userid}
|
||||||
u={u}
|
u={{ userid: u.userid, name: u.name, mobile: u.mobile }}
|
||||||
isTeacher={!!teacherChecks[u.userid]}
|
isTeacher={!!teacherChecks[u.userid]}
|
||||||
onToggle={() => {
|
onToggle={() => {
|
||||||
setTeacherChecks((prev) => ({
|
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
|
const syncTabItems = config
|
||||||
? [
|
? [
|
||||||
@@ -323,15 +401,55 @@ const IntegrationConfigPage: React.FC = () => {
|
|||||||
获取组织架构
|
获取组织架构
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</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 && (
|
{drawerOpen && (
|
||||||
<Drawer
|
<Drawer
|
||||||
title="钉钉组织架构 — 勾选老师"
|
title="钉钉组织架构 — 勾选老师"
|
||||||
open={drawerOpen}
|
open={drawerOpen}
|
||||||
onClose={() => setDrawerOpen(false)}
|
onClose={() => { setDrawerOpen(false); setClassMarks({}); }}
|
||||||
width={520}
|
width={520}
|
||||||
footer={
|
footer={
|
||||||
<Space>
|
<Space>
|
||||||
<Button onClick={() => setDrawerOpen(false)}>取消</Button>
|
<Button onClick={() => { setDrawerOpen(false); setClassMarks({}); }}>取消</Button>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined />}
|
||||||
|
|||||||
Reference in New Issue
Block a user