diff --git a/apps/admin/src/pages/IntegrationConfig/index.tsx b/apps/admin/src/pages/IntegrationConfig/index.tsx index e601eea..1bbd275 100644 --- a/apps/admin/src/pages/IntegrationConfig/index.tsx +++ b/apps/admin/src/pages/IntegrationConfig/index.tsx @@ -1,10 +1,13 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { Card, Form, Input, Button, Space, message, Spin, Switch, Alert, Descriptions, Tag, + Tabs, Drawer, Tree, Checkbox, Select, TreeSelect, } from 'antd'; import { SaveOutlined, ApiOutlined, CheckCircleOutlined, CloseCircleOutlined, + SyncOutlined, ReloadOutlined, } from '@ant-design/icons'; +import type { DataNode } from 'antd/es/tree'; import api from '../../api'; interface DingTalkConfig { @@ -14,6 +17,42 @@ interface DingTalkConfig { startEnable: boolean; } +interface DingOrgTreeNodeExt { + id: number; + name: string; + parentId: number; + children: DingOrgTreeNodeExt[]; + users: Array<{ userid: string; name: string; mobile: string }>; +} + +interface RoleItem { + id: number; + name: string; + status: number; +} + +interface OrgTreeNodeRaw { + id: number; + name: string; + children?: OrgTreeNodeRaw[]; +} + +interface OrgTreeResponse { + success: boolean; + data: OrgTreeNodeRaw[]; +} + +interface OrgTreeWithUsersResponse { + success: boolean; + data: DingOrgTreeNodeExt[]; +} + +interface ImportUsersResponse { + teacherCount: number; + studentCount: number; + skipped: number; +} + const IntegrationConfigPage: React.FC = () => { const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); @@ -22,6 +61,18 @@ const IntegrationConfigPage: React.FC = () => { const [verified, setVerified] = useState(null); const [form] = Form.useForm(); + // ── Sync Users Tab ── + const [syncRootDeptId, setSyncRootDeptId] = useState(undefined); + const [orgTree, setOrgTree] = useState([]); + const [drawerOpen, setDrawerOpen] = useState(false); + const [fetchingTree, setFetchingTree] = useState(false); + const [importing, setImporting] = useState(false); + const [teacherChecks, setTeacherChecks] = useState>({}); + const [teacherRoles, setTeacherRoles] = useState>({}); + const [roles, setRoles] = useState>([]); + const [defaultTeacherRoleId, setDefaultTeacherRoleId] = useState(null); + const [deptPickerTree, setDeptPickerTree] = useState }>>([]); + const fetchConfig = async () => { setLoading(true); try { @@ -41,6 +92,7 @@ const IntegrationConfigPage: React.FC = () => { useEffect(() => { fetchConfig(); + fetchRoles(); }, []); const handleSave = async () => { @@ -77,6 +129,283 @@ const IntegrationConfigPage: React.FC = () => { } }; + const fetchRoles = async () => { + try { + const res = await api.get('/rbac/roles'); + const activeRoles = res.filter((r) => r.status !== 0); + setRoles(activeRoles); + const teacherRole = activeRoles.find((r) => r.name === '班主任'); + setDefaultTeacherRoleId(teacherRole?.id || activeRoles[0]?.id || null); + } catch { + // ignore + } + }; + + const loadDeptTree = async () => { + try { + const res = await api.get('/sync/dingtalk/org-tree'); + if (res.success && res.data) { + const toTreeNode = (nodes: OrgTreeNodeRaw[]): Array<{ title: string; value: number; children?: Array<{ title: string; value: number; children?: unknown[] }> }> => + nodes.map((n) => ({ + title: n.name, + value: n.id, + children: n.children ? toTreeNode(n.children) : undefined, + })); + setDeptPickerTree(toTreeNode(res.data)); + } + } catch { + // ignore + } + }; + + const handleFetchOrgTree = async () => { + setFetchingTree(true); + try { + const params: Record = {}; + if (syncRootDeptId) params.rootDeptId = String(syncRootDeptId); + const res = await api.get('/sync/dingtalk/org-tree-with-users', { params }); + if (res.success && res.data) { + setOrgTree(res.data); + setTeacherChecks({}); + setTeacherRoles({}); + setDrawerOpen(true); + } else { + message.error('获取组织架构失败'); + } + } catch (e: unknown) { + const err = e as { message?: string }; + message.error(err?.message || '获取组织架构失败'); + } finally { + setFetchingTree(false); + } + }; + + const handleImportUsers = async () => { + setImporting(true); + try { + const allUsers: Array<{ + dingUserId: string; + name: string; + mobile: string; + }> = []; + + const flatten = (nodes: DingOrgTreeNodeExt[]) => { + for (const node of nodes) { + allUsers.push(...node.users); + flatten(node.children); + } + }; + flatten(orgTree); + + const payload = { + users: allUsers.map((u) => ({ + dingUserId: u.userid, + name: u.name, + mobile: u.mobile, + roleId: teacherChecks[u.userid] + ? (teacherRoles[u.userid] || defaultTeacherRoleId) + : null, + })), + }; + + const res = await api.post('/sync/dingtalk/import-users', payload); + message.success( + `导入完成:${res.teacherCount} 位老师,${res.studentCount} 位学生` + + (res.skipped > 0 ? `,${res.skipped} 已跳过` : ''), + ); + setDrawerOpen(false); + } catch (e: unknown) { + const err = e as { message?: string }; + message.error(err?.message || '导入失败'); + } finally { + setImporting(false); + } + }; + + const buildTreeData = (nodes: DingOrgTreeNodeExt[]): DataNode[] => { + return nodes.map((node) => ({ + title: node.name, + key: `dept-${node.id}`, + children: [ + ...buildTreeData(node.children), + ...node.users.map((u) => ({ + title: ( + + { + setTeacherChecks((prev) => ({ + ...prev, + [u.userid]: e.target.checked, + })); + if (!e.target.checked) { + setTeacherRoles((prev) => { + const next = { ...prev }; + delete next[u.userid]; + return next; + }); + } + }} + > + 老师 + + {u.name} + {u.mobile && ( + {u.mobile} + )} + {teacherChecks[u.userid] && ( + + + + + + + + + + + + + + + + + + ), + }, + ...syncTabItems, + ]; + return ( @@ -84,54 +413,7 @@ const IntegrationConfigPage: React.FC = () => { {verified === false && } color="error">未连接} }> - - {config && ( - - {config.corpId || '-'} - {config.agentId || '-'} - - - {config.startEnable ? '已启用' : '未启用'} - - - - )} - - - -
- - - - - - - - - - - - - - - - -
-
+ ); };