新增主页,优化评查点结果一致性的显示效果
This commit is contained in:
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { Sidebar } from './Sidebar';
|
import { Sidebar } from './Sidebar';
|
||||||
// import { Header } from './Header';
|
// import { Header } from './Header';
|
||||||
import { Breadcrumb } from './Breadcrumb';
|
import { Breadcrumb } from './Breadcrumb';
|
||||||
import { useMatches } from '@remix-run/react';
|
import { useMatches, useLocation } from '@remix-run/react';
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -11,7 +11,7 @@ interface LayoutProps {
|
|||||||
// 添加一个接口表示路由handle可能包含的属性
|
// 添加一个接口表示路由handle可能包含的属性
|
||||||
interface RouteHandle {
|
interface RouteHandle {
|
||||||
hideBreadcrumb?: boolean;
|
hideBreadcrumb?: boolean;
|
||||||
[key: string]: any;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Match {
|
interface Match {
|
||||||
@@ -23,9 +23,14 @@ interface Match {
|
|||||||
export function Layout({ children }: LayoutProps) {
|
export function Layout({ children }: LayoutProps) {
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||||
const matches = useMatches() as Match[];
|
const matches = useMatches() as Match[];
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
// 检查当前路径是否应该隐藏侧边栏
|
||||||
|
const noLayoutPaths = ['/login', '/'];
|
||||||
|
const shouldHideSidebar = noLayoutPaths.includes(location.pathname);
|
||||||
|
|
||||||
// 检查当前路由是否应该隐藏默认面包屑
|
// 检查当前路由是否应该隐藏默认面包屑
|
||||||
const shouldHideBreadcrumb = matches.some(match =>
|
const shouldHideBreadcrumb = shouldHideSidebar || matches.some(match =>
|
||||||
match.handle && match.handle.hideBreadcrumb === true
|
match.handle && match.handle.hideBreadcrumb === true
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -43,6 +48,11 @@ export function Layout({ children }: LayoutProps) {
|
|||||||
localStorage.setItem('sidebarCollapsed', String(newState));
|
localStorage.setItem('sidebarCollapsed', String(newState));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 如果是无布局页面,只渲染内容
|
||||||
|
if (shouldHideSidebar) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="layout-container">
|
<div className="layout-container">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
|
|||||||
@@ -19,10 +19,17 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
|
|||||||
const [expandedMenus, setExpandedMenus] = useState<Record<string, boolean>>({});
|
const [expandedMenus, setExpandedMenus] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
const menuItems: MenuItem[] = [
|
const menuItems: MenuItem[] = [
|
||||||
|
{
|
||||||
|
id: 'contract-search',
|
||||||
|
title: '智能搜索',
|
||||||
|
path: '/contract-search',
|
||||||
|
hideBreadcrumb: true,
|
||||||
|
icon: 'ri-search-line'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'home',
|
id: 'home',
|
||||||
title: '系统概览',
|
title: '系统概览',
|
||||||
path: '/',
|
path: '/home',
|
||||||
icon: 'ri-home-line'
|
icon: 'ri-home-line'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -459,15 +459,24 @@ export function ReviewPointsList({
|
|||||||
|
|
||||||
// 获取所有consistency规则中的fields
|
// 获取所有consistency规则中的fields
|
||||||
const allConsistencyFields: string[][] = [];
|
const allConsistencyFields: string[][] = [];
|
||||||
|
|
||||||
|
// 存储 sourceField 和 targetField 的映射关系
|
||||||
|
const pairsMapping: Record<string, string> = {};
|
||||||
|
|
||||||
consistencyRules.forEach(rule => {
|
consistencyRules.forEach(rule => {
|
||||||
if (rule.config?.fields) {
|
if (rule.config?.fields) {
|
||||||
allConsistencyFields.push(rule.config.fields);
|
allConsistencyFields.push(rule.config.fields);
|
||||||
}else if (rule.config?.pairs) {
|
} else if (rule.config?.pairs) {
|
||||||
// 处理pairs情况,提取sourceField和targetField
|
// 处理pairs情况,提取sourceField和targetField
|
||||||
const fields: string[] = [];
|
const fields: string[] = [];
|
||||||
rule.config.pairs.forEach(pair => {
|
rule.config.pairs.forEach(pair => {
|
||||||
if (pair.sourceField) fields.push(pair.sourceField);
|
if (pair.sourceField) fields.push(pair.sourceField);
|
||||||
if (pair.targetField) fields.push(pair.targetField);
|
if (pair.targetField) fields.push(pair.targetField);
|
||||||
|
|
||||||
|
// 记录 sourceField 和 targetField 的映射关系
|
||||||
|
if (pair.sourceField && pair.targetField) {
|
||||||
|
pairsMapping[pair.sourceField] = pair.targetField;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (fields.length > 0) {
|
if (fields.length > 0) {
|
||||||
allConsistencyFields.push(fields);
|
allConsistencyFields.push(fields);
|
||||||
@@ -507,10 +516,136 @@ export function ReviewPointsList({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 对每个分组内的条目按照 sourceField 和 targetField 的关系进行排序
|
||||||
|
Object.keys(groupedContent).forEach(groupKey => {
|
||||||
|
if (groupKey !== 'default' && groupedContent[groupKey].length > 1) {
|
||||||
|
// 创建一个新数组用于存储排序后的结果
|
||||||
|
const sortedEntries: Array<[string, { page?: number | string, value?: object }]> = [];
|
||||||
|
const entriesMap = new Map(groupedContent[groupKey]);
|
||||||
|
|
||||||
|
// 找出所有的源字段和目标字段对
|
||||||
|
const processed = new Set<string>();
|
||||||
|
|
||||||
|
// 构建一个字段之间的连接关系图,用于处理嵌套关系
|
||||||
|
const fieldChains: Array<string[]> = [];
|
||||||
|
|
||||||
|
// 遍历所有映射关系,构建字段链
|
||||||
|
const buildFieldChains = () => {
|
||||||
|
// 创建一个图结构,记录每个字段的后继字段
|
||||||
|
const graph: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
// 根据映射关系建立图
|
||||||
|
Object.entries(pairsMapping).forEach(([source, target]) => {
|
||||||
|
if (!graph[source]) graph[source] = [];
|
||||||
|
graph[source].push(target);
|
||||||
|
|
||||||
|
// 确保目标字段在图中有一个空数组
|
||||||
|
if (!graph[target]) graph[target] = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 查找所有在当前分组中的字段
|
||||||
|
const fieldsInGroup = new Set(Array.from(entriesMap.keys()));
|
||||||
|
|
||||||
|
// 找出入度为0的节点(即只作为sourceField而不是任何targetField的字段)
|
||||||
|
const startNodes: string[] = [];
|
||||||
|
for (const field of fieldsInGroup) {
|
||||||
|
// 检查该字段是否作为targetField存在
|
||||||
|
const isTarget = Object.values(pairsMapping).includes(field);
|
||||||
|
// 如果该字段是sourceField但不是targetField,则为起始节点
|
||||||
|
if (!isTarget && field in pairsMapping) {
|
||||||
|
startNodes.push(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从每个起始节点开始,使用DFS构建字段链
|
||||||
|
for (const startNode of startNodes) {
|
||||||
|
const chain: string[] = [];
|
||||||
|
const dfs = (node: string) => {
|
||||||
|
// 如果该节点不在当前分组中,则跳过
|
||||||
|
if (!fieldsInGroup.has(node)) return;
|
||||||
|
|
||||||
|
chain.push(node);
|
||||||
|
// 遍历所有后继节点
|
||||||
|
for (const nextNode of graph[node] || []) {
|
||||||
|
dfs(nextNode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dfs(startNode);
|
||||||
|
|
||||||
|
// 如果链不为空,则添加到字段链列表中
|
||||||
|
if (chain.length > 0) {
|
||||||
|
fieldChains.push(chain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理环形依赖或没有入度为0的节点的情况
|
||||||
|
// 找出未被处理的字段
|
||||||
|
const processedInChains = new Set(fieldChains.flat());
|
||||||
|
const remainingFields = Array.from(fieldsInGroup).filter(f => !processedInChains.has(f));
|
||||||
|
|
||||||
|
// 将剩余字段按照pairsMapping的关系组织成链
|
||||||
|
while (remainingFields.length > 0) {
|
||||||
|
const field = remainingFields.shift()!;
|
||||||
|
|
||||||
|
// 如果该字段已经在某个链中,则跳过
|
||||||
|
if (processedInChains.has(field)) continue;
|
||||||
|
|
||||||
|
const chain: string[] = [field];
|
||||||
|
processedInChains.add(field);
|
||||||
|
|
||||||
|
// 向后查找链
|
||||||
|
let currentField = field;
|
||||||
|
while (currentField in pairsMapping) {
|
||||||
|
const nextField = pairsMapping[currentField];
|
||||||
|
// 如果下一个字段不在分组中或已处理,则中断
|
||||||
|
if (!fieldsInGroup.has(nextField) || processedInChains.has(nextField)) break;
|
||||||
|
|
||||||
|
chain.push(nextField);
|
||||||
|
processedInChains.add(nextField);
|
||||||
|
currentField = nextField;
|
||||||
|
|
||||||
|
// 从剩余字段中移除
|
||||||
|
const index = remainingFields.indexOf(nextField);
|
||||||
|
if (index !== -1) {
|
||||||
|
remainingFields.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chain.length > 0) {
|
||||||
|
fieldChains.push(chain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
buildFieldChains();
|
||||||
|
|
||||||
|
// 根据字段链构建排序后的结果
|
||||||
|
fieldChains.forEach(chain => {
|
||||||
|
chain.forEach(field => {
|
||||||
|
if (entriesMap.has(field) && !processed.has(field)) {
|
||||||
|
sortedEntries.push([field, entriesMap.get(field)!]);
|
||||||
|
processed.add(field);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加剩余未处理的字段
|
||||||
|
for (const [key] of groupedContent[groupKey]) {
|
||||||
|
if (!processed.has(key)) {
|
||||||
|
sortedEntries.push([key, entriesMap.get(key)!]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用排序后的结果替换原数组
|
||||||
|
groupedContent[groupKey] = sortedEntries;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 渲染各个分组 */}
|
{/* 渲染各个分组 */}
|
||||||
{Object.entries(groupedContent).map(([groupKey, entries], groupIndex) => {
|
{Object.entries(groupedContent).map(([groupKey, entries]) => {
|
||||||
if (entries.length === 0) return null;
|
if (entries.length === 0) return null;
|
||||||
|
|
||||||
// 非默认组添加边框
|
// 非默认组添加边框
|
||||||
|
|||||||
+131
-281
@@ -1,324 +1,174 @@
|
|||||||
// import React from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { type MetaFunction, type LoaderFunctionArgs, redirect } from "@remix-run/node";
|
import { useNavigate, Form } from '@remix-run/react';
|
||||||
import { useLoaderData } from "@remix-run/react";
|
import { type MetaFunction, type ActionFunctionArgs, LoaderFunctionArgs, redirect } from "@remix-run/node";
|
||||||
import { Card } from "~/components/ui/Card";
|
import styles from "~/styles/pages/home.css?url";
|
||||||
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 dayjs from 'dayjs';
|
||||||
import { getUserSession } from "~/root";
|
// import { getUserSession, logout } 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 = () => [
|
export const links = () => [
|
||||||
{ rel: "stylesheet", href: homeStyles },
|
{ rel: "stylesheet", href: styles }
|
||||||
...fileTagLinks()
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const meta: MetaFunction = () => {
|
export const meta: MetaFunction = () => {
|
||||||
return [
|
return [
|
||||||
{ title: "中国烟草AI合同及卷宗审核系统 - 首页" },
|
{ title: "中国烟草AI合同及卷宗审核系统 - 首页" },
|
||||||
{ name: "description", content: "AI审核系统首页" }
|
{ name: "description", content: "中国烟草AI合同及卷宗审核系统首页" },
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
// API 响应的类型定义
|
// 处理登出请求
|
||||||
// interface StatsData {
|
export async function action({ request }: ActionFunctionArgs) {
|
||||||
// totalFiles: number;
|
const formData = await request.formData();
|
||||||
// reviewedFiles: number;
|
const intent = formData.get("intent");
|
||||||
// pendingFiles: number;
|
|
||||||
// passRate: number;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 添加认证检查
|
if (intent === "logout") {
|
||||||
export async function loader({ request }: LoaderFunctionArgs) {
|
// return logout(request);
|
||||||
// 检查用户登录状态
|
|
||||||
// const { isAuthenticated } = await getUserSession(request);
|
|
||||||
|
|
||||||
// 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 null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证用户登录状态
|
||||||
|
// export async function loader({ request }: LoaderFunctionArgs) {
|
||||||
|
// const { isAuthenticated } = await getUserSession(request);
|
||||||
|
|
||||||
|
// if (!isAuthenticated) {
|
||||||
|
// return redirect("/login");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return Response.json({ isAuthenticated });
|
||||||
|
// }
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { homeData, recentFiles: initialRecentFiles } = useLoaderData<typeof loader>();
|
const navigate = useNavigate();
|
||||||
// 使用useState存储最近文档数据,初始值为loader加载的数据
|
|
||||||
const [recentFiles, setRecentFiles] = useState<DocumentUI[]>(initialRecentFiles || []);
|
|
||||||
const [currentDateTime, setCurrentDateTime] = useState({
|
const [currentDateTime, setCurrentDateTime] = useState({
|
||||||
date: '',
|
date: '',
|
||||||
time: ''
|
time: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新当前时间
|
// 更新日期时间
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 使用dayjs格式化日期和时间
|
|
||||||
const updateDateTime = () => {
|
const updateDateTime = () => {
|
||||||
const now = dayjs();
|
const now = dayjs();
|
||||||
|
// 格式化日期: YYYY/MM/DD
|
||||||
setCurrentDateTime({
|
setCurrentDateTime({
|
||||||
date: now.format('YYYY年MM月DD日'),
|
date: now.format('YYYY/MM/DD'),
|
||||||
time: now.format('HH:mm:ss')
|
time: now.format('HH:mm:ss')
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 立即更新一次
|
// 初始化时间
|
||||||
updateDateTime();
|
updateDateTime();
|
||||||
|
|
||||||
// 设置计时器,每秒更新一次
|
// 每秒更新一次
|
||||||
const timerID = setInterval(updateDateTime, 1000);
|
const timerID = setInterval(updateDateTime, 1000);
|
||||||
|
|
||||||
// 清理函数,组件卸载时清除计时器
|
|
||||||
return () => clearInterval(timerID);
|
return () => clearInterval(timerID);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 修改useEffect定时器,每10秒自动获取最近文档数据
|
// 处理模块点击
|
||||||
// 按照定时器更新最近文档
|
const handleModuleClick = (path: string) => {
|
||||||
useEffect(() => {
|
navigate(path);
|
||||||
// 定义一个函数用于获取最新的文档数据
|
};
|
||||||
const fetchLatestDocuments = async () => {
|
|
||||||
try {
|
// 处理键盘事件
|
||||||
const documentSearchParams = {
|
const handleKeyDown = (path: string, e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
page: 1,
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
pageSize: 10,
|
handleModuleClick(path);
|
||||||
order: 'updated_at.desc'
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('定时获取最新文档数据...');
|
// 处理登出
|
||||||
const responseDocuments = await getDocuments(documentSearchParams);
|
const handleLogout = () => {
|
||||||
|
// 使用Form组件提交登出请求
|
||||||
if (responseDocuments.error) {
|
const form = document.getElementById('logout-form') as HTMLFormElement;
|
||||||
console.error('获取最近文档数据失败', responseDocuments.error);
|
if (form) {
|
||||||
return;
|
form.submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取新的文档数据
|
|
||||||
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="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>
|
|
||||||
</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" />
|
|
||||||
{/* <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>
|
|
||||||
</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 (
|
return (
|
||||||
<div className="stat-card">
|
<div className="home-page">
|
||||||
<div className="stat-title">{title}</div>
|
{/* 登出表单 - 隐藏 */}
|
||||||
<div className="stat-value">{value}</div>
|
<Form method="post" id="logout-form" className="hidden">
|
||||||
{trend && (
|
<input type="hidden" name="intent" value="logout" />
|
||||||
<div className={`stat-trend ${trend.isUp ? 'trend-up' : 'trend-down'}`}>
|
</Form>
|
||||||
<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>
|
<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>
|
||||||
)}
|
<div className="user-info">
|
||||||
<i className={`${icon} stat-icon`}></i>
|
<span className="datetime">{currentDateTime.date} {currentDateTime.time}</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>
|
||||||
|
|
||||||
|
{/* 主要内容 */}
|
||||||
|
<main className="index-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="合同管理"
|
||||||
|
>
|
||||||
|
<i className="ri-file-list-2-fill text-[3rem] text-[#269b6c]"></i>
|
||||||
|
<span className="module-name">合同管理</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 案卷智能评查模块 */}
|
||||||
|
<div
|
||||||
|
className="module-card"
|
||||||
|
onClick={() => handleModuleClick('/home')}
|
||||||
|
onKeyDown={(e) => handleKeyDown('/home', e)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label="案卷智能评查"
|
||||||
|
>
|
||||||
|
<i className="ri-folder-shared-fill text-[3rem] text-[#269b6c]"></i>
|
||||||
|
<span className="module-name">案卷智能评查</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 智慧法务大模型模块 */}
|
||||||
|
<div
|
||||||
|
className="module-card"
|
||||||
|
onClick={() => handleModuleClick('/prompts')}
|
||||||
|
onKeyDown={(e) => handleKeyDown('/prompts', e)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label="智慧法务大模型"
|
||||||
|
>
|
||||||
|
<i className="ri-robot-2-fill text-[3rem] text-[#269b6c]"></i>
|
||||||
|
<span className="module-name">智慧法务大模型</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{/* 底部山水背景 */}
|
||||||
|
<footer className="footer">
|
||||||
|
<div className="mountains-bg"></div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
</div>
|
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
// import { useState } from 'react';
|
||||||
|
// import styles from '~/styles/pages/contract-search.css?url';
|
||||||
|
|
||||||
|
// export const links = () => [
|
||||||
|
// { rel: 'stylesheet', href: styles }
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// export default function ContractSearchIndex() {
|
||||||
|
// const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
// const handleSearchInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
// setSearchQuery(e.target.value);
|
||||||
|
// // 自动调整高度
|
||||||
|
// e.target.style.height = 'auto';
|
||||||
|
// e.target.style.height = Math.max(80, e.target.scrollHeight) + 'px';
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleSearch = () => {
|
||||||
|
// if (searchQuery.trim()) {
|
||||||
|
// console.log('搜索查询:', searchQuery);
|
||||||
|
// // 这里可以添加实际的搜索逻辑
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleCategoryClick = (categoryName: string) => {
|
||||||
|
// console.log('选择分类:', categoryName);
|
||||||
|
// // 这里可以添加跳转到相应分类的逻辑
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleKeyDown = (e: React.KeyboardEvent, categoryName: string) => {
|
||||||
|
// if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
// e.preventDefault();
|
||||||
|
// handleCategoryClick(categoryName);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="content-area">
|
||||||
|
// <div className="search-hero">
|
||||||
|
// <h1 className="search-title">AI智能合同模板搜索</h1>
|
||||||
|
// <p className="search-subtitle">输入合同名称、用途或关键内容,快速找到最适合的模板</p>
|
||||||
|
|
||||||
|
// <div className="search-container">
|
||||||
|
// <div className="search-box">
|
||||||
|
// <textarea
|
||||||
|
// className="search-textarea"
|
||||||
|
// placeholder="例如:销售合同、设备采购协议、包含违约责任条款的合同..."
|
||||||
|
// value={searchQuery}
|
||||||
|
// onChange={handleSearchInputChange}
|
||||||
|
// aria-label="搜索输入框"
|
||||||
|
// ></textarea>
|
||||||
|
// <div className="search-actions">
|
||||||
|
// <div className="search-tips">
|
||||||
|
// <i className="ri-lightbulb-line mr-1"></i>
|
||||||
|
// 支持自然语言描述,AI将为您匹配最相关的模板
|
||||||
|
// </div>
|
||||||
|
// <button
|
||||||
|
// className="search-btn"
|
||||||
|
// onClick={handleSearch}
|
||||||
|
// aria-label="开始搜索"
|
||||||
|
// >
|
||||||
|
// <i className="ri-search-line"></i>
|
||||||
|
// 智能搜索
|
||||||
|
// </button>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <div className="quick-categories">
|
||||||
|
// <button
|
||||||
|
// className="category-card"
|
||||||
|
// onClick={() => handleCategoryClick('销售合同')}
|
||||||
|
// onKeyDown={(e) => handleKeyDown(e, '销售合同')}
|
||||||
|
// tabIndex={0}
|
||||||
|
// aria-label="销售合同分类"
|
||||||
|
// >
|
||||||
|
// <div className="category-icon">
|
||||||
|
// <i className="ri-handshake-line"></i>
|
||||||
|
// </div>
|
||||||
|
// <div className="category-title">销售合同</div>
|
||||||
|
// <div className="category-count">128个模板</div>
|
||||||
|
// </button>
|
||||||
|
// <button
|
||||||
|
// className="category-card"
|
||||||
|
// onClick={() => handleCategoryClick('采购合同')}
|
||||||
|
// onKeyDown={(e) => handleKeyDown(e, '采购合同')}
|
||||||
|
// tabIndex={0}
|
||||||
|
// aria-label="采购合同分类"
|
||||||
|
// >
|
||||||
|
// <div className="category-icon">
|
||||||
|
// <i className="ri-shopping-cart-line"></i>
|
||||||
|
// </div>
|
||||||
|
// <div className="category-title">采购合同</div>
|
||||||
|
// <div className="category-count">96个模板</div>
|
||||||
|
// </button>
|
||||||
|
// <button
|
||||||
|
// className="category-card"
|
||||||
|
// onClick={() => handleCategoryClick('物流运输')}
|
||||||
|
// onKeyDown={(e) => handleKeyDown(e, '物流运输')}
|
||||||
|
// tabIndex={0}
|
||||||
|
// aria-label="物流运输分类"
|
||||||
|
// >
|
||||||
|
// <div className="category-icon">
|
||||||
|
// <i className="ri-truck-line"></i>
|
||||||
|
// </div>
|
||||||
|
// <div className="category-title">物流运输</div>
|
||||||
|
// <div className="category-count">64个模板</div>
|
||||||
|
// </button>
|
||||||
|
// <button
|
||||||
|
// className="category-card"
|
||||||
|
// onClick={() => handleCategoryClick('人事劳务')}
|
||||||
|
// onKeyDown={(e) => handleKeyDown(e, '人事劳务')}
|
||||||
|
// tabIndex={0}
|
||||||
|
// aria-label="人事劳务分类"
|
||||||
|
// >
|
||||||
|
// <div className="category-icon">
|
||||||
|
// <i className="ri-user-settings-line"></i>
|
||||||
|
// </div>
|
||||||
|
// <div className="category-title">人事劳务</div>
|
||||||
|
// <div className="category-count">52个模板</div>
|
||||||
|
// </button>
|
||||||
|
// <button
|
||||||
|
// className="category-card"
|
||||||
|
// onClick={() => handleCategoryClick('租赁合同')}
|
||||||
|
// onKeyDown={(e) => handleKeyDown(e, '租赁合同')}
|
||||||
|
// tabIndex={0}
|
||||||
|
// aria-label="租赁合同分类"
|
||||||
|
// >
|
||||||
|
// <div className="category-icon">
|
||||||
|
// <i className="ri-building-line"></i>
|
||||||
|
// </div>
|
||||||
|
// <div className="category-title">租赁合同</div>
|
||||||
|
// <div className="category-count">38个模板</div>
|
||||||
|
// </button>
|
||||||
|
// <button
|
||||||
|
// className="category-card"
|
||||||
|
// onClick={() => handleCategoryClick('保密协议')}
|
||||||
|
// onKeyDown={(e) => handleKeyDown(e, '保密协议')}
|
||||||
|
// tabIndex={0}
|
||||||
|
// aria-label="保密协议分类"
|
||||||
|
// >
|
||||||
|
// <div className="category-icon">
|
||||||
|
// <i className="ri-shield-check-line"></i>
|
||||||
|
// </div>
|
||||||
|
// <div className="category-title">保密协议</div>
|
||||||
|
// <div className="category-count">24个模板</div>
|
||||||
|
// </button>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Outlet } from "@remix-run/react";
|
||||||
|
import { type MetaFunction } from "@remix-run/node";
|
||||||
|
|
||||||
|
export const meta: MetaFunction = () => {
|
||||||
|
return [
|
||||||
|
{ title: "智能搜索 - 中国烟草AI合同及卷宗审核系统" },
|
||||||
|
{
|
||||||
|
name: "contract-search",
|
||||||
|
content: "智能搜索模块,包括智能搜索功能"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handle = {
|
||||||
|
breadcrumb: "智能搜索"
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置列表路由布局
|
||||||
|
*/
|
||||||
|
export default function ContractSearchLayout() {
|
||||||
|
return <Outlet />;
|
||||||
|
}
|
||||||
+284
-140
@@ -1,180 +1,324 @@
|
|||||||
import { useState, useEffect } from 'react';
|
// import React from 'react';
|
||||||
import { useNavigate, Form } from '@remix-run/react';
|
import { type MetaFunction, type LoaderFunctionArgs, redirect } from "@remix-run/node";
|
||||||
import { type MetaFunction, type ActionFunctionArgs, LoaderFunctionArgs, redirect, json } from "@remix-run/node";
|
import { useLoaderData } from "@remix-run/react";
|
||||||
import styles from "~/styles/pages/home.css?url";
|
import { Card } from "~/components/ui/Card";
|
||||||
import { getUserSession, logout } from "~/root";
|
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 = () => [
|
export const links = () => [
|
||||||
{ rel: "stylesheet", href: styles }
|
{ rel: "stylesheet", href: homeStyles },
|
||||||
|
...fileTagLinks()
|
||||||
];
|
];
|
||||||
|
|
||||||
export const meta: MetaFunction = () => {
|
export const meta: MetaFunction = () => {
|
||||||
return [
|
return [
|
||||||
{ title: "中国烟草AI合同及卷宗审核系统 - 首页" },
|
{ title: "中国烟草AI合同及卷宗审核系统 - 首页" },
|
||||||
{ name: "description", content: "中国烟草AI合同及卷宗审核系统首页" },
|
{ name: "description", content: "AI审核系统首页" }
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理登出请求
|
// API 响应的类型定义
|
||||||
export async function action({ request }: ActionFunctionArgs) {
|
// interface StatsData {
|
||||||
const formData = await request.formData();
|
// totalFiles: number;
|
||||||
const intent = formData.get("intent");
|
// reviewedFiles: number;
|
||||||
|
// pendingFiles: number;
|
||||||
|
// passRate: number;
|
||||||
|
// }
|
||||||
|
|
||||||
if (intent === "logout") {
|
// 添加认证检查
|
||||||
return logout(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证用户登录状态
|
|
||||||
export async function loader({ request }: LoaderFunctionArgs) {
|
export async function loader({ request }: LoaderFunctionArgs) {
|
||||||
const { isAuthenticated } = await getUserSession(request);
|
// 检查用户登录状态
|
||||||
|
// const { isAuthenticated } = await getUserSession(request);
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
// if (!isAuthenticated) {
|
||||||
return redirect("/login");
|
// 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() {
|
export default function Home() {
|
||||||
const navigate = useNavigate();
|
const { homeData, recentFiles: initialRecentFiles } = useLoaderData<typeof loader>();
|
||||||
const [currentTime, setCurrentTime] = useState('');
|
// 使用useState存储最近文档数据,初始值为loader加载的数据
|
||||||
const [currentDate, setCurrentDate] = useState('');
|
const [recentFiles, setRecentFiles] = useState<DocumentUI[]>(initialRecentFiles || []);
|
||||||
|
const [currentDateTime, setCurrentDateTime] = useState({
|
||||||
|
date: '',
|
||||||
|
time: ''
|
||||||
|
});
|
||||||
|
|
||||||
// 更新日期时间
|
// 更新当前时间
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// 使用dayjs格式化日期和时间
|
||||||
const updateDateTime = () => {
|
const updateDateTime = () => {
|
||||||
const now = new Date();
|
const now = dayjs();
|
||||||
// 格式化日期: YYYY/MM/DD
|
setCurrentDateTime({
|
||||||
const date = now.toLocaleDateString('zh-CN', {
|
date: now.format('YYYY年MM月DD日'),
|
||||||
year: 'numeric',
|
time: now.format('HH:mm:ss')
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit'
|
|
||||||
}).replace(/\//g, '/');
|
|
||||||
|
|
||||||
// 格式化时间: HH:MM
|
|
||||||
const time = now.toLocaleTimeString('zh-CN', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: false
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setCurrentDate(date);
|
|
||||||
setCurrentTime(time);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化时间
|
// 立即更新一次
|
||||||
updateDateTime();
|
updateDateTime();
|
||||||
|
|
||||||
// 每分钟更新一次
|
// 设置计时器,每秒更新一次
|
||||||
const interval = setInterval(updateDateTime, 60000);
|
const timerID = setInterval(updateDateTime, 1000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
// 清理函数,组件卸载时清除计时器
|
||||||
|
return () => clearInterval(timerID);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 处理模块点击
|
// 修改useEffect定时器,每10秒自动获取最近文档数据
|
||||||
const handleModuleClick = (path: string) => {
|
// 按照定时器更新最近文档
|
||||||
navigate(path);
|
useEffect(() => {
|
||||||
};
|
// 定义一个函数用于获取最新的文档数据
|
||||||
|
const fetchLatestDocuments = async () => {
|
||||||
|
try {
|
||||||
|
const documentSearchParams = {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
order: 'updated_at.desc'
|
||||||
|
};
|
||||||
|
|
||||||
// 处理键盘事件
|
console.log('定时获取最新文档数据...');
|
||||||
const handleKeyDown = (path: string, e: React.KeyboardEvent<HTMLDivElement>) => {
|
const responseDocuments = await getDocuments(documentSearchParams);
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
handleModuleClick(path);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理登出
|
if (responseDocuments.error) {
|
||||||
const handleLogout = () => {
|
console.error('获取最近文档数据失败', responseDocuments.error);
|
||||||
// 使用Form组件提交登出请求
|
return;
|
||||||
const form = document.getElementById('logout-form') as HTMLFormElement;
|
}
|
||||||
if (form) {
|
|
||||||
form.submit();
|
// 获取新的文档数据
|
||||||
}
|
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 (
|
return (
|
||||||
<div className="home-page">
|
<div className="dashboard-container">
|
||||||
{/* 登出表单 - 隐藏 */}
|
{/* 页面头部 */}
|
||||||
<Form method="post" id="logout-form" className="hidden">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<input type="hidden" name="intent" value="logout" />
|
<h2 className="text-xl font-medium">系统概览</h2>
|
||||||
</Form>
|
<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>
|
||||||
|
|
||||||
{/* 头部 */}
|
{/* 统计卡片区域 */}
|
||||||
<header className="header">
|
<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="logo-container">
|
<div className="stat-grid ">
|
||||||
<img src="/logo.png" alt="中国烟草" className="logo" />
|
<StatCard
|
||||||
<span className="logo-text">中国烟草</span>
|
title="今日待审文件"
|
||||||
<span className="logo-text-en">CHINA TOBACCO</span>
|
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>
|
||||||
<div className="user-info">
|
</Card>
|
||||||
<span className="datetime">{currentDate} {currentTime}</span>
|
|
||||||
<div className="user">
|
{/* 快捷访问区域 */}
|
||||||
<img src="/avatar.png" alt="用户头像" className="avatar" />
|
<Card title="快捷访问" icon="ri-speed-line" className="mt-6 transition-all duration-200 hover:shadow-[0_4px_15px_rgba(0,0,0,0.1)]">
|
||||||
<span className="username">系统管理员</span>
|
<div className="shortcut-grid">
|
||||||
<button
|
<ShortcutItem icon="ri-upload-cloud-line" label="上传文件" to="/files/upload" />
|
||||||
onClick={handleLogout}
|
<ShortcutItem icon="ri-file-list-3-line" label="文档列表" to="/documents" />
|
||||||
className="logout-button"
|
<ShortcutItem icon="ri-list-check-3" label="评查点列表" to="/rules" />
|
||||||
aria-label="登出"
|
<ShortcutItem icon="ri-folder-open-line" label="评查点分组" to="/rule-groups" />
|
||||||
>
|
{/* <ShortcutItem icon="ri-file-chart-line" label="评查详情" to="/reviews" /> */}
|
||||||
<i className="ri-logout-box-line"></i>
|
<ShortcutItem icon="ri-file-list-line" label="文档类型" to="/document-types" />
|
||||||
</button>
|
{/* <ShortcutItem icon="ri-settings-3-line" label="系统设置" to="/settings" /> */}
|
||||||
</div>
|
<ShortcutItem icon="ri-chat-1-line" label="提示词管理" to="/prompts" />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</Card>
|
||||||
|
|
||||||
{/* 主要内容 */}
|
{/* 最近文档区域 */}
|
||||||
<main className="main-content">
|
<Card
|
||||||
<h1 className="welcome-text">- 欢迎来到智慧法务平台 -</h1>
|
title="最近文档"
|
||||||
|
icon="ri-file-list-3-line"
|
||||||
<div className="modules-container">
|
extra={<Button to="/documents" size="small">查看全部</Button>}
|
||||||
{/* 合同管理模块 */}
|
className="mt-6"
|
||||||
<div
|
>
|
||||||
className="module-card"
|
<div className="doc-list">
|
||||||
onClick={() => handleModuleClick('/documents')}
|
{recentFiles.map((file: DocumentUI) => (
|
||||||
onKeyDown={(e) => handleKeyDown('/documents', e)}
|
<div key={file.id} className="doc-item">
|
||||||
role="button"
|
<div className="doc-info">
|
||||||
tabIndex={0}
|
<FileTag
|
||||||
aria-label="合同管理"
|
extension={file.name.endsWith('.pdf') ? 'pdf' : 'docx'}
|
||||||
>
|
showIcon={true}
|
||||||
<div className="module-icon contract-icon"></div>
|
showText={false}
|
||||||
<span className="module-name">合同管理</span>
|
showBackground={false}
|
||||||
</div>
|
size="lg"
|
||||||
|
className="mr-2"
|
||||||
{/* 案卷智能评查模块 */}
|
/>
|
||||||
<div
|
<div>
|
||||||
className="module-card"
|
<div className="doc-name">{file.name}</div>
|
||||||
onClick={() => handleModuleClick('/')}
|
<div className="doc-meta">
|
||||||
onKeyDown={(e) => handleKeyDown('/', e)}
|
<Tag size="sm" className="mr-2">
|
||||||
role="button"
|
{file.typeName}
|
||||||
tabIndex={0}
|
</Tag>
|
||||||
aria-label="案卷智能评查"
|
<span className="text-gray-500">·</span>
|
||||||
>
|
<span className="ml-2 text-gray-500">{file.updatedAt}</span>
|
||||||
<div className="module-icon review-icon"></div>
|
</div>
|
||||||
<span className="module-name">案卷智能评查</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="doc-status">
|
||||||
{/* 智慧法务大模型模块 */}
|
{(() => {
|
||||||
<div
|
const fileStatus = file.fileStatus || "-";
|
||||||
className="module-card"
|
const status = fileProcessingStatusOptions.find(s => s.value === fileStatus) ||
|
||||||
onClick={() => handleModuleClick('/prompts')}
|
fileProcessingStatusOptions[0];
|
||||||
onKeyDown={(e) => handleKeyDown('/prompts', e)}
|
const isSpinning = fileStatus !== "Processed";
|
||||||
role="button"
|
return (
|
||||||
tabIndex={0}
|
<div className={`inline-flex items-center px-2 py-1 rounded-full text-xs bg-${status.color}-100 text-${status.color}-800`}>
|
||||||
aria-label="智慧法务大模型"
|
<i className={`${status.icon} ${isSpinning ? "animate-spin" : ""} mr-1`}></i>
|
||||||
>
|
<span>{status.label}</span>
|
||||||
<div className="module-icon ai-icon"></div>
|
</div>
|
||||||
<span className="module-name">智慧法务大模型</span>
|
);
|
||||||
</div>
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</Card>
|
||||||
|
|
||||||
{/* 底部山水背景 */}
|
|
||||||
<footer className="footer">
|
|
||||||
<div className="mountains-bg"></div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
/* 合同搜索页面样式 */
|
||||||
|
:root {
|
||||||
|
--primary-color: #00684a;
|
||||||
|
--primary-hover: #005a40;
|
||||||
|
--primary-light: rgba(0, 104, 74, 0.1);
|
||||||
|
--success-color: #52c41a;
|
||||||
|
--warning-color: #faad14;
|
||||||
|
--error-color: #ff4d4f;
|
||||||
|
--text-color: rgba(0, 0, 0, 0.85);
|
||||||
|
--text-secondary: rgba(0, 0, 0, 0.45);
|
||||||
|
--border-color: #f0f0f0;
|
||||||
|
--bg-gray: #f5f5f5;
|
||||||
|
--gradient-bg: linear-gradient(135deg, #f8fffe 0%, #f0f9ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
@apply flex-1 p-6 overflow-y-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-hero {
|
||||||
|
@apply text-center py-16 bg-gradient-to-r from-green-50 to-blue-50 rounded-2xl mb-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-title {
|
||||||
|
@apply text-3xl font-semibold text-gray-800 mb-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-subtitle {
|
||||||
|
@apply text-base text-gray-600 mb-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
@apply max-w-xl mx-auto relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
@apply bg-white border-2 border-gray-200 rounded-xl p-4 shadow-md transition-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box:focus-within {
|
||||||
|
@apply border-green-600 shadow-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-textarea {
|
||||||
|
@apply w-full min-h-[80px] border-none outline-none resize-vertical text-base leading-relaxed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-actions {
|
||||||
|
@apply flex justify-between items-center mt-3 pt-3 border-t border-gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-tips {
|
||||||
|
@apply text-xs text-gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn {
|
||||||
|
@apply bg-green-700 text-white border-none rounded-lg py-2.5 px-6 text-sm font-medium cursor-pointer transition-all flex items-center gap-1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn:hover {
|
||||||
|
@apply bg-green-800 transform -translate-y-0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-categories {
|
||||||
|
@apply grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4 mt-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card {
|
||||||
|
@apply bg-white rounded-xl p-5 text-center cursor-pointer transition-all border border-gray-100 shadow-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card:hover {
|
||||||
|
@apply transform -translate-y-0.5 shadow-md border-green-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-icon {
|
||||||
|
@apply w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center mx-auto mb-3 text-green-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-title {
|
||||||
|
@apply text-base font-medium mb-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-count {
|
||||||
|
@apply text-xs text-gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-search-container {
|
||||||
|
@apply w-full p-6 bg-white rounded-lg shadow-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-header {
|
||||||
|
@apply mb-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-description {
|
||||||
|
@apply text-gray-600 text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
@apply mb-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form-row {
|
||||||
|
@apply flex flex-wrap items-center gap-4 mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form-item {
|
||||||
|
@apply flex-grow min-w-[200px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form-label {
|
||||||
|
@apply block text-sm font-medium text-gray-700 mb-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form-input {
|
||||||
|
@apply w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form-select {
|
||||||
|
@apply w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form-date {
|
||||||
|
@apply w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-buttons {
|
||||||
|
@apply flex justify-end gap-4 mt-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
@apply px-4 py-2 rounded-md font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button-primary {
|
||||||
|
@apply bg-blue-600 text-white hover:bg-blue-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button-secondary {
|
||||||
|
@apply bg-gray-200 text-gray-700 hover:bg-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
@apply mt-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-header {
|
||||||
|
@apply flex justify-between items-center mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-title {
|
||||||
|
@apply text-lg font-medium text-gray-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-count {
|
||||||
|
@apply text-gray-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-table {
|
||||||
|
@apply w-full border-collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-table th {
|
||||||
|
@apply px-4 py-3 bg-gray-100 text-left text-xs font-medium text-gray-600 uppercase tracking-wider;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-table td {
|
||||||
|
@apply px-4 py-3 border-t border-gray-200 text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-actions {
|
||||||
|
@apply flex gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-action {
|
||||||
|
@apply text-blue-600 hover:text-blue-800 cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results {
|
||||||
|
@apply py-12 text-center text-gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
@apply mt-6 flex justify-center;
|
||||||
|
}
|
||||||
+27
-25
@@ -2,8 +2,9 @@
|
|||||||
.home-page {
|
.home-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100vh;
|
height: 100vh;
|
||||||
background-color: #f0f7f4;
|
/* height: 100%; */
|
||||||
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 头部样式 */
|
/* 头部样式 */
|
||||||
@@ -11,7 +12,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.75rem 2rem;
|
padding: 0.75rem 1rem;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
@@ -22,19 +23,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
height: 36px;
|
height: 60px;
|
||||||
margin-right: 0.75rem;
|
margin-right: 0.15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-text {
|
.logo-text {
|
||||||
font-size: 1.125rem;
|
font-size: 1.8rem;
|
||||||
font-weight: 600;
|
font-weight: 800;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-text-en {
|
.logo-text-en {
|
||||||
font-size: 0.75rem;
|
margin-top: -0.2rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
color: #666;
|
color: #666;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.datetime {
|
.datetime {
|
||||||
font-size: 0.875rem;
|
font-size:1rem;
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,13 +73,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 主要内容区域 */
|
/* 主要内容区域 */
|
||||||
.main-content {
|
.index-main-content {
|
||||||
|
border-radius: 0.5rem 0.5rem 0 0;
|
||||||
|
height: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 3rem 1.5rem;
|
justify-content: center;
|
||||||
|
margin: 1rem;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
background-color: #f0f7f4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-text {
|
.welcome-text {
|
||||||
@@ -98,11 +104,10 @@
|
|||||||
|
|
||||||
.module-card {
|
.module-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 250px;
|
width: 250px;
|
||||||
padding: 2rem 1.5rem;
|
padding: 2rem 1.5rem;
|
||||||
background-color: white;
|
background: linear-gradient(to bottom, #ebebeb, #ffffff);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
@@ -111,19 +116,11 @@
|
|||||||
|
|
||||||
.module-card:hover {
|
.module-card:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
|
border: 1px solid #269b6c;
|
||||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-icon {
|
/* .contract-icon {
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contract-icon {
|
|
||||||
background-image: url('/images/contract-icon.svg');
|
background-image: url('/images/contract-icon.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,9 +130,10 @@
|
|||||||
|
|
||||||
.ai-icon {
|
.ai-icon {
|
||||||
background-image: url('/images/ai-icon.svg');
|
background-image: url('/images/ai-icon.svg');
|
||||||
}
|
} */
|
||||||
|
|
||||||
.module-name {
|
.module-name {
|
||||||
|
margin-left: 1rem;
|
||||||
font-size: 1.125rem;
|
font-size: 1.125rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #333;
|
color: #333;
|
||||||
@@ -144,8 +142,12 @@
|
|||||||
/* 底部山水背景 */
|
/* 底部山水背景 */
|
||||||
.footer {
|
.footer {
|
||||||
height: 200px;
|
height: 200px;
|
||||||
|
margin: 1rem;
|
||||||
|
margin-top: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
background-color: #f0f7f4;
|
||||||
|
border-radius: 0 0 0.5rem 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mountains-bg {
|
.mountains-bg {
|
||||||
|
|||||||
+1629
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user