508 lines
20 KiB
TypeScript
508 lines
20 KiB
TypeScript
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 type { EntryModule } from '~/api/home/home';
|
||
import { getUserSession, logout } from "~/api/login/auth.server";
|
||
import { toastService } from '~/components/ui';
|
||
import { DOCUMENT_URL, CROSS_CHECKING_ONLY_MODE, CROSS_CHECKING_ONLY_PORT, getCurrentPort } 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);
|
||
|
||
// 🔑 获取用户地区并查询入口模块
|
||
let entryModules: EntryModule[] = [];
|
||
if (frontendJWT) {
|
||
const { getEntryModules } = await import('~/api/home/home');
|
||
entryModules = await getEntryModules(frontendJWT);
|
||
console.log(`📦 [Index Loader] 获取到 ${entryModules.length} 个入口模块`);
|
||
} else {
|
||
console.warn('⚠️ [Index Loader] 缺少 JWT,返回空模块列表');
|
||
}
|
||
|
||
// 🔑 检查用户是否有系统设置权限
|
||
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
|
||
// console.log('🔍 [Index Loader] 顶级路由paths:', routesResult.data?.map(r => r.path));
|
||
|
||
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
|
||
}))
|
||
.sort((a, b) => {
|
||
if (a.path === '/rule-groups') return -1;
|
||
if (b.path === '/rule-groups') return 1;
|
||
return 0;
|
||
});
|
||
}
|
||
}
|
||
|
||
// 检查是否存在顶级路由 '/cross-checking'
|
||
// 🔒 交叉评查访问控制:
|
||
// - CROSS_CHECKING_ONLY_MODE=false 时,所有端口都可访问(根据后端权限)
|
||
// - CROSS_CHECKING_ONLY_MODE=true 时,只有 51707 端口可访问
|
||
const currentPort = getCurrentPort();
|
||
if (!CROSS_CHECKING_ONLY_MODE || currentPort === CROSS_CHECKING_ONLY_PORT) {
|
||
hasCrossCheckingAccess = routesResult.data.some(route => route.path === '/cross-checking');
|
||
} else {
|
||
hasCrossCheckingAccess = false; // CROSS_CHECKING_ONLY_MODE=true 且非51707端口不显示交叉评查入口
|
||
}
|
||
|
||
// 检查是否存在顶级路由 '/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 ? '有' : '没有'}智慧法务助手权限`);
|
||
}
|
||
}
|
||
|
||
// 🔑 判断是否启用交叉评查专属模式
|
||
// 条件:CROSS_CHECKING_ONLY_MODE=true 且 当前端口为 51707
|
||
// 注意:currentPort 已在上面的权限检查中获取,这里复用(如果在 if 块外需要则重新获取)
|
||
const currentPortForMode = getCurrentPort();
|
||
const isCrossCheckingOnlyMode = CROSS_CHECKING_ONLY_MODE && currentPortForMode === CROSS_CHECKING_ONLY_PORT;
|
||
|
||
if (isCrossCheckingOnlyMode) {
|
||
console.log(`🔒 [Index Loader] 交叉评查专属模式已启用 (端口: ${currentPortForMode})`);
|
||
}
|
||
|
||
// 返回用户信息、入口模块和权限给客户端
|
||
return Response.json({
|
||
userRole,
|
||
userInfo,
|
||
entryModules,
|
||
hasSettingsAccess,
|
||
hasCrossCheckingAccess,
|
||
hasChatLLMAccess,
|
||
settingsChildren,
|
||
isCrossCheckingOnlyMode // 新增:交叉评查专属模式标志
|
||
});
|
||
}
|
||
|
||
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: EntryModule) => {
|
||
// 提取文档类型 IDs
|
||
const typeIds = module.documentTypes.map((dt) => dt.id) || [];
|
||
|
||
if (module.requiresDocumentTypes && 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.iconPath || '')
|
||
}
|
||
|
||
const targetPath = module.targetPath || '/home';
|
||
if (targetPath === '/chat-with-llm/chat' && typeof window !== 'undefined') {
|
||
sessionStorage.setItem('selectedModulePicPath', module.iconPath || '/images/icon_assistant.png');
|
||
}
|
||
navigate(targetPath);
|
||
};
|
||
|
||
// 处理键盘事件
|
||
const handleKeyDown = (module: EntryModule, e: React.KeyboardEvent<HTMLDivElement>) => {
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
handleModuleClick(module);
|
||
}
|
||
};
|
||
|
||
// 获取模块图标(根据模块 path 或 id)
|
||
const getModuleIcon = (module: EntryModule) => {
|
||
if (module.iconPath){
|
||
if (module.iconPath.startsWith('/images/')) {
|
||
return module.iconPath;
|
||
}
|
||
return `${DOCUMENT_URL}${module.iconPath}`;
|
||
}
|
||
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 preferredSettingsPath =
|
||
loaderData.settingsChildren.find((child: { path: string; title: string }) => child.path === '/rule-groups')?.path ||
|
||
loaderData.settingsChildren[0].path;
|
||
console.log(`📌 [Index] 系统设置:跳转到首选子路由 ${preferredSettingsPath}`);
|
||
navigate(preferredSettingsPath);
|
||
};
|
||
|
||
// 处理进入交叉评查
|
||
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 && !loaderData.isCrossCheckingOnlyMode && (
|
||
<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.isCrossCheckingOnlyMode ? (
|
||
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) => {
|
||
(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="text-center text-gray-500 py-8">
|
||
暂无可用模块
|
||
</div>
|
||
)
|
||
) : (
|
||
/* 正常模式:显示所有入口模块 */
|
||
loaderData.entryModules && loaderData.entryModules.length > 0 ? (
|
||
<>
|
||
{loaderData.entryModules.map((module: EntryModule) => {
|
||
const isLLMModule = module.targetPath === '/chat-with-llm/chat';
|
||
const isCrossCheckingModule = module.targetPath === '/cross-checking';
|
||
|
||
// 🔑 如果是智慧法务助手且用户没有访问权限,则不渲染该模块
|
||
if (isLLMModule && !loaderData.hasChatLLMAccess) {
|
||
return null;
|
||
}
|
||
|
||
// 交叉评查已在下面独立渲染,避免首页重复出现两张卡片
|
||
if (isCrossCheckingModule) {
|
||
return null;
|
||
}
|
||
|
||
return (
|
||
<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={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>
|
||
);
|
||
})}
|
||
|
||
{/* 交叉评查入口 - 独立渲染,不依赖智慧法务助手模块 */}
|
||
{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) => {
|
||
(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="text-center text-gray-500 py-8">
|
||
暂无可用模块
|
||
</div>
|
||
)
|
||
)}
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
);
|
||
}
|