feat: AI Worker 平台 MVP v1.0.0 - 多租户/项目管理/AI Worker/任务编排/HITL审核/成本治理

This commit is contained in:
2026-08-12 13:31:20 +08:00
parent fcca0e6325
commit 27ad31ff89
+311
View File
@@ -0,0 +1,311 @@
import { useState, useEffect } from 'react';
import {
Card, Table, Button, Modal, Tag, Space, message, Input,
Typography, Empty, Badge, Tooltip,
} from 'antd';
import {
CheckOutlined, CloseOutlined, EyeOutlined, FileSearchOutlined,
} from '@ant-design/icons';
import { reviewApi } from '@/api';
import type { Review, ReviewDecisionPayload } from '@/types';
const { TextArea } = Input;
const { Text, Paragraph } = Typography;
const reviewStatusConfig: Record<string, { label: string; color: string }> = {
pending: { label: '待审核', color: 'orange' },
approved: { label: '已通过', color: 'green' },
rejected: { label: '已拒绝', color: 'red' },
escalated: { label: '已升级', color: 'purple' },
};
const reviewTypeConfig: Record<string, string> = {
approval: '审批',
acceptance: '验收',
arbitration: '仲裁',
quality_check: '质量检查',
};
export default function Reviews() {
const [reviews, setReviews] = useState<Review[]>([]);
const [loading, setLoading] = useState(false);
const [statusFilter, setStatusFilter] = useState<string>('pending');
const [detailModal, setDetailModal] = useState<Review | null>(null);
const [decisionModal, setDecisionModal] = useState<{
review: Review;
decision: 'approved' | 'rejected';
} | null>(null);
const [comment, setComment] = useState('');
const fetchData = async () => {
setLoading(true);
try {
const data = await reviewApi.list({ status: statusFilter === 'all' ? undefined : statusFilter });
setReviews(data);
} catch {
message.error('加载审核列表失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [statusFilter]);
const handleDecision = async () => {
if (!decisionModal) return;
try {
const payload: ReviewDecisionPayload = {
comment,
status: decisionModal.decision,
};
await reviewApi.decision(String(decisionModal.review.id), payload);
message.success(
decisionModal.decision === 'approved' ? '已通过审核' : '已拒绝并退回',
);
setDecisionModal(null);
setComment('');
fetchData();
} catch {
message.error('操作失败');
}
};
const openDecision = (review: Review, decision: 'approved' | 'rejected') => {
setDecisionModal({ review, decision });
setComment('');
};
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 60,
},
{
title: '任务',
key: 'task',
render: (_: any, record: Review) => (
<Space direction="vertical" size={0}>
<Text strong> #{record.task_id}</Text>
<Text type="secondary" style={{ fontSize: 12 }}>
#{record.project_id}
</Text>
</Space>
),
},
{
title: '审核类型',
dataIndex: 'review_type',
key: 'review_type',
render: (type: string) => (
<Tag>{reviewTypeConfig[type] || type}</Tag>
),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => {
const cfg = reviewStatusConfig[status] || { label: status, color: 'default' };
return <Badge status={cfg.color as any} text={cfg.label} />;
},
},
{
title: '提交时间',
dataIndex: 'submitted_at',
key: 'submitted_at',
render: (time: string) => (
<Text style={{ fontSize: 12 }}>
{time ? new Date(time).toLocaleString('zh-CN') : '-'}
</Text>
),
},
{
title: '审核人',
key: 'reviewer',
render: (_: any, record: Review) => (
record.reviewer_id ? (
<Text style={{ fontSize: 12 }}>
#{record.reviewer_id}
{record.reviewer_role ? ` (${record.reviewer_role})` : ''}
</Text>
) : (
<Text type="secondary">-</Text>
)
),
},
{
title: '操作',
key: 'action',
width: 220,
render: (_: any, record: Review) => (
<Space>
<Tooltip title="查看详情">
<Button
size="small"
icon={<EyeOutlined />}
onClick={() => setDetailModal(record)}
/>
</Tooltip>
{record.status === 'pending' && (
<>
<Button
size="small"
type="primary"
icon={<CheckOutlined />}
onClick={() => openDecision(record, 'approved')}
>
</Button>
<Button
size="small"
danger
icon={<CloseOutlined />}
onClick={() => openDecision(record, 'rejected')}
>
</Button>
</>
)}
</Space>
),
},
];
const filterButtons = ['pending', 'approved', 'rejected', 'all'].map((s) => (
<Button
key={s}
type={statusFilter === s ? 'primary' : 'default'}
onClick={() => setStatusFilter(s)}
>
{s === 'all' ? '全部' : reviewStatusConfig[s]?.label || s}
</Button>
));
return (
<div>
<Card
title={
<Space>
<FileSearchOutlined />
</Space>
}
extra={<Space>{filterButtons}</Space>}
>
{reviews.length === 0 && !loading ? (
<Empty
description={
statusFilter === 'pending'
? '暂无待审核项'
: '暂无审核记录'
}
/>
) : (
<Table
columns={columns}
dataSource={reviews}
rowKey="id"
loading={loading}
pagination={{ pageSize: 10 }}
/>
)}
</Card>
{/* 审核详情弹窗 */}
<Modal
title={`审核详情 #${detailModal?.id || ''}`}
open={!!detailModal}
onCancel={() => setDetailModal(null)}
footer={<Button onClick={() => setDetailModal(null)}></Button>}
width={640}
>
{detailModal && (
<div>
<Space style={{ marginBottom: 12 }}>
<Tag>{reviewTypeConfig[detailModal.review_type] || detailModal.review_type}</Tag>
<Badge
status={reviewStatusConfig[detailModal.status]?.color as any}
text={reviewStatusConfig[detailModal.status]?.label || detailModal.status}
/>
</Space>
<div style={{ marginBottom: 12 }}>
<Text type="secondary"> ID: </Text>
<Text strong>{detailModal.task_id}</Text>
<Text type="secondary"> | ID: </Text>
<Text strong>{detailModal.project_id}</Text>
</div>
<div style={{ marginBottom: 12 }}>
<Text type="secondary">: </Text>
<Text>
{detailModal.submitted_at
? new Date(detailModal.submitted_at).toLocaleString('zh-CN')
: '-'}
</Text>
</div>
{detailModal.reviewed_at && (
<div style={{ marginBottom: 12 }}>
<Text type="secondary">: </Text>
<Text>{new Date(detailModal.reviewed_at).toLocaleString('zh-CN')}</Text>
</div>
)}
<Text type="secondary">AI :</Text>
<Card
size="small"
style={{ marginTop: 8, marginBottom: 12, maxHeight: 300, overflow: 'auto' }}
>
<Paragraph style={{ whiteSpace: 'pre-wrap', marginBottom: 0 }}>
{detailModal.review_content}
</Paragraph>
</Card>
{detailModal.reviewer_comment && (
<div>
<Text type="secondary">:</Text>
<Card size="small" style={{ marginTop: 8 }}>
<Text>{detailModal.reviewer_comment}</Text>
</Card>
</div>
)}
</div>
)}
</Modal>
{/* 审核决策弹窗 */}
<Modal
title={
decisionModal?.decision === 'approved' ? '确认通过审核' : '确认拒绝(退回返工)'
}
open={!!decisionModal}
onOk={handleDecision}
onCancel={() => setDecisionModal(null)}
okText="确认"
cancelText="取消"
okButtonProps={{
danger: decisionModal?.decision === 'rejected',
type: decisionModal?.decision === 'approved' ? 'primary' : 'default',
}}
>
<Text>:</Text>
<TextArea
rows={4}
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder={
decisionModal?.decision === 'approved'
? '产出符合要求,准予通过...'
: '请说明拒绝原因,AI 将根据反馈返工...'
}
style={{ marginTop: 8 }}
/>
</Modal>
</div>
);
}