添加交叉评查任务的文档列表,评查详情的意见列表

This commit is contained in:
2025-07-17 17:48:13 +08:00
parent 348128bbe0
commit e4ce41cebe
11 changed files with 1602 additions and 40 deletions
+249 -14
View File
@@ -1,4 +1,4 @@
import { postgrestPost } from "../postgrest-client";
// import { postgrestPost } from "../postgrest-client";
import { API_BASE_URL } from "../../config/api-config";
/**
@@ -39,6 +39,10 @@ export interface CrossCheckingOpinion {
status: string;
created_at: string;
updated_at?: string;
is_vote: boolean; // 当前用户是否已投票
voter_count: number; // 投票人数
proposer_name: string; // 意见发起人姓名
current_user_is_proposer: boolean; // 当前用户是否为意见发起人
}
/**
@@ -100,25 +104,198 @@ export async function submitCrossCheckingOpinion(
}
/**
* 获取交叉评查意见列表
* 获取交叉评查意见列表(支持分页)
* @param documentId 文档ID
* @returns 意见列表
* @param page 页码
* @param pageSize 每页大小
* @returns 意见列表和总数
*/
export async function getCrossCheckingOpinions(documentId: string | number): Promise<ApiResponse<CrossCheckingOpinion[]>> {
export async function getCrossCheckingOpinions(
documentId: string | number,
page: number = 1,
pageSize: number = 10
): Promise<ApiResponse<{ opinions: CrossCheckingOpinion[], total: number }>> {
try {
const response = await postgrestPost('rpc/get_cross_checking_opinions', {
p_document_id: documentId
});
// 模拟数据 - 后续替换为真实API调用
const mockOpinions: CrossCheckingOpinion[] = [
{
id: 1,
evaluation_point_id: 101,
document_id: documentId,
audit_point: "合同主体信息核查",
found_issue: "合同签署方信息不完整",
audit_opinion: "合同中缺少乙方的详细联系方式,建议补充完整的地址和联系电话",
deduction_score: -2,
status: "pending",
created_at: "2024-01-15 10:30:00",
is_vote: false,
voter_count: 3,
proposer_name: "张三",
current_user_is_proposer: false
},
{
id: 2,
evaluation_point_id: 102,
document_id: documentId,
audit_point: "合同金额核查",
found_issue: "合同金额与预算不符",
audit_opinion: "合同总金额超出预算范围,需要重新评估或调整预算",
deduction_score: -5,
status: "approved",
created_at: "2024-01-14 14:20:00",
is_vote: true,
voter_count: 5,
proposer_name: "李四",
current_user_is_proposer: true
},
{
id: 3,
evaluation_point_id: 103,
document_id: documentId,
audit_point: "合同条款审查",
found_issue: "违约责任条款不明确",
audit_opinion: "合同中违约责任的具体计算方式和赔偿标准需要进一步明确",
deduction_score: -3,
status: "rejected",
created_at: "2024-01-13 09:15:00",
is_vote: false,
voter_count: 2,
proposer_name: "王五",
current_user_is_proposer: false
},
{
id: 4,
evaluation_point_id: 103,
document_id: documentId,
audit_point: "合同条款审查",
found_issue: "违约责任条款不明确",
audit_opinion: "合同中违约责任的具体计算方式和赔偿标准需要进一步明确",
deduction_score: -3,
status: "rejected",
created_at: "2024-01-13 09:15:00",
is_vote: false,
voter_count: 2,
proposer_name: "王五",
current_user_is_proposer: false
},
{
id: 5,
evaluation_point_id: 103,
document_id: documentId,
audit_point: "合同条款审查",
found_issue: "违约责任条款不明确",
audit_opinion: "合同中违约责任的具体计算方式和赔偿标准需要进一步明确",
deduction_score: -3,
status: "rejected",
created_at: "2024-01-13 09:15:00",
is_vote: false,
voter_count: 2,
proposer_name: "王五",
current_user_is_proposer: false
},
{
id: 6,
evaluation_point_id: 103,
document_id: documentId,
audit_point: "合同条款审查",
found_issue: "违约责任条款不明确",
audit_opinion: "合同中违约责任的具体计算方式和赔偿标准需要进一步明确",
deduction_score: -3,
status: "rejected",
created_at: "2024-01-13 09:15:00",
is_vote: false,
voter_count: 2,
proposer_name: "王五",
current_user_is_proposer: false
},
{
id: 7,
evaluation_point_id: 103,
document_id: documentId,
audit_point: "合同条款审查",
found_issue: "违约责任条款不明确",
audit_opinion: "合同中违约责任的具体计算方式和赔偿标准需要进一步明确",
deduction_score: -3,
status: "rejected",
created_at: "2024-01-13 09:15:00",
is_vote: false,
voter_count: 2,
proposer_name: "王五",
current_user_is_proposer: false
},
{
id: 8,
evaluation_point_id: 103,
document_id: documentId,
audit_point: "合同条款审查",
found_issue: "违约责任条款不明确",
audit_opinion: "合同中违约责任的具体计算方式和赔偿标准需要进一步明确",
deduction_score: -3,
status: "rejected",
created_at: "2024-01-13 09:15:00",
is_vote: false,
voter_count: 2,
proposer_name: "王五",
current_user_is_proposer: false
},
{
id: 9,
evaluation_point_id: 103,
document_id: documentId,
audit_point: "合同条款审查",
found_issue: "违约责任条款不明确",
audit_opinion: "合同中违约责任的具体计算方式和赔偿标准需要进一步明确",
deduction_score: -3,
status: "rejected",
created_at: "2024-01-13 09:15:00",
is_vote: false,
voter_count: 2,
proposer_name: "王五",
current_user_is_proposer: false
},
{
id: 10,
evaluation_point_id: 103,
document_id: documentId,
audit_point: "合同条款审查",
found_issue: "违约责任条款不明确",
audit_opinion: "合同中违约责任的具体计算方式和赔偿标准需要进一步明确",
deduction_score: -3,
status: "rejected",
created_at: "2024-01-13 09:15:00",
is_vote: false,
voter_count: 2,
proposer_name: "王五",
current_user_is_proposer: false
},
{
id: 11,
evaluation_point_id: 103,
document_id: documentId,
audit_point: "合同条款审查",
found_issue: "违约责任条款不明确",
audit_opinion: "合同中违约责任的具体计算方式和赔偿标准需要进一步明确",
deduction_score: -3,
status: "rejected",
created_at: "2024-01-13 09:15:00",
is_vote: false,
voter_count: 2,
proposer_name: "王五",
current_user_is_proposer: false
},
];
if (response.error) {
return {
error: response.error,
status: response.status || 500
};
}
// 模拟分页
const total = mockOpinions.length;
const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedOpinions = mockOpinions.slice(startIndex, endIndex);
return {
data: (response.data as CrossCheckingOpinion[]) || []
data: {
opinions: paginatedOpinions,
total: total
}
};
} catch (error) {
console.error('获取交叉评查意见失败:', error);
@@ -128,3 +305,61 @@ export async function getCrossCheckingOpinions(documentId: string | number): Pro
};
}
}
/**
* 意见操作类型
*/
export type OpinionActionType = 'agree' | 'disagree' | 'withdraw_vote' | 'withdraw_opinion';
/**
* 意见操作请求参数
*/
export interface OpinionActionRequest {
opinionId: string | number;
action: OpinionActionType;
}
/**
* 执行意见操作(赞同、反对、撤销投票、撤销意见)
* @param actionData 操作数据
* @returns 操作结果
*/
export async function performOpinionAction(
actionData: OpinionActionRequest
): Promise<ApiResponse<{ success: boolean; message: string }>> {
try {
// 模拟API调用延迟
await new Promise(resolve => setTimeout(resolve, 500));
let message = '';
switch (actionData.action) {
case 'agree':
message = '已赞同该意见';
break;
case 'disagree':
message = '已反对该意见';
break;
case 'withdraw_vote':
message = '已撤销投票';
break;
case 'withdraw_opinion':
message = '已撤销意见';
break;
default:
throw new Error('无效的操作类型');
}
return {
data: {
success: true,
message: message
}
};
} catch (error) {
console.error('执行意见操作失败:', error);
return {
error: error instanceof Error ? error.message : '操作失败',
status: 500
};
}
}
+48 -9
View File
@@ -109,7 +109,7 @@ const mockTasks: CrossCheckingTask[] = [
status: CrossCheckingTaskStatus.COMPLETED,
score: 95,
operation: '查看结果',
documentIds: [1, 2, 3, 4, 5]
documentIds: [1355, 1356, 1357, 1358, 1359, 1360, 1361, 1362, 1363, 1364, 1365, 1366, 1367, 1368, 1369, 1370,1371,1372,1373,1374]
},
{
id: 4,
@@ -148,7 +148,7 @@ const mockTasks: CrossCheckingTask[] = [
*/
export async function getCrossCheckingTasks(params: TaskListParams = {}): Promise<ApiResponse<TaskListResponse>> {
try {
// 模拟API延迟
// TODO 这个需要对接获取交叉评查任务列表的接口 模拟API延迟
await new Promise(resolve => setTimeout(resolve, 500));
const {
@@ -296,15 +296,24 @@ export async function deleteCrossCheckingTask(taskId: number): Promise<ApiRespon
}
/**
* 获取任务详情
* 获取任务详情及相关文档
* @param taskId 任务ID
* @returns 任务详情
* @param documentIds 指定的文档ID数组,用于筛选任务包含的文档
* @param page 页码,默认为1
* @param pageSize 每页大小,默认为10
* @returns 任务详情和文档列表
*/
export async function getCrossCheckingTaskDetail(taskId: number): Promise<ApiResponse<CrossCheckingTask>> {
export async function getCrossCheckingTaskDetail(
taskId: number,
documentIds: number[],
page: number = 1,
pageSize: number = 10
): Promise<ApiResponse<{
task: CrossCheckingTask;
files: import('../evaluation_points/rules-files').ReviewFileUI[];
total: number;
}>> {
try {
// 模拟API延迟
await new Promise(resolve => setTimeout(resolve, 300));
const task = mockTasks.find(t => t.id === taskId);
if (!task) {
return {
@@ -313,9 +322,39 @@ export async function getCrossCheckingTaskDetail(taskId: number): Promise<ApiRes
};
}
let files: import('../evaluation_points/rules-files').ReviewFileUI[] = [];
let total = 0;
// 如果提供了documentIds,则调用getReviewFiles获取相关文档
if (documentIds && documentIds.length > 0) {
const { getReviewFiles } = await import('../evaluation_points/rules-files');
const reviewFilesResponse = await getReviewFiles({
page: page,
pageSize: pageSize,
sortOrder: 'upload_time_desc'
}, documentIds);
if (reviewFilesResponse.error) {
return {
success: false,
error: reviewFilesResponse.error
};
}
files = reviewFilesResponse.data?.files || [];
total = reviewFilesResponse.data?.total || 0;
}
console.log('files', files);
return {
success: true,
data: task
data: {
task,
files,
total
}
};
} catch (error) {
console.error('获取任务详情失败:', error);
+1 -1
View File
@@ -308,7 +308,7 @@ export async function getReviewPoints(fileId: string) {
// console.log('groupsMap-------', groupsMap);
//从scoring_proposals表中获取评分提案数据,用于交叉评查
//从cross_scoring_proposals表中获取评分提案数据,用于交叉评查
const scoringProposalsParams: PostgrestParams = {
select: '*',
filter: {
+2 -1
View File
@@ -204,7 +204,7 @@ export function getFileExtension(fileName: string): string {
* @param searchParams 搜索参数
* @returns 评查文件列表和总数
*/
export async function getReviewFiles(searchParams: DocumentSearchParams = {}): Promise<{
export async function getReviewFiles(searchParams: DocumentSearchParams = {}, documentIds: number[] | null = null): Promise<{
data?: { files: ReviewFileUI[], total: number };
error?: string;
status?: number;
@@ -241,6 +241,7 @@ export async function getReviewFiles(searchParams: DocumentSearchParams = {}): P
p_evaluations_status: reviewStatus ? mapUIToReviewStatus(reviewStatus) : null,
p_date_from: dateFrom || null,
p_date_to: dateTo || null,
p_document_ids: documentIds || null,
};
const listParams = {
@@ -0,0 +1,286 @@
import { Modal } from '../ui/Modal';
import { Table } from '../ui/Table';
import { Button } from '../ui/Button';
import { FileIcon } from '../ui/FileIcon';
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 { toastService } from '../ui/Toast';
// 导出样式链接
export const links = () => [];
interface DocumentListModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
files: ReviewFileUI[];
onViewFile?: (fileId: string) => void;
loading?: boolean;
// 分页相关属性
currentPage?: number;
pageSize?: number;
total?: number;
onPageChange?: (page: number) => void;
onPageSizeChange?: (size: number) => void;
}
export function DocumentListModal({
isOpen,
onClose,
title,
files,
onViewFile,
loading = false,
// 分页属性,使用默认值
currentPage = 1,
pageSize = 10,
total = 0,
onPageChange,
onPageSizeChange
}: DocumentListModalProps) {
// 查看评查文件
const handleReviewFileClick = async (fileId: string, auditStatus: number | null) => {
// 检查audit_status是否为0,如果是则更新为2
if (auditStatus === 0 || auditStatus === null) {
try {
const response = await updateDocumentAuditStatus(fileId, 2);
if (response.error) {
throw new Error(response.error);
}
} catch (error) {
console.error('更新文件审核状态时出错:', error);
toastService.error(`更新文件审核状态时出错:${error instanceof Error ? error.message : '未知错误'}`);
return;
}
}
// 如果有自定义的查看处理函数,则调用它
if (onViewFile) {
onViewFile(fileId);
}
};
// 渲染问题摘要
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>
);
}
// 显示问题列表
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 text-secondary">-</div>;
};
// 定义表格列配置
const columns = [
{
title: "文件名称",
key: "fileName",
width: "30%",
render: (_: unknown, file: ReviewFileUI) => (
<div className="flex">
<div className="flex-shrink-0 flex items-center self-center">
<FileIcon fileName={file.fileName} 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="text-xs text-secondary mt-2">
{file.fileCode}
</div>
</div>
</div>
)
},
{
title: "文件类型",
key: "fileType",
width: "12%",
render: (_: unknown, file: ReviewFileUI) => (
<FileTypeTag
type="other"
typeName={file.fileType}
text={file.fileType}
size="sm"
showIcon={false}
colorMode="light"
/>
)
},
{
title: "上传时间",
key: "uploadTime",
width: "12%",
render: (_: unknown, file: ReviewFileUI) => {
const [date, time] = file.uploadTime.split(' ');
return (
<div>
<span className="text-base">{date}</span>
<br />
<span className="text-xs text-secondary">{time}</span>
</div>
);
}
},
{
title: "评查统计",
key: "reviewStatus",
width: "12%",
render: (_: unknown, file: ReviewFileUI) =>
// 要文件切分处理完之后,再显示评查统计
file.status === 'Processed' ? (
<div>
{file.passCount > 0 && (
<StatusBadge
status="pass"
text={`通过(${file.passCount})`}
showIcon={true}
className="my-2"
/>
)}
{file.warningCount > 0 && (
<StatusBadge
status="warning"
text={`警告(${file.warningCount})`}
showIcon={true}
className="my-2"
/>
)}
{file.failCount > 0 && (
<StatusBadge
status="fail"
text={`不通过(${file.failCount})`}
showIcon={true}
className="my-2"
/>
)}
</div>
) : (
<div className="text-sm">
-
</div>
)
},
{
title: "问题摘要",
key: "issues",
width: "20%",
render: (_: unknown, file: ReviewFileUI) => renderIssues(file)
},
{
title: "操作",
key: "operation",
width: "14%",
render: (_: unknown, file: ReviewFileUI) => (
<>
<Button
type="default"
size="small"
icon="ri-eye-line"
onClick={() => handleReviewFileClick(file.id, file.auditStatus)}
disabled={file.status !== 'Processed'}
className="mr-2"
>
</Button>
</>
)
}
];
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={title}
size="full"
className="document-list-modal"
>
<div className="px-6 py-4">
{loading ? (
// 显示loading状态
<div className="py-8">
<LoadingIndicator text="正在加载文档列表..." />
</div>
) : files.length === 0 ? (
// 无数据状态
<div className="text-center py-8 text-gray-500">
</div>
) : (
// 有数据时显示表格和分页
<>
<div className="mb-4 flex items-center">
<i className="ri-file-list-3-line text-primary text-lg mr-2"></i>
<span className="text-sm text-secondary"></span>
<span className="text-base font-normal text-primary ml-1 mr-1">{total || files.length}</span>
<span className="text-sm text-secondary"></span>
</div>
<Table
columns={columns}
dataSource={files}
rowKey="id"
emptyText="暂无文件数据"
className="files-table table-auto-height"
/>
{/* 分页组件 - 只有在提供了分页回调函数且总数大于每页大小时才显示 */}
{(onPageChange || onPageSizeChange) && total > pageSize ? (
<Pagination
currentPage={currentPage}
total={total}
pageSize={pageSize}
onChange={onPageChange || (() => {})}
onPageSizeChange={onPageSizeChange}
showTotal={true}
showPageSizeChanger={!!onPageSizeChange}
pageSizeOptions={[10, 20, 30, 50]}
/>
) : (
<div className="text-sm text-gray-500 mt-4 text-center">
{total} {pageSize}
{total <= pageSize && " (无需分页)"}
</div>
)}
</>
)}
</div>
</Modal>
);
}
@@ -21,7 +21,18 @@ import { toastService } from '../ui/Toast';
import { createPortal } from 'react-dom'; // 导入React Portal API,用于将组件渲染到DOM树的不同位置
import { Tooltip } from '../ui/Tooltip';
import { Modal } from '../ui/Modal';
import { submitCrossCheckingOpinion, type SubmitOpinionRequest } from '../../api/cross-checking/cross-file-result';
import { Table } from '../ui/Table';
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 '../../styles/components/TooltipStyles.css';
/**
@@ -106,6 +117,8 @@ export interface ReviewPoint {
actionContent?: string;
failMessage?: string;
passMessage?: string;
score?: number; // 评查点满分
finalScore?: number; // 评查点已获得分数
evaluationConfig?: {
rules?: Array<{
type: string;
@@ -445,6 +458,15 @@ export function ReviewPointsList({
});
const [isSubmittingOpinion, setIsSubmittingOpinion] = useState(false);
// 意见列表模态框相关状态
const [isOpinionListModalOpen, setIsOpinionListModalOpen] = useState(false);
const [opinionListData, setOpinionListData] = useState<CrossCheckingOpinion[]>([]);
const [opinionListLoading, setOpinionListLoading] = useState(false);
const [opinionListTotal, setOpinionListTotal] = useState(0);
const [opinionListCurrentPage, setOpinionListCurrentPage] = useState(1);
const [opinionListPageSize, setOpinionListPageSize] = useState(10);
const [performingAction, setPerformingAction] = useState<string | null>(null);
// 存放评查点ID与有效页码的映射
const [effectivePages, setEffectivePages] = useState<Record<string, number>>({});
@@ -491,6 +513,110 @@ export function ReviewPointsList({
}));
};
/**
* 加载意见列表数据
*/
const loadOpinionListData = async (page: number = 1, pageSize: number = 10) => {
console.log('加载意见列表数据', selectedReviewPoint);
if (!selectedReviewPoint?.documentId) return;
setOpinionListLoading(true);
try {
console.log('加载意见列表数据', selectedReviewPoint.documentId, page, pageSize);
const response = await getCrossCheckingOpinions(selectedReviewPoint.documentId, page, pageSize);
console.log('意见列表数据', response);
if (response.error) {
toastService.error(response.error);
return;
}
setOpinionListData(response.data?.opinions || []);
setOpinionListTotal(response.data?.total || 0);
setOpinionListCurrentPage(page);
setOpinionListPageSize(pageSize);
} catch (error) {
console.error('加载意见列表失败:', error);
toastService.error('加载意见列表失败');
} finally {
setOpinionListLoading(false);
}
};
/**
* 打开意见列表模态框
*/
const handleOpenOpinionListModal = (reviewPoint: ReviewPoint) => {
console.log('查看reviewPoints', reviewPoints);
if (scoringProposals.length+1 === 0) {
toastService.warning('当前文件尚未有人提出过意见');
return;
}
setSelectedReviewPoint(reviewPoint);
setIsOpinionListModalOpen(true);
console.log('打开意见列表模态框');
loadOpinionListData(1, 10);
};
/**
* 关闭意见列表模态框
*/
const handleCloseOpinionListModal = () => {
setIsOpinionListModalOpen(false);
setOpinionListData([]);
setOpinionListTotal(0);
setOpinionListCurrentPage(1);
setOpinionListPageSize(10);
};
/**
* 处理意见操作(赞同、反对、撤销投票、撤销意见)
*/
const handleOpinionAction = async (opinionId: string | number, action: OpinionActionType) => {
const actionKey = `${opinionId}-${action}`;
setPerformingAction(actionKey);
try {
const response = await performOpinionAction({ opinionId, action });
if (response.error) {
toastService.error(response.error);
return;
}
toastService.success(response.data?.message || '操作成功');
// 重新加载数据
await loadOpinionListData(opinionListCurrentPage, opinionListPageSize);
} catch (error) {
console.error('操作失败:', error);
toastService.error('操作失败,请稍后重试');
} finally {
setPerformingAction(null);
}
};
/**
* 处理意见列表分页变化
*/
const handleOpinionListPageChange = (page: number) => {
loadOpinionListData(page, opinionListPageSize);
};
/**
* 处理意见列表每页大小变化
*/
const handleOpinionListPageSizeChange = (size: number) => {
loadOpinionListData(1, size);
};
/**
* 刷新意见列表
*/
const handleRefreshOpinionList = () => {
loadOpinionListData(opinionListCurrentPage, opinionListPageSize);
};
/**
* 提交意见
*/
@@ -2110,7 +2236,12 @@ export function ReviewPointsList({
<>
<div className="relative">
{/* 悬浮的意见数量显示 - 固定在左侧 */}
<div className="absolute left-[-35px] top-16 z-10 group cursor-pointer">
<button
className="absolute left-[-35px] top-16 z-10 group cursor-pointer"
onClick={() => handleOpenOpinionListModal(reviewPoints[0])}
type="button"
aria-label="查看意见列表"
>
{/* 默认状态:竖向排列,窄宽度 */}
<div className="flex flex-col items-center bg-blue-50 px-2 py-2 rounded-lg border border-blue-200 shadow-md transition-all duration-300 group-hover:scale-0 group-hover:opacity-0 origin-top-right">
<i className="ri-chat-1-line text-blue-600 text-base"></i>
@@ -2132,7 +2263,7 @@ export function ReviewPointsList({
</div>
</button>
</div>
</div>
</button>
<div className="review-points-panel select-text">
<TooltipPortal />
@@ -2292,7 +2423,9 @@ export function ReviewPointsList({
{/* 扣分 */}
<div>
<label htmlFor="deduction-score" className="block text-sm font-medium text-gray-700 mb-2">
<span className="text-red-500">*</span>
(+/-)
<span className="text-red-500 ml-1">*</span>
<span className="text-xs text-gray-500 ml-1"> {selectedReviewPoint?.score} , {selectedReviewPoint?.finalScore} </span>
</label>
<input
id="deduction-score"
@@ -2300,23 +2433,201 @@ export function ReviewPointsList({
value={opinionForm.deductionScore}
onChange={(e) => {
const value = parseFloat(e.target.value);
if (!isNaN(value) && value >= 0) {
if (!isNaN(value)) {
// 限制到1位小数
const roundedValue = Math.round(value * 10) / 10;
handleOpinionFormChange('deductionScore', roundedValue);
}
}}
step="0.1"
min="0"
min="-100"
max="100"
placeholder="0.0"
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-1 focus:ring-green-700 focus:border-green-700 focus:outline-none"
/>
<p className="mt-1 text-xs text-gray-500">1</p>
<p className="mt-1 text-xs text-gray-500">1</p>
</div>
</div>
</Modal>
</div>
{/* 意见列表模态框 */}
<Modal
isOpen={isOpinionListModalOpen}
onClose={handleCloseOpinionListModal}
title="意见列表"
size="full"
className="opinion-list-modal"
>
<div className="px-6 py-4">
{/* 刷新按钮 */}
<div className="mb-4 flex justify-between items-center">
<div className="flex items-center">
<i className="ri-chat-1-line text-primary text-lg mr-2"></i>
<span className="text-sm text-secondary"></span>
<span className="text-base font-normal text-primary ml-1 mr-1">{opinionListTotal}</span>
<span className="text-sm text-secondary"></span>
</div>
<Button
type="default"
size="small"
icon="ri-refresh-line"
onClick={handleRefreshOpinionList}
disabled={opinionListLoading}
>
</Button>
</div>
{opinionListLoading ? (
<div className="py-8">
<LoadingIndicator text="正在加载意见列表..." />
</div>
) : opinionListData.length === 0 ? (
<div className="text-center py-8 text-gray-500">
</div>
) : (
<>
<Table
columns={[
{
title: "审查点名称",
key: "audit_point",
width: "15%",
render: (_: unknown, record: CrossCheckingOpinion) => (
<div className="text-sm">{record.audit_point}</div>
)
},
{
title: "发现问题",
key: "found_issue",
width: "20%",
render: (_: unknown, record: CrossCheckingOpinion) => (
<div className="text-sm text-left">{record.found_issue}</div>
)
},
{
title: "审查意见",
key: "audit_opinion",
width: "25%",
render: (_: unknown, record: CrossCheckingOpinion) => (
<div className="text-sm text-left">{record.audit_opinion}</div>
)
},
{
title: "评分",
key: "deduction_score",
width: "8%",
align: "center" as const,
render: (_: unknown, record: CrossCheckingOpinion) => (
<span className={`text-sm font-medium ${record.deduction_score >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{record.deduction_score > 0 ? '+' : ''}{record.deduction_score}
</span>
)
},
{
title: "投票人",
key: "voter_count",
width: "8%",
align: "center" as const,
render: (_: unknown, record: CrossCheckingOpinion) => (
<span className="text-sm">{record.voter_count}</span>
)
},
{
title: "意见发起人",
key: "proposer_name",
width: "10%",
render: (_: unknown, record: CrossCheckingOpinion) => (
<div className="text-sm">{record.proposer_name}</div>
)
},
{
title: "操作",
key: "operation",
width: "14%",
align: "center" as const,
render: (_: unknown, record: CrossCheckingOpinion) => {
const isPerforming = (action: string) => performingAction === `${record.id}-${action}`;
return (
<div className="flex flex-wrap gap-1">
{/* 根据is_vote字段显示不同按钮 */}
{!record.is_vote ? (
<>
<Button
type="default"
size="small"
onClick={() => handleOpinionAction(record.id, 'agree')}
disabled={isPerforming('agree')}
className="text-green-600 border-green-600 hover:bg-green-50"
>
{isPerforming('agree') ? '处理中...' : '赞同'}
</Button>
<Button
type="default"
size="small"
onClick={() => handleOpinionAction(record.id, 'disagree')}
disabled={isPerforming('disagree')}
className="text-red-600 border-red-600 hover:bg-red-50"
>
{isPerforming('disagree') ? '处理中...' : '反对'}
</Button>
</>
) : (
<Button
type="default"
size="small"
onClick={() => handleOpinionAction(record.id, 'withdraw_vote')}
disabled={isPerforming('withdraw_vote')}
className="text-orange-600 border-orange-600 hover:bg-orange-50"
>
{isPerforming('withdraw_vote') ? '处理中...' : '撤销投票'}
</Button>
)}
{/* 如果当前用户是意见发起人,显示撤销意见按钮 */}
{record.current_user_is_proposer && (
<Button
type="danger"
size="small"
onClick={() => handleOpinionAction(record.id, 'withdraw_opinion')}
disabled={isPerforming('withdraw_opinion')}
>
{isPerforming('withdraw_opinion') ? '处理中...' : '撤销意见'}
</Button>
)}
</div>
);
}
}
]}
dataSource={opinionListData}
rowKey="id"
emptyText="暂无意见数据"
className="opinion-list-table"
/>
{/* 分页组件 */}
{opinionListTotal > opinionListPageSize && (
<Pagination
currentPage={opinionListCurrentPage}
total={opinionListTotal}
pageSize={opinionListPageSize}
onChange={handleOpinionListPageChange}
onPageSizeChange={handleOpinionListPageSizeChange}
showTotal={true}
showPageSizeChanger={true}
pageSizeOptions={[10, 20, 30, 50]}
/>
)}
</>
)}
</div>
</Modal>
</div>
</>
);
}
+1
View File
@@ -6,3 +6,4 @@ export { FileInfo } from './FileInfo';
export { FilePreview } from './FilePreview';
export { ReviewPointsList } from './ReviewPointsList';
export type { ReviewPoint } from './ReviewPointsList';
export { DocumentListModal } from './DocumentListModal';
+2 -2
View File
@@ -32,9 +32,9 @@ interface ApiConfig {
const configs: Record<string, ApiConfig> = {
// 开发环境
development: {
baseUrl: 'http://172.16.0.55:8008',
// baseUrl: 'http://172.16.0.55:8008',
// baseUrl: 'http://172.16.0.81:3000',
// baseUrl: 'http://nas.7bm.co:3000',
baseUrl: 'http://nas.7bm.co:3000',
// documentUrl: 'http://172.16.0.81:9000/docauditai/',
documentUrl: 'http://172.16.0.55:8008/docauditai/',
uploadUrl: 'http://172.16.0.55:8008/admin/documents',
+131 -4
View File
@@ -4,6 +4,7 @@ import { useLoaderData, useSearchParams, useNavigate, useFetcher } from "@remix-
import { Button } from '~/components/ui/Button';
import { Card } from '~/components/ui/Card';
import { Tag } from '~/components/ui/Tag';
import { DocumentListModal } from '~/components/cross-checking';
import crossCheckingStyles from "~/styles/pages/cross-checking_index.css?url";
import { Table } from '~/components/ui/Table';
@@ -14,12 +15,14 @@ import {
getCrossCheckingTasks,
getCrossCheckingStats,
deleteCrossCheckingTask,
getCrossCheckingTaskDetail,
type CrossCheckingTask,
type TaskListParams,
CrossCheckingTaskStatus,
CrossCheckingTaskType,
CrossCheckingDocType
} from '~/api/cross-checking/cross-files';
import type { ReviewFileUI } from '~/api/evaluation_points/rules-files';
export const links = () => [
{ rel: "stylesheet", href: crossCheckingStyles }
@@ -168,6 +171,24 @@ export default function CrossCheckingIndex() {
// 状态管理
const [isDeleting, setIsDeleting] = useState(false);
const [modalState, setModalState] = useState<{
isOpen: boolean;
title: string;
files: ReviewFileUI[];
loading: boolean;
// 分页相关状态
currentPage: number;
pageSize: number;
total: number;
}>({
isOpen: false,
title: '',
files: [],
loading: false,
currentPage: 1,
pageSize: 10,
total: 0
});
// 获取进度条样式类
const getProgressClass = (progress: number) => {
@@ -176,10 +197,101 @@ export default function CrossCheckingIndex() {
return 'high';
};
// 处理查看结果
// const handleViewResult = (taskId: number, docIds: number[]) => {
// // 根据taskId获取关联的
// };
// 处理查看结果 - 打开文档列表模态框
const handleViewResult = async (taskId: number, documentIds: number[]) => {
// 存储任务信息用于分页
setCurrentTaskInfo({ taskId, documentIds });
// 打开模态框
setModalState(prev => ({
...prev,
isOpen: true,
currentPage: 1,
pageSize: 10
}));
// 加载第一页数据
await loadModalData(taskId, documentIds, 1, 10);
};
// 关闭模态框
const handleCloseModal = () => {
setModalState({
isOpen: false,
title: '',
files: [],
loading: false,
currentPage: 1,
pageSize: 10,
total: 0
});
setCurrentTaskInfo(null);
};
// 处理文档查看 - 导航到评查详情页
const handleViewFile = (fileId: string) => {
navigate(`/cross-checking/result?id=${fileId}&previousRoute=crossChecking`);
};
// 存储当前任务信息用于分页
const [currentTaskInfo, setCurrentTaskInfo] = useState<{
taskId: number;
documentIds: number[];
} | null>(null);
// 加载分页数据
const loadModalData = async (taskId: number, documentIds: number[], page: number = 1, pageSize: number = 10) => {
try {
setModalState(prev => ({
...prev,
loading: true
}));
// 调用支持分页的API,传递分页参数
const response = await getCrossCheckingTaskDetail(taskId, documentIds, page, pageSize);
if (response.error) {
throw new Error(response.error);
}
const { task, files, total } = response.data!;
setModalState(prev => ({
...prev,
loading: false,
title: `${task.taskName} - 文档列表`,
files: files,
total: total,
currentPage: page,
pageSize: pageSize
}));
} catch (error) {
console.error('获取任务文档列表失败:', error);
toastService.error(`获取任务文档列表失败: ${error instanceof Error ? error.message : '未知错误'}`);
setModalState(prev => ({
...prev,
loading: false
}));
}
};
// 处理模态框分页变化
const handleModalPageChange = (page: number) => {
if (currentTaskInfo) {
loadModalData(currentTaskInfo.taskId, currentTaskInfo.documentIds, page, modalState.pageSize);
}
};
// 处理模态框每页大小变化
const handleModalPageSizeChange = (size: number) => {
if (currentTaskInfo) {
loadModalData(currentTaskInfo.taskId, currentTaskInfo.documentIds, 1, size);
}
};
// 渲染进度条
const renderProgress = (progress: number) => (
@@ -548,6 +660,21 @@ export default function CrossCheckingIndex() {
)}
</div>
</Card>
{/* 文档列表模态框 */}
<DocumentListModal
isOpen={modalState.isOpen}
onClose={handleCloseModal}
title={modalState.title}
files={modalState.files}
onViewFile={handleViewFile}
loading={modalState.loading}
currentPage={modalState.currentPage}
pageSize={modalState.pageSize}
total={modalState.total}
onPageChange={handleModalPageChange}
onPageSizeChange={handleModalPageSizeChange}
/>
</div>
);
}
+1 -1
View File
@@ -439,7 +439,7 @@ export default function CrossCheckingResult() {
// 添加前置路由
if (loaderData.previousRoute) {
if (loaderData.previousRoute === 'cross-checking') {
if (loaderData.previousRoute === 'crossChecking') {
items.unshift({ title: "交叉评查", to: "/cross-checking" });
}
}