// import React from 'react'; import { type MetaFunction } from "@remix-run/node"; import { useLoaderData, useNavigate, Form } from "@remix-run/react"; import { Card } from "~/components/ui/Card"; import { Button } from "~/components/ui/Button"; import { FileTag, links as fileTagLinks } from "~/components/ui/FileTag"; // import { FileTypeTag, links as fileTypeTagLinks } from "~/components/ui/FileTypeTag"; import { Tag } from "~/components/ui/Tag"; import homeStyles from "~/styles/pages/sys_overview.css?url"; import { getDocumentsListFromAPI, type DocumentUI } from "~/api/files/documents"; import { useState, useEffect } from "react"; import { getHomeData, getTopErrorPoints, getTopRiskUsers, type TopErrorPointsResponse, type TopRiskUsersResponse } from "~/api/home/home"; import dayjs from 'dayjs'; // import type { UserRole } from '~/api/login/auth.server'; import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { logout, getUserSession } from "~/api/login/auth.server"; // 文件处理状态选项 const fileProcessingStatusOptions = [ { value: "Waiting", label: "上传中", icon: "ri-loader-line", color: "blue" }, { value: "Cutting", label: "切分中", icon: "ri-loader-line", color: "purple" }, { value: "Extractioning", label: "抽取中", icon: "ri-loader-line", color: "cyan" }, { value: "Evaluationing", label: "评查中", icon: "ri-loader-line", color: "teal" }, { value: "Processed", label: "已完成", icon: "ri-check-line", color: "green" }, ]; export const links = () => [ { rel: "stylesheet", href: homeStyles }, ...fileTagLinks() ]; export const meta: MetaFunction = () => { return [ { title: "中国烟草AI合同及卷宗审核系统 - 首页" }, { name: "description", content: "AI审核系统首页" } ]; }; // API 响应的类型定义 // interface StatsData { // totalFiles: number; // reviewedFiles: number; // pendingFiles: number; // passRate: number; // } // 添加认证检查 export async function loader({ request }: LoaderFunctionArgs) { try { // 从根loader获取用户角色 const { userRole, userInfo, frontendJWT, isAuthenticated } = await getUserSession(request); // 🔑 检查用户是否已登录且有用户信息 if (!isAuthenticated || !userInfo) { console.warn("⚠️ [Home Loader] 用户未登录或缺少用户信息,重定向到登录页"); const url = new URL(request.url); return Response.redirect(`/login?redirect=${encodeURIComponent(url.pathname)}`, 302); } // 返回默认值,实际数据将在客户端根据 sessionStorage 加载 return Response.json({ homeData: { todayPendingFiles: 0, monthlyReviewedFiles: 0, monthlyReviewGrowth: { value: 0, isUp: true }, monthlyPassRate: 0, passRateGrowth: { value: 0, isUp: true }, issuesDetected: 0, issuesGrowth: { value: 0, isUp: true } }, recentFiles: [], userRole: userRole, userInfo, frontendJWT }); } catch (error) { // 错误处理 console.error('Failed to fetch dashboard data:', error); return Response.json( { error: '获取数据失败,请稍后重试' }, { status: 500 } ); } } // 处理登出请求 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 default function Home() { const navigate = useNavigate(); const { homeData: initialHomeData, recentFiles: initialRecentFiles, userRole: serverUserRole, userInfo, frontendJWT } = useLoaderData(); const [recentFiles, setRecentFiles] = useState(initialRecentFiles || []); const [homeData, setHomeData] = useState(initialHomeData); const [currentDateTime, setCurrentDateTime] = useState({ date: '', time: '' }); // 独立的loading状态管理 const [loadingStates, setLoadingStates] = useState({ stats: true, // 统计信息 recentFiles: true, // 最近文档 errorPoints: true, // 高频错误评查点 riskUsers: true // 高风险用户 }); // 统计数据状态(初始时标记为不可用,加载后根据API响应更新) const [topErrorPoints, setTopErrorPoints] = useState({ available: false, total: 0, items: [] }); const [topRiskUsers, setTopRiskUsers] = useState({ available: false, total: 0, items: [] }); // 打印服务器端传递的用户角色 useEffect(() => { console.log('服务器返回的用户角色:', serverUserRole); }, [serverUserRole]); // 更新当前时间 useEffect(() => { // 使用dayjs格式化日期和时间 const updateDateTime = () => { const now = dayjs(); setCurrentDateTime({ date: now.format('YYYY年MM月DD日'), time: now.format('HH:mm:ss') }); }; // 立即更新一次 updateDateTime(); // 设置计时器,每秒更新一次 const timerID = setInterval(updateDateTime, 1000); // 清理函数,组件卸载时清除计时器 return () => clearInterval(timerID); }, []); // 处理登出操作 const handleLogout = () => { // 清除sessionStorage中的所有数据 if (typeof window !== 'undefined') { sessionStorage.removeItem('userRole'); sessionStorage.removeItem('documentTypeIds'); sessionStorage.removeItem('frontendJWT'); sessionStorage.removeItem('userInfo'); sessionStorage.removeItem('accessToken'); sessionStorage.removeItem('isAuthenticated'); // 可以根据需要清除其他会话数据 sessionStorage.clear(); } // 使用Form组件提交登出请求 const form = document.getElementById('logout-form') as HTMLFormElement; if (form) { form.submit(); } else { // 如果找不到表单,直接导航到登录页 navigate('/login'); } }; // 在客户端挂载时,根据 sessionStorage 中的 documentTypeIds 加载正确的数据 useEffect(() => { const loadData = async () => { try { // 从 sessionStorage 获取 documentTypeIds const documentTypeIdsStr = sessionStorage.getItem('documentTypeIds'); const documentTypeIds = documentTypeIdsStr ? JSON.parse(documentTypeIdsStr) : []; // 从 documentTypeIds 推断 reviewType(用于 getHomeData) const reviewType = inferReviewType(documentTypeIds); // 并行加载所有数据,每个数据加载完成后立即更新对应的loading状态 await Promise.all([ // 加载统计信息 (async () => { try { const newHomeData = await getHomeData(reviewType, userInfo.user_id, frontendJWT); setHomeData(newHomeData); setLoadingStates(prev => ({ ...prev, stats: false })); } catch (error) { console.error('加载统计信息失败:', error); setLoadingStates(prev => ({ ...prev, stats: false })); } })(), // 加载最近文档 (async () => { try { const docs = await loadDocuments(documentTypeIds); setRecentFiles(docs); setLoadingStates(prev => ({ ...prev, recentFiles: false })); } catch (error) { console.error('加载最近文档失败:', error); setLoadingStates(prev => ({ ...prev, recentFiles: false })); } })(), // 加载高频错误评查点 (async () => { try { const errorPointsData = await getTopErrorPoints(10, undefined, undefined, documentTypeIds, frontendJWT); setTopErrorPoints(errorPointsData); setLoadingStates(prev => ({ ...prev, errorPoints: false })); } catch (error) { console.error('加载高频错误评查点失败:', error); setLoadingStates(prev => ({ ...prev, errorPoints: false })); } })(), // 加载高风险用户 (async () => { try { const riskUsersData = await getTopRiskUsers(5, undefined, undefined, documentTypeIds, frontendJWT); setTopRiskUsers(riskUsersData); setLoadingStates(prev => ({ ...prev, riskUsers: false })); } catch (error) { console.error('加载高风险用户失败:', error); setLoadingStates(prev => ({ ...prev, riskUsers: false })); } })() ]); } catch (error) { console.error('加载数据失败:', error); // 确保所有loading状态都被重置 setLoadingStates({ stats: false, recentFiles: false, errorPoints: false, riskUsers: false }); } }; loadData(); }, []); // 仅在组件挂载时执行一次 // 从 documentTypeIds 推断 reviewType(用于 getHomeData API) const inferReviewType = (documentTypeIds: number[]): string | null => { if (!documentTypeIds || documentTypeIds.length === 0) return null; if (documentTypeIds.includes(1)) return 'contract'; if (documentTypeIds.includes(2) || documentTypeIds.includes(3)) return 'record'; return null; }; // 加载文档数据的函数 const loadDocuments = async (documentTypeIds: number[]) => { try { if (!frontendJWT) { console.error('缺少 JWT token'); return []; } const baseParams = { page: 1, pageSize: 10, token: frontendJWT }; // 直接使用 documentTypeIds 查询 if (documentTypeIds && documentTypeIds.length > 0) { const response = await getDocumentsListFromAPI({ ...baseParams, documentTypeIds: documentTypeIds }); if (!response.error && response.data) { return response.data.documents; } } else { // 没有指定类型,获取所有文档 const response = await getDocumentsListFromAPI(baseParams); if (!response.error && response.data) { return response.data.documents; } } return []; // 默认返回空数组 } catch (error) { console.error('加载文档数据失败:', error); return []; } }; // 修改useEffect定时器,每10秒自动获取最近文档数据 // 按照定时器更新最近文档 // useEffect(() => { // // 避免在加载状态下进行自动更新 // if (loadingStates.recentFiles) return; // const fetchLatestDocuments = async () => { // const documentTypeIdsStr = sessionStorage.getItem('documentTypeIds'); // const documentTypeIds = documentTypeIdsStr ? JSON.parse(documentTypeIdsStr) : []; // const docs = await loadDocuments(documentTypeIds); // setRecentFiles(docs); // }; // // 设置10秒的定时器 // const timerID = setInterval(fetchLatestDocuments, 10000); // // 组件卸载时清除定时器 // return () => { // clearInterval(timerID); // }; // }, [loadingStates.recentFiles]); // 仅依赖最近文档的loading状态 return (
{/* 登出表单 - 隐藏 */}
{/* 页面头部 */}

