feat: 1. 完善全局路由的访问权限的验证。 2. 完善接口返回的树形路由结构 3.优化评查点列表的查询,改用表连接的方式,废弃使用数据库的rpc函数,同时进行地区隔离和权限隔离。

4. 删除冗余的评查文件列表。      5.完善上传文档 页面初始化查询数据的时候 查询文件类型(改成动态指定)  6. 添加获取入口模块的查询接口。    7.完善服务端中判断token的有效性,失效则跳转到登录页。
8. 重构layout和sidebar的页面,改成由动态权限路由来渲染对应的菜单栏。       9.重构入口页面,通过动态查询根据不同地区的人返回不同的入口。
This commit is contained in:
2025-11-20 01:35:30 +08:00
parent adfb84a31d
commit 2edde8a8ab
23 changed files with 1201 additions and 2154 deletions
+4 -2
View File
@@ -96,9 +96,11 @@ export function Breadcrumb({ items = [], className = '' }: BreadcrumbProps) {
{index === breadcrumbs.length - 1 ? (
<span className="text-gray-900 font-medium">{item.title}</span>
) : (
<Link
to={item.to || '#'}
<Link
to={item.to || '#'}
className="hover:text-primary-600"
prefetch="intent"
preventScrollReset={false}
>
{item.title}
</Link>
+1 -40
View File
@@ -5,16 +5,6 @@ import { Breadcrumb } from './Breadcrumb';
import { useMatches, useLocation } from '@remix-run/react';
import type { UserRole } from '~/root';
// 定义应用模块类型
type AppModule = 'contract' | 'record' | 'model' | '';
// 应用模块与reviewType的映射
const REVIEW_TYPE_TO_APP: Record<string, AppModule> = {
'contract': 'contract', // 合同管理
'record': 'record', // 案卷智能评查
'model': 'model' // 智慧法务大模型
};
interface LayoutProps {
children: React.ReactNode;
userRole?: UserRole;
@@ -35,7 +25,6 @@ interface Match {
export function Layout({ children, userRole = 'developer' as UserRole, frontendJWT = '' }: LayoutProps) {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [selectedApp, setSelectedApp] = useState<AppModule>('');
const [effectiveUserRole, setEffectiveUserRole] = useState<UserRole>(userRole);
const [effectiveFrontendJWT, setEffectiveFrontendJWT] = useState<string>(frontendJWT);
const matches = useMatches() as Match[];
@@ -83,7 +72,7 @@ export function Layout({ children, userRole = 'developer' as UserRole, frontendJ
}
}, [userRole, frontendJWT]);
// 从sessionStorage中获取侧边栏状态和reviewType
// 从localStorage中获取侧边栏状态
useEffect(() => {
// 检查是否为移动端
const isMobile = window.innerWidth <= 768;
@@ -97,35 +86,8 @@ export function Layout({ children, userRole = 'developer' as UserRole, frontendJ
} else if (savedState) {
setSidebarCollapsed(savedState === 'true');
}
// 从sessionStorage获取reviewType并设置对应的应用模块
if (typeof window !== 'undefined') {
try {
const reviewType = sessionStorage.getItem('reviewType');
if (reviewType && REVIEW_TYPE_TO_APP[reviewType]) {
setSelectedApp(REVIEW_TYPE_TO_APP[reviewType]);
}
} catch (error) {
console.error('获取reviewType失败:', error);
}
}
}, []);
// 路由变化时,检查并更新应用模块
useEffect(() => {
if (typeof window !== 'undefined') {
try {
const reviewType = sessionStorage.getItem('reviewType');
// console.log('Layout 路由变化, reviewType:', reviewType, '路径:', location.pathname);
if (reviewType && REVIEW_TYPE_TO_APP[reviewType]) {
setSelectedApp(REVIEW_TYPE_TO_APP[reviewType]);
}
} catch (error) {
console.error('路由变化时获取reviewType失败:', error);
}
}
}, [location.pathname]);
const toggleSidebar = () => {
const newState = !sidebarCollapsed;
setSidebarCollapsed(newState);
@@ -150,7 +112,6 @@ export function Layout({ children, userRole = 'developer' as UserRole, frontendJ
collapsed={sidebarCollapsed}
onToggle={toggleSidebar}
userRole={effectiveUserRole}
selectedApp={selectedApp}
frontendJWT={effectiveFrontendJWT}
/>
+74 -186
View File
@@ -8,36 +8,15 @@ interface SidebarProps {
collapsed: boolean;
userRole: UserRole;
frontendJWT?: string;
selectedApp?: string; // 添加所选应用模块参数
}
// 已移除 APP_MENU_MAP:路由的显示/隐藏由后端 is_hidden 字段控制
// 只保留特殊规则:
// - /chat-with-llm 只在 model 模块中显示
// - /contract-template 只在 contract 模块中显示
// 应用模块名称映射
const APP_NAME_MAP: Record<string, string> = {
'contract': '合同管理',
'record': '案卷智能评查',
'model': '智慧法务大模型'
};
// 应用模块图标映射
const APP_ICON_MAP: Record<string, string> = {
'contract': '/images/icon_hetong.png',
'record': '/images/icon_anjuan.png',
'model': '/images/icon_assistant.png'
};
export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '', selectedApp = '' }: SidebarProps) {
export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: SidebarProps) {
const location = useLocation();
const [expandedMenus, setExpandedMenus] = useState<Record<string, boolean>>({});
const [currentApp, setCurrentApp] = useState<string>(''); // 初始设置为空字符串而不是selectedApp
const [isLoading, setIsLoading] = useState<boolean>(true); // 添加加载状态
const [menuItems, setMenuItems] = useState<MenuItem[]>([]); // 动态菜单项
const [isLoadingRoutes, setIsLoadingRoutes] = useState<boolean>(true); // 路由加载状态
const [isMobile, setIsMobile] = useState<boolean>(false); // 移动端检测
const [selectedModuleName, setSelectedModuleName] = useState<string>(''); // 当前选中的模块名称
const navigate = useNavigate();
// 移动端检测
@@ -57,6 +36,8 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '', selec
// 获取用户路由权限
useEffect(() => {
console.log('🔍 [Sidebar] useEffect 触发,开始获取路由权限');
const fetchUserRoutes = async () => {
setIsLoadingRoutes(true);
try {
@@ -69,19 +50,19 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '', selec
}
if (!jwt) {
console.error('❌ [Sidebar] JWT token 未找到');
console.error('❌ [Sidebar] JWT token 未找到props.frontendJWT:', frontendJWT, 'localStorage.access_token:', localStorage.getItem('access_token'));
setMenuItems([]);
setIsLoadingRoutes(false);
return;
}
console.log('🔍 [Sidebar] 当前用户角色:', userRole);
const roleKey = mapUserRoleToRoleKey(userRole);
const result = await getUserRoutesByRole(roleKey, jwt);
// console.log('🔍 [Sidebar] 当前用户角色:', userRole, 'JWT前20字符:', jwt.substring(0, 20));
// console.log('🔍 [Sidebar] 映射后的角色key:', roleKey);
const result = await getUserRoutesByRole(userRole, jwt);
if (result.success && result.data) {
setMenuItems(result.data);
console.log('✅ [Sidebar] 用户路由权限加载成功:', result.data);
// console.log('✅ [Sidebar] 用户路由权限加载成功:', result.data);
} else {
console.error('❌ [Sidebar] 获取用户路由权限失败:', result.error);
@@ -108,93 +89,20 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '', selec
fetchUserRoutes();
}, [userRole, frontendJWT, navigate]);
// 组件挂载后从 sessionStorage 读取初始 reviewType
// 从 sessionStorage 读取当前选中的模块名称
useEffect(() => {
let mounted = true;
setIsLoading(true); // 设置加载状态
try {
const reviewType = sessionStorage.getItem('reviewType');
// console.log('初始 reviewType:', reviewType);
if (reviewType && mounted) {
setCurrentApp(reviewType);
} else if (selectedApp && mounted) {
// 如果没有reviewType,但有selectedApp,使用selectedApp
setCurrentApp(selectedApp);
}
} catch (error) {
console.error('读取 reviewType 失败:', error);
} finally {
// 使用setTimeout确保状态在DOM更新后再变化,避免闪烁
setTimeout(() => {
if (mounted) {
setIsLoading(false); // 数据加载完成
if (typeof window !== 'undefined') {
try {
const moduleName = sessionStorage.getItem('selectedModuleName');
if (moduleName) {
setSelectedModuleName(moduleName);
console.log('📌 [Sidebar] 当前选中模块:', moduleName);
}
}, 0);
}
return () => {
mounted = false;
};
}, [selectedApp]);
// 从 sessionStorage 获取 reviewType 并设置当前应用模块
useEffect(() => {
// 监听 sessionStorage 变化(主要用于多标签页情况)
const handleStorageChange = (e: StorageEvent) => {
if (e.key === 'reviewType' && e.newValue) {
setCurrentApp(e.newValue);
}
};
// 添加事件监听器
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, []);
// 监听路由变化,重新检查 reviewType
useEffect(() => {
let mounted = true;
try {
const reviewType = sessionStorage.getItem('reviewType');
// 只有当reviewType变化时才设置加载状态和更新currentApp
if (reviewType && reviewType !== currentApp && mounted) {
setIsLoading(true); // 路由变化时设置加载状态
setCurrentApp(reviewType);
// 使用setTimeout确保状态在DOM更新后再变化,避免闪烁
setTimeout(() => {
if (mounted) {
setIsLoading(false);
}
}, 0);
}
} catch (error) {
console.error('路由变化时读取 reviewType 失败:', error);
if (mounted) {
setIsLoading(false);
} catch (error) {
console.error('❌ [Sidebar] 读取 selectedModuleName 失败:', error);
}
}
return () => {
mounted = false;
};
}, [location.pathname, currentApp]);
// 监听 selectedApp 属性变化
useEffect(() => {
if (selectedApp && selectedApp !== currentApp) {
setIsLoading(true); // 设置加载状态
setCurrentApp(selectedApp);
// 使用setTimeout确保状态在DOM更新后再变化,避免闪烁
setTimeout(() => {
setIsLoading(false); // 数据加载完成
}, 0);
}
}, [selectedApp, currentApp]);
}, [location.pathname]); // 路由变化时重新读取
// 初始化展开状态,默认全部展开
useEffect(() => {
@@ -238,73 +146,72 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '', selec
// console.log('子菜单点击:', child.title, '路径:', child.path);
};
// 检查是否通过51707端口访问(省局)
const isPort51707 = typeof window !== 'undefined' && window.location.port === '51707';
const isPort51707 = typeof window !== 'undefined' && window.location.port === '51707'
// 根据当前应用模式过滤菜单项
const filteredMenuItems = menuItems
.filter(item => {
// 如果是51707端口,只显示交叉评查相关菜单
if (isPort51707) {
// 如果当前应用是智慧法务大模型,只显示AI对话菜单
if (currentApp === 'model') {
return item.path && item.path.startsWith('/chat-with-llm');
} else {
return item.path && item.path.startsWith('/cross-checking');
}
// 处理菜单项:清理子菜单结构
const processedMenuItems: MenuItem[] = menuItems.filter(item =>{
// 如果是省局访问
if(isPort51707){
if (selectedModuleName === '智慧法务大模型'){
return item.path && item.path.startsWith('/chat-with-llm')
}
return item.path && item.path.startsWith('/cross-checking')
}
// 特殊规则1/chat-with-llm 只在 model 模块中显示
// 🔑 如果选择了"智慧法务大模型",只显示 /chat-with-llm 相关菜单
if (selectedModuleName === '智慧法务大模型') {
return item.path === '/chat-with-llm' || item.path?.startsWith('/chat-with-llm/');
}
// 🔑 如果选择了包含"合同"的模块
if (selectedModuleName.includes('合同')) {
// 排除智慧法务大模型专属菜单
if (item.path === '/chat-with-llm' || item.path?.startsWith('/chat-with-llm/')) {
return currentApp === 'model';
return false;
}
// 特殊规则2/contract-template 只在 contract 模块中显示
if (item.path === '/contract-template' || item.path?.startsWith('/contract-template/')) {
return currentApp === 'contract';
}
// 其他路由:后端已通过 is_hidden 控制显示/隐藏,这里全部保留
// 保留其他所有菜单(包括 /contract-template
return true;
})
.map(item => {
// 处理子菜单:过滤隐藏的子菜单
if (item.children && item.children.length > 0) {
// 过滤掉 hideBreadcrumb=true 的子菜单(这些通常是隐藏菜单)
const visibleChildren = item.children.filter(child => !child.hideBreadcrumb);
}
// 如果过滤后没有可见的子菜单,返回不带子菜单的父级(变成可点击的单级菜单)
if (visibleChildren.length === 0) {
const { children, ...itemWithoutChildren } = item;
return itemWithoutChildren;
}
// 🔑 其他模块:排除特殊菜单
// 排除智慧法务大模型专属菜单
if (item.path === '/chat-with-llm' || item.path?.startsWith('/chat-with-llm/')) {
return false;
}
// 排除合同专属菜单
if (item.path === '/contract-template' || item.path?.startsWith('/contract-template/')) {
return false;
}
// 如果还有可见的子菜单,返回带过滤后子菜单的项
return { ...item, children: visibleChildren };
}
// 保留其他菜单
return true;
// 处理空 children 数组或 undefined 的情况
if (item.children !== undefined) {
// 如果 children 存在但为空数组,移除它(让父级变成可点击的单级菜单)
}).map((item): MenuItem => {
// 处理子菜单:过滤隐藏的子菜单
if (item.children && item.children.length > 0) {
// 过滤掉 hideBreadcrumb=true 的子菜单(这些通常是隐藏菜单)
const visibleChildren = item.children.filter(child => !child.hideBreadcrumb);
// 如果过滤后没有可见的子菜单,返回不带子菜单的父级(变成可点击的单级菜单)
if (visibleChildren.length === 0) {
const { children, ...itemWithoutChildren } = item;
return itemWithoutChildren;
return itemWithoutChildren as MenuItem;
}
// 没有子菜单的项直接返回
return item;
});
// 如果还有可见的子菜单,返回带过滤后子菜单的项
return { ...item, children: visibleChildren };
}
// filteredMenuItems = filteredMenuItems.map(item => {
// if(item.children && item.children.length > 0){
// const children = item.children.filter(child => {
// const isUploadByPath = child.path === '/files/upload' || child.path?.startsWith('/files/upload')
// const isUploadByTitle = child.title === '文件上传'
// return !(isUploadByPath || isUploadByTitle)
// })
// return { ...item, children}
// }
// return item
// })
// 处理空 children 数组或 undefined 的情况
if (item.children !== undefined) {
// 如果 children 存在但为空数组,移除它(让父级变成可点击的单级菜单)
const { children, ...itemWithoutChildren } = item;
return itemWithoutChildren as MenuItem;
}
// 没有子菜单的项直接返回
return item;
});
return (
<>
@@ -353,27 +260,8 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '', selec
</button>
</div>
{!collapsed && (
<div className="px-4 py-3 border-b border-gray-100">
<div className="flex items-center text-green-700">
{isLoading ? (
// 加载中状态,只显示加载图标,保留布局
<div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-green-700 mr-2"></div>
<span className="font-medium text-gray-500">...</span>
</div>
) : (
<>
<img src={APP_ICON_MAP[currentApp] || ''} alt={APP_NAME_MAP[currentApp] || ''} className="w-6 h-6 mr-2" />
<span className="font-medium">{APP_NAME_MAP[currentApp] || ''}</span>
</>
)}
</div>
</div>
)}
<div className="py-4 px-[10px] flex-1 overflow-y-auto sidebar-scroll-area">
{isLoading || isLoadingRoutes ? (
{isLoadingRoutes ? (
// 加载中状态显示,保留菜单布局结构
<div className="py-2">
{Array(5).fill(0).map((_, index) => (
@@ -387,7 +275,7 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '', selec
</div>
) : (
// 数据加载完成后显示菜单
filteredMenuItems.map((item) => (
processedMenuItems.map((item) => (
<div key={item.id} className={`${collapsed ? 'px-0' : ''}`}>
{!item.children ? (
<Link