feat: AI Worker 平台 MVP v1.0.0 - 多租户/项目管理/AI Worker/任务编排/HITL审核/成本治理
This commit is contained in:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user