89 lines
12 KiB
TypeScript
89 lines
12 KiB
TypeScript
import { App, Button, Card, Drawer, Flex, Form, Input, InputNumber, Select, Space, Switch, Table, Tag, Typography } from 'antd';
|
||
import { useCallback, useEffect, useState } from 'react';
|
||
import { platformRequest } from '../api/platform';
|
||
|
||
interface Definition { id: string; code: string; name: string; category: string; valueType: string | number; allowRuntimeManagement: boolean; isSensitive: boolean; description?: string }
|
||
interface Version { id: string; definitionId: string; environment: string; version: number; status: string | number; value?: unknown; secretRef?: string; reason: string; publishedAt?: string; createdAt: string }
|
||
interface Template { id: string; code: string; name: string; channel: string | number; subjectTemplate: string; bodyTemplate: string; enabled: boolean; variables: unknown }
|
||
interface Delivery { id: string; templateId: string; recipientRoleCode: string; channel: string | number; status: string | number; subject: string; attempts: number; lastError?: string; createdAt: string }
|
||
interface Paged<T> { items: T[]; total: number; page: number; pageSize: number }
|
||
|
||
const statusName: Record<string, string> = { '0': '草稿', '1': '已发布', '2': '已归档', Draft: '草稿', Published: '已发布', Retired: '已归档', Pending: '待发送', Processing: '发送中', Sent: '已发送', Failed: '失败', Cancelled: '已取消', '3': '失败', '4': '已取消' };
|
||
|
||
export function ConfigurationCenterPage() {
|
||
const { message, modal } = App.useApp();
|
||
const [definitions, setDefinitions] = useState<Definition[]>([]);
|
||
const [selected, setSelected] = useState<Definition>();
|
||
const [versions, setVersions] = useState<Version[]>([]);
|
||
const [open, setOpen] = useState(false);
|
||
const [loading, setLoading] = useState(false);
|
||
const [form] = Form.useForm<{ environment: string; value?: string; secretRef?: string; reason: string }>();
|
||
|
||
const loadDefinitions = useCallback(async () => {
|
||
const values = await platformRequest<Definition[]>('GET', '/api/platform-admin/governance/configuration/definitions');
|
||
setDefinitions(values); setSelected((current) => current ?? values[0]);
|
||
}, []);
|
||
const loadVersions = useCallback(async (definition?: Definition) => {
|
||
if (!definition) return setVersions([]);
|
||
setVersions(await platformRequest<Version[]>('GET', '/api/platform-admin/governance/configuration/versions', { query: { definitionCode: definition.code } }));
|
||
}, []);
|
||
useEffect(() => { setLoading(true); loadDefinitions().catch((error) => message.error(error instanceof Error ? error.message : '配置定义加载失败')).finally(() => setLoading(false)); }, [loadDefinitions, message]);
|
||
useEffect(() => { void loadVersions(selected); }, [loadVersions, selected]);
|
||
|
||
const saveDraft = async () => {
|
||
if (!selected) return;
|
||
const values = await form.validateFields();
|
||
let parsed: unknown;
|
||
if (!selected.isSensitive) {
|
||
try { parsed = selected.valueType === 'Json' || String(selected.valueType) === '3' ? JSON.parse(values.value || '{}') : values.value; }
|
||
catch { message.error('配置值不是合法 JSON'); return; }
|
||
}
|
||
setLoading(true);
|
||
try {
|
||
await platformRequest('POST', '/api/platform-admin/governance/configuration/drafts', { body: { definitionCode: selected.code, environment: values.environment, value: parsed, secretRef: values.secretRef, reason: values.reason } });
|
||
setOpen(false); form.resetFields(); message.success('配置草稿已保存'); await loadVersions(selected);
|
||
} catch (error) { message.error(error instanceof Error ? error.message : '配置草稿保存失败'); }
|
||
finally { setLoading(false); }
|
||
};
|
||
const transition = (version: Version, action: 'publish' | 'rollback') => modal.confirm({
|
||
title: action === 'publish' ? `发布 v${version.version}` : `回滚到 v${version.version}`,
|
||
content: '变更会保留完整版本和平台审计记录。',
|
||
onOk: async () => {
|
||
const body = action === 'rollback' ? { reason: `回滚到 v${version.version}` } : undefined;
|
||
await platformRequest('POST', `/api/platform-admin/governance/configuration/versions/{versionId}/${action}`, { path: { versionId: version.id }, body });
|
||
message.success(action === 'publish' ? '配置已发布' : '配置已回滚'); await loadVersions(selected);
|
||
},
|
||
});
|
||
return <div className="business-page"><Flex justify="space-between" align="end"><div><Typography.Text type="secondary">平台治理</Typography.Text><Typography.Title level={2}>类型化配置中心</Typography.Title><Typography.Paragraph type="secondary">配置按定义、环境和版本发布;敏感项只保存密钥引用,安全控制项禁止在线修改。</Typography.Paragraph></div>{selected?.allowRuntimeManagement && <Button type="primary" onClick={() => { form.setFieldsValue({ environment: 'all' }); setOpen(true); }}>新建草稿</Button>}</Flex>
|
||
<Flex gap={16} align="start"><Card title="配置定义" style={{ width: 360 }}><Table size="small" rowKey="id" pagination={false} dataSource={definitions} columns={[{ title: '名称', dataIndex: 'name' }, { title: '分类', dataIndex: 'category' }]} onRow={(record) => ({ onClick: () => setSelected(record) })} /></Card>
|
||
<Card title={selected ? `${selected.name} · ${selected.code}` : '版本'} style={{ flex: 1 }}><Table rowKey="id" loading={loading} dataSource={versions} columns={[{ title: '环境', dataIndex: 'environment' }, { title: '版本', dataIndex: 'version' }, { title: '状态', dataIndex: 'status', render: (value) => <Tag>{statusName[String(value)] ?? String(value)}</Tag> }, { title: '原因', dataIndex: 'reason' }, { title: '操作', render: (_, row: Version) => <Space>{['0', 'Draft'].includes(String(row.status)) && <Button size="small" onClick={() => transition(row, 'publish')}>发布</Button>}<Button size="small" onClick={() => transition(row, 'rollback')}>回滚到此版本</Button></Space> }]} /></Card></Flex>
|
||
<Drawer title="新建配置草稿" width={560} open={open} onClose={() => setOpen(false)} footer={<Flex justify="end" gap={8}><Button onClick={() => setOpen(false)}>取消</Button><Button type="primary" loading={loading} onClick={() => void saveDraft()}>保存草稿</Button></Flex>}><Form form={form} layout="vertical"><Form.Item name="environment" label="环境" rules={[{ required: true }]}><Input /></Form.Item>{selected?.isSensitive ? <Form.Item name="secretRef" label="密钥引用" rules={[{ required: true }]}><Input placeholder="vault://..." /></Form.Item> : <Form.Item name="value" label="配置值" rules={[{ required: true }]}><Input.TextArea rows={8} /></Form.Item>}<Form.Item name="reason" label="变更原因" rules={[{ required: true, min: 3 }]}><Input.TextArea rows={3} /></Form.Item></Form></Drawer>
|
||
</div>;
|
||
}
|
||
|
||
export function NotificationCenterPage() {
|
||
const { message } = App.useApp();
|
||
const [templates, setTemplates] = useState<Template[]>([]);
|
||
const [deliveries, setDeliveries] = useState<Delivery[]>([]);
|
||
const [templateOpen, setTemplateOpen] = useState(false);
|
||
const [sendOpen, setSendOpen] = useState(false);
|
||
const [templateForm] = Form.useForm();
|
||
const [sendForm] = Form.useForm();
|
||
const load = useCallback(async () => {
|
||
const [templateItems, deliveryPage] = await Promise.all([
|
||
platformRequest<Template[]>('GET', '/api/platform-admin/governance/notifications/templates'),
|
||
platformRequest<Paged<Delivery>>('GET', '/api/platform-admin/governance/notifications/deliveries', { query: { page: 1, pageSize: 100 } }),
|
||
]); setTemplates(templateItems); setDeliveries(deliveryPage.items);
|
||
}, []);
|
||
useEffect(() => { load().catch((error) => message.error(error instanceof Error ? error.message : '通知中心加载失败')); }, [load, message]);
|
||
const saveTemplate = async () => { const values = await templateForm.validateFields(); await platformRequest('PUT', '/api/platform-admin/governance/notifications/templates', { body: { ...values, variables: [] } }); setTemplateOpen(false); templateForm.resetFields(); message.success('模板已保存'); await load(); };
|
||
const send = async () => { const values = await sendForm.validateFields(); let variables = {}; try { variables = JSON.parse(values.variables || '{}'); } catch { return message.error('变量必须是 JSON 对象'); } await platformRequest('POST', '/api/platform-admin/governance/notifications/send', { body: { templateId: values.templateId, roleCodes: values.roleCodes.split(',').map((item: string) => item.trim()).filter(Boolean), variables, idempotencyKey: crypto.randomUUID() } }); setSendOpen(false); sendForm.resetFields(); message.success('通知投递已创建'); await load(); };
|
||
const retry = async (id: string) => { await platformRequest('POST', '/api/platform-admin/governance/notifications/deliveries/{deliveryId}/retry', { path: { deliveryId: id } }); message.success('已重新入队'); await load(); };
|
||
return <div className="business-page"><Flex justify="space-between" align="end"><div><Typography.Text type="secondary">平台治理</Typography.Text><Typography.Title level={2}>通知中心</Typography.Title><Typography.Paragraph type="secondary">按平台岗位发送站内、短信或邮件通知,并跟踪投递与失败重试。</Typography.Paragraph></div><Space><Button onClick={() => setTemplateOpen(true)}>新建模板</Button><Button type="primary" onClick={() => setSendOpen(true)}>发送通知</Button></Space></Flex>
|
||
<Card title="通知模板"><Table rowKey="id" dataSource={templates} pagination={false} columns={[{ title: '名称', dataIndex: 'name' }, { title: '编码', dataIndex: 'code' }, { title: '渠道', dataIndex: 'channel' }, { title: '启用', dataIndex: 'enabled', render: (value) => <Tag color={value ? 'green' : 'default'}>{value ? '启用' : '停用'}</Tag> }]} /></Card>
|
||
<Card title="投递记录" style={{ marginTop: 16 }}><Table rowKey="id" dataSource={deliveries} columns={[{ title: '主题', dataIndex: 'subject' }, { title: '岗位', dataIndex: 'recipientRoleCode' }, { title: '渠道', dataIndex: 'channel' }, { title: '状态', dataIndex: 'status', render: (value) => <Tag>{statusName[String(value)] ?? String(value)}</Tag> }, { title: '尝试', dataIndex: 'attempts' }, { title: '错误', dataIndex: 'lastError' }, { title: '操作', render: (_, row: Delivery) => ['0', '3', 'Pending', 'Failed'].includes(String(row.status)) ? <Button size="small" onClick={() => void retry(row.id)}>重试</Button> : null }]} /></Card>
|
||
<Drawer title="通知模板" width={560} open={templateOpen} onClose={() => setTemplateOpen(false)} footer={<Button type="primary" onClick={() => void saveTemplate()}>保存</Button>}><Form form={templateForm} layout="vertical" initialValues={{ channel: 'InApp', enabled: true }}><Form.Item name="code" label="编码" rules={[{ required: true }]}><Input /></Form.Item><Form.Item name="name" label="名称" rules={[{ required: true }]}><Input /></Form.Item><Form.Item name="channel" label="渠道"><Select options={['InApp', 'Sms', 'Email'].map(value => ({ value, label: value }))} /></Form.Item><Form.Item name="subjectTemplate" label="主题模板" rules={[{ required: true }]}><Input /></Form.Item><Form.Item name="bodyTemplate" label="正文模板" rules={[{ required: true }]}><Input.TextArea rows={6} /></Form.Item><Form.Item name="enabled" label="启用" valuePropName="checked"><Switch /></Form.Item></Form></Drawer>
|
||
<Drawer title="发送平台通知" width={560} open={sendOpen} onClose={() => setSendOpen(false)} footer={<Button type="primary" onClick={() => void send()}>创建投递</Button>}><Form form={sendForm} layout="vertical"><Form.Item name="templateId" label="模板" rules={[{ required: true }]}><Select options={templates.map(item => ({ value: item.id, label: item.name }))} /></Form.Item><Form.Item name="roleCodes" label="目标岗位编码(逗号分隔)" rules={[{ required: true }]}><Input /></Form.Item><Form.Item name="variables" label="模板变量 JSON"><Input.TextArea rows={6} placeholder='{"name":"示例"}' /></Form.Item></Form></Drawer>
|
||
</div>;
|
||
}
|