接入feat(cross-checking): 整合组织架构数据并优化意见列表功能
- 更新 API 配置,使用新的后端服务地址- 移除前端模拟数据,改为从后端获取真实数据- 优化意见列表接口,支持分页和用户身份验证 - 调整前端界面,适应新的数据结构和功能需求
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user