添加jwt验证,添加交叉评查首页加载对接接口,评查任务文档列表对接接口,意见列表对接接口

This commit is contained in:
2025-07-22 14:37:37 +08:00
parent de953283e3
commit 47664fc0e8
19 changed files with 1988 additions and 557 deletions
@@ -7,9 +7,9 @@ import { FileTypeTag } from '../ui/FileTypeTag';
import { StatusBadge } from '../ui/StatusBadge';
import { Pagination } from '../ui/Pagination';
import { LoadingIndicator } from '../ui/SkeletonScreen';
import type { ReviewFileUI } from '~/api/evaluation_points/rules-files';
// import { updateDocumentAuditStatus } from '~/api/evaluation_points/rules-files';
import { updateDocumentAuditStatus, type TaskDocument } from '~/api/cross-checking/cross-files'; // 更新导入
import { toastService } from '../ui/Toast';
import { formatDate } from '~/utils';
// 导出样式链接
export const links = () => [];
@@ -18,7 +18,7 @@ interface DocumentListModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
files: ReviewFileUI[];
files: TaskDocument[]; // 更新类型
onViewFile?: (fileId: string) => void;
loading?: boolean;
// 分页相关属性
@@ -49,13 +49,10 @@ export function DocumentListModal({
// 检查audit_status是否为0,如果是则更新为2
if (auditStatus === 0 || auditStatus === null) {
try {
// TODO: 这里需要从父组件传递 userId或者重新设计这个函数的调用方式
// 暂时跳过状态更新,直接进入查看
// const response = await updateDocumentAuditStatus(fileId, 2, userId);
// if (response.error) {
// throw new Error(response.error);
// }
console.warn('DocumentListModal: 跳过审核状态更新,需要传递 userId 参数');
// TODO: 不需要传递userId直接使用fileId找到对应文档,然后更新文档状态
// 更新文档状态
const updatedFile = await updateDocumentAuditStatus(fileId, 2);
console.log('更新后的文档状态:', updatedFile);
} catch (error) {
console.error('更新文件审核状态时出错:', error);
toastService.error(`更新文件审核状态时出错:${error instanceof Error ? error.message : '未知错误'}`);
@@ -70,60 +67,74 @@ export function DocumentListModal({
};
// 渲染问题摘要
const renderIssues = (file: ReviewFileUI) => {
// 如果文件状态为完成
if (file.status === 'Processed') {
// 如果没有问题,显示"所有评查点均通过"
if (file.warningCount <= 0 && file.failCount <= 0) {
return (
<div className="text-sm text-success">
<i className="ri-check-double-line mr-1"></i>
</div>
);
}
const renderIssues = (file: TaskDocument) => {
// 如果文件有问题信息
if (file.issues && file.issues.length > 0) {
// 最多显示2个问题
const displayIssues = file.issues.slice(0, 2);
// 显示问题列表
if (file.issues && file.issues.length > 0) {
// 最多显示2个问题
const displayIssues = file.issues.slice(0, 2);
return (
<div className="text-sm">
{displayIssues.map((issue, index) => (
<div key={index} className="mb-1">
<i className="ri-circle-fill mr-1 text-yellow-400"></i>
{issue.message}
</div>
))}
{file.issues.length > 2 && (
<div className="text-secondary mt-1">
{file.issues.length - 2} ...
</div>
)}
</div>
);
}
return (
<div className="text-sm">
{displayIssues.map((issue, index) => (
<div key={index} className="mb-1">
<i className={`ri-circle-fill mr-1 ${
issue.severity === 'error' ? 'text-red-400' :
issue.severity === 'warning' ? 'text-yellow-400' :
'text-blue-400'
}`}></i>
{issue.message}
</div>
))}
{file.issues.length > 2 && (
<div className="text-secondary mt-1">
{file.issues.length - 2} ...
</div>
)}
</div>
);
}
// 如果没有问题信息,根据状态显示
if (file.evaluations_status === 1) {
return (
<div className="text-sm text-success">
<i className="ri-check-double-line mr-1"></i>
</div>
);
}
// 其他状态显示占位符
return <div className="text-sm text-secondary">-</div>;
};
// 获取文件大小的友好显示
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// 定义表格列配置
const columns = [
{
title: "文件名称",
key: "fileName",
width: "30%",
render: (_: unknown, file: ReviewFileUI) => (
render: (_: unknown, file: TaskDocument) => (
<div className="flex">
<div className="flex-shrink-0 flex items-center self-center">
<FileIcon fileName={file.fileName} className="text-lg w-10 h-10" />
<FileIcon fileName={file.file_name} className="text-lg w-10 h-10" />
</div>
<div className="min-w-0 flex-1 flex flex-col py-2 ml-2">
<div className="font-normal text-base break-words whitespace-normal leading-normal" title={file.fileName}>{file.fileName}</div>
<div className="font-normal text-base break-words whitespace-normal leading-normal" title={file.file_name}>{file.file_name}</div>
<div className="text-xs text-secondary mt-2">
{file.fileCode}
{file.file_code}
</div>
<div className="text-xs text-secondary mt-1">
{formatFileSize(file.file_size)}
</div>
</div>
</div>
@@ -132,12 +143,12 @@ export function DocumentListModal({
{
title: "文件类型",
key: "fileType",
width: "12%",
render: (_: unknown, file: ReviewFileUI) => (
width: "10%",
render: (_: unknown, file: TaskDocument) => (
<FileTypeTag
type="other"
typeName={file.fileType}
text={file.fileType}
typeName={file.file_type_name}
text={file.file_type_name}
size="sm"
showIcon={false}
colorMode="light"
@@ -148,13 +159,15 @@ export function DocumentListModal({
title: "上传时间",
key: "uploadTime",
width: "12%",
render: (_: unknown, file: ReviewFileUI) => {
const [date, time] = file.uploadTime.split(' ');
render: (_: unknown, file: TaskDocument) => {
const uploadTime = formatDate(file.upload_time).split(' ');
const date = uploadTime[0];
const time = uploadTime[1];
return (
<div>
<span className="text-base">{date}</span>
<span className="text-base">{date}</span> {/* 2025-07-22 */}
<br />
<span className="text-xs text-secondary">{time}</span>
<span className="text-xs text-secondary">{time}</span> {/* 10:00:00 */}
</div>
);
}
@@ -163,34 +176,42 @@ export function DocumentListModal({
title: "评查统计",
key: "reviewStatus",
width: "12%",
render: (_: unknown, file: ReviewFileUI) =>
render: (_: unknown, file: TaskDocument) =>
// 要文件切分处理完之后,再显示评查统计
file.status === 'Processed' ? (
<div>
{file.passCount > 0 && (
{file.pass_count > 0 && (
<StatusBadge
status="pass"
text={`通过(${file.passCount})`}
text={`通过(${file.pass_count})`}
showIcon={true}
className="my-2"
/>
)}
{file.warningCount > 0 && (
{file.warning_count > 0 && (
<StatusBadge
status="warning"
text={`警告(${file.warningCount})`}
text={`警告(${file.warning_count})`}
showIcon={true}
className="my-2"
/>
)}
{file.failCount > 0 && (
{file.fail_count > 0 && (
<StatusBadge
status="fail"
text={`不通过(${file.failCount})`}
text={`不通过(${file.fail_count})`}
showIcon={true}
className="my-2"
/>
)}
{/* {file.manual_count > 0 && (
<StatusBadge
status="pending"
text={`需人工(${file.manual_count})`}
showIcon={true}
className="my-2"
/>
)} */}
</div>
) : (
@@ -199,23 +220,43 @@ export function DocumentListModal({
</div>
)
},
{
title: "评查分数",
key: "score",
width: "8%",
render: (_: unknown, file: TaskDocument) => (
<div className="text-center">
{file.final_score ? (
<span className={`font-medium ${
file.final_score >= 90 ? 'text-green-600' :
file.final_score >= 70 ? 'text-yellow-600' :
'text-red-600'
}`}>
{file.final_score}
</span>
) : (
<span className="text-gray-400">-</span>
)}
</div>
)
},
{
title: "问题摘要",
key: "issues",
width: "20%",
render: (_: unknown, file: ReviewFileUI) => renderIssues(file)
render: (_: unknown, file: TaskDocument) => renderIssues(file)
},
{
title: "操作",
key: "operation",
width: "14%",
render: (_: unknown, file: ReviewFileUI) => (
width: "auto",
render: (_: unknown, file: TaskDocument) => (
<>
<Button
type="default"
size="small"
icon="ri-eye-line"
onClick={() => handleReviewFileClick(file.id, file.auditStatus)}
onClick={() => handleReviewFileClick(file.document_id.toString(), file.audit_status)}
disabled={file.status !== 'Processed'}
className="mr-2"
>
@@ -258,13 +299,13 @@ export function DocumentListModal({
<Table
columns={columns}
dataSource={files}
rowKey="id"
rowKey="document_id"
emptyText="暂无文件数据"
className="files-table table-auto-height"
/>
{/* 分页组件 - 只有在提供了分页回调函数且总数大于每页大小时才显示 */}
{(onPageChange || onPageSizeChange) && total > pageSize ? (
{onPageChange && total > 0 && (
<Pagination
currentPage={currentPage}
total={total}
@@ -275,11 +316,6 @@ export function DocumentListModal({
showPageSizeChanger={!!onPageSizeChange}
pageSizeOptions={[10, 20, 30, 50]}
/>
) : (
<div className="text-sm text-gray-500 mt-4 text-center">
{total} {pageSize}
{total <= pageSize && ""}
</div>
)}
</>
)}
@@ -26,13 +26,11 @@ import { Pagination } from '../ui/Pagination';
import { Button } from '../ui/Button';
import { LoadingIndicator } from '../ui/SkeletonScreen';
import {
submitCrossCheckingOpinion,
getCrossCheckingOpinions,
performOpinionAction,
type SubmitOpinionRequest,
type CrossCheckingOpinion,
type OpinionActionType
} from '../../api/cross-checking/cross-file-result';
import { useFetcher } from '@remix-run/react';
// import '../../styles/components/TooltipStyles.css';
/**
@@ -168,6 +166,7 @@ interface ReviewPointsListProps {
onReviewPointSelect: (id: string, page?: number) => void;
onStatusChange?: (id: string, editAuditStatusId: string | number, status: string, message: string) => void;
scoringProposals?: ScoringProposal[];
jwtToken?: string; // 添加JWT token参数
}
/**
@@ -424,12 +423,14 @@ export function ReviewPointsList({
statistics,
activeReviewPointResultId,
onReviewPointSelect,
scoringProposals = []
scoringProposals = [],
jwtToken
}: ReviewPointsListProps) {
// 状态管理
const [searchText, setSearchText] = useState(''); // 搜索文本
const [statusFilter, setStatusFilter] = useState<string | null>(null); // 状态过滤
const [evaluationResultIds, setEvaluationResultIds] = useState<number[]>([]); // 评分提案的evaluation_result_id
const fetcher = useFetcher();
// 在组件中使用scoringProposals(这里只是简单使用以避免linter警告)
// 将来可以用于显示相关的评分提案信息
@@ -467,6 +468,54 @@ export function ReviewPointsList({
const [opinionListPageSize, setOpinionListPageSize] = useState(10);
const [performingAction, setPerformingAction] = useState<string | null>(null);
// 监听fetcher状态变化 - 获取意见列表数据
useEffect(() => {
if (fetcher.data && fetcher.state === 'idle' && opinionListLoading) {
const data = fetcher.data as {
success?: boolean;
data?: {
opinions: CrossCheckingOpinion[];
total: number;
};
error?: string;
};
if (data.success && data.data) {
console.log('意见列表数据', data.data);
setOpinionListData(data.data.opinions || []);
setOpinionListTotal(data.data.total || 0);
// 使用当前状态值而不是依赖项中的值
setOpinionListCurrentPage(prev => prev);
setOpinionListPageSize(prev => prev);
} else {
console.error('加载意见列表失败:', data.error);
toastService.error(data.error || '加载意见列表失败');
}
setOpinionListLoading(false);
}
}, [fetcher.data, fetcher.state, opinionListLoading]);
// 监听fetcher状态变化 - 提交意见
useEffect(() => {
if (fetcher.data && fetcher.state === 'idle' && isSubmittingOpinion) {
const data = fetcher.data as {
success?: boolean;
error?: string;
};
if (data.success) {
toastService.success('意见提交成功');
handleCloseOpinionModal();
} else {
console.error('提交意见失败:', data.error);
toastService.error(data.error || '提交意见失败');
}
setIsSubmittingOpinion(false);
}
}, [fetcher.data, fetcher.state, isSubmittingOpinion]);
// 存放评查点ID与有效页码的映射
const [effectivePages, setEffectivePages] = useState<Record<string, number>>({});
@@ -525,22 +574,19 @@ export function ReviewPointsList({
setOpinionListLoading(true);
try {
console.log('加载意见列表数据', targetDocumentId, page, pageSize);
const response = await getCrossCheckingOpinions(targetDocumentId, page, pageSize);
console.log('意见列表数据', response);
if (response.error) {
toastService.error(response.error);
return;
}
// 使用 fetcher 调用路由的 action
const formData = new FormData();
formData.append("intent", "getCrossCheckingOpinions");
formData.append("documentId", targetDocumentId.toString());
formData.append("page", page.toString());
formData.append("pageSize", pageSize.toString());
fetcher.submit(formData, { method: "POST" });
setOpinionListData(response.data?.opinions || []);
setOpinionListTotal(response.data?.total || 0);
setOpinionListCurrentPage(page);
setOpinionListPageSize(pageSize);
} catch (error) {
console.error('加载意见列表失败:', error);
toastService.error('加载意见列表失败');
} finally {
setOpinionListLoading(false);
}
};
@@ -580,7 +626,7 @@ export function ReviewPointsList({
setPerformingAction(actionKey);
try {
const response = await performOpinionAction({ opinionId, action });
const response = await performOpinionAction({ opinionId, action }, jwtToken);
if (response.error) {
toastService.error(response.error);
@@ -648,28 +694,21 @@ export function ReviewPointsList({
setIsSubmittingOpinion(true);
try {
const opinionData: SubmitOpinionRequest = {
reviewPointResultId: selectedReviewPoint.id,
documentId: selectedReviewPoint.documentId || '',
auditPoint: opinionForm.auditPoint,
foundIssue: opinionForm.foundIssue,
auditOpinion: opinionForm.auditOpinion,
deductionScore: opinionForm.deductionScore
};
// 使用 fetcher 调用路由的 action
const formData = new FormData();
formData.append("intent", "submitCrossCheckingOpinion");
formData.append("reviewPointResultId", selectedReviewPoint.id);
formData.append("documentId", selectedReviewPoint.documentId || '');
formData.append("auditPoint", opinionForm.auditPoint);
formData.append("foundIssue", opinionForm.foundIssue);
formData.append("auditOpinion", opinionForm.auditOpinion);
formData.append("deductionScore", opinionForm.deductionScore.toString());
const response = await submitCrossCheckingOpinion(opinionData);
if (response.error) {
toastService.error(response.error);
return;
}
toastService.success('意见提交成功');
handleCloseOpinionModal();
fetcher.submit(formData, { method: "POST" });
} catch (error) {
console.error('提交意见失败:', error);
toastService.error('提交意见失败,请稍后重试');
} finally {
setIsSubmittingOpinion(false);
}
};