测通完成评查,投票,意见列表,任务列表,任务关联文档列表的内容。剩余创建任务,提出意见的完善

This commit is contained in:
2025-07-23 10:22:51 +08:00
parent 47664fc0e8
commit 8800e982ab
13 changed files with 750 additions and 331 deletions
@@ -65,6 +65,31 @@ export function DocumentListModal({
onViewFile(fileId);
}
};
// 审核状态选项及样式 - 与documents._index.tsx保持一致
const auditStatusMapping: Record<string, { label: string; color: string; icon: string }> = {
"-1": { label: "不通过", color: "red", icon: "ri-close-line" },
"-2": { label: "警告", color: "yellow", icon: "ri-alert-line" },
"0": { label: "待审核", color: "blue", icon: "ri-time-line" },
"1": { label: "通过", color: "green", icon: "ri-check-line" },
"2": { label: "审核中", color: "purple", icon: "ri-search-line" },
};
// 渲染审核状态
const renderAuditStatus = (file: TaskDocument) => {
// 处理audit_status为null或undefined的情况,默认为0(待审核)
const auditStatus = file.audit_status != null ? file.audit_status : 0;
const statusKey = auditStatus.toString();
const statusInfo = auditStatusMapping[statusKey] || auditStatusMapping["0"];
return (
<div className={`inline-flex items-center px-2 py-1 rounded-full text-xs bg-${statusInfo.color}-100 text-${statusInfo.color}-800`}>
<i className={`${statusInfo.icon} mr-1`}></i>
<span>{statusInfo.label}</span>
</div>
);
};
// 渲染问题摘要
const renderIssues = (file: TaskDocument) => {
@@ -143,7 +168,7 @@ export function DocumentListModal({
{
title: "文件类型",
key: "fileType",
width: "10%",
width: "8%",
render: (_: unknown, file: TaskDocument) => (
<FileTypeTag
type="other"
@@ -158,7 +183,7 @@ export function DocumentListModal({
{
title: "上传时间",
key: "uploadTime",
width: "12%",
width: "8%",
render: (_: unknown, file: TaskDocument) => {
const uploadTime = formatDate(file.upload_time).split(' ');
const date = uploadTime[0];
@@ -175,7 +200,7 @@ export function DocumentListModal({
{
title: "评查统计",
key: "reviewStatus",
width: "12%",
width: "10%",
render: (_: unknown, file: TaskDocument) =>
// 要文件切分处理完之后,再显示评查统计
file.status === 'Processed' ? (
@@ -225,7 +250,7 @@ export function DocumentListModal({
key: "score",
width: "8%",
render: (_: unknown, file: TaskDocument) => (
<div className="text-center">
<div className="text-left">
{file.final_score ? (
<span className={`font-medium ${
file.final_score >= 90 ? 'text-green-600' :
@@ -240,6 +265,12 @@ export function DocumentListModal({
</div>
)
},
{
title: '审核状态',
key: 'auditStatus',
width: '8%',
render: (_: unknown, file: TaskDocument) => renderAuditStatus(file)
},
{
title: "问题摘要",
key: "issues",
@@ -322,4 +353,4 @@ export function DocumentListModal({
</div>
</Modal>
);
}
}
+133 -103
View File
@@ -30,7 +30,7 @@ import {
type CrossCheckingOpinion,
type OpinionActionType
} from '../../api/cross-checking/cross-file-result';
import { useFetcher } from '@remix-run/react';
import { useFetcher, useNavigate } from '@remix-run/react';
// import '../../styles/components/TooltipStyles.css';
/**
@@ -159,6 +159,11 @@ interface ScoringProposal {
document_id: string | number;
}
interface UserInfo {
id: number;
[key: string]: unknown;
}
interface ReviewPointsListProps {
reviewPoints: ReviewPoint[];
statistics: Statistics;
@@ -167,6 +172,7 @@ interface ReviewPointsListProps {
onStatusChange?: (id: string, editAuditStatusId: string | number, status: string, message: string) => void;
scoringProposals?: ScoringProposal[];
jwtToken?: string; // 添加JWT token参数
userInfo?: UserInfo; // 添加用户信息参数
}
/**
@@ -424,7 +430,8 @@ export function ReviewPointsList({
activeReviewPointResultId,
onReviewPointSelect,
scoringProposals = [],
jwtToken
jwtToken,
userInfo
}: ReviewPointsListProps) {
// 状态管理
const [searchText, setSearchText] = useState(''); // 搜索文本
@@ -436,7 +443,7 @@ export function ReviewPointsList({
// 将来可以用于显示相关的评分提案信息
useEffect(() => {
if (scoringProposals && scoringProposals.length > 0) {
console.log('收到评分提案数据:', scoringProposals.length, '个提案');
// console.log('收到评分提案数据:', scoringProposals.length, '个提案');
// 获取提案的evaluation_result_id
const evaluationResultIds = scoringProposals.map(proposal => Number(proposal.evaluation_result_id));
setEvaluationResultIds(evaluationResultIds);
@@ -471,27 +478,31 @@ export function ReviewPointsList({
// 监听fetcher状态变化 - 获取意见列表数据
useEffect(() => {
if (fetcher.data && fetcher.state === 'idle' && opinionListLoading) {
const data = fetcher.data as {
success?: boolean;
data?: {
opinions: CrossCheckingOpinion[];
total: number;
};
error?: string;
const data = fetcher.data as {
success?: boolean;
data?: {
opinions: CrossCheckingOpinion[];
total: number;
pagination?: {
page: number;
page_size: number;
total: number;
total_pages: number;
};
};
error?: string;
};
if (data.success && data.data) {
console.log('意见列表数据', data.data);
console.log('data.data', data.data);
setOpinionListData(data.data.opinions || []);
setOpinionListTotal(data.data.total || 0);
// 使用当前状态值而不是依赖项中的值
setOpinionListCurrentPage(prev => prev);
setOpinionListPageSize(prev => prev);
if (data.data.pagination) {
setOpinionListCurrentPage(data.data.pagination.page);
setOpinionListPageSize(data.data.pagination.page_size);
}
} else {
console.error('加载意见列表失败:', data.error);
toastService.error(data.error || '加载意见列表失败');
}
setOpinionListLoading(false);
}
}, [fetcher.data, fetcher.state, opinionListLoading]);
@@ -568,12 +579,11 @@ export function ReviewPointsList({
const loadOpinionListData = async (page: number = 1, pageSize: number = 10, documentId?: string | number) => {
// 使用传入的documentId或者从selectedReviewPoint获取
const targetDocumentId = documentId || selectedReviewPoint?.documentId;
console.log('加载意见列表数据', targetDocumentId);
if (!targetDocumentId) return;
setOpinionListLoading(true);
try {
console.log('加载意见列表数据', targetDocumentId, page, pageSize);
// 使用 fetcher 调用路由的 action
const formData = new FormData();
@@ -595,8 +605,8 @@ export function ReviewPointsList({
* 打开意见列表模态框
*/
const handleOpenOpinionListModal = (reviewPoint: ReviewPoint) => {
console.log('查看reviewPoints', reviewPoints);
if (scoringProposals.length+1 === 0) {
console.log('查看reviewPoint', reviewPoint);
if (scoringProposals.length === 0) {
toastService.warning('当前文件尚未有人提出过意见');
return;
}
@@ -626,7 +636,7 @@ export function ReviewPointsList({
setPerformingAction(actionKey);
try {
const response = await performOpinionAction({ opinionId, action }, jwtToken);
const response = await performOpinionAction({ opinionId, action }, jwtToken, userInfo as { user_id: number } | undefined);
if (response.error) {
toastService.error(response.error);
@@ -634,12 +644,14 @@ export function ReviewPointsList({
}
toastService.success(response.data?.message || '操作成功');
// console.log('即将重新加载数据');
// 重新加载数据
await loadOpinionListData(opinionListCurrentPage, opinionListPageSize);
} catch (error) {
console.error('操作失败:', error);
toastService.error('操作失败,请稍后重试');
toastService.error(error instanceof Error ? error.message : '操作失败,请稍后重试');
} finally {
setPerformingAction(null);
}
@@ -649,6 +661,7 @@ export function ReviewPointsList({
* 处理意见列表分页变化
*/
const handleOpinionListPageChange = (page: number) => {
setOpinionListCurrentPage(page);
loadOpinionListData(page, opinionListPageSize);
};
@@ -656,6 +669,7 @@ export function ReviewPointsList({
* 处理意见列表每页大小变化
*/
const handleOpinionListPageSizeChange = (size: number) => {
setOpinionListPageSize(size);
loadOpinionListData(1, size);
};
@@ -2550,7 +2564,7 @@ export function ReviewPointsList({
{
title: "问题描述",
key: "problem_message",
width: "20%",
width: "18%",
render: (_: unknown, record: CrossCheckingOpinion) => (
<div className="text-sm text-left">{record.problem_message}</div>
)
@@ -2566,7 +2580,7 @@ export function ReviewPointsList({
{
title: "调整分数",
key: "proposed_score",
width: "8%",
width: "5%",
align: "center" as const,
render: (_: unknown, record: CrossCheckingOpinion) => (
<span className={`text-sm font-medium ${record.proposed_score >= 0 ? 'text-green-600' : 'text-red-600'}`}>
@@ -2576,8 +2590,8 @@ export function ReviewPointsList({
},
{
title: "投票人",
key: "voter_count",
width: "8%",
key: "votes",
width: "25%",
align: "center" as const,
render: (_: unknown, record: CrossCheckingOpinion) => {
// 投票类型配置
@@ -2604,7 +2618,6 @@ export function ReviewPointsList({
border: "border border-gray-200"
}
];
return (
<div className="flex flex-col gap-1.5 py-1 min-w-[120px]">
{voterGroups.map((group) => (
@@ -2633,7 +2646,7 @@ export function ReviewPointsList({
{
title: "意见发起人",
key: "proposer",
width: "10%",
width: "4%",
render: (_: unknown, record: CrossCheckingOpinion) => (
<div className="flex items-center justify-center">
<span
@@ -2644,6 +2657,14 @@ export function ReviewPointsList({
</div>
)
},
{
title: "发起时间",
key: "created_at",
width: "18%",
render: (_: unknown, record: CrossCheckingOpinion) => (
<div className="text-sm text-left">{record.created_at}</div>
)
},
{
title: "操作",
key: "operation",
@@ -2652,19 +2673,19 @@ export function ReviewPointsList({
render: (_: unknown, record: CrossCheckingOpinion) => {
const isPerforming = (action: string) => performingAction === `${record.proposal_id}-${action}`;
return (
<OpinionActions record={record} isPerforming={isPerforming} handleOpinionAction={handleOpinionAction} />
<OpinionActions record={record} isPerforming={isPerforming} handleOpinionAction={handleOpinionAction} userInfo={userInfo as { user_id: number } | undefined} />
);
}
}
]}
dataSource={opinionListData}
rowKey="id"
rowKey="proposal_id"
emptyText="暂无意见数据"
className="opinion-list-table"
/>
{/* 分页组件 */}
{opinionListTotal > opinionListPageSize && (
{opinionListTotal > 0 && (
<Pagination
currentPage={opinionListCurrentPage}
total={opinionListTotal}
@@ -2673,7 +2694,7 @@ export function ReviewPointsList({
onPageSizeChange={handleOpinionListPageSizeChange}
showTotal={true}
showPageSizeChanger={true}
pageSizeOptions={[10, 20, 30, 50]}
pageSizeOptions={[5,10, 20, 30, 50]}
/>
)}
</>
@@ -2686,27 +2707,24 @@ export function ReviewPointsList({
}
// 操作按钮区美化+弹窗确认组件
function OpinionActions({ record, isPerforming, handleOpinionAction }: {
function OpinionActions({ record, isPerforming, handleOpinionAction, userInfo }: {
record: CrossCheckingOpinion;
isPerforming: (action: string) => boolean;
handleOpinionAction: (id: string | number, action: OpinionActionType) => void;
userInfo?: { user_id: number };
}) {
const canVote = record.can_vote !== false;
const [showWithdrawModal, setShowWithdrawModal] = useState(false);
const [withdrawType, setWithdrawType] = useState<'withdraw_vote' | 'withdraw_opinion' | null>(null);
const [showModal, setShowModal] = useState<null | OpinionActionType>(null);
const [countdown, setCountdown] = useState(3);
const [counting, setCounting] = useState(false);
const handleWithdraw = (type: 'withdraw_vote' | 'withdraw_opinion') => {
setWithdrawType(type);
setShowWithdrawModal(true);
setCountdown(3);
setCounting(true);
};
useEffect(() => {
let timer: NodeJS.Timeout;
if (showWithdrawModal && counting && countdown > 0) {
if (
showModal &&
(showModal === 'withdraw_opinion' || showModal === 'withdraw_vote') &&
counting &&
countdown > 0
) {
timer = setTimeout(() => {
setCountdown((c) => c - 1);
}, 1000);
@@ -2714,89 +2732,108 @@ function OpinionActions({ record, isPerforming, handleOpinionAction }: {
setCounting(false);
}
return () => clearTimeout(timer);
}, [showWithdrawModal, counting, countdown]);
}, [showModal, counting, countdown]);
const handleWithdrawConfirm = () => {
if (withdrawType && countdown === 0) {
handleOpinionAction(record.proposal_id, withdrawType);
setShowWithdrawModal(false);
setWithdrawType(null);
const handleConfirm = () => {
if (showModal === 'withdraw_opinion' || showModal === 'withdraw_vote') {
if (countdown === 0) {
handleOpinionAction(record.proposal_id, showModal);
setShowModal(null);
setCountdown(3);
setCounting(false);
}
} else {
// 赞同/反对等操作直接执行
handleOpinionAction(record.proposal_id, showModal!);
setShowModal(null);
setCountdown(3);
setCounting(false);
}
};
const handleWithdrawCancel = () => {
setShowWithdrawModal(false);
setWithdrawType(null);
const handleCancel = () => {
setShowModal(null);
setCountdown(3);
setCounting(false);
};
// 判断是否是发起人
const isProposer = userInfo && record.proposer_id === userInfo.user_id;
return (
<div className="flex gap-3">
<Button
type="default"
className="bg-green-700 hover:bg-green-800 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3"
onClick={() => handleOpinionAction(record.proposal_id, 'agree')}
disabled={isPerforming('agree') || !canVote}
>
{isPerforming('agree') ? '处理中...' : '赞同'}
</Button>
<Button
type="default"
className="bg-red-700 hover:bg-red-800 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3"
onClick={() => handleOpinionAction(record.proposal_id, 'disagree')}
disabled={isPerforming('disagree') || !canVote}
>
{isPerforming('disagree') ? '处理中...' : '反对'}
</Button>
{(!canVote || record.is_vote) && (
{/* 仅当can_vote为true时显示赞同/反对按钮 */}
{record.can_vote && (
<>
<Button
type="default"
className="bg-green-700 hover:bg-green-800 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3"
onClick={() => { setShowModal('agree'); }}
disabled={isPerforming('agree')}
>
{isPerforming('agree') ? '处理中...' : '赞同'}
</Button>
<Button
type="default"
className="bg-red-700 hover:bg-red-800 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3"
onClick={() => { setShowModal('disagree'); }}
disabled={isPerforming('disagree')}
>
{isPerforming('disagree') ? '处理中...' : '反对'}
</Button>
</>
)}
{/* 仅当can_vote为false时显示撤销投票按钮 */}
{!record.can_vote && (
<Button
type="default"
className="bg-yellow-600 hover:bg-yellow-700 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3"
onClick={() => handleWithdraw('withdraw_vote')}
onClick={() => { setShowModal('withdraw_vote'); setCounting(true); }}
disabled={isPerforming('withdraw_vote')}
>
{isPerforming('withdraw_vote') ? '处理中...' : '撤销投票'}
</Button>
)}
{record.current_user_is_proposer && (
{/* 仅当是发起人才显示撤销意见按钮 */}
{isProposer && (
<Button
type="default"
className="bg-yellow-600 hover:bg-yellow-700 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3"
onClick={() => handleWithdraw('withdraw_opinion')}
className="bg-yellow-600 hover:bg-red-700 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3"
onClick={() => { setShowModal('withdraw_opinion'); setCounting(true); }}
disabled={isPerforming('withdraw_opinion')}
>
{isPerforming('withdraw_opinion') ? '处理中...' : '撤销意见'}
</Button>
)}
{showWithdrawModal && (
{/* 确认操作模态框 */}
{showModal && (
<Modal
isOpen={showWithdrawModal}
onClose={handleWithdrawCancel}
title="确认撤销"
isOpen={!!showModal}
onClose={handleCancel}
title="确认操作"
size="small"
className=""
footer={
<div className="flex justify-end gap-3">
<Button
type="default"
className="min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap bg-gray-500 hover:bg-gray-600 text-white shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3"
onClick={handleWithdrawCancel}
onClick={handleCancel}
>
</Button>
<Button
type="default"
className={`bg-red-700 hover:bg-red-800 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3 ${countdown > 0 ? 'opacity-60 cursor-not-allowed' : ''}`}
onClick={handleWithdrawConfirm}
disabled={countdown > 0}
className={`bg-green-700 hover:bg-green-800 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3 ${(showModal === 'withdraw_opinion' || showModal === 'withdraw_vote') && countdown > 0 ? 'opacity-60 cursor-not-allowed' : ''}`}
onClick={handleConfirm}
disabled={(showModal === 'withdraw_opinion' || showModal === 'withdraw_vote') && countdown > 0}
>
{countdown > 0 ? `确认撤销(${countdown})` : '确认撤销'}
{(showModal === 'withdraw_opinion' || showModal === 'withdraw_vote') && countdown > 0 ? `确认(${countdown})` : '确认'}
</Button>
</div>
}
>
<div className="flex flex-col items-center justify-center text-base text-gray-700 py-4 text-center">
<div className="mb-2"></div>
<div className="mb-2"></div>
<div className="text-sm text-gray-500"><span className="font-bold text-primary">{record.evaluation_point_name || record.proposal_id}</span></div>
</div>
</Modal>
@@ -2820,6 +2857,8 @@ const openResultModal = (recordId: string) => {
// 交叉评查记录操作按钮组件
export function ActionButtons({ record }: { record: CrossCheckingRecord }) {
const navigate = useNavigate();
// 根据记录状态确定按钮类型
const getButtonConfig = () => {
switch (record.status) {
@@ -2828,22 +2867,14 @@ export function ActionButtons({ record }: { record: CrossCheckingRecord }) {
text: '去评查',
bgColor: 'bg-blue-600',
hoverColor: 'hover:bg-blue-700',
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
)
icon: <span className="ri-edit-2-line text-lg mr-1"></span>
};
case 'in_progress':
return {
text: '进行中',
bgColor: 'bg-gray-500',
hoverColor: 'hover:bg-gray-600',
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1 animate-pulse" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)
icon: <span className="ri-loader-4-line text-lg mr-1 animate-spin"></span>
};
case 'completed':
default:
@@ -2851,23 +2882,22 @@ export function ActionButtons({ record }: { record: CrossCheckingRecord }) {
text: '查看结果',
bgColor: 'bg-green-600',
hoverColor: 'hover:bg-green-700',
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)
icon: <span className="ri-eye-line text-lg mr-1"></span>
};
}
};
const buttonConfig = getButtonConfig();
// 处理按钮点击事件
/**
* 处理按钮点击事件
* 使用React Router的navigate方法替代window.location.href,避免页面刷新
*/
const handleAction = () => {
switch (record.status) {
case 'pending':
// 跳转到评查页面
window.location.href = `/review/${record.id}`;
// 使用navigate跳转到评查页面,避免页面刷新
navigate(`/review/${record.id}`);
break;
case 'in_progress':
// 进行中状态不执行操作
+1 -1
View File
@@ -180,7 +180,7 @@ export function MessageModal({
</h3>
)}
<div id="message-modal-content" className="message-modal-message">
<div id="message-modal-content" className="message-modal-message" style={{ whiteSpace: 'pre-line' }}>
{message}
</div>
+4 -1
View File
@@ -1,5 +1,6 @@
// app/components/ui/Modal.tsx
import React, { useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
import modalStyles from '~/styles/components/modal.css?url';
// 导出样式
@@ -102,7 +103,7 @@ export function Modal({
if (!isOpen) return null;
return (
const modalNode = (
<div
className="modal-backdrop"
aria-hidden="true"
@@ -156,4 +157,6 @@ export function Modal({
/>
</div>
);
return ReactDOM.createPortal(modalNode, document.body);
}
+1
View File
@@ -202,6 +202,7 @@ export function Toast({
aria-live="polite"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={{ zIndex: 99999, position: 'relative' }}
>
<div className="toast-content">
<div className="toast-icon-wrapper">