Files
leaudit-platform-frontend/app/routes/_index.tsx
T
LiangShiyong d4000cd292 fix: 1. 继续对齐交叉评查的接口,完善创建交叉评查的逻辑 和 相关组件的渲染布局。
2. 文档的基本信息修改改用接口。      3. 重新完善角色权限管理的页面逻辑。     4.将评查点列表中的返回逻辑改用浏览器的记忆返回。
2025-12-12 12:00:36 +08:00

451 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import { useNavigate, Form, useLoaderData } from '@remix-run/react';
import { type MetaFunction, type ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
import styles from "~/styles/pages/home.css?url";
import dayjs from 'dayjs';
import { getUserSession, logout } from "~/api/login/auth.server";
import { toastService } from '~/components/ui';
import { DOCUMENT_URL } from '~/config/api-config';
export const links = () => [
{ rel: "stylesheet", href: styles }
];
export const meta: MetaFunction = () => {
return [
{ title: "中国烟草AI合同及卷宗审核系统 - 首页" },
{ name: "description", content: "中国烟草AI合同及卷宗审核系统首页" },
];
};
// 处理登出请求
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const intent = formData.get("intent");
if (intent === "logout") {
return logout(request);
}
return null;
}
// 获取用户信息
export async function loader({ request }: LoaderFunctionArgs) {
// 🔒 认证检查已在 getUserSession() 中统一处理
// 如果未认证,会自动重定向到登录页,不会执行到这里
const { userRole, userInfo, frontendJWT } = await getUserSession(request);
// 🔑 获取用户地区并查询入口模块
const userArea = userInfo?.area || null;
// console.log('🔍 [Index Loader] 用户地区:', userArea);
// console.log('🔍 [Index Loader] 用户角色:', userRole);
let entryModules = [];
if (userRole && frontendJWT) {
const { getEntryModules } = await import('~/api/home/home');
entryModules = await getEntryModules(userRole,userArea, frontendJWT);
console.log(`📦 [Index Loader] 获取到 ${entryModules.length} 个入口模块`);
} else {
console.warn('⚠️ [Index Loader] 用户角色为空,返回空模块列表');
}
// 🔑 检查用户是否有系统设置权限
let hasSettingsAccess = false;
let hasCrossCheckingAccess = false;
let hasChatLLMAccess = false;
let settingsChildren: { path: string; title: string }[] = [];
if (userRole && frontendJWT) {
const { getUserRoutesByRole } = await import('~/api/auth/user-routes');
const routesResult = await getUserRoutesByRole(userRole, frontendJWT, true); // includeHidden=true
if (routesResult.success && routesResult.data) {
// 查找 '/settings' 路由及其子路由
const settingsRoute = routesResult.data.find(route => route.path === '/settings');
if (settingsRoute) {
hasSettingsAccess = true;
// 提取子路由信息(仅 path 和 title
if (settingsRoute.children && settingsRoute.children.length > 0) {
settingsChildren = settingsRoute.children.map(child => ({
path: child.path,
title: child.title
}));
}
}
// 检查是否存在顶级路由 '/cross-checking'
hasCrossCheckingAccess = routesResult.data.some(route => route.path === '/cross-checking');
// 检查是否存在顶级路由 '/chat-with-llm'
hasChatLLMAccess = routesResult.data.some(route => route.path === '/chat-with-llm');
// console.log(`🔑 [Index Loader] 用户${hasSettingsAccess ? '有' : '没有'}系统设置权限`);
// console.log(`🔑 [Index Loader] 系统设置子路由数量: ${settingsChildren.length}`);
// console.log(`🔑 [Index Loader] 用户${hasCrossCheckingAccess ? '有' : '没有'}交叉评查权限`);
// console.log(`🔑 [Index Loader] 用户${hasChatLLMAccess ? '有' : '没有'}智慧法务助手权限`);
}
}
// 返回用户信息、入口模块和权限给客户端
return Response.json({ userRole, userInfo, entryModules, hasSettingsAccess, hasCrossCheckingAccess, hasChatLLMAccess, settingsChildren });
}
export default function Index() {
const navigate = useNavigate();
const loaderData = useLoaderData<typeof loader>();
const [currentDateTime, setCurrentDateTime] = useState({
date: '',
time: ''
});
// 检查是否通过51707端口访问
// const [isPort51707, setIsPort51707] = useState(false);
// 用户信息:优先使用服务端返回的,否则从 localStorage 读取
const [userInfo, setUserInfo] = useState(loaderData.userInfo);
const [userRole, setUserRole] = useState(loaderData.userRole);
useEffect(() => {
if (typeof window !== 'undefined') {
// setIsPort51707(window.location.port === '51707');
// 如果服务端没有返回用户信息,从 localStorage 读取
if (!loaderData.userInfo || !loaderData.userRole) {
const storedUserInfoStr = localStorage.getItem('user_info');
if (storedUserInfoStr) {
try {
const storedUserInfo = JSON.parse(storedUserInfoStr);
console.log('📖 [Index] 从 localStorage 读取用户信息:', storedUserInfo);
setUserInfo(storedUserInfo);
setUserRole(storedUserInfo.user_role || '');
} catch (error) {
console.error('❌ [Index] 解析 localStorage 用户信息失败:', error);
}
}
}
}
}, [loaderData.userInfo, loaderData.userRole]);
// 打印用户角色
useEffect(() => {
// console.log('📋 [Index] 当前用户角色:', userRole);
// console.log('👤 [Index] 当前用户信息:', userInfo);
}, [userRole, userInfo]);
// 🔑 清除系统设置模式和交叉评查模式标志(当用户返回首页时)
useEffect(() => {
if (typeof window !== 'undefined') {
const settingsMode = sessionStorage.getItem('settingsMode');
const crossCheckingMode = sessionStorage.getItem('crossCheckingMode');
if (settingsMode === 'true') {
sessionStorage.removeItem('settingsMode');
// console.log('🔄 [Index] 清除系统设置模式标志');
}
if (crossCheckingMode === 'true') {
sessionStorage.removeItem('crossCheckingMode');
// console.log('🔄 [Index] 清除交叉评查模式标志');
}
}
}, []); // 只在组件挂载时执行一次
// 更新日期时间
useEffect(() => {
const updateDateTime = () => {
const now = dayjs();
// 格式化日期: YYYY/MM/DD
setCurrentDateTime({
date: now.format('YYYY/MM/DD'),
time: now.format('HH:mm:ss')
});
};
// 初始化时间
updateDateTime();
// 每秒更新一次
const timerID = setInterval(updateDateTime, 1000);
return () => clearInterval(timerID);
}, []);
// 处理模块点击
const handleModuleClick = (module: typeof loaderData.entryModules[0]) => {
// 提取文档类型 IDs
const typeIds = module.document_types?.map(dt => dt.id) || [];
// 🔑 验证文档类型(智慧法务助手除外)
if (module.name !== '智慧法务助手' && typeIds.length === 0) {
toastService.error('该入口尚未关联文档类型,无法进入');
console.warn('⚠️ [Index] 模块未关联文档类型:', module.name);
return; // 阻止进入
}
if (typeof window !== 'undefined') {
// 🔑 清除各页面的筛选条件缓存(切换入口模块时重置)
sessionStorage.removeItem('rules.searchParams');
// 🔑 存储到 sessionStorage(用于客户端请求)
if (typeIds.length > 0) {
sessionStorage.setItem('documentTypeIds', JSON.stringify(typeIds));
// console.log('📝 [Index] 存储到客户端 sessionStorage:', typeIds);
} else {
// 清空文档类型数据
sessionStorage.removeItem('documentTypeIds');
}
// 存储模块信息
sessionStorage.setItem('selectedModuleId', String(module.id));
sessionStorage.setItem('selectedModuleName', module.name);
sessionStorage.setItem('selectedModulePicPath', module.path)
}
// 🔑 根据模块名称决定跳转路径
let targetPath = '/home'; // 默认跳转到首页
if (module.name.includes('合同')) {
// 合同相关模块 → 跳转到合同模板搜索
targetPath = '/contract-template/search';
// console.log('📌 [Index] 合同模块,跳转到:', targetPath);
} else if (module.name === '智慧法务助手') {
// 智慧法务助手 → 跳转到 AI 对话
targetPath = '/chat-with-llm/chat';
sessionStorage.setItem('selectedModulePicPath', '/images/icon_assistant.png')
// console.log('📌 [Index] 智慧法务助手,跳转到:', targetPath);
} else {
// console.log('📌 [Index] 其他模块,跳转到:', targetPath);
}
navigate(targetPath);
};
// 处理键盘事件
const handleKeyDown = (module: typeof loaderData.entryModules[0], e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Enter' || e.key === ' ') {
handleModuleClick(module);
}
};
// 获取模块图标(根据模块 path 或 id)
const getModuleIcon = (module: typeof loaderData.entryModules[0]) => {
// 根据 path 判断图标
// if (module.path?.includes('ht')) {
// return '/images/icon_hetong.png';
// } else if (module.path?.includes('aj')) {
// return '/images/icon_anjuan.png';
// } else if (module.path?.includes('nw')) {
// return '/images/icon_assistant.png';
// }
// 默认图标
if (module.path){
return `${DOCUMENT_URL}${module.path}`
}
return '/images/icon_assistant.png';
};
// 处理登出
const handleLogout = () => {
// 清除sessionStorage中的所有数据
if (typeof window !== 'undefined') {
sessionStorage.clear();
}
// 使用Form组件提交登出请求
const form = document.getElementById('logout-form') as HTMLFormElement;
if (form) {
form.submit();
} else {
// 如果找不到表单,直接导航到登录页
navigate('/login');
}
};
// 处理进入系统设置
const handleEnterSettings = () => {
// 🔑 检查是否有系统设置的子路由
if (!loaderData.settingsChildren || loaderData.settingsChildren.length === 0) {
// 没有子路由,显示错误提示
toastService.error('您无权限访问或页面丢失');
console.warn('⚠️ [Index] 系统设置没有可访问的子路由');
return;
}
if (typeof window !== 'undefined') {
// 🔑 设置标志:表示用户通过系统设置入口进入
sessionStorage.setItem('settingsMode', 'true');
// 清除模块相关的标志(因为不是从入口模块进入)
sessionStorage.removeItem('selectedModuleId');
sessionStorage.removeItem('selectedModuleName');
sessionStorage.removeItem('selectedModulePicPath');
// 清除交叉评查模式标志
sessionStorage.removeItem('crossCheckingMode');
}
// 跳转到第一个子路由
const firstChildPath = loaderData.settingsChildren[0].path;
console.log(`📌 [Index] 系统设置:跳转到第一个子路由 ${firstChildPath}`);
navigate(firstChildPath);
};
// 处理进入交叉评查
const handleEnterCrossChecking = () => {
if (typeof window !== 'undefined') {
// 🔑 设置标志:表示用户通过交叉评查入口进入
sessionStorage.setItem('crossCheckingMode', 'true');
sessionStorage.setItem('selectedModuleName', '交叉评查')
sessionStorage.setItem('selectedModulePicPath', '/images/icon_cross@2x.png')
// 清除模块相关的标志(因为不是从入口模块进入)
sessionStorage.removeItem('selectedModuleId');
// sessionStorage.removeItem('selectedModuleName');
// sessionStorage.removeItem('selectedModulePicPath');
// 清除系统设置模式标志
sessionStorage.removeItem('settingsMode');
}
// 跳转到交叉评查的默认页面
navigate('/cross-checking');
};
return (
<div className="home-page">
{/* 登出表单 - 隐藏 */}
<Form method="post" id="logout-form" className="hidden">
<input type="hidden" name="intent" value="logout" />
</Form>
{/* 头部 */}
<header className="header">
<div className="logo-container">
<img src="/logo.svg" alt="中国烟草" className="logo" />
<div className="flex flex-col">
<span className="logo-text "></span>
<span className="logo-text-en">CHINA TOBACCO</span>
</div>
</div>
<div className="user-info">
{/* 系统设置按钮 - 只在有权限时显示 */}
{loaderData.hasSettingsAccess && (
<button
onClick={handleEnterSettings}
className="settings-button"
aria-label="系统设置"
title="系统设置"
>
<i className="ri-settings-4-line"></i>
</button>
)}
<span className="datetime">{currentDateTime.date} {currentDateTime.time}</span>
<div className="user">
{(() => {
const displayName = (userInfo?.nick_name || (userInfo as { nickname?: string })?.nickname || (userInfo as { name?: string })?.name || '') as string;
const lastChar = displayName ? displayName.charAt(displayName.length - 1) : '用';
return (
<>
<div className="avatar w-10 h-10 rounded-full bg-primary text-white flex items-center justify-center">
<span>{lastChar}</span>
</div>
<span className="username ml-2">{displayName || '未知用户'}</span>
</>
);
})()}
<button
onClick={handleLogout}
className="logout-button"
aria-label="登出"
>
<i className="ri-logout-box-line"></i>
</button>
</div>
</div>
</header>
{/* 主要内容 */}
<main className="index-main-content">
<div className="index-main-content-container">
<h1 className="welcome-text">- -</h1>
{/* 模块网格区域 */}
<div className="modules-container">
{/* 动态渲染入口模块 */}
{loaderData.entryModules && loaderData.entryModules.length > 0 ? (
<>
{loaderData.entryModules.map((module) => {
// 判断是否为智慧法务助手,如果是且有交叉评查权限,则在其之前插入交叉评查卡片
const isLLMModule = module.name === '智慧法务助手';
// 🔑 如果是智慧法务助手且用户没有访问权限,则不渲染该模块
if (isLLMModule && !loaderData.hasChatLLMAccess) {
return null;
}
return (
<React.Fragment key={module.id}>
{/* 在智慧法务助手之前插入交叉评查入口 */}
{isLLMModule && loaderData.hasCrossCheckingAccess && (
<div
className="module-card"
onClick={handleEnterCrossChecking}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleEnterCrossChecking();
}
}}
role="button"
tabIndex={0}
aria-label="交叉评查"
>
<img
src="/images/icon_cross@2x.png"
alt="交叉评查"
className="w-12 h-12 mx-1"
onError={(e) => {
// 如果图片加载失败,使用 icon
(e.target as HTMLImageElement).style.display = 'none';
const parent = (e.target as HTMLImageElement).parentElement;
if (parent) {
const icon = document.createElement('i');
icon.className = 'ri-shuffle-line';
icon.style.fontSize = '48px';
icon.style.color = 'var(--color-primary)';
parent.insertBefore(icon, parent.firstChild);
}
}}
/>
<span className="module-name"></span>
</div>
)}
{/* 渲染原有模块 */}
<div
className="module-card"
onClick={() => handleModuleClick(module)}
onKeyDown={(e) => handleKeyDown(module, e)}
role="button"
tabIndex={0}
aria-label={module.name}
>
<img
src={isLLMModule ? '/images/icon_assistant.png' : getModuleIcon(module)}
alt={module.name}
className="w-12 h-12 mx-1"
/>
<span className="module-name">{module.name}</span>
</div>
</React.Fragment>
);
})}
</>
) : (
<div className="text-center text-gray-500 py-8">
</div>
)}
</div>
</div>
</main>
</div>
);
}