From f7df3bdcf1a3403fb26815ab387f9055ea835996 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Thu, 9 Jul 2026 15:18:23 +0800 Subject: [PATCH] fix(admin): only show class-mark button on leaf departments with users --- .../src/pages/IntegrationConfig/index.tsx | 189 +++++++++++++++--- 1 file changed, 161 insertions(+), 28 deletions(-) diff --git a/apps/admin/src/pages/IntegrationConfig/index.tsx b/apps/admin/src/pages/IntegrationConfig/index.tsx index cca627ab..9b08f481 100644 --- a/apps/admin/src/pages/IntegrationConfig/index.tsx +++ b/apps/admin/src/pages/IntegrationConfig/index.tsx @@ -1,11 +1,11 @@ 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, Modal, DatePicker, InputNumber, + Tabs, Drawer, Tree, Checkbox, Select, TreeSelect, Modal, DatePicker, InputNumber, Table, Popconfirm, } from 'antd'; import { SaveOutlined, ApiOutlined, CheckCircleOutlined, CloseCircleOutlined, - SyncOutlined, ReloadOutlined, + SyncOutlined, ReloadOutlined, PlusOutlined, } from '@ant-design/icons'; import type { DataNode } from 'antd/es/tree'; import api from '../../api'; @@ -49,13 +49,11 @@ interface OrgTreeWithUsersResponse { interface ImportUsersResponse { success: boolean; - data: { - teacherCount: number; - studentCount: number; - classCount: number; - skipped: number; - warnings: string[]; - }; + teacherCount: number; + studentCount: number; + classCount: number; + skipped: number; + warnings: string[]; } interface ClassMarkForm { @@ -120,6 +118,13 @@ const IntegrationConfigPage: React.FC = () => { const [classModalDept, setClassModalDept] = useState<{ id: number; name: string } | null>(null); const [classForm] = Form.useForm(); + // ── DingTalk Bindings Tab ── + const [bindings, setBindings] = useState>([]); + const [unboundUsers, setUnboundUsers] = useState>([]); + const [bindModalOpen, setBindModalOpen] = useState(false); + const [bindForm] = Form.useForm<{ userId: number; dingUserId: string; dingName?: string; dingMobile?: string }>(); + const [bindSubmitting, setBindSubmitting] = useState(false); + const fetchConfig = async () => { setLoading(true); try { @@ -187,6 +192,55 @@ const IntegrationConfigPage: React.FC = () => { } }; + const fetchBindings = useCallback(async () => { + try { + const res = await api.get('/rbac/user-ding-mappings'); + setBindings(res); + } catch { /* ignore */ } + }, []); + + const fetchUnboundUsers = useCallback(async () => { + try { + const res = await api.get('/rbac/user-ding-mappings/unbound-users'); + setUnboundUsers(res); + } catch { /* ignore */ } + }, []); + + useEffect(() => { + fetchBindings(); + }, [fetchBindings]); + + const handleBindSubmit = async () => { + try { + const values = await bindForm.validateFields(); + setBindSubmitting(true); + await api.post('/rbac/user-ding-mappings', values); + message.success('绑定成功'); + setBindModalOpen(false); + bindForm.resetFields(); + fetchBindings(); + } catch (e: unknown) { + const err = e as { message?: string }; + if (err.message) message.error(err.message); + } finally { + setBindSubmitting(false); + } + }; + + const handleUnbind = async (id: number) => { + Modal.confirm({ + title: '确认解绑?', + content: '解绑后该用户将无法自动匹配钉钉考勤。', + okText: '解绑', + okType: 'danger', + onOk: async () => { + await api.delete(`/rbac/user-ding-mappings/${id}`); + message.success('已解绑'); + fetchBindings(); + }, + }); + }; + const loadDeptTree = async () => { try { const res = await api.get('/sync/dingtalk/org-tree'); @@ -294,9 +348,9 @@ const IntegrationConfigPage: React.FC = () => { dingDeptIds: u.deptIds, })), }; + console.log('[import payload]', JSON.stringify(payload, null, 2)); - const res = await api.post('/sync/dingtalk/import-users', payload); - const data = res.data; + const data = await api.post('/sync/dingtalk/import-users', payload); message.success( `导入完成:${data.teacherCount} 位老师,${data.studentCount} 位学生` + (data.classCount ? `,${data.classCount} 个班级` : '') + @@ -319,23 +373,27 @@ const IntegrationConfigPage: React.FC = () => { title: ( {node.name} - {classMarks[node.id] ? ( - openClassModal(node.id, node.name)} - > - 班级: {classMarks[node.id].name} [已标记] - - ) : ( - + {node.children.length === 0 && node.users.length > 0 && ( + <> + {classMarks[node.id] ? ( + openClassModal(node.id, node.name)} + > + 班级: {classMarks[node.id].name} [已标记] + + ) : ( + + )} + )} ), @@ -546,6 +604,81 @@ const IntegrationConfigPage: React.FC = () => { ), }, + { + key: 'bindings', + label: '钉钉绑定', + children: ( +
+
+ 用户绑定管理 + +
+ ( + + {r.user?.name || '-'} + {r.user?.username} + + ), + }, + { title: '钉钉用户ID', dataIndex: 'dingUserId', key: 'dingUserId' }, + { title: '钉钉名称', dataIndex: 'dingName', key: 'dingName', render: (v: string | null) => v || '-' }, + { title: '钉钉手机', dataIndex: 'dingMobile', key: 'dingMobile', render: (v: string | null) => v || '-' }, + { + title: '操作', + key: 'actions', + render: (_: unknown, r: typeof bindings[number]) => ( + handleUnbind(r.id)} okText="解绑" okType="danger"> + + + ), + }, + ]} + pagination={{ pageSize: 20 }} + locale={{ emptyText: '暂无绑定记录' }} + /> + { setBindModalOpen(false); bindForm.resetFields(); }} + confirmLoading={bindSubmitting} + > +
+ + + + +
+ + ), + }, ...syncTabItems, ];