fix:修复前端路由权限校验。修复交叉评查与普通评查结果的ai建议的替换效果不一致。
This commit is contained in:
@@ -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) => {
|
||||||
|
|||||||
@@ -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
@@ -73,22 +73,85 @@ function extractAllPaths(menuItems: MenuItem[]): string[] {
|
|||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助函数:检查路径是否在允许列表中
|
/**
|
||||||
|
* 检查路径段是否看起来像动态ID(允许访问)
|
||||||
|
*
|
||||||
|
* 动态ID的特征:
|
||||||
|
* - 纯数字:123、456
|
||||||
|
* - UUID格式:550e8400-e29b-41d4-a716-446655440000
|
||||||
|
* - 包含数字+特殊字符的混合ID:abc-123、doc_456
|
||||||
|
*
|
||||||
|
* 固定路由的特征(需要在菜单中明确配置):
|
||||||
|
* - 纯英文单词:upload、edit、create、list
|
||||||
|
* - 多单词路由:create-task、edit-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-446655440000(UUID)
|
||||||
|
* - 拒绝:/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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user