import React, { useEffect, useState, useMemo } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useApiMutation } from '../../hooks/useApiMutation'; import { validateResponse } from '../../utils/validate'; import { integrationConfigSchema } from '../../api/schemas'; import { Alert, Button, Card, Descriptions, Divider, Form, Input, Space, Spin, Tag, } from 'antd'; import { ApiOutlined, CheckCircleOutlined, CloseCircleOutlined, SaveOutlined, } from '@ant-design/icons'; import api from '../../api'; import { message } from '../../ui/app-message'; import { usePermission } from '../../hooks/usePermission'; import PermissionButton from '../../components/PermissionButton'; import { buildDingTalkConfigPayload, isAppSecretRequired, type DingTalkConfigFormValues, } from './integration-config-form'; import { cacheDingTalkDraft, cacheDingTalkServerSnapshot, commitDingTalkConfig, readDingTalkConfigCache, } from './integration-config-cache'; import { IntegrationOrgSyncPanel } from './IntegrationOrgSyncPanel'; interface DingTalkConfig { agentId: string; corpId: string; } const IntegrationConfigPage: React.FC = () => { const initialCache = useMemo(() => readDingTalkConfigCache(), []); const { hasPermission, hasAllPermissions } = usePermission(); const canCreateClass = hasPermission('class:create'); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(false); const [form] = Form.useForm(); const queryClient = useQueryClient(); const { data: serverConfig = { config: initialCache.config, verified: initialCache.verified }, isLoading: configLoading, isFetching: configFetching, } = useQuery<{ config: DingTalkConfig | null; verified: boolean | null }>({ queryKey: ['integration', 'config'], queryFn: async () => { try { const res = await api.get<{ success: boolean; data: Array<{ type: string; verify: boolean; config: DingTalkConfig }>; }>('/integration/config'); const validated = validateResponse<{ success: boolean; data: Array<{ type: string; verify: boolean; config: DingTalkConfig }>; }>(integrationConfigSchema, res); const dt = validated.data?.find((c) => c.type === 'DINGTALK'); return { config: dt?.config ?? null, verified: dt ? dt.verify : null }; } catch { // not configured return { config: initialCache.config, verified: initialCache.verified }; } }, }); const config = serverConfig.config; const verified = serverConfig.verified; const loading = !initialCache.loaded && (configLoading || configFetching); const saveMutation = useApiMutation( async (payload: Record) => api.post('/integration/config', { type: 'DINGTALK', config: payload }), { invalidate: [['integration', 'config']] }, ); // 初始表单值来自本地缓存(外部存储同步) useEffect(() => { form.setFieldsValue(initialCache.formValues); }, [form, initialCache]); // 服务端配置同步进 localStorage 缓存,并回填表单 useEffect(() => { cacheDingTalkServerSnapshot(config, verified); if (config) form.setFieldsValue(readDingTalkConfigCache().formValues); }, [config, verified, form]); const handleSave = async () => { const values = await form.validateFields(); const payload = buildDingTalkConfigPayload(values); setSaving(true); try { await saveMutation.mutateAsync(payload); message.success('配置已保存'); commitDingTalkConfig({ corpId: payload.corpId, agentId: payload.agentId }); form.setFieldValue('appSecret', undefined); } catch { // 错误提示由 useApiMutation 统一处理 } finally { setSaving(false); } }; const handleTest = async () => { const values = await form.validateFields(); const payload = buildDingTalkConfigPayload(values); setTesting(true); try { const res = await api.post<{ success: boolean; message: string }>( '/integration/config/test', { type: 'DINGTALK', config: payload, }, ); queryClient.setQueryData(['integration', 'config'], (prev) => ({ ...(prev ?? { config: initialCache.config, verified: initialCache.verified }), verified: res.success, })); message.success(res.message); } catch (e: unknown) { const err = e as { message?: string }; queryClient.setQueryData(['integration', 'config'], (prev) => ({ ...(prev ?? { config: initialCache.config, verified: initialCache.verified }), verified: false, })); message.error(err?.message || '连接失败'); } finally { setTesting(false); } }; return ( {verified === true && ( } color="success"> 已连接 )} {verified === false && ( } color="error"> 未连接 )} } > {config && ( {config.corpId || '-'} {config.agentId || '-'} 手动触发 )}
cacheDingTalkDraft(values)} style={{ maxWidth: 520 }} > } loading={saving} onClick={handleSave} > 保存配置
{config && hasAllPermissions('sync:read', 'class:view', 'class:edit') && ( <> 组织用户导入 )}
); }; export default IntegrationConfigPage;