feat: 1. 添加交叉评查中的相关页面的按钮与权限的绑定控制。 2. 完善权限校验的hook函数,添加指定的交叉评查的相关的权限。
fix: 1. 修复交叉评查中无法高亮文档的问题。
This commit is contained in:
@@ -151,7 +151,7 @@ export async function submitCrossCheckingOpinion(
|
||||
evaluation_point_id: Number(opinionData.evaluationPointId), // 强制转数字
|
||||
proposed_score: opinionData.deductionScore,
|
||||
reason: opinionData.auditOpinion,
|
||||
proposer_id: userInfo?.user_id || 1,
|
||||
proposer_id: userInfo?.user_id,
|
||||
evaluation_result_id: opinionData.reviewPointResultId
|
||||
};
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -35,6 +35,34 @@ interface RootLoaderData {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 交叉评查模块默认权限配置
|
||||
* 当 permissionMap 中没有配置时,使用此默认配置
|
||||
*/
|
||||
const CROSS_CHECKING_DEFAULT_PERMISSIONS: Record<string, string[]> = {
|
||||
'/cross-checking': [
|
||||
'cross_review:task:read',
|
||||
// 'cross_review:task:create',
|
||||
// 'cross_review:document:complete',
|
||||
'cross_review:progress:view',
|
||||
'cross_review:proposal:create',
|
||||
'cross_review:proposal:delete',
|
||||
'cross_review:proposal:read',
|
||||
// 'cross_review:proposal:vote'
|
||||
],
|
||||
'/cross-checking/upload': [
|
||||
// 'cross_review:task:create'
|
||||
],
|
||||
'/cross-checking/result': [
|
||||
// 'cross_review:document:complete',
|
||||
'cross_review:progress:view',
|
||||
'cross_review:proposal:create',
|
||||
'cross_review:proposal:delete',
|
||||
'cross_review:proposal:read',
|
||||
// 'cross_review:proposal:vote'
|
||||
]
|
||||
};
|
||||
|
||||
export function usePermission() {
|
||||
const rootData = useRouteLoaderData("root") as RootLoaderData;
|
||||
const location = useLocation();
|
||||
@@ -46,7 +74,28 @@ export function usePermission() {
|
||||
// 🔑 根据当前路由获取权限列表
|
||||
const currentPath = location.pathname;
|
||||
// console.log('currentPath', currentPath)
|
||||
const currentPermissions = permissionMap[currentPath] || [];
|
||||
|
||||
// 获取当前路由的权限:优先使用 permissionMap,否则使用交叉评查默认配置
|
||||
const getCrossCheckingPermissions = (): string[] => {
|
||||
// 检查是否是交叉评查相关路由
|
||||
if (currentPath.startsWith('/cross-checking')) {
|
||||
// 精确匹配
|
||||
if (CROSS_CHECKING_DEFAULT_PERMISSIONS[currentPath]) {
|
||||
return CROSS_CHECKING_DEFAULT_PERMISSIONS[currentPath];
|
||||
}
|
||||
// 处理带参数的路由,如 /cross-checking/result?id=xxx
|
||||
const basePath = currentPath.split('?')[0];
|
||||
if (CROSS_CHECKING_DEFAULT_PERMISSIONS[basePath]) {
|
||||
return CROSS_CHECKING_DEFAULT_PERMISSIONS[basePath];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// 优先使用 permissionMap 中的权限,如果没有则使用交叉评查默认权限
|
||||
const currentPermissions = permissionMap[currentPath]?.length > 0
|
||||
? permissionMap[currentPath]
|
||||
: getCrossCheckingPermissions();
|
||||
|
||||
// 向后兼容:如果存在旧的permissions数组,也要支持
|
||||
const legacyPermissions = rootData?.permissions || [];
|
||||
|
||||
@@ -251,9 +251,9 @@ export default function CrossCheckingIndex() {
|
||||
const fetcher = useFetcher();
|
||||
|
||||
// 权限控制
|
||||
const { canCreate, canView } = usePermission();
|
||||
const canCreateTask = canCreate('cross_review');
|
||||
const canViewTask = canView('cross_review');
|
||||
const { hasPermission } = usePermission();
|
||||
const canCreateTask = hasPermission('cross_review:task:create');
|
||||
const canViewTask = hasPermission('cross_review:task:read');
|
||||
|
||||
// 状态管理
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
@@ -29,6 +29,7 @@ import crossCheckingStyles from "~/styles/cross-checking-result.css?url";
|
||||
import { getReviewPoints, updateReviewResult, getReviewPoints_fromApi} from "~/api/evaluation_points/reviews";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import { confirmReviewResults, checkProposalVotes, findIsProposer } from "~/api/cross-checking/cross-file-result";
|
||||
import { usePermission } from "~/hooks/usePermission";
|
||||
|
||||
// 导入交叉评查详情页面组件
|
||||
import {
|
||||
@@ -332,10 +333,19 @@ export default function CrossCheckingResult() {
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
const { document, reviewPoints, statistics, reviewInfo, scoring_proposals, jwtToken, userInfo, isProposer, taskId, taskName } = loaderData;
|
||||
const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态
|
||||
|
||||
// 权限控制
|
||||
const { hasPermission } = usePermission();
|
||||
const canCompleteDocument = hasPermission('cross_review:document:complete'); // 完成评查按钮
|
||||
const canReadProposal = hasPermission('cross_review:proposal:read'); // 查看意见列表
|
||||
const canCreateProposal = hasPermission('cross_review:proposal:create'); // 提出建议按钮
|
||||
const canDeleteProposal = hasPermission('cross_review:proposal:delete'); // 撤销意见按钮
|
||||
const canVoteProposal = hasPermission('cross_review:proposal:vote'); // 赞同/反对按钮
|
||||
const [reviewData, setReviewData] = useState<ReviewData | null>(null);
|
||||
const [activeReviewPointResultId, setActiveReviewPointResultId] = useState<string | null>(null);
|
||||
const [targetPage, setTargetPage] = useState<number | undefined>(undefined);
|
||||
const [charPositions, setCharPositions] = useState<CharPosition[] | undefined>(undefined);
|
||||
const [highlightValue, setHighlightValue] = useState<string | undefined>(undefined);
|
||||
const [localScoringProposals, setLocalScoringProposals] = useState<ScoringProposal[]>(scoring_proposals || []); // 本地状态管理scoringProposals
|
||||
const [aiSuggestionReplace, setAiSuggestionReplace] = useState<{
|
||||
searchText: string;
|
||||
@@ -419,22 +429,25 @@ export default function CrossCheckingResult() {
|
||||
}, [document, reviewPoints, statistics, reviewInfo]);
|
||||
|
||||
|
||||
const handleReviewPointSelect = useCallback((reviewPointId: string, page?: number, charPositions?: CharPosition[]) => {
|
||||
const handleReviewPointSelect = useCallback((reviewPointId: string, page?: number, charPositions?: CharPosition[], value?: string) => {
|
||||
// 如果点击的是相同的评查点,但有page参数,先重置targetPage以确保useEffect能够触发
|
||||
if (reviewPointId === activeReviewPointResultId && page) {
|
||||
setTargetPage(undefined);
|
||||
setCharPositions(undefined);
|
||||
// 使用setTimeout确保状态更新后再设置新的targetPage和charPositions
|
||||
setHighlightValue(undefined);
|
||||
// 使用setTimeout确保状态更新后再设置新的targetPage、charPositions和highlightValue
|
||||
setTimeout(() => {
|
||||
setActiveReviewPointResultId(reviewPointId);
|
||||
setTargetPage(page);
|
||||
setCharPositions(charPositions);
|
||||
setHighlightValue(value);
|
||||
}, 0);
|
||||
} else {
|
||||
// 正常设置activeReviewPointId、targetPage和charPositions
|
||||
// 正常设置activeReviewPointId、targetPage、charPositions和highlightValue
|
||||
setActiveReviewPointResultId(reviewPointId);
|
||||
setTargetPage(page);
|
||||
setCharPositions(charPositions);
|
||||
setHighlightValue(value);
|
||||
}
|
||||
}, [activeReviewPointResultId]);
|
||||
|
||||
@@ -730,8 +743,8 @@ export default function CrossCheckingResult() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 完成评查按钮 */}
|
||||
{isProposer && (
|
||||
{/* 完成评查按钮 - 需要 isProposer 且拥有 cross_review:document:complete 权限 */}
|
||||
{isProposer && canCompleteDocument && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
@@ -778,6 +791,7 @@ export default function CrossCheckingResult() {
|
||||
activeReviewPointResultId={activeReviewPointResultId}
|
||||
targetPage={targetPage}
|
||||
charPositions={charPositions}
|
||||
highlightValue={highlightValue}
|
||||
aiSuggestionReplace={aiSuggestionReplace}
|
||||
/>
|
||||
</div>
|
||||
@@ -796,6 +810,10 @@ export default function CrossCheckingResult() {
|
||||
onOpinionSubmitted={handleOpinionSubmitted}
|
||||
fileFormat={reviewData.fileInfo.fileFormat}
|
||||
onAiSuggestionReplace={handleAiSuggestionReplace}
|
||||
canReadProposal={canReadProposal}
|
||||
canCreateProposal={canCreateProposal}
|
||||
canDeleteProposal={canDeleteProposal}
|
||||
canVoteProposal={canVoteProposal}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -322,11 +322,11 @@ export default function ReviewDetails() {
|
||||
} | undefined>(undefined);
|
||||
|
||||
// 🐛 调试:打印 loader 返回的完整数据到浏览器控制台
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
console.group('📦 [Reviews] Loader 数据');
|
||||
// useEffect(() => {
|
||||
// if (typeof window !== 'undefined') {
|
||||
// console.group('📦 [Reviews] Loader 数据');
|
||||
// console.log('完整数据:', loaderData);
|
||||
console.log('文档信息:', document);
|
||||
// console.log('文档信息:', document);
|
||||
// console.log('评查点数量:', reviewPoints?.length);
|
||||
// console.log('评查点数量:', reviewPoints);
|
||||
// console.log('统计信息:', statistics);
|
||||
@@ -334,9 +334,9 @@ export default function ReviewDetails() {
|
||||
// console.log('比对文档:', comparison_document);
|
||||
// console.log('用户信息:', loaderData.userInfo);
|
||||
// console.log('JWT Token (前20位):', frontendJWT?.substring(0, 20) + '...');
|
||||
console.groupEnd();
|
||||
}
|
||||
}, [loaderData, document, reviewPoints, statistics, reviewInfo, comparison_document, frontendJWT]);
|
||||
// console.groupEnd();
|
||||
// }
|
||||
// }, [loaderData, document, reviewPoints, statistics, reviewInfo, comparison_document, frontendJWT]);
|
||||
|
||||
// loader 数据加载出错
|
||||
useEffect(()=>{
|
||||
|
||||
Reference in New Issue
Block a user