Files
tiku-backend.net/Tiku.PlatformAdmin.Web/src/pages/ApprovalCenterPage.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

127 lines
7.8 KiB
TypeScript

import { CheckOutlined, CloseOutlined, ReloadOutlined, StopOutlined } from '@ant-design/icons';
import { Alert, App, Button, Card, Descriptions, Drawer, Flex, Form, Input, Space, Table, Tabs, Tag, Typography } from 'antd';
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { components } from '../api/schema.generated';
import { platformRequest } from '../api/platform';
import { useAuth } from '../auth/AuthProvider';
type Approval = components['schemas']['PlatformApprovalRequestItem'];
type Policy = components['schemas']['PlatformApprovalPolicyItem'];
const statusLabels: Record<string, string> = {
'0': '待审批', '1': '已批准', '2': '已拒绝', '3': '执行中', '4': '成功', '5': '失败', '6': '已撤销', '7': '已过期',
Pending: '待审批', Approved: '已批准', Rejected: '已拒绝', Executing: '执行中', Succeeded: '成功', Failed: '失败', Cancelled: '已撤销', Expired: '已过期',
};
function statusColor(status: unknown) {
const value = String(status);
if (['4', 'Succeeded'].includes(value)) return 'green';
if (['5', 'Failed', '2', 'Rejected'].includes(value)) return 'red';
if (['0', 'Pending'].includes(value)) return 'gold';
if (['3', 'Executing'].includes(value)) return 'blue';
return 'default';
}
export function ApprovalCenterPage() {
const auth = useAuth();
const { message, modal } = App.useApp();
const [items, setItems] = useState<Approval[]>([]);
const [policies, setPolicies] = useState<Policy[]>([]);
const [selected, setSelected] = useState<Approval | null>(null);
const [loading, setLoading] = useState(false);
const [form] = Form.useForm<{ reason: string }>();
const load = useCallback(async () => {
setLoading(true);
try {
const [requests, policyItems] = await Promise.all([
platformRequest<Approval[]>('GET', '/api/platform-admin/approvals', { query: { limit: 200 } }),
platformRequest<Policy[]>('GET', '/api/platform-admin/approvals/policies'),
]);
setItems(requests);
setPolicies(policyItems);
setSelected((current) => current ? requests.find((item) => item.id === current.id) ?? null : null);
} catch (error) {
message.error(error instanceof Error ? error.message : '审批中心加载失败');
} finally {
setLoading(false);
}
}, [message]);
useEffect(() => { void load(); }, [load]);
const decide = async (action: 'approve' | 'reject' | 'cancel') => {
if (!selected) return;
const { reason } = await form.validateFields();
modal.confirm({
title: action === 'approve' ? '确认批准并执行' : action === 'reject' ? '确认拒绝' : '确认撤销',
content: action === 'approve' ? '批准时会重新校验权限和目标状态,成功后立即执行。' : '该决定会写入不可省略的平台审计日志。',
okButtonProps: { danger: action !== 'approve' },
onOk: async () => {
setLoading(true);
try {
const result = await platformRequest<Approval>('POST', `/api/platform-admin/approvals/{requestId}/${action}`, {
path: { requestId: selected.id }, body: { reason },
});
setSelected(result);
form.resetFields();
message.success(action === 'approve' ? '审批已批准,等待 Worker 执行' : '审批状态已更新');
await load();
} catch (error) {
message.error(error instanceof Error ? error.message : '审批操作失败');
} finally {
setLoading(false);
}
},
});
};
const columns = useMemo(() => [
{ title: '审批单', dataIndex: 'requestNo', key: 'requestNo' },
{ title: '策略', dataIndex: 'policyCode', key: 'policyCode' },
{ title: '目标', key: 'target', render: (_: unknown, row: Approval) => `${row.targetType} / ${row.targetId}` },
{ title: '金额', dataIndex: 'amountCents', key: 'amount', render: (value: unknown) => value == null ? '-' : `¥${(Number(value) / 100).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}` },
{ title: '状态', dataIndex: 'status', key: 'status', render: (value: unknown) => <Tag color={statusColor(value)}>{statusLabels[String(value)] ?? String(value)}</Tag> },
{ title: '提交时间', dataIndex: 'createdAt', key: 'createdAt', render: (value: string) => new Date(value).toLocaleString('zh-CN') },
], []);
const pending = selected && ['0', 'Pending'].includes(String(selected.status));
const canDecide = pending && selected.requestedBy !== auth.user?.userId && auth.hasPermission('platform:approval:decide');
const canCancel = pending && selected.requestedBy === auth.user?.userId;
return (
<div className="business-page">
<Flex justify="space-between" align="end" gap={16} wrap>
<div><Typography.Text type="secondary"></Typography.Text><Typography.Title level={2}></Typography.Title><Typography.Paragraph type="secondary"></Typography.Paragraph></div>
<Button icon={<ReloadOutlined />} loading={loading} onClick={() => void load()}></Button>
</Flex>
<Tabs items={[
{ key: 'requests', label: '审批任务', children: <Card><Table rowKey="id" loading={loading} dataSource={items} columns={columns} pagination={{ pageSize: 20 }} onRow={(record) => ({ onClick: () => { setSelected(record); form.resetFields(); } })} /></Card> },
{ key: 'policies', label: '审批策略', children: <Card><Table rowKey="id" dataSource={policies} pagination={false} columns={[
{ title: '策略', dataIndex: 'name' }, { title: '编码', dataIndex: 'code' },
{ title: '规则', render: (_: unknown, row: Policy) => row.alwaysRequireApproval ? '始终审批' : row.amountThresholdCents ? `达到 ¥${(Number(row.amountThresholdCents) / 100).toLocaleString('zh-CN')}` : '即时执行' },
{ title: '版本', dataIndex: 'version' }, { title: '有效期', dataIndex: 'expiresAfterHours', render: (value: unknown) => `${String(value)} 小时` },
]} /></Card> },
]} />
<Drawer title={selected?.requestNo} width={680} open={Boolean(selected)} onClose={() => setSelected(null)}>
{selected && <Space direction="vertical" size={16} style={{ width: '100%' }}>
{selected.error && <Alert type="error" showIcon message="执行失败" description={selected.error} />}
<Descriptions bordered size="small" column={1} items={[
{ key: 'status', label: '状态', children: <Tag color={statusColor(selected.status)}>{statusLabels[String(selected.status)] ?? String(selected.status)}</Tag> },
{ key: 'command', label: '命令', children: selected.commandType },
{ key: 'target', label: '目标', children: `${selected.targetType} / ${selected.targetId}` },
{ key: 'reason', label: '申请原因', children: selected.requestReason || '-' },
{ key: 'expires', label: '审批期限', children: new Date(selected.expiresAt).toLocaleString('zh-CN') },
{ key: 'snapshot', label: '脱敏快照', children: <pre style={{ whiteSpace: 'pre-wrap' }}>{JSON.stringify(selected.requestSnapshot, null, 2)}</pre> },
]} />
{(canDecide || canCancel) && <Form form={form} layout="vertical"><Form.Item name="reason" label="决定原因" rules={[{ required: true, min: 3 }]}><Input.TextArea rows={3} /></Form.Item></Form>}
<Flex justify="end" gap={8}>
{canCancel && <Button danger icon={<StopOutlined />} onClick={() => void decide('cancel')}></Button>}
{canDecide && <><Button danger icon={<CloseOutlined />} onClick={() => void decide('reject')}></Button><Button type="primary" icon={<CheckOutlined />} onClick={() => void decide('approve')}></Button></>}
</Flex>
</Space>}
</Drawer>
</div>
);
}