feat: 1. 添加交叉评查中的相关页面的按钮与权限的绑定控制。 2. 完善权限校验的hook函数,添加指定的交叉评查的相关的权限。
fix: 1. 修复交叉评查中无法高亮文档的问题。
This commit is contained in:
@@ -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';
|
||||
Reference in New Issue
Block a user