diff --git a/frontend/src/pages/Reviews.tsx b/frontend/src/pages/Reviews.tsx new file mode 100644 index 0000000..f16c48d --- /dev/null +++ b/frontend/src/pages/Reviews.tsx @@ -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 = { + pending: { label: '待审核', color: 'orange' }, + approved: { label: '已通过', color: 'green' }, + rejected: { label: '已拒绝', color: 'red' }, + escalated: { label: '已升级', color: 'purple' }, +}; + +const reviewTypeConfig: Record = { + approval: '审批', + acceptance: '验收', + arbitration: '仲裁', + quality_check: '质量检查', +}; + +export default function Reviews() { + const [reviews, setReviews] = useState([]); + const [loading, setLoading] = useState(false); + const [statusFilter, setStatusFilter] = useState('pending'); + const [detailModal, setDetailModal] = useState(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) => ( + + 任务 #{record.task_id} + + 项目 #{record.project_id} + + + ), + }, + { + title: '审核类型', + dataIndex: 'review_type', + key: 'review_type', + render: (type: string) => ( + {reviewTypeConfig[type] || type} + ), + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (status: string) => { + const cfg = reviewStatusConfig[status] || { label: status, color: 'default' }; + return ; + }, + }, + { + title: '提交时间', + dataIndex: 'submitted_at', + key: 'submitted_at', + render: (time: string) => ( + + {time ? new Date(time).toLocaleString('zh-CN') : '-'} + + ), + }, + { + title: '审核人', + key: 'reviewer', + render: (_: any, record: Review) => ( + record.reviewer_id ? ( + + #{record.reviewer_id} + {record.reviewer_role ? ` (${record.reviewer_role})` : ''} + + ) : ( + - + ) + ), + }, + { + title: '操作', + key: 'action', + width: 220, + render: (_: any, record: Review) => ( + + + + + + )} + + ), + }, + ]; + + const filterButtons = ['pending', 'approved', 'rejected', 'all'].map((s) => ( + + )); + + return ( +
+ + + 审核管理 + + } + extra={{filterButtons}} + > + {reviews.length === 0 && !loading ? ( + + ) : ( + + )} + + + {/* 审核详情弹窗 */} + setDetailModal(null)} + footer={} + width={640} + > + {detailModal && ( +
+ + {reviewTypeConfig[detailModal.review_type] || detailModal.review_type} + + + +
+ 任务 ID: + {detailModal.task_id} + | 项目 ID: + {detailModal.project_id} +
+ +
+ 提交时间: + + {detailModal.submitted_at + ? new Date(detailModal.submitted_at).toLocaleString('zh-CN') + : '-'} + +
+ + {detailModal.reviewed_at && ( +
+ 审核时间: + {new Date(detailModal.reviewed_at).toLocaleString('zh-CN')} +
+ )} + + AI 产出内容: + + + {detailModal.review_content} + + + + {detailModal.reviewer_comment && ( +
+ 审核意见: + + {detailModal.reviewer_comment} + +
+ )} +
+ )} +
+ + {/* 审核决策弹窗 */} + setDecisionModal(null)} + okText="确认" + cancelText="取消" + okButtonProps={{ + danger: decisionModal?.decision === 'rejected', + type: decisionModal?.decision === 'approved' ? 'primary' : 'default', + }} + > + 请输入审核意见: +