Files
leaudit-platform-frontend/app/routes/_index.tsx
T
LiangShiyong d09d5b709d Merge branch 'PingChuan' into shiy-login
# Conflicts:
#	app/config/api-config.ts
fix: 1. 修复无法加载数据的问题:没有从入口页中进来会缺少数据。
2. 加强后端接口关于token的校验错误和权限校验错误的管理。

feat: 1. 对接后端的数据看板的接口。
2. 将系统设置单独抽出来作为管理员的固定一个入口。
2025-11-22 15:57:22 +08:00

341 lines
12 KiB
TypeScript

import { 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';
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;
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'
hasSettingsAccess = routesResult.data.some(route => route.path === '/settings');
// console.log(`🔑 [Index Loader] 用户${hasSettingsAccess ? '有' : '没有'}系统设置权限`);
}
}
// 返回用户信息、入口模块和系统设置权限给客户端
return Response.json({ userRole, userInfo, entryModules, hasSettingsAccess });
}
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');
if (settingsMode === 'true') {
sessionStorage.removeItem('settingsMode');
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(用于客户端请求)
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';
// 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';
}
// 默认图标
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 (typeof window !== 'undefined') {
// 🔑 设置标志:表示用户通过系统设置入口进入
sessionStorage.setItem('settingsMode', 'true');
// 清除模块相关的标志(因为不是从入口模块进入)
sessionStorage.removeItem('selectedModuleId');
sessionStorage.removeItem('selectedModuleName');
sessionStorage.removeItem('selectedModulePicPath');
}
// 跳转到系统设置的默认页面
navigate('/rule-groups');
};
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">
<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) => (
<div
key={module.id}
className="module-card"
onClick={() => handleModuleClick(module)}
onKeyDown={(e) => handleKeyDown(module, e)}
role="button"
tabIndex={0}
aria-label={module.name}
>
<img
src={getModuleIcon(module)}
alt={module.name}
className="w-12 h-12 mx-1"
/>
<span className="module-name">{module.name}</span>
</div>
))}
{/* 🔑 系统设置入口 - 只有有权限的用户才能看到 */}
{loaderData.hasSettingsAccess && (
<div
className="module-card"
onClick={handleEnterSettings}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleEnterSettings();
}
}}
role="button"
tabIndex={0}
aria-label="系统设置"
>
<i className="ri-settings-4-line text-5xl text-primary"></i>
<span className="module-name"></span>
</div>
)}
</>
) : (
<div className="text-center text-gray-500 py-8">
</div>
)}
</div>
</div>
</main>
</div>
);
}