@@ -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-5 00" > < / span >
< / span >
) }
< i className = "ri-chat-1-line text-blue-600 text-lg mb-0.5" > < / i >
< span className = "text-lg text-blue-7 00 font-bold leading-tight" > { scoringProposals . length } < / span >
< div className = "flex flex-col items-center text-[10px] text-blue-6 00 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-4 00 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-6 00 text-lg mb-0.5" > < / i >
< span className = "text-lg text-blue-7 00 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"