Files
tiku-backend.net/Tiku.PlatformAdmin.Web/src/pages/GovernanceCenterPage.tsx
xiong 290a0c7bd7
Some checks failed
ci / release-gate (push) Has been cancelled
feat(platform): harden governance and approval workflows
2026-08-03 11:43:18 +08:00

89 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>;
}