feat: 1. 添加axios全局路由拦截进行自动添加请求jwt。 2.重新整理路由表。 3. 文档列表新增版本差异对比。 4.菜单路由可访问列表通过对接接口返回,添加全局路由检测。
5. 修改统一认证登录和管理员登录是通过接口形式进行,存储返回的accessToken。 6. 修改交叉评查的部分样式
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 客户端认证守卫组件
|
||||
*
|
||||
* 在客户端检查 localStorage 中的 token
|
||||
* 如果未认证且访问的是需要认证的路径,则跳转到登录页
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate, useLocation } from '@remix-run/react';
|
||||
import { isAuthenticated } from '~/utils/auth-storage';
|
||||
|
||||
interface ClientAuthGuardProps {
|
||||
isPublicPath: boolean;
|
||||
}
|
||||
|
||||
export function ClientAuthGuard({ isPublicPath }: ClientAuthGuardProps) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
console.log('🔍 [Auth Guard] useEffect 触发', {
|
||||
isPublicPath,
|
||||
pathname: location.pathname
|
||||
});
|
||||
|
||||
// 如果是公共路径,不需要检查认证
|
||||
if (isPublicPath) {
|
||||
console.log('✅ [Auth Guard] 公共路径,跳过认证检查');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查客户端是否已认证(localStorage 中有 token)
|
||||
const token = localStorage.getItem('access_token');
|
||||
const authenticated = isAuthenticated();
|
||||
|
||||
console.log('🔍 [Auth Guard] 认证检查', {
|
||||
token: token ? `${token.substring(0, 20)}...` : null,
|
||||
authenticated,
|
||||
pathname: location.pathname
|
||||
});
|
||||
|
||||
if (!authenticated) {
|
||||
console.log('🔒 [Auth Guard] 未认证,重定向到登录页');
|
||||
|
||||
// 保存当前路径,登录后可以跳转回来
|
||||
const redirectTo = location.pathname !== '/login' ? location.pathname : '/';
|
||||
|
||||
// 跳转到登录页,并传递重定向目标
|
||||
navigate(`/login?redirect=${encodeURIComponent(redirectTo)}`, { replace: true });
|
||||
} else {
|
||||
console.log('✅ [Auth Guard] 已认证,允许访问');
|
||||
}
|
||||
}, [isPublicPath, navigate, location.pathname]);
|
||||
|
||||
// 这个组件不渲染任何内容
|
||||
return null;
|
||||
}
|
||||
@@ -36,6 +36,8 @@ 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[];
|
||||
const location = useLocation();
|
||||
|
||||
@@ -48,6 +50,39 @@ export function Layout({ children, userRole = 'developer' as UserRole, frontendJ
|
||||
match.handle && match.handle.hideBreadcrumb === true
|
||||
);
|
||||
|
||||
// 从 localStorage 读取用户信息和 JWT 作为备用方案
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
try {
|
||||
// 如果服务端没有传递 userRole,从 localStorage 读取
|
||||
if (!userRole || userRole === '') {
|
||||
const storedUserInfoStr = localStorage.getItem('user_info');
|
||||
if (storedUserInfoStr) {
|
||||
const storedUserInfo = JSON.parse(storedUserInfoStr);
|
||||
const storedUserRole = storedUserInfo.user_role || 'common';
|
||||
console.log('📖 [Layout] 从 localStorage 读取用户角色:', storedUserRole);
|
||||
setEffectiveUserRole(storedUserRole as UserRole);
|
||||
}
|
||||
} else {
|
||||
setEffectiveUserRole(userRole);
|
||||
}
|
||||
|
||||
// 如果服务端没有传递 frontendJWT,从 localStorage 读取
|
||||
if (!frontendJWT || frontendJWT === '') {
|
||||
const storedToken = localStorage.getItem('access_token');
|
||||
if (storedToken) {
|
||||
console.log('📖 [Layout] 从 localStorage 读取 JWT token');
|
||||
setEffectiveFrontendJWT(storedToken);
|
||||
}
|
||||
} else {
|
||||
setEffectiveFrontendJWT(frontendJWT);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [Layout] 读取 localStorage 失败:', error);
|
||||
}
|
||||
}, [userRole, frontendJWT]);
|
||||
|
||||
// 从sessionStorage中获取侧边栏状态和reviewType
|
||||
useEffect(() => {
|
||||
// 检查是否为移动端
|
||||
@@ -62,7 +97,7 @@ export function Layout({ children, userRole = 'developer' as UserRole, frontendJ
|
||||
} else if (savedState) {
|
||||
setSidebarCollapsed(savedState === 'true');
|
||||
}
|
||||
|
||||
|
||||
// 从sessionStorage获取reviewType并设置对应的应用模块
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
@@ -111,12 +146,12 @@ export function Layout({ children, userRole = 'developer' as UserRole, frontendJ
|
||||
return (
|
||||
<div className="layout-container">
|
||||
{/* 侧边栏始终保留,不再使用条件渲染 */}
|
||||
<Sidebar
|
||||
collapsed={sidebarCollapsed}
|
||||
onToggle={toggleSidebar}
|
||||
userRole={userRole}
|
||||
<Sidebar
|
||||
collapsed={sidebarCollapsed}
|
||||
onToggle={toggleSidebar}
|
||||
userRole={effectiveUserRole}
|
||||
selectedApp={selectedApp}
|
||||
frontendJWT={frontendJWT}
|
||||
frontendJWT={effectiveFrontendJWT}
|
||||
/>
|
||||
|
||||
<div className={`main-content ${sidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
|
||||
|
||||
@@ -11,11 +11,28 @@ interface SidebarProps {
|
||||
selectedApp?: string; // 添加所选应用模块参数
|
||||
}
|
||||
|
||||
// 定义不同应用模块下显示的菜单项ID
|
||||
// 定义不同应用模块下显示的菜单路径(使用路由路径进行匹配)
|
||||
const APP_MENU_MAP = {
|
||||
'contract': ['home', 'contract-template', 'file-management', 'rule-management', 'cross-checking', 'system-settings'],
|
||||
'record': ['home', 'file-management', 'rule-management', 'cross-checking', 'system-settings'],
|
||||
'model': ['chat-with-llm']
|
||||
'contract': [
|
||||
'/home', // 系统概览
|
||||
'/documents', // 文档管理
|
||||
'/contract-template', // 合同模板
|
||||
'/rules', // 评查规则库
|
||||
'/cross-checking', // 交叉评查
|
||||
// '/chat-with-llm', // AI法务助手
|
||||
'/settings' // 系统设置
|
||||
],
|
||||
'record': [
|
||||
'/home', // 系统概览
|
||||
'/documents', // 文档管理
|
||||
'/rules', // 评查规则库
|
||||
'/cross-checking', // 交叉评查
|
||||
// '/chat-with-llm', // AI法务助手
|
||||
'/settings' // 系统设置
|
||||
],
|
||||
'model': [
|
||||
'/chat-with-llm' // AI法务助手
|
||||
]
|
||||
};
|
||||
|
||||
// 应用模块名称映射
|
||||
@@ -62,28 +79,43 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '', selec
|
||||
const fetchUserRoutes = async () => {
|
||||
setIsLoadingRoutes(true);
|
||||
try {
|
||||
console.log('userRole', userRole);
|
||||
// 优先使用传入的 frontendJWT,否则从 localStorage 读取
|
||||
let jwt = frontendJWT;
|
||||
|
||||
if (!jwt && typeof window !== 'undefined') {
|
||||
jwt = localStorage.getItem('access_token') || '';
|
||||
console.log('📖 [Sidebar] 从 localStorage 读取 JWT');
|
||||
}
|
||||
|
||||
if (!jwt) {
|
||||
console.error('❌ [Sidebar] JWT token 未找到');
|
||||
setMenuItems([]);
|
||||
setIsLoadingRoutes(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔍 [Sidebar] 当前用户角色:', userRole);
|
||||
const roleKey = mapUserRoleToRoleKey(userRole);
|
||||
const result = await getUserRoutesByRole(roleKey, frontendJWT);
|
||||
|
||||
const result = await getUserRoutesByRole(roleKey, jwt);
|
||||
|
||||
if (result.success && result.data) {
|
||||
setMenuItems(result.data);
|
||||
console.log('用户路由权限加载成功:', result.data);
|
||||
console.log('✅ [Sidebar] 用户路由权限加载成功:', result.data);
|
||||
} else {
|
||||
console.error('获取用户路由权限失败:', result.error);
|
||||
|
||||
console.error('❌ [Sidebar] 获取用户路由权限失败:', result.error);
|
||||
|
||||
// 如果需要重定向到首页
|
||||
if (result.shouldRedirectToHome) {
|
||||
console.log('重定向到首页');
|
||||
console.log('🔄 [Sidebar] 重定向到首页');
|
||||
navigate('/');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 其他错误情况,使用空数组
|
||||
setMenuItems([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户路由权限时发生错误:', error);
|
||||
console.error('❌ [Sidebar] 获取用户路由权限时发生错误:', error);
|
||||
// 发生异常时也重定向到首页
|
||||
navigate('/');
|
||||
return;
|
||||
@@ -93,7 +125,7 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '', selec
|
||||
};
|
||||
|
||||
fetchUserRoutes();
|
||||
}, [userRole, navigate]);
|
||||
}, [userRole, frontendJWT, navigate]);
|
||||
|
||||
// 组件挂载后从 sessionStorage 读取初始 reviewType
|
||||
useEffect(() => {
|
||||
@@ -225,37 +257,60 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '', selec
|
||||
// console.log('子菜单点击:', child.title, '路径:', child.path);
|
||||
};
|
||||
|
||||
// 获取当前应用模式下应显示的菜单ID列表
|
||||
const visibleMenuIds = APP_MENU_MAP[currentApp as keyof typeof APP_MENU_MAP] || APP_MENU_MAP['contract'];
|
||||
// const visibleMenuIds = APP_MENU_MAP[currentApp as keyof typeof APP_MENU_MAP]
|
||||
// console.log('当前应用模式:', currentApp, '可见菜单ID:', visibleMenuIds);
|
||||
// 获取当前应用模式下应显示的菜单路径列表
|
||||
const visibleMenuPaths = APP_MENU_MAP[currentApp as keyof typeof APP_MENU_MAP] || APP_MENU_MAP['contract'];
|
||||
// console.log('当前应用模式:', currentApp, '可见菜单路径:', visibleMenuPaths);
|
||||
|
||||
// 检查是否通过51707端口访问(省局)
|
||||
// const isPort51708 = typeof window !== 'undefined' && window.location.port === '51708';
|
||||
const isPort51707 = typeof window !== 'undefined' && window.location.port === '51707';
|
||||
|
||||
// 根据当前应用模式过滤菜单项
|
||||
const filteredMenuItems = menuItems.filter(item => {
|
||||
// 如果是51707端口,只显示交叉评查相关菜单
|
||||
if (isPort51707) {
|
||||
// 如果当前应用是智慧法务大模型,只显示AI对话菜单
|
||||
if (currentApp === 'model') {
|
||||
return item.id === 'chat-with-llm' ||
|
||||
(item.path && item.path.startsWith('/chat-with-llm'));
|
||||
}else{
|
||||
return item.id === 'cross-checking' ||
|
||||
(item.path && item.path.startsWith('/cross-checking'))
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查当前菜单是否在所选应用模式中显示(使用路径匹配)
|
||||
if (!visibleMenuPaths.includes(item.path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查当前菜单是否在所选应用模式中显示
|
||||
if (!visibleMenuIds.includes(item.id)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(item => {
|
||||
// 处理子菜单:过滤隐藏的子菜单
|
||||
if (item.children && item.children.length > 0) {
|
||||
// 过滤掉 hideBreadcrumb=true 的子菜单(这些通常是隐藏菜单)
|
||||
const visibleChildren = item.children.filter(child => !child.hideBreadcrumb);
|
||||
|
||||
return true;
|
||||
});
|
||||
// 如果过滤后没有可见的子菜单,返回不带子菜单的父级(变成可点击的单级菜单)
|
||||
if (visibleChildren.length === 0) {
|
||||
const { children, ...itemWithoutChildren } = item;
|
||||
return itemWithoutChildren;
|
||||
}
|
||||
|
||||
// 如果还有可见的子菜单,返回带过滤后子菜单的项
|
||||
return { ...item, children: visibleChildren };
|
||||
}
|
||||
|
||||
// 处理空 children 数组或 undefined 的情况
|
||||
if (item.children !== undefined) {
|
||||
// 如果 children 存在但为空数组,移除它(让父级变成可点击的单级菜单)
|
||||
const { children, ...itemWithoutChildren } = item;
|
||||
return itemWithoutChildren;
|
||||
}
|
||||
|
||||
// 没有子菜单的项直接返回
|
||||
return item;
|
||||
});
|
||||
|
||||
// filteredMenuItems = filteredMenuItems.map(item => {
|
||||
// if(item.children && item.children.length > 0){
|
||||
|
||||
@@ -49,7 +49,7 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
|
||||
// 根据来源页面返回
|
||||
const previousRoute = fileInfo.previousRoute || 'documents';
|
||||
const returnTo = previousRoute === 'documents'
|
||||
? "/documents"
|
||||
? "/documents/list"
|
||||
: previousRoute === 'filesUpload'
|
||||
? "/files/upload"
|
||||
: "/rules-files";
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 问题数量差异显示组件
|
||||
* 用于显示文档版本之间的问题数量对比
|
||||
*/
|
||||
|
||||
interface IssuesDiffProps {
|
||||
currentIssues: number | null;
|
||||
previousIssues?: number | null;
|
||||
issuesDiff?: number;
|
||||
issuesDiffType?: 'increase' | 'decrease' | 'same';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function IssuesDiff({
|
||||
currentIssues,
|
||||
previousIssues,
|
||||
issuesDiff,
|
||||
issuesDiffType,
|
||||
className = ''
|
||||
}: IssuesDiffProps) {
|
||||
// 如果当前问题数量为 null,显示 "-"
|
||||
if (currentIssues === null) {
|
||||
return <span className={`issues-number ${className}`}>-</span>;
|
||||
}
|
||||
|
||||
// 如果没有上一个版本或上一个版本问题数量为 null,只显示当前数量
|
||||
if (previousIssues === null || previousIssues === undefined || issuesDiffType === undefined) {
|
||||
return <span className={`issues-number ${className}`}>{currentIssues}</span>;
|
||||
}
|
||||
|
||||
// 显示当前数量 + 差异
|
||||
return (
|
||||
<div className={`issues-count-wrapper ${className}`}>
|
||||
<span className="issues-number">{currentIssues}</span>
|
||||
{issuesDiff !== undefined && issuesDiffType && (
|
||||
<span className={`issues-diff ${issuesDiffType}`}>
|
||||
{issuesDiffType === 'increase' && (
|
||||
<>
|
||||
<i className="ri-arrow-up-line"></i>
|
||||
<span>+{issuesDiff}</span>
|
||||
</>
|
||||
)}
|
||||
{issuesDiffType === 'decrease' && (
|
||||
<>
|
||||
<i className="ri-arrow-down-line"></i>
|
||||
<span>-{issuesDiff}</span>
|
||||
</>
|
||||
)}
|
||||
{issuesDiffType === 'same' && (
|
||||
<>
|
||||
<i className="ri-subtract-line"></i>
|
||||
<span>0</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -113,19 +113,24 @@ const MultiCascader: React.FC<MultiCascaderProps> = ({
|
||||
<div className="flex items-center py-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-checkbox h-4 w-4 text-[var(--color-primary,#00684a)] rounded border-gray-300 focus:ring-[var(--color-primary,#00684a)]"
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
style={{
|
||||
accentColor: '#00684a', // 固定为绿色(烟草企业绿)
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
checked={allChecked}
|
||||
ref={el => { if (el) el.indeterminate = !allChecked && someChecked; }}
|
||||
onChange={e => handleItemCheck(option, e.target.checked)}
|
||||
id={`cascader-${option.value}`}
|
||||
/>
|
||||
<label htmlFor={`cascader-${option.value}`} className="ml-2 text-sm flex-1">
|
||||
<label htmlFor={`cascader-${option.value}`} className="ml-2 text-sm flex-1 cursor-pointer">
|
||||
{option.label}
|
||||
</label>
|
||||
{hasChildren && (
|
||||
<button
|
||||
type="button"
|
||||
className="ml-2 text-gray-400 hover:text-gray-600"
|
||||
className="ml-2"
|
||||
style={{ color: 'gray' }} // 展开图标固定为绿色
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleExpand(option.value);
|
||||
|
||||
@@ -29,7 +29,4 @@ export { MessageModal } from './MessageModal';
|
||||
export { LoadingBar } from './LoadingBar';
|
||||
export { RouteChangeLoader } from './RouteChangeLoader';
|
||||
export { FileProgress } from './FileProgress';
|
||||
export { ProcessingSteps } from './ProcessingSteps';
|
||||
|
||||
// 示例组件(开发环境使用)
|
||||
export { TooltipExample } from '../../routes/examples/TooltipExample';
|
||||
export { ProcessingSteps } from './ProcessingSteps';
|
||||
Reference in New Issue
Block a user