fix:修复前端路由权限校验。修复交叉评查与普通评查结果的ai建议的替换效果不一致。

This commit is contained in:
2025-12-10 09:10:57 +08:00
parent ad3f244a1b
commit ba517d7b9c
4 changed files with 196 additions and 19 deletions
+68 -4
View File
@@ -199,7 +199,23 @@ const FALLBACK_MENU_DATA: Record<string, MenuItem[]> = {
title: '交叉评查', title: '交叉评查',
path: '/cross-checking', path: '/cross-checking',
icon: 'ri-color-filter-line', icon: 'ri-color-filter-line',
order: 7 order: 7,
children: [
{
id: 'cross-checking-upload',
title: '创建任务',
path: '/cross-checking/upload',
icon: 'ri-upload-cloud-line',
order: 1
},
{
id: 'cross-checking-result',
title: '评查结果',
path: '/cross-checking/result',
icon: 'ri-file-list-3-line',
order: 2
}
]
} }
], ],
'common': [ 'common': [
@@ -291,7 +307,23 @@ const FALLBACK_MENU_DATA: Record<string, MenuItem[]> = {
title: '交叉评查', title: '交叉评查',
path: '/cross-checking', path: '/cross-checking',
icon: 'ri-color-filter-line', icon: 'ri-color-filter-line',
order: 7 order: 7,
children: [
{
id: 'cross-checking-upload',
title: '创建任务',
path: '/cross-checking/upload',
icon: 'ri-upload-cloud-line',
order: 1
},
{
id: 'cross-checking-result',
title: '评查结果',
path: '/cross-checking/result',
icon: 'ri-file-list-3-line',
order: 2
}
]
} }
], ],
'deptLeader': [ 'deptLeader': [
@@ -390,7 +422,23 @@ const FALLBACK_MENU_DATA: Record<string, MenuItem[]> = {
title: '交叉评查', title: '交叉评查',
path: '/cross-checking', path: '/cross-checking',
icon: 'ri-color-filter-line', icon: 'ri-color-filter-line',
order: 7 order: 7,
children: [
{
id: 'cross-checking-upload',
title: '创建任务',
path: '/cross-checking/upload',
icon: 'ri-upload-cloud-line',
order: 1
},
{
id: 'cross-checking-result',
title: '评查结果',
path: '/cross-checking/result',
icon: 'ri-file-list-3-line',
order: 2
}
]
} }
], ],
'groupLeader': [ 'groupLeader': [
@@ -482,7 +530,23 @@ const FALLBACK_MENU_DATA: Record<string, MenuItem[]> = {
title: '交叉评查', title: '交叉评查',
path: '/cross-checking', path: '/cross-checking',
icon: 'ri-color-filter-line', icon: 'ri-color-filter-line',
order: 7 order: 7,
children: [
{
id: 'cross-checking-upload',
title: '创建任务',
path: '/cross-checking/upload',
icon: 'ri-upload-cloud-line',
order: 1
},
{
id: 'cross-checking-result',
title: '评查结果',
path: '/cross-checking/result',
icon: 'ri-file-list-3-line',
order: 2
}
]
} }
] ]
}; };
@@ -449,7 +449,9 @@ export function ReviewPointsList({
scoringProposals = [], scoringProposals = [],
jwtToken, jwtToken,
userInfo, userInfo,
onOpinionSubmitted onOpinionSubmitted,
fileFormat,
onAiSuggestionReplace
}: ReviewPointsListProps) { }: ReviewPointsListProps) {
// 状态管理 // 状态管理
const [searchText, setSearchText] = useState(''); // 搜索文本 const [searchText, setSearchText] = useState(''); // 搜索文本
@@ -1971,10 +1973,15 @@ export function ReviewPointsList({
// 渲染AI建议(ai_suggestion // 渲染AI建议(ai_suggestion
if (config.ai_suggestion?.suggestions && Object.keys(config.ai_suggestion.suggestions).length > 0) { if (config.ai_suggestion?.suggestions && Object.keys(config.ai_suggestion.suggestions).length > 0) {
// 判断是否为PDF文档(禁用替换按钮)
fileFormat = fileFormat?.replace(/\./g,'')
const isPDF = fileFormat?.toUpperCase() === 'PDF';
// 遍历suggestions对象的key-value对 // 遍历suggestions对象的key-value对
Object.entries(config.ai_suggestion.suggestions).forEach(([key, suggestionValue], index) => { Object.entries(config.ai_suggestion.suggestions).forEach(([key, suggestionValue], index) => {
// 检查建议值是否存在(null 或有值都要渲染) // 检查建议值是否存在(null 或有值都要渲染)
const hasSuggestedValue = suggestionValue.suggested_value !== null && suggestionValue.suggested_value.trim() !== ''; const hasSuggestedValue = suggestionValue.suggested_value !== null && suggestionValue.suggested_value.trim() !== '';
const isReplaceDisabled = !hasSuggestedValue || isPDF;
fieldElements.push( fieldElements.push(
<div key={`ai-suggestion-${index}`} className="mb-3"> <div key={`ai-suggestion-${index}`} className="mb-3">
@@ -1986,7 +1993,7 @@ export function ReviewPointsList({
{/* 原因说明 */} {/* 原因说明 */}
<div className="mb-2 p-2 bg-amber-50 rounded border border-amber-200 text-xs text-gray-700"> <div className="mb-2 p-2 bg-amber-50 rounded border border-amber-200 text-xs text-gray-700">
<div className="flex items-start"> <div className="flex items-center">
<i className="ri-information-line text-amber-600 mr-1 mt-0.5"></i> <i className="ri-information-line text-amber-600 mr-1 mt-0.5"></i>
<div> <div>
<span>{suggestionValue.reason}</span> <span>{suggestionValue.reason}</span>
@@ -1999,14 +2006,14 @@ export function ReviewPointsList({
</div> </div>
</div> </div>
{/* 建议内容显示 */} {/* 建议内容和替换按钮 */}
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
{/* 文本输入框 */} {/* 文本输入框 */}
<textarea <textarea
value={suggestionValue.suggested_value || ''} value={suggestionValue.suggested_value || ''}
readOnly readOnly
disabled={!hasSuggestedValue} disabled={!hasSuggestedValue}
className={`flex-1 p-2 border rounded text-xs resize-none overflow-y-auto ${ className={`flex-1 p-2 border rounded text-xs resize-none overflow-y-auto focus:outline-none ${
hasSuggestedValue hasSuggestedValue
? 'border-gray-200 bg-gray-50 text-gray-700 cursor-not-allowed' ? 'border-gray-200 bg-gray-50 text-gray-700 cursor-not-allowed'
: 'border-gray-200 bg-gray-100 text-gray-400 cursor-not-allowed' : 'border-gray-200 bg-gray-100 text-gray-400 cursor-not-allowed'
@@ -2014,6 +2021,46 @@ export function ReviewPointsList({
aria-label={`${key}的AI建议内容`} aria-label={`${key}的AI建议内容`}
placeholder={!hasSuggestedValue ? '暂无建议值' : ''} placeholder={!hasSuggestedValue ? '暂无建议值' : ''}
/> />
{/* 意见替换按钮 */}
{ !isPDF &&
<button
type="button"
onClick={() => {
if (!isReplaceDisabled && onAiSuggestionReplace && config.fields) {
// 从 config.fields[key] 中获取对应的字段信息
const fieldData = config.fields[key];
if (fieldData) {
// 调用回调函数,传递搜索文本(原文)、替换文本(AI建议)和页码
onAiSuggestionReplace(
fieldData.value || '', // 搜索文本(使用 suggestions 的 key对应的config中的key的value值)
suggestionValue.suggested_value || '', // 替换文本(AI建议的 suggested_value
Number(fieldData.page) || 1 // 页码
);
} else {
toastService.error(`未找到字段 ${key} 的原始数据`);
}
}
}}
disabled={isReplaceDisabled}
className={`px-3 py-2 text-xs rounded whitespace-nowrap transition-colors
${isReplaceDisabled
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
: 'bg-primary text-white hover:bg-primary-hover active:bg-primary'
}`}
title={
isPDF
? 'PDF文档不支持替换'
: !hasSuggestedValue
? '暂无建议值,无法替换'
: '点击执行一键替换'
}
aria-label={`替换${key}的内容`}
>
<i className="ri-exchange-line mr-1"></i>
</button>
}
</div> </div>
</div> </div>
); );
@@ -2545,7 +2592,7 @@ export function ReviewPointsList({
tabIndex={0} tabIndex={0}
style={{ userSelect: 'text' }} style={{ userSelect: 'text' }}
onClick={() => { onClick={() => {
console.log('reviewPoint', reviewPoint); // console.log('reviewPoint', reviewPoint);
handleReviewPointClick(reviewPoint.id); handleReviewPointClick(reviewPoint.id);
}} }}
onKeyDown={(e) => { onKeyDown={(e) => {
+5 -3
View File
@@ -1708,7 +1708,7 @@ export function ReviewPointsList({
value={suggestionValue.suggested_value || ''} value={suggestionValue.suggested_value || ''}
readOnly readOnly
disabled={!hasSuggestedValue} disabled={!hasSuggestedValue}
className={`flex-1 p-2 border rounded text-xs resize-none overflow-y-auto ${ className={`flex-1 p-2 border rounded text-xs resize-none overflow-y-auto focus:outline-none ${
hasSuggestedValue hasSuggestedValue
? 'border-gray-200 bg-gray-50 text-gray-700 cursor-not-allowed' ? 'border-gray-200 bg-gray-50 text-gray-700 cursor-not-allowed'
: 'border-gray-200 bg-gray-100 text-gray-400 cursor-not-allowed' : 'border-gray-200 bg-gray-100 text-gray-400 cursor-not-allowed'
@@ -2194,7 +2194,9 @@ export function ReviewPointsList({
(reviewPoint.legalBasis.articles && Array.isArray(reviewPoint.legalBasis.articles) && reviewPoint.legalBasis.articles.length > 0)) && ( (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="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
<div className="flex justify-between items-center mb-1"> <div className="flex justify-between items-center mb-1">
<span className="text-xs font-medium"></span> <span className="text-xs font-medium">
</span>
</div> </div>
{reviewPoint.legalBasis.name && ( {reviewPoint.legalBasis.name && (
<p className="text-xs text-left mb-1 select-text">{reviewPoint.legalBasis.name}</p> <p className="text-xs text-left mb-1 select-text">{reviewPoint.legalBasis.name}</p>
@@ -2413,7 +2415,7 @@ export function ReviewPointsList({
tabIndex={0} tabIndex={0}
style={{ userSelect: 'text' }} style={{ userSelect: 'text' }}
onClick={() => { onClick={() => {
console.log('reviewPoint', reviewPoint); // console.log('reviewPoint', reviewPoint);
handleReviewPointClick(reviewPoint.id); handleReviewPointClick(reviewPoint.id);
}} }}
onKeyDown={(e) => { onKeyDown={(e) => {
+71 -7
View File
@@ -73,22 +73,85 @@ function extractAllPaths(menuItems: MenuItem[]): string[] {
return paths; return paths;
} }
// 辅助函数:检查路径是否在允许列表中 /**
* ID访
*
* ID的特征
* - 123456
* - UUID格式550e8400-e29b-41d4-a716-446655440000
* - +IDabc-123doc_456
*
*
* - uploadeditcreatelist
* - create-taskedit-profile
*
* @param segment '123' 'upload'
* @returns true ID访false
*/
function isDynamicIdSegment(segment: string): boolean {
// 1. 纯数字(最常见的动态ID)
if (/^\d+$/.test(segment)) {
return true;
}
// 2. UUID格式(包含连字符的十六进制字符串)
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment)) {
return true;
}
// 3. 包含数字的混合ID(如:doc-123、user_456、item123
// 但排除纯英文单词+连字符的组合(如:create-task、edit-profile
if (/\d/.test(segment) && !/^[a-z]+(-[a-z]+)*$/i.test(segment)) {
return true;
}
// 其他情况视为固定路由(需要在菜单中明确配置)
return false;
}
/**
*
*
*
* 1. pathname allowedPaths
* 2. ID的子路径
* - /documents/123
* - /documents/550e8400-e29b-41d4-a716-446655440000UUID
* - /documents/upload
* 3. '/'
*
* @param pathname 访
* @param allowedPaths 访
* @returns true 访false 访
*/
function isPathAllowed(pathname: string, allowedPaths: string[]): boolean { function isPathAllowed(pathname: string, allowedPaths: string[]): boolean {
// 精确匹配 // 1. 精确匹配
if (allowedPaths.includes(pathname)) { if (allowedPaths.includes(pathname)) {
return true; return true;
} }
// 前缀匹配(处理动态路由和子路 // 2. 动态路由匹配(只允许看起来像ID的子路
// 例如:allowedPaths 包含 '/documents',则 '/documents/123' 也应该被允许
for (const allowedPath of allowedPaths) { for (const allowedPath of allowedPaths) {
if (pathname.startsWith(allowedPath + '/')) { if (pathname.startsWith(allowedPath + '/')) {
return true; // 提取子路径部分(例如:'/documents/123' -> '123'
const subPath = pathname.substring(allowedPath.length + 1);
// 支持多级嵌套路由(例如:/documents/123/edit
const segments = subPath.split('/');
// 检查第一个路径段是否是动态ID
// 如果是动态ID,允许访问(后续路径段不再检查,因为通常是操作动作)
// 如果不是动态ID,则必须在 allowedPaths 中明确配置
const firstSegment = segments[0];
if (isDynamicIdSegment(firstSegment)) {
return true; // 动态ID路由,允许访问
}
// 如果不是动态ID,继续检查是否有精确匹配(已在第1步检查过)
} }
} }
// 根路径特殊处理(仅根路径 '/' 对所有已登录用户开放) // 3. 根路径特殊处理(仅根路径 '/' 对所有已登录用户开放)
if (pathname === '/') { if (pathname === '/') {
return true; // 根路径重定向到首页,始终允许 return true; // 根路径重定向到首页,始终允许
} }
@@ -166,7 +229,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
if (routesResult.success && routesResult.data) { if (routesResult.success && routesResult.data) {
// 从菜单数据中提取所有允许的路径 // 从菜单数据中提取所有允许的路径
allowedPaths = extractAllPaths(routesResult.data); allowedPaths = extractAllPaths(routesResult.data);
// console.log("🔑 [Root Loader] 用户允许的路由:", allowedPaths); console.log("🔑 [Root Loader] 用户允许的路由:", allowedPaths);
// ✅ 保存权限映射表 // ✅ 保存权限映射表
if (routesResult.permissionMap) { if (routesResult.permissionMap) {
@@ -175,6 +238,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
} }
// 检查当前路径是否在允许列表中 // 检查当前路径是否在允许列表中
console.log("🔑 [Root Loader] 检查当前路径是否在允许列表中...", pathname, allowedPaths);
const isAllowedPath = isPathAllowed(pathname, allowedPaths); const isAllowedPath = isPathAllowed(pathname, allowedPaths);
if (!isAllowedPath) { if (!isAllowedPath) {