feat: add DingTalk integration config page

This commit is contained in:
2026-07-09 10:14:51 +08:00
parent 7df7695073
commit e8dec3b171
3 changed files with 151 additions and 0 deletions

View File

@@ -29,6 +29,7 @@ import AttendancePage from './pages/Attendance';
import TeacherWorkspacePage from './pages/TeacherWorkspace';
import NotificationsPage from './pages/Notifications';
import DepartmentsPage from './pages/Departments';
import IntegrationConfigPage from './pages/IntegrationConfig';
import PermissionRoute from './components/PermissionRoute';
const PrivateRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
@@ -262,6 +263,15 @@ const App: React.FC = () => {
</PermissionRoute>
}
/>
<Route
path="integration-config"
element={
<PermissionRoute permission="integration:read">
<IntegrationConfigPage />
</PermissionRoute>
}
/>
</Route>
</Routes>
</BrowserRouter>

View File

@@ -25,6 +25,7 @@ import {
CheckCircleOutlined,
LaptopOutlined,
BellOutlined,
ApiOutlined,
} from '@ant-design/icons';
import { usePermission } from '../hooks/usePermission';
import NotificationBell from '../components/NotificationBell';
@@ -113,6 +114,7 @@ const allMenuItems: MenuItemType[] = [
{ key: '/operation-logs', icon: <AuditOutlined />, label: '操作日志', permission: 'log:view' },
{ key: '/roles', icon: <SafetyOutlined />, label: '角色管理', permission: 'role:view' },
{ key: '/permissions', icon: <KeyOutlined />, label: '权限一览', permission: 'role:view' },
{ key: '/integration-config', icon: <ApiOutlined />, label: '钉钉集成配置', permission: 'integration:read' },
{ key: '/users', icon: <SettingOutlined />, label: '账号管理', permission: 'user:view' },
],
},

View File

@@ -0,0 +1,139 @@
import React, { useEffect, useState } from 'react';
import {
Card, Form, Input, Button, Space, message, Spin, Switch, Alert, Descriptions, Tag,
} from 'antd';
import {
SaveOutlined, ApiOutlined, CheckCircleOutlined, CloseCircleOutlined,
} from '@ant-design/icons';
import api from '../../api';
interface DingTalkConfig {
agentId: string;
appSecret: string;
corpId: string;
startEnable: boolean;
}
const IntegrationConfigPage: React.FC = () => {
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [testing, setTesting] = useState(false);
const [config, setConfig] = useState<DingTalkConfig | null>(null);
const [verified, setVerified] = useState<boolean | null>(null);
const [form] = Form.useForm<DingTalkConfig>();
const fetchConfig = async () => {
setLoading(true);
try {
const res = await api.get<{ success: boolean; data: Array<{ type: string; verify: boolean; config: DingTalkConfig }> }>('/integration/config');
const dt = res.data?.find((c) => c.type === 'DINGTALK');
if (dt) {
setConfig(dt.config);
setVerified(dt.verify);
form.setFieldsValue(dt.config);
}
} catch {
// not configured
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchConfig();
}, []);
const handleSave = async () => {
const values = await form.validateFields();
setSaving(true);
try {
await api.post('/integration/config', { type: 'DINGTALK', config: values });
message.success('配置已保存');
await fetchConfig();
} catch (e: unknown) {
const err = e as { message?: string };
message.error(err?.message || '保存失败');
} finally {
setSaving(false);
}
};
const handleTest = async () => {
const values = await form.validateFields();
setTesting(true);
try {
const res = await api.post<{ success: boolean; message: string }>('/integration/config/test', {
type: 'DINGTALK',
config: values,
});
setVerified(res.success);
message.success(res.message);
} catch (e: unknown) {
const err = e as { message?: string };
setVerified(false);
message.error(err?.message || '连接失败');
} finally {
setTesting(false);
}
};
return (
<Card title="钉钉集成配置" extra={
<Space>
{verified === true && <Tag icon={<CheckCircleOutlined />} color="success"></Tag>}
{verified === false && <Tag icon={<CloseCircleOutlined />} color="error"></Tag>}
</Space>
}>
<Spin spinning={loading}>
{config && (
<Descriptions size="small" column={2} style={{ marginBottom: 24 }}>
<Descriptions.Item label="CorpId">{config.corpId || '-'}</Descriptions.Item>
<Descriptions.Item label="AppKey">{config.agentId || '-'}</Descriptions.Item>
<Descriptions.Item label="启用同步">
<Tag color={config.startEnable ? 'green' : 'default'}>
{config.startEnable ? '已启用' : '未启用'}
</Tag>
</Descriptions.Item>
</Descriptions>
)}
<Alert
type="info"
message="配置钉钉应用凭证后,可使用组织架构同步、考勤导入和排班同步功能。"
style={{ marginBottom: 24 }}
showIcon
/>
<Form form={form} layout="vertical" style={{ maxWidth: 480 }}>
<Form.Item name="corpId" label="CorpId企业ID" rules={[{ required: true, message: '请输入 CorpId' }]}>
<Input placeholder="dingxxxxxxxx" />
</Form.Item>
<Form.Item name="agentId" label="AppKey应用凭证" rules={[{ required: true, message: '请输入 AppKey' }]}>
<Input placeholder="从钉钉开放平台获取" />
</Form.Item>
<Form.Item
name="appSecret"
label="AppSecret应用密钥"
rules={[{ required: true, message: '请输入 AppSecret' }]}
extra="保存后仅返回脱敏信息,重新编辑时需再次输入完整密钥"
>
<Input.Password placeholder="从钉钉开放平台获取" />
</Form.Item>
<Form.Item name="startEnable" label="启用同步" valuePropName="checked">
<Switch />
</Form.Item>
<Space>
<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={handleSave}>
</Button>
<Button icon={<ApiOutlined />} loading={testing} onClick={handleTest}>
</Button>
</Space>
</Form>
</Spin>
</Card>
);
};
export default IntegrationConfigPage;