feat: 1. 添加交叉评查中的相关页面的按钮与权限的绑定控制。 2. 完善权限校验的hook函数,添加指定的交叉评查的相关的权限。

fix: 1. 修复交叉评查中无法高亮文档的问题。
This commit is contained in:
2025-12-12 16:10:05 +08:00
parent d4000cd292
commit 847f7b2b5a
9 changed files with 214 additions and 140 deletions
+1 -1
View File
@@ -987,7 +987,7 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
{/* 搜索替换测试面板 - 移动到左上角并添加关闭按钮 */}
{showSearchReplacePanel && (
<div className="absolute top-2 left-2 z-50 bg-white bg-opacity-10 px-3 py-2 rounded shadow-lg border border-gray-200">
<div className="absolute top-2 left-2 z-50 bg-white bg-opacity-70 px-3 py-2 rounded shadow-lg border border-gray-200">
{/* 标题栏和关闭按钮 */}
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-gray-700 font-medium"></span>
@@ -25,14 +25,14 @@ import { Table } from '../ui/Table';
import { Pagination } from '../ui/Pagination';
import { Button } from '../ui/Button';
import { LoadingIndicator } from '../ui/SkeletonScreen';
import {
import {
performOpinionAction,
submitCrossCheckingOpinion,
type CrossCheckingOpinion,
type OpinionActionType
type OpinionActionType,
type SubmitOpinionRequest
} from '../../api/cross-checking/cross-file-result';
import { useFetcher, useNavigate } from '@remix-run/react';
import { API_BASE_URL } from '~/config/api-config';
import axios from 'axios';
// import '../../styles/components/TooltipStyles.css';
/**
@@ -190,6 +190,11 @@ interface ReviewPointsListProps {
onOpinionSubmitted?: (newProposal: ScoringProposal) => void; // 新增:意见提交成功后的回调
fileFormat?: string; // 文件格式(用于判断是否为PDF)
onAiSuggestionReplace?: (searchText: string, replaceText: string, pageNumber: number) => void; // AI建议替换回调
// 权限控制
canReadProposal?: boolean; // 查看意见列表权限
canCreateProposal?: boolean; // 提出建议权限
canDeleteProposal?: boolean; // 撤销意见权限
canVoteProposal?: boolean; // 赞同/反对权限
}
/**
@@ -451,7 +456,12 @@ export function ReviewPointsList({
userInfo,
onOpinionSubmitted,
fileFormat,
onAiSuggestionReplace
onAiSuggestionReplace,
// 权限控制 - 默认为 true 保持向后兼容
canReadProposal = true,
canCreateProposal = true,
canDeleteProposal = true,
canVoteProposal = true
}: ReviewPointsListProps) {
// 状态管理
const [searchText, setSearchText] = useState(''); // 搜索文本
@@ -784,58 +794,58 @@ export function ReviewPointsList({
// 打印最终请求体
// console.log('最终请求体:', data);
// console.log('jwtToken:', jwtToken);
// 用 axios + application/json 提交
// 组装 submitCrossCheckingOpinion 需要的参数
const opinionData: SubmitOpinionRequest = {
reviewPointResultId: data.evaluation_result_id,
documentId: data.document_id,
evaluationPointId: data.evaluation_point_id,
auditOpinion: data.reason,
deductionScore: data.proposed_score
};
try {
const response = await axios.post(`${API_BASE_URL.replace(/\/$/, '')}/admin/cross_review/proposals`, data, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${jwtToken}`,
}
});
const result = response.data;
if (result.code === 200 || result.code === 0) {
const response = await submitCrossCheckingOpinion(
opinionData,
jwtToken,
{ user_id: Number(userInfo.user_id) }
);
if (response.error) {
throw new Error(response.error);
}
if (response.data?.success) {
toastService.success('意见提交成功');
// 创建新的提案对象
const newProposal: ScoringProposal = {
id: result.id || Date.now(), // 使用返回的ID或时间戳作为临时ID
id: response.data.data?.id || Date.now(), // 使用返回的ID或时间戳作为临时ID
evaluation_result_id: data.evaluation_result_id,
proposer_id: data.proposer_id as number,
proposed_score: data.proposed_score,
reason: data.reason,
status: 'pending', // 默认状态
created_at: new Date().toISOString(),
created_at: response.data.data?.created_at || new Date().toISOString(),
updated_at: new Date().toISOString(),
document_id: data.document_id
};
// 更新本地状态
setLocalScoringProposals(prev => [...prev, newProposal]);
// 调用父组件回调(如果提供)
if (onOpinionSubmitted) {
onOpinionSubmitted(newProposal);
}
handleCloseOpinionModal();
} else {
throw new Error(result.msg || '提交意见失败')
// toastService.error(result.msg || '提交意见失败');
throw new Error('提交意见失败');
}
} catch (error) {
console.error('提交意见失败:', error);
// 正确处理 axios 错误响应
let errorMessage = '提交意见失败,请稍后重试';
if (axios.isAxiosError(error) && error.response?.data) {
// 从 axios 错误响应中提取 msg 字段
errorMessage = error.response.data.msg || errorMessage;
} else if (error instanceof Error) {
// 处理普通 Error 对象
errorMessage = error.message || errorMessage;
}
const errorMessage = error instanceof Error ? error.message : '提交意见失败,请稍后重试';
toastService.error(errorMessage);
}
setIsSubmittingOpinion(false);
@@ -2406,32 +2416,25 @@ export function ReviewPointsList({
{reviewPoint.legalBasis && (typeof reviewPoint.legalBasis === 'object') && (
(reviewPoint.legalBasis.name || reviewPoint.legalBasis.content ||
(reviewPoint.legalBasis.articles && Array.isArray(reviewPoint.legalBasis.articles) && reviewPoint.legalBasis.articles.length > 0)) && (
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
<div className="flex justify-between items-center mb-1">
<span className="text-xs font-medium"></span>
</div>
<div className="law-reference mb-3 select-text">
{reviewPoint.legalBasis.name && (
<p className="text-xs text-left mb-1 select-text">{reviewPoint.legalBasis.name}</p>
)}
{reviewPoint.legalBasis.content && (
<p className="text-xs text-left mb-1 select-text"><span className="font-medium"></span>{reviewPoint.legalBasis.content}</p>
<div className="law-reference-title">{reviewPoint.legalBasis.name}</div>
)}
{reviewPoint.legalBasis.articles && Array.isArray(reviewPoint.legalBasis.articles) && reviewPoint.legalBasis.articles.length > 0 && (
<div>
<p className="text-xs text-left font-medium mb-1"></p>
<ul className="list-disc pl-4 select-text">
{reviewPoint.legalBasis.articles.map((item, index) => (
<li key={index} className="text-xs text-left select-text">
{typeof item === 'string' ? item :
typeof item === 'object' && item !== null ?
(item.name ? `${item.name}: ${item.content || ''}` :
item.content || JSON.stringify(item)) :
String(item)}
</li>
))}
</ul>
<div className="law-reference-articles">
{reviewPoint.legalBasis.articles.map((item, index) => (
<span key={index} className="law-article">
{typeof item === 'string' ? item :
typeof item === 'object' && item !== null ?
(item.name || item.content || JSON.stringify(item)) :
String(item)}
</span>
))}
</div>
)}
{reviewPoint.legalBasis.content && (
<div className="law-reference-content">{reviewPoint.legalBasis.content}</div>
)}
</div>
)
)}
@@ -2539,30 +2542,32 @@ export function ReviewPointsList({
return (
<>
<div className="relative">
{/* 悬浮的意见数量显示 - 固定在左侧 */}
<button
className="absolute left-[-35px] top-16 z-10 cursor-pointer"
onClick={() => handleOpenOpinionListModal(reviewPoints[0])}
type="button"
aria-label="查看意见列表"
>
<div className={`relative flex flex-col items-center bg-gradient-to-br from-blue-50 to-blue-100 px-2 py-2 rounded-lg border border-blue-300 shadow-md transition-all duration-200 ease-out hover:scale-110 hover:shadow-xl active:scale-95 ${scoringProposals.length === 0 ? 'opacity-50' : 'opacity-100'}`}>
{/* 脉动提示点 - 仅当有意见时显示 */}
{scoringProposals.length > 0 && (
<span className="absolute -top-1 -right-1 flex h-3 w-3">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-3 w-3 bg-blue-500"></span>
</span>
)}
<i className="ri-chat-1-line text-blue-600 text-lg mb-0.5"></i>
<span className="text-lg text-blue-700 font-bold leading-tight">{scoringProposals.length}</span>
<div className="flex flex-col items-center text-[10px] text-blue-600 leading-tight mt-0.5">
<span></span>
<span></span>
<span></span>
{/* 悬浮的意见数量显示 - 固定在左侧,需要 canReadProposal 权限 */}
{canReadProposal && (
<button
className="absolute left-[-35px] top-16 z-10 cursor-pointer"
onClick={() => handleOpenOpinionListModal(reviewPoints[0])}
type="button"
aria-label="查看意见列表"
>
<div className={`relative flex flex-col items-center bg-gradient-to-br from-blue-50 to-blue-100 px-2 py-2 rounded-lg border border-blue-300 shadow-md transition-all duration-200 ease-out hover:scale-110 hover:shadow-xl active:scale-95 ${scoringProposals.length === 0 ? 'opacity-50' : 'opacity-100'}`}>
{/* 脉动提示点 - 仅当有意见时显示 */}
{scoringProposals.length > 0 && (
<span className="absolute -top-1 -right-1 flex h-3 w-3">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-3 w-3 bg-blue-500"></span>
</span>
)}
<i className="ri-chat-1-line text-blue-600 text-lg mb-0.5"></i>
<span className="text-lg text-blue-700 font-bold leading-tight">{scoringProposals.length}</span>
<div className="flex flex-col items-center text-[10px] text-blue-600 leading-tight mt-0.5">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</button>
</button>
)}
<div className="review-points-panel select-text">
<TooltipPortal />
@@ -2623,17 +2628,19 @@ export function ReviewPointsList({
</div> */}
{/* </div> */}
<div className="flex ml-2 flex-shrink-0 min-w-[15%]">
{/* 提出意见按钮 */}
<div className="flex items-center">
<button className="bg-green-700 hover:bg-green-600 text-white px-2 py-1 rounded-md text-xs"
onClick={(e) => {
e.stopPropagation(); // 阻止事件冒泡,避免触发卡片点击
handleOpenOpinionModal(reviewPoint);
}}
>
<i className="ri-chat-1-line mr-1"></i>
</button>
</div>
{/* 提出意见按钮 - 需要 canCreateProposal 权限 */}
{canCreateProposal && (
<div className="flex items-center">
<button className="bg-green-700 hover:bg-green-600 text-white px-2 py-1 rounded-md text-xs"
onClick={(e) => {
e.stopPropagation(); // 阻止事件冒泡,避免触发卡片点击
handleOpenOpinionModal(reviewPoint);
}}
>
<i className="ri-chat-1-line mr-1"></i>
</button>
</div>
)}
{renderStatusBadge(reviewPoint.status, reviewPoint.result,reviewPoint.title)}
</div>
</div>
@@ -2944,7 +2951,14 @@ export function ReviewPointsList({
render: (_: unknown, record: CrossCheckingOpinion) => {
const isPerforming = (action: string) => performingAction === `${record.proposal_id}-${action}`;
return (
<OpinionActions record={record} isPerforming={isPerforming} handleOpinionAction={handleOpinionAction} userInfo={userInfo as { user_id: number } | undefined} />
<OpinionActions
record={record}
isPerforming={isPerforming}
handleOpinionAction={handleOpinionAction}
userInfo={userInfo as { user_id: number } | undefined}
canVoteProposal={canVoteProposal}
canDeleteProposal={canDeleteProposal}
/>
);
}
}
@@ -2980,11 +2994,13 @@ export function ReviewPointsList({
}
// 操作按钮区美化+弹窗确认组件
function OpinionActions({ record, isPerforming, handleOpinionAction, userInfo }: {
function OpinionActions({ record, isPerforming, handleOpinionAction, userInfo, canVoteProposal = true, canDeleteProposal = true }: {
record: CrossCheckingOpinion;
isPerforming: (action: string) => boolean;
handleOpinionAction: (id: string | number, action: OpinionActionType) => void;
userInfo?: { user_id: number };
canVoteProposal?: boolean; // 赞同/反对/撤销投票权限
canDeleteProposal?: boolean; // 撤销意见权限
}) {
const [showModal, setShowModal] = useState<null | OpinionActionType>(null);
const [countdown, setCountdown] = useState(3);
@@ -3030,12 +3046,12 @@ function OpinionActions({ record, isPerforming, handleOpinionAction, userInfo }:
};
// 判断是否是发起人
const isProposer = userInfo && record.proposer_id === userInfo.user_id;
const isProposer = userInfo && record.proposer_id === Number(userInfo.user_id);
return (
<div className="flex gap-3">
{/* 仅当can_vote为true时显示赞同/反对按钮 */}
{record.can_vote && (
{/* 仅当can_vote为true且有canVoteProposal权限时显示赞同/反对按钮 */}
{record.can_vote && canVoteProposal && (
<>
<Button
type="default"
@@ -3055,8 +3071,8 @@ function OpinionActions({ record, isPerforming, handleOpinionAction, userInfo }:
</Button>
</>
)}
{/* 仅当can_vote为false时显示撤销投票按钮 */}
{!record.can_vote && !isProposer && (
{/* 仅当can_vote为false且有canVoteProposal权限时显示撤销投票按钮 */}
{!record.can_vote && !isProposer && canVoteProposal && (
<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"
@@ -3066,8 +3082,8 @@ function OpinionActions({ record, isPerforming, handleOpinionAction, userInfo }:
{isPerforming('withdraw_vote') ? '处理中...' : '撤销投票'}
</Button>
)}
{/* 仅当是发起人才显示撤销意见按钮 */}
{isProposer && (
{/* 仅当是发起人且有canDeleteProposal权限才显示撤销意见按钮 */}
{isProposer && canDeleteProposal && (
<Button
type="default"
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"
+1 -1
View File
@@ -5,5 +5,5 @@
export { FileInfo } from './FileInfo';
export { FilePreview } from '../reviews/FilePreview';
export { ReviewPointsList } from './ReviewPointsList';
export type { ReviewPoint } from './ReviewPointsList';
export type { ReviewPoint, CharPosition } from './ReviewPointsList';
export { DocumentListModal } from './DocumentListModal';
+14 -23
View File
@@ -2192,34 +2192,25 @@ export function ReviewPointsList({
{reviewPoint.legalBasis && (typeof reviewPoint.legalBasis === 'object') && (
(reviewPoint.legalBasis.name || reviewPoint.legalBasis.content ||
(reviewPoint.legalBasis.articles && Array.isArray(reviewPoint.legalBasis.articles) && reviewPoint.legalBasis.articles.length > 0)) && (
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
<div className="flex justify-between items-center mb-1">
<span className="text-xs font-medium">
</span>
</div>
<div className="law-reference mb-3 select-text">
{reviewPoint.legalBasis.name && (
<p className="text-xs text-left mb-1 select-text">{reviewPoint.legalBasis.name}</p>
)}
{reviewPoint.legalBasis.content && (
<p className="text-xs text-left mb-1 select-text"><span className="font-medium"></span>{reviewPoint.legalBasis.content}</p>
<div className="law-reference-title">{reviewPoint.legalBasis.name}</div>
)}
{reviewPoint.legalBasis.articles && Array.isArray(reviewPoint.legalBasis.articles) && reviewPoint.legalBasis.articles.length > 0 && (
<div>
<p className="text-xs text-left font-medium mb-1"></p>
<ul className="list-disc pl-4 select-text">
{reviewPoint.legalBasis.articles.map((item, index) => (
<li key={index} className="text-xs text-left select-text">
{typeof item === 'string' ? item :
typeof item === 'object' && item !== null ?
(item.name ? `${item.name}: ${item.content || ''}` :
item.content || JSON.stringify(item)) :
String(item)}
</li>
))}
</ul>
<div className="law-reference-articles">
{reviewPoint.legalBasis.articles.map((item, index) => (
<span key={index} className="law-article">
{typeof item === 'string' ? item :
typeof item === 'object' && item !== null ?
(item.name || item.content || JSON.stringify(item)) :
String(item)}
</span>
))}
</div>
)}
{reviewPoint.legalBasis.content && (
<div className="law-reference-content">{reviewPoint.legalBasis.content}</div>
)}
</div>
)
)}