系统概览

{currentDateTime.date} | {currentDateTime.time}
{userInfo.nick_name.charAt(userInfo.nick_name.length-1)}
{/*

{userRole === 'developer' ? '系统管理员' : '普通用户'}

*/}

{userInfo.nick_name}

{/*

{userRole === 'developer' ? '超级管理员' : '标准权限'}

*/}
{/* 登出操作 */}
{/* 统计卡片区域 */} {loadingStates.stats ? ( ) : (
)}
{/* 快捷访问区域 */} {/*
*/} {/* 高频错误评查点 */} {topErrorPoints.available && ( {loadingStates.errorPoints ? ( ) : topErrorPoints.total > 0 ? (
{topErrorPoints.items.map((item) => ( ))}
排名 评查点名称 出错人数
{item.rank} {item.point_name} {item.error_user_count} 人
) : (

暂无高频错误评查点数据

)}
)} {/* 高风险用户 */} {topRiskUsers.available && ( {loadingStates.riskUsers ? ( ) : topRiskUsers.total > 0 ? (
{topRiskUsers.items.map((item) => ( ))}
排名 用户 部门 累计出错 平均出错
{item.rank} {item.user_name} {item.department} {item.total_errors} 次 {item.avg_errors_per_doc.toFixed(2)}
) : (

暂无高风险用户数据

)}
)} {/* 最近文档区域 */} 查看全部} className="mt-6" > {loadingStates.recentFiles ? ( ) : (
{recentFiles.length > 0 ? ( recentFiles.map((file: DocumentUI) => (
{file.name}
{file.typeName} · {file.updatedAt}
{(() => { const fileStatus = file.fileStatus || "-"; const status = fileProcessingStatusOptions.find(s => s.value === fileStatus) || fileProcessingStatusOptions[0]; const isSpinning = fileStatus !== "Processed"; return (
{status.label}
); })()}
)) ) : (

暂无最近文档

)}
)}
); } // 统计卡片组件 interface StatCardProps { title: string; value: number | string; icon: string; trend?: { value: number; isUp: boolean; }; } function StatCard({ title, value, icon, trend }: StatCardProps) { return (
{title}
{value}
{trend && (
{trend.value}% 较上月
)}
); } // 快捷方式组件 interface ShortcutItemProps { icon: string; label: string; to: string; } function ShortcutItem({ icon, label, to }: ShortcutItemProps) { return ( ); } // Loading骨架屏组件 interface LoadingSkeletonProps { type?: 'stats' | 'table' | 'list'; rows?: number; } function LoadingSkeleton({ type = 'list', rows = 3 }: LoadingSkeletonProps) { if (type === 'stats') { return (
{[1, 2, 3, 4].map((i) => (
))}
); } if (type === 'table') { return (
{/* 表头 */}
{[1, 2, 3].map((i) => (
))}
{/* 表格行 */} {Array.from({ length: rows }).map((_, i) => (
{[1, 2, 3].map((j) => (
))}
))}
); } // 默认列表类型 return (
{Array.from({ length: rows }).map((_, i) => (
))}
); }