接入feat(cross-checking): 整合组织架构数据并优化意见列表功能

- 更新 API 配置,使用新的后端服务地址- 移除前端模拟数据,改为从后端获取真实数据- 优化意见列表接口,支持分页和用户身份验证
- 调整前端界面,适应新的数据结构和功能需求
This commit is contained in:
2025-07-20 21:29:42 +08:00
parent e4ce41cebe
commit 4d5ec6cdb7
7 changed files with 758 additions and 597 deletions
@@ -247,7 +247,7 @@ export function DocumentListModal({
<>
<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-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>
@@ -275,7 +275,7 @@ export function DocumentListModal({
) : (
<div className="text-sm text-gray-500 mt-4 text-center">
{total} {pageSize}
{total <= pageSize && " (无需分页)"}
{total <= pageSize && ""}
</div>
)}
</>
@@ -2231,6 +2231,14 @@ export function ReviewPointsList({
return result;
};
// 在ReviewPointsList组件内部
useEffect(() => {
if (isOpinionListModalOpen && selectedReviewPoint?.documentId) {
loadOpinionListData(1, opinionListPageSize);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpinionListModalOpen, selectedReviewPoint?.documentId]);
// 组件主渲染函数
return (
<>
@@ -2492,37 +2500,37 @@ export function ReviewPointsList({
<Table
columns={[
{
title: "查点名称",
key: "audit_point",
title: "查点名称",
key: "evaluation_point_name",
width: "15%",
render: (_: unknown, record: CrossCheckingOpinion) => (
<div className="text-sm">{record.audit_point}</div>
<div className="text-sm">{record.evaluation_point_name}</div>
)
},
{
title: "发现问题",
key: "found_issue",
title: "问题描述",
key: "problem_message",
width: "20%",
render: (_: unknown, record: CrossCheckingOpinion) => (
<div className="text-sm text-left">{record.found_issue}</div>
<div className="text-sm text-left">{record.problem_message}</div>
)
},
{
title: "审查意见",
key: "audit_opinion",
title: "调整理由",
key: "reason",
width: "25%",
render: (_: unknown, record: CrossCheckingOpinion) => (
<div className="text-sm text-left">{record.audit_opinion}</div>
<div className="text-sm text-left">{record.reason}</div>
)
},
{
title: "评分",
key: "deduction_score",
title: "调整分数",
key: "proposed_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 className={`text-sm font-medium ${record.proposed_score >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{record.proposed_score > 0 ? '+' : ''}{record.proposed_score}
</span>
)
},
@@ -2531,74 +2539,80 @@ export function ReviewPointsList({
key: "voter_count",
width: "8%",
align: "center" as const,
render: (_: unknown, record: CrossCheckingOpinion) => (
<span className="text-sm">{record.voter_count}</span>
)
render: (_: unknown, record: CrossCheckingOpinion) => {
// 投票类型配置
const voterGroups = [
{
type: "agree",
voters: record.agree_voters,
color: "text-green-700",
bg: "bg-green-100",
border: "border border-green-200"
},
{
type: "disagree",
voters: record.disagree_voters,
color: "text-red-700",
bg: "bg-red-100",
border: "border border-red-200"
},
{
type: "pending",
voters: record.pending_voters,
color: "text-gray-700",
bg: "bg-gray-100",
border: "border border-gray-200"
}
];
return (
<div className="flex flex-col gap-1.5 py-1 min-w-[120px]">
{voterGroups.map((group) => (
Array.isArray(group.voters) && group.voters.length > 0 && (
<div key={group.type} className="flex flex-wrap gap-1">
{group.voters.map((name, idx) => (
<span
key={`${group.type}-${name}-${idx}`}
className={`
px-1.5 py-0.5 rounded text-xs font-medium
${group.color} ${group.bg} ${group.border}
whitespace-nowrap overflow-hidden text-ellipsis max-w-[80px]
transition-all hover:scale-[1.03] hover:shadow-sm
`}
>
{name}
</span>
))}
</div>
)
))}
</div>
);
}
},
{
title: "意见发起人",
key: "proposer_name",
key: "proposer",
width: "10%",
render: (_: unknown, record: CrossCheckingOpinion) => (
<div className="text-sm">{record.proposer_name}</div>
<div className="flex items-center justify-center">
<span
className="px-1.5 py-0.5 rounded text-xs font-medium text-yellow-700 bg-yellow-100 border border-yellow-200 whitespace-nowrap overflow-hidden text-ellipsis max-w-[80px] transition-all hover:scale-[1.03] hover:shadow-sm"
>
{record.proposer}
</span>
</div>
)
},
{
title: "操作",
key: "operation",
width: "14%",
width: "18%",
align: "center" as const,
render: (_: unknown, record: CrossCheckingOpinion) => {
const isPerforming = (action: string) => performingAction === `${record.id}-${action}`;
const isPerforming = (action: string) => performingAction === `${record.proposal_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>
<OpinionActions record={record} isPerforming={isPerforming} handleOpinionAction={handleOpinionAction} />
);
}
}
@@ -2626,8 +2640,225 @@ export function ReviewPointsList({
)}
</div>
</Modal>
</div>
</>
);
}
}
// 操作按钮区美化+弹窗确认组件
function OpinionActions({ record, isPerforming, handleOpinionAction }: {
record: CrossCheckingOpinion;
isPerforming: (action: string) => boolean;
handleOpinionAction: (id: string | number, action: OpinionActionType) => void;
}) {
const canVote = record.can_vote !== false;
const [showWithdrawModal, setShowWithdrawModal] = useState(false);
const [withdrawType, setWithdrawType] = useState<'withdraw_vote' | 'withdraw_opinion' | null>(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) {
timer = setTimeout(() => {
setCountdown((c) => c - 1);
}, 1000);
} else if (countdown === 0) {
setCounting(false);
}
return () => clearTimeout(timer);
}, [showWithdrawModal, counting, countdown]);
const handleWithdrawConfirm = () => {
if (withdrawType && countdown === 0) {
handleOpinionAction(record.proposal_id, withdrawType);
setShowWithdrawModal(false);
setWithdrawType(null);
setCountdown(3);
setCounting(false);
}
};
const handleWithdrawCancel = () => {
setShowWithdrawModal(false);
setWithdrawType(null);
setCountdown(3);
setCounting(false);
};
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) && (
<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')}
disabled={isPerforming('withdraw_vote')}
>
{isPerforming('withdraw_vote') ? '处理中...' : '撤销投票'}
</Button>
)}
{record.current_user_is_proposer && (
<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')}
disabled={isPerforming('withdraw_opinion')}
>
{isPerforming('withdraw_opinion') ? '处理中...' : '撤销意见'}
</Button>
)}
{showWithdrawModal && (
<Modal
isOpen={showWithdrawModal}
onClose={handleWithdrawCancel}
title="确认撤销"
size="small"
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}
>
</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}
>
{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="text-sm text-gray-500"><span className="font-bold text-primary">{record.evaluation_point_name || record.proposal_id}</span></div>
</div>
</Modal>
)}
</div>
);
}
// 交叉评查记录类型定义
export interface CrossCheckingRecord {
id: string;
status: 'pending' | 'in_progress' | 'completed';
// 可以根据需要添加更多字段
}
// 打开结果弹窗的函数(需要根据实际需求实现)
const openResultModal = (recordId: string) => {
// 这里实现打开结果弹窗的逻辑
console.log('打开结果弹窗:', recordId);
};
// 交叉评查记录操作按钮组件
export function ActionButtons({ record }: { record: CrossCheckingRecord }) {
// 根据记录状态确定按钮类型
const getButtonConfig = () => {
switch (record.status) {
case 'pending':
return {
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>
)
};
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>
)
};
case 'completed':
default:
return {
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>
)
};
}
};
const buttonConfig = getButtonConfig();
// 处理按钮点击事件
const handleAction = () => {
switch (record.status) {
case 'pending':
// 跳转到评查页面
window.location.href = `/review/${record.id}`;
break;
case 'in_progress':
// 进行中状态不执行操作
break;
case 'completed':
// 打开结果弹窗或页面
openResultModal(record.id);
break;
default:
break;
}
};
return (
<button
onClick={handleAction}
disabled={record.status === 'in_progress'}
className={`
flex items-center justify-center
px-4 py-2 rounded-lg text-white text-sm font-medium
shadow transition-all duration-200
${buttonConfig.bgColor}
${buttonConfig.hoverColor}
${record.status === 'in_progress'
? 'cursor-not-allowed opacity-90'
: 'transform hover:-translate-y-0.5 hover:shadow-md'}
min-w-[100px] whitespace-nowrap
`}
>
{buttonConfig.icon}
{buttonConfig.text}
</button>
);
}