473 lines
16 KiB
TypeScript
473 lines
16 KiB
TypeScript
// 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 { getDocuments, type DocumentUI, type DocumentSearchParams } from "~/api/files/documents";
|
|
import { useState, useEffect } from "react";
|
|
import { getHomeData } 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 } = await getUserSession(request);
|
|
|
|
|
|
// 返回默认值,实际数据将在客户端根据 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: [],
|
|
reviewType: null,
|
|
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 } = useLoaderData<typeof loader>();
|
|
const [recentFiles, setRecentFiles] = useState<DocumentUI[]>(initialRecentFiles || []);
|
|
const [homeData, setHomeData] = useState(initialHomeData);
|
|
const [currentDateTime, setCurrentDateTime] = useState({
|
|
date: '',
|
|
time: ''
|
|
});
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
// const userRole = serverUserRole as UserRole;
|
|
|
|
// 打印服务器端传递的用户角色
|
|
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('reviewType');
|
|
sessionStorage.removeItem('previousReviewType');
|
|
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 中的 reviewType 加载正确的数据
|
|
useEffect(() => {
|
|
const loadData = async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
// 从 sessionStorage 获取 reviewType
|
|
const reviewType = sessionStorage.getItem('reviewType');
|
|
|
|
// 加载主页数据
|
|
const newHomeData = await getHomeData(reviewType || undefined,userInfo.user_id);
|
|
setHomeData(newHomeData);
|
|
|
|
// 加载文档数据
|
|
const docs = await loadDocuments(reviewType);
|
|
setRecentFiles(docs);
|
|
|
|
setIsLoading(false);
|
|
} catch (error) {
|
|
console.error('加载数据失败:', error);
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
loadData();
|
|
}, []); // 仅在组件挂载时执行一次
|
|
|
|
// 加载文档数据的函数
|
|
const loadDocuments = async (reviewType: string | null) => {
|
|
try {
|
|
const documentSearchParams: DocumentSearchParams = {
|
|
page: 1,
|
|
pageSize: 10,
|
|
userId: userInfo.user_id
|
|
};
|
|
|
|
// 根据 reviewType 添加过滤条件
|
|
if (reviewType === 'contract') {
|
|
documentSearchParams.documentType = '1';
|
|
|
|
const response = await getDocuments(documentSearchParams);
|
|
if (!response.error && response.data) {
|
|
// console.log('合同文档数据',response.data.documents);
|
|
return response.data.documents;
|
|
}
|
|
} else if (reviewType === 'record') {
|
|
// 获取类型 2 的文档
|
|
const response1 = await getDocuments({
|
|
...documentSearchParams,
|
|
documentType: '2'
|
|
});
|
|
|
|
// 获取类型 3 的文档
|
|
const response2 = await getDocuments({
|
|
...documentSearchParams,
|
|
documentType: '3'
|
|
});
|
|
|
|
if (!response1.error && !response2.error && response1.data && response2.data) {
|
|
// 合并文档并排序
|
|
const mergedDocs = [...response1.data.documents, ...response2.data.documents];
|
|
mergedDocs.sort((a, b) =>
|
|
new Date(b.updatedAt || '').getTime() - new Date(a.updatedAt || '').getTime()
|
|
);
|
|
|
|
// 限制数量
|
|
// console.log('卷宗文档数据',mergedDocs);
|
|
return mergedDocs.slice(0, documentSearchParams.pageSize);
|
|
}
|
|
} else {
|
|
// 没有特定类型,获取所有文档
|
|
const response = await getDocuments(documentSearchParams);
|
|
if (!response.error && response.data) {
|
|
return response.data.documents;
|
|
}
|
|
}
|
|
return []; // 默认返回空数组
|
|
} catch (error) {
|
|
console.error('加载文档数据失败:', error);
|
|
return [];
|
|
}
|
|
};
|
|
|
|
// 监听 sessionStorage 中 reviewType 的变化
|
|
useEffect(() => {
|
|
const handleStorageChange = async () => {
|
|
const currentReviewType = sessionStorage.getItem('reviewType');
|
|
const previousReviewType = sessionStorage.getItem('previousReviewType');
|
|
|
|
// 如果 reviewType 发生变化
|
|
if (currentReviewType !== previousReviewType) {
|
|
setIsLoading(true);
|
|
|
|
// 更新主页数据
|
|
const newHomeData = await getHomeData(currentReviewType || undefined,userInfo.user_id);
|
|
setHomeData(newHomeData);
|
|
|
|
// 更新文档数据
|
|
const docs = await loadDocuments(currentReviewType);
|
|
setRecentFiles(docs);
|
|
|
|
// 保存当前 reviewType 为上一次的值,用于比较
|
|
sessionStorage.setItem('previousReviewType', currentReviewType || '');
|
|
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
// 设置初始的 previousReviewType
|
|
const initialReviewType = sessionStorage.getItem('reviewType');
|
|
sessionStorage.setItem('previousReviewType', initialReviewType || '');
|
|
|
|
// 设置定期检查
|
|
const checkInterval = setInterval(handleStorageChange, 1000);
|
|
|
|
return () => {
|
|
clearInterval(checkInterval);
|
|
};
|
|
}, []);
|
|
|
|
// 修改useEffect定时器,每10秒自动获取最近文档数据
|
|
// 按照定时器更新最近文档
|
|
useEffect(() => {
|
|
// 避免在加载状态下进行自动更新
|
|
if (isLoading) return;
|
|
|
|
const fetchLatestDocuments = async () => {
|
|
const reviewType = sessionStorage.getItem('reviewType');
|
|
const docs = await loadDocuments(reviewType);
|
|
setRecentFiles(docs);
|
|
};
|
|
|
|
// 设置10秒的定时器
|
|
const timerID = setInterval(fetchLatestDocuments, 10000);
|
|
|
|
// 组件卸载时清除定时器
|
|
return () => {
|
|
clearInterval(timerID);
|
|
};
|
|
}, [isLoading]); // 仅依赖 isLoading 状态
|
|
|
|
return (
|
|
<div className="dashboard-container">
|
|
{/* 登出表单 - 隐藏 */}
|
|
<Form method="post" id="logout-form" className="hidden">
|
|
<input type="hidden" name="intent" value="logout" />
|
|
</Form>
|
|
|
|
{/* 页面头部 */}
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h2 className="text-xl font-medium">系统概览</h2>
|
|
<div className="text-sm text-gray-500 flex flex-row items-center justify-between">
|
|
<div className="flex items-center">
|
|
<span id="current-date">{currentDateTime.date}</span>
|
|
<span className="mx-2">|</span>
|
|
<span id="current-time">{currentDateTime.time}</span>
|
|
</div>
|
|
<div className="user-profile p-4 border-b border-gray-100 flex items-center">
|
|
<div className="avatar w-10 h-10 rounded-full bg-primary text-white flex items-center justify-center">
|
|
<span>{userInfo.nick_name.charAt(userInfo.nick_name.length-1)}</span>
|
|
</div>
|
|
<div className="ml-1">
|
|
{/* <p className="text-sm font-medium mb-0">{userRole === 'developer' ? '系统管理员' : '普通用户'}</p> */}
|
|
<p className="text-sm font-medium mb-0">{userInfo.nick_name}</p>
|
|
{/* <p className="text-xs text-gray-500 mb-0">{userRole === 'developer' ? '超级管理员' : '标准权限'}</p> */}
|
|
</div>
|
|
</div>
|
|
{/* 登出操作 */}
|
|
<Button
|
|
type="default"
|
|
size="small"
|
|
className="ml-4 hover:bg-gray-100"
|
|
onClick={handleLogout}
|
|
>
|
|
<i className="ri-logout-box-line mr-1"></i>
|
|
登出
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 统计卡片区域 */}
|
|
<Card title="统计信息" icon="ri-bar-chart-line" className="mt-6 transition-all duration-200 hover:shadow-[0_4px_15px_rgba(0,0,0,0.1)]">
|
|
<div className="stat-grid ">
|
|
<StatCard
|
|
title="今日待审文件"
|
|
value={homeData.todayPendingFiles}
|
|
icon="ri-inbox-line"
|
|
/>
|
|
<StatCard
|
|
title="本月已审核文件"
|
|
value={homeData.monthlyReviewedFiles}
|
|
icon="ri-file-search-line"
|
|
trend={{ value: homeData.monthlyReviewGrowth.value, isUp: homeData.monthlyReviewGrowth.isUp }}
|
|
/>
|
|
<StatCard
|
|
title="本月审核通过率"
|
|
value={homeData.monthlyPassRate}
|
|
icon="ri-percent-line"
|
|
trend={{ value: homeData.passRateGrowth.value, isUp: homeData.passRateGrowth.isUp }}
|
|
/>
|
|
<StatCard
|
|
title="本月问题检出数"
|
|
value={homeData.issuesDetected}
|
|
icon="ri-error-warning-line"
|
|
trend={{ value: homeData.issuesGrowth.value, isUp: homeData.issuesGrowth.isUp }}
|
|
/>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* 快捷访问区域 */}
|
|
<Card title="快捷访问" icon="ri-speed-line" className="mt-6 transition-all duration-200 hover:shadow-[0_4px_15px_rgba(0,0,0,0.1)]">
|
|
<div className="shortcut-grid">
|
|
<ShortcutItem icon="ri-upload-cloud-line" label="上传文件" to="/files/upload" />
|
|
<ShortcutItem icon="ri-file-list-3-line" label="文档列表" to="/documents" />
|
|
<ShortcutItem icon="ri-list-check-3" label="评查点列表" to="/rules" />
|
|
<ShortcutItem icon="ri-folder-open-line" label="评查点分组" to="/rule-groups" />
|
|
</div>
|
|
</Card>
|
|
|
|
{/* 最近文档区域 */}
|
|
<Card
|
|
title="最近文档"
|
|
icon="ri-file-list-3-line"
|
|
extra={<Button to="/documents" size="small">查看全部</Button>}
|
|
className="mt-6"
|
|
>
|
|
<div className="doc-list">
|
|
{recentFiles.map((file: DocumentUI) => (
|
|
<div key={file.id} className="doc-item">
|
|
<div className="doc-info">
|
|
<FileTag
|
|
extension={file.name.endsWith('.pdf') ? 'pdf' : 'docx'}
|
|
showIcon={true}
|
|
showText={false}
|
|
showBackground={false}
|
|
size="lg"
|
|
className="mr-2"
|
|
/>
|
|
<div>
|
|
<div className="doc-name">{file.name}</div>
|
|
<div className="doc-meta">
|
|
<Tag size="sm" className="mr-2">
|
|
{file.typeName}
|
|
</Tag>
|
|
<span className="text-gray-500">·</span>
|
|
<span className="ml-2 text-gray-500">{file.updatedAt}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="doc-status">
|
|
{(() => {
|
|
const fileStatus = file.fileStatus || "-";
|
|
const status = fileProcessingStatusOptions.find(s => s.value === fileStatus) ||
|
|
fileProcessingStatusOptions[0];
|
|
const isSpinning = fileStatus !== "Processed";
|
|
return (
|
|
<div className={`inline-flex items-center px-2 py-1 rounded-full text-xs bg-${status.color}-100 text-${status.color}-800`}>
|
|
<i className={`${status.icon} ${isSpinning ? "animate-spin" : ""} mr-1`}></i>
|
|
<span>{status.label}</span>
|
|
</div>
|
|
);
|
|
})()}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 统计卡片组件
|
|
interface StatCardProps {
|
|
title: string;
|
|
value: number | string;
|
|
icon: string;
|
|
trend?: {
|
|
value: number;
|
|
isUp: boolean;
|
|
};
|
|
}
|
|
|
|
function StatCard({ title, value, icon, trend }: StatCardProps) {
|
|
return (
|
|
<div className="stat-card">
|
|
<div className="stat-title">{title}</div>
|
|
<div className="stat-value">{value}</div>
|
|
{trend && (
|
|
<div className={`stat-trend ${title === '本月问题检出数' ? !trend.isUp? 'trend-up':'trend-down' : trend.isUp ? 'trend-up' : 'trend-down'}`}>
|
|
<i className={`mr-1 ${trend.isUp ? 'ri-arrow-up-s-line' : 'ri-arrow-down-s-line'}`}></i>
|
|
<span>{trend.value}%</span>
|
|
<span className="ml-1 text-gray-500">较上月</span>
|
|
</div>
|
|
)}
|
|
<i className={`${icon} stat-icon`}></i>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 快捷方式组件
|
|
interface ShortcutItemProps {
|
|
icon: string;
|
|
label: string;
|
|
to: string;
|
|
}
|
|
|
|
function ShortcutItem({ icon, label, to }: ShortcutItemProps) {
|
|
return (
|
|
<Button
|
|
to={to}
|
|
type="default"
|
|
className="shortcut-item"
|
|
>
|
|
<i className={`${icon} shortcut-icon text-2xl`}></i>
|
|
<span className="shortcut-label">{label}</span>
|
|
</Button>
|
|
);
|
|
}
|