新增主页,优化评查点结果一致性的显示效果
This commit is contained in:
+288
-144
@@ -1,180 +1,324 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate, Form } from '@remix-run/react';
|
||||
import { type MetaFunction, type ActionFunctionArgs, LoaderFunctionArgs, redirect, json } from "@remix-run/node";
|
||||
import styles from "~/styles/pages/home.css?url";
|
||||
import { getUserSession, logout } from "~/root";
|
||||
// import React from 'react';
|
||||
import { type MetaFunction, type LoaderFunctionArgs, redirect } from "@remix-run/node";
|
||||
import { useLoaderData } 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 } from "~/api/files/documents";
|
||||
import { useState, useEffect } from "react";
|
||||
import { getHomeData } from "~/api/home/home";
|
||||
import dayjs from 'dayjs';
|
||||
// import { getUserSession } from "~/root";
|
||||
|
||||
// 文件处理状态选项
|
||||
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: styles }
|
||||
{ rel: "stylesheet", href: homeStyles },
|
||||
...fileTagLinks()
|
||||
];
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "中国烟草AI合同及卷宗审核系统 - 首页" },
|
||||
{ name: "description", content: "中国烟草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;
|
||||
}
|
||||
// API 响应的类型定义
|
||||
// interface StatsData {
|
||||
// totalFiles: number;
|
||||
// reviewedFiles: number;
|
||||
// pendingFiles: number;
|
||||
// passRate: number;
|
||||
// }
|
||||
|
||||
// 验证用户登录状态
|
||||
// 添加认证检查
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const { isAuthenticated } = await getUserSession(request);
|
||||
// 检查用户登录状态
|
||||
// const { isAuthenticated } = await getUserSession(request);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return redirect("/login");
|
||||
// if (!isAuthenticated) {
|
||||
// return redirect("/login");
|
||||
// }
|
||||
|
||||
try {
|
||||
const documentSearchParams = {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
order: 'updated_at.desc'
|
||||
};
|
||||
|
||||
// 获取最近文档数据
|
||||
const responseDocuments = await getDocuments(documentSearchParams);
|
||||
if (responseDocuments.error) {
|
||||
console.error('获取最近文档数据失败', responseDocuments.error);
|
||||
return Response.json({ error: responseDocuments.error }, { status: responseDocuments.status || 500 });
|
||||
}
|
||||
const recentFiles = responseDocuments.data?.documents || [];
|
||||
console.log("recentFiles-------",recentFiles);
|
||||
|
||||
|
||||
const homeData = await getHomeData();
|
||||
console.log("homeData-------",homeData);
|
||||
|
||||
|
||||
return Response.json({ homeData, recentFiles });
|
||||
} catch (error) {
|
||||
// 错误处理
|
||||
console.error('Failed to fetch dashboard data:', error);
|
||||
return Response.json(
|
||||
{ error: '获取数据失败,请稍后重试' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return json({ isAuthenticated });
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const navigate = useNavigate();
|
||||
const [currentTime, setCurrentTime] = useState('');
|
||||
const [currentDate, setCurrentDate] = useState('');
|
||||
const { homeData, recentFiles: initialRecentFiles } = useLoaderData<typeof loader>();
|
||||
// 使用useState存储最近文档数据,初始值为loader加载的数据
|
||||
const [recentFiles, setRecentFiles] = useState<DocumentUI[]>(initialRecentFiles || []);
|
||||
const [currentDateTime, setCurrentDateTime] = useState({
|
||||
date: '',
|
||||
time: ''
|
||||
});
|
||||
|
||||
// 更新日期时间
|
||||
// 更新当前时间
|
||||
useEffect(() => {
|
||||
// 使用dayjs格式化日期和时间
|
||||
const updateDateTime = () => {
|
||||
const now = new Date();
|
||||
// 格式化日期: YYYY/MM/DD
|
||||
const date = now.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
}).replace(/\//g, '/');
|
||||
|
||||
// 格式化时间: HH:MM
|
||||
const time = now.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
const now = dayjs();
|
||||
setCurrentDateTime({
|
||||
date: now.format('YYYY年MM月DD日'),
|
||||
time: now.format('HH:mm:ss')
|
||||
});
|
||||
|
||||
setCurrentDate(date);
|
||||
setCurrentTime(time);
|
||||
};
|
||||
|
||||
// 初始化时间
|
||||
// 立即更新一次
|
||||
updateDateTime();
|
||||
|
||||
// 每分钟更新一次
|
||||
const interval = setInterval(updateDateTime, 60000);
|
||||
// 设置计时器,每秒更新一次
|
||||
const timerID = setInterval(updateDateTime, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
// 清理函数,组件卸载时清除计时器
|
||||
return () => clearInterval(timerID);
|
||||
}, []);
|
||||
|
||||
// 处理模块点击
|
||||
const handleModuleClick = (path: string) => {
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
// 处理键盘事件
|
||||
const handleKeyDown = (path: string, e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleModuleClick(path);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理登出
|
||||
const handleLogout = () => {
|
||||
// 使用Form组件提交登出请求
|
||||
const form = document.getElementById('logout-form') as HTMLFormElement;
|
||||
if (form) {
|
||||
form.submit();
|
||||
}
|
||||
};
|
||||
// 修改useEffect定时器,每10秒自动获取最近文档数据
|
||||
// 按照定时器更新最近文档
|
||||
useEffect(() => {
|
||||
// 定义一个函数用于获取最新的文档数据
|
||||
const fetchLatestDocuments = async () => {
|
||||
try {
|
||||
const documentSearchParams = {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
order: 'updated_at.desc'
|
||||
};
|
||||
|
||||
console.log('定时获取最新文档数据...');
|
||||
const responseDocuments = await getDocuments(documentSearchParams);
|
||||
|
||||
if (responseDocuments.error) {
|
||||
console.error('获取最近文档数据失败', responseDocuments.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取新的文档数据
|
||||
const newRecentFiles = responseDocuments.data?.documents || [];
|
||||
|
||||
// 检查数据是否有变化
|
||||
if (JSON.stringify(newRecentFiles) !== JSON.stringify(recentFiles)) {
|
||||
console.log('文档数据已更新,直接更新状态');
|
||||
// 直接更新状态,不需要刷新页面
|
||||
setRecentFiles(newRecentFiles);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('自动获取文档数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 设置10秒的定时器
|
||||
const timerID = setInterval(fetchLatestDocuments, 10000);
|
||||
|
||||
// 组件卸载时清除定时器
|
||||
return () => {
|
||||
console.log('清除文档数据自动更新定时器');
|
||||
clearInterval(timerID);
|
||||
};
|
||||
}, []); // 不再依赖recentFiles,避免循环依赖
|
||||
|
||||
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.png" alt="中国烟草" className="logo" />
|
||||
<span className="logo-text">中国烟草</span>
|
||||
<span className="logo-text-en">CHINA TOBACCO</span>
|
||||
<div className="dashboard-container">
|
||||
{/* 页面头部 */}
|
||||
<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>管</span>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm font-medium">系统管理员</p>
|
||||
<p className="text-xs text-gray-500">超级管理员</p>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<div className="user-info">
|
||||
<span className="datetime">{currentDate} {currentTime}</span>
|
||||
<div className="user">
|
||||
<img src="/avatar.png" alt="用户头像" className="avatar" />
|
||||
<span className="username">系统管理员</span>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="logout-button"
|
||||
aria-label="登出"
|
||||
>
|
||||
<i className="ri-logout-box-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</Card>
|
||||
|
||||
{/* 主要内容 */}
|
||||
<main className="main-content">
|
||||
<h1 className="welcome-text">- 欢迎来到智慧法务平台 -</h1>
|
||||
|
||||
<div className="modules-container">
|
||||
{/* 合同管理模块 */}
|
||||
<div
|
||||
className="module-card"
|
||||
onClick={() => handleModuleClick('/documents')}
|
||||
onKeyDown={(e) => handleKeyDown('/documents', e)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="合同管理"
|
||||
>
|
||||
<div className="module-icon contract-icon"></div>
|
||||
<span className="module-name">合同管理</span>
|
||||
</div>
|
||||
|
||||
{/* 案卷智能评查模块 */}
|
||||
<div
|
||||
className="module-card"
|
||||
onClick={() => handleModuleClick('/')}
|
||||
onKeyDown={(e) => handleKeyDown('/', e)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="案卷智能评查"
|
||||
>
|
||||
<div className="module-icon review-icon"></div>
|
||||
<span className="module-name">案卷智能评查</span>
|
||||
</div>
|
||||
|
||||
{/* 智慧法务大模型模块 */}
|
||||
<div
|
||||
className="module-card"
|
||||
onClick={() => handleModuleClick('/prompts')}
|
||||
onKeyDown={(e) => handleKeyDown('/prompts', e)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="智慧法务大模型"
|
||||
>
|
||||
<div className="module-icon ai-icon"></div>
|
||||
<span className="module-name">智慧法务大模型</span>
|
||||
</div>
|
||||
{/* 快捷访问区域 */}
|
||||
<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" />
|
||||
{/* <ShortcutItem icon="ri-file-chart-line" label="评查详情" to="/reviews" /> */}
|
||||
<ShortcutItem icon="ri-file-list-line" label="文档类型" to="/document-types" />
|
||||
{/* <ShortcutItem icon="ri-settings-3-line" label="系统设置" to="/settings" /> */}
|
||||
<ShortcutItem icon="ri-chat-1-line" label="提示词管理" to="/prompts" />
|
||||
</div>
|
||||
</main>
|
||||
</Card>
|
||||
|
||||
{/* 底部山水背景 */}
|
||||
<footer className="footer">
|
||||
<div className="mountains-bg"></div>
|
||||
</footer>
|
||||
{/* 最近文档区域 */}
|
||||
<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 ${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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user