280 lines
8.2 KiB
TypeScript
280 lines
8.2 KiB
TypeScript
// import React from 'react';
|
||
import { type MetaFunction } from "@remix-run/node";
|
||
import { useLoaderData } from "@remix-run/react";
|
||
import { Card } from "~/components/ui/Card";
|
||
import { Button } from "~/components/ui/Button";
|
||
|
||
export const links = () => [
|
||
{ rel: "stylesheet", href: "app/styles/pages/home.css" }
|
||
];
|
||
|
||
export const meta: MetaFunction = () => {
|
||
return [
|
||
{ title: "中国烟草AI合同及卷宗审核系统 - 首页" },
|
||
{ name: "description", content: "AI审核系统首页" }
|
||
];
|
||
};
|
||
|
||
// API 响应的类型定义
|
||
interface StatsData {
|
||
totalFiles: number;
|
||
reviewedFiles: number;
|
||
pendingFiles: number;
|
||
passRate: number;
|
||
}
|
||
|
||
interface RecentFile {
|
||
id: string;
|
||
name: string;
|
||
type: string;
|
||
reviewStatus: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
// interface LoaderData {
|
||
// stats: StatsData;
|
||
// recentFiles: RecentFile[];
|
||
// }
|
||
|
||
|
||
// 模拟数据,实际项目中应该从API获取
|
||
export async function loader() {
|
||
try {
|
||
// 实际项目中这里应该是 API 调用
|
||
// const response = await fetch('/api/dashboard/stats');
|
||
// const stats: StatsData = await response.json();
|
||
|
||
// const filesResponse = await fetch('/api/files/recent');
|
||
// const recentFiles: RecentFile[] = await filesResponse.json();
|
||
|
||
// 模拟数据
|
||
const stats = {
|
||
totalFiles: 156,
|
||
reviewedFiles: 124,
|
||
pendingFiles: 32,
|
||
passRate: 92.5
|
||
} as StatsData;
|
||
|
||
const recentFiles = [
|
||
{
|
||
id: "1",
|
||
name: "2023年度烟草专卖零售许可证.pdf",
|
||
type: "专卖许可证",
|
||
reviewStatus: "pass",
|
||
updatedAt: "2023-12-24 14:30"
|
||
},
|
||
{
|
||
id: "2",
|
||
name: "烟草制品购销合同(2023-12).docx",
|
||
type: "合同文档",
|
||
reviewStatus: "warning",
|
||
updatedAt: "2023-12-23 09:15"
|
||
},
|
||
{
|
||
id: "3",
|
||
name: "专卖管理处罚决定书(2023-145).pdf",
|
||
type: "行政处罚决定书",
|
||
reviewStatus: "fail",
|
||
updatedAt: "2023-12-22 16:45"
|
||
},
|
||
{
|
||
id: "4",
|
||
name: "2023年第四季度采购合同.docx",
|
||
type: "合同文档",
|
||
reviewStatus: "pass",
|
||
updatedAt: "2023-12-20 11:20"
|
||
},
|
||
{
|
||
id: "5",
|
||
name: "广告宣传协议书.pdf",
|
||
type: "合同文档",
|
||
reviewStatus: "pass",
|
||
updatedAt: "2023-12-18 15:30"
|
||
}
|
||
] as RecentFile[];
|
||
|
||
return Response.json({ stats, recentFiles });
|
||
} catch (error) {
|
||
// 错误处理
|
||
console.error('Failed to fetch dashboard data:', error);
|
||
return Response.json(
|
||
{ error: '获取数据失败,请稍后重试' },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
}
|
||
|
||
export default function Index() {
|
||
const { stats, recentFiles } = useLoaderData<typeof loader>();
|
||
|
||
return (
|
||
<div className="dashboard-container">
|
||
{/* 页面标识 */}
|
||
<div className="mb-4 p-3 bg-yellow-100 border border-yellow-300 rounded text-yellow-800">
|
||
<h3 className="font-bold text-lg">当前页面: 首页 (_index.tsx)</h3>
|
||
<p>如果你看到这个提示,说明你正在浏览首页,而不是评查点列表页面。</p>
|
||
<div className="mt-2">
|
||
<a href="/debug" className="text-blue-600 hover:underline">查看路由诊断页面</a> |
|
||
<a href="/rules" className="ml-2 text-blue-600 hover:underline">通过原生链接访问评查点列表</a>
|
||
</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={stats.totalFiles}
|
||
icon="ri-file-list-3-line"
|
||
/>
|
||
<StatCard
|
||
title="已审核"
|
||
value={stats.reviewedFiles}
|
||
icon="ri-check-double-line"
|
||
trend={{ value: 5.2, isUp: true }}
|
||
/>
|
||
<StatCard
|
||
title="待审核"
|
||
value={stats.pendingFiles}
|
||
icon="ri-time-line"
|
||
trend={{ value: 2.1, isUp: false }}
|
||
/>
|
||
<StatCard
|
||
title="通过率"
|
||
value={`${stats.passRate}%`}
|
||
icon="ri-pie-chart-line"
|
||
trend={{ value: 1.5, isUp: true }}
|
||
/>
|
||
</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/new" />
|
||
<ShortcutItem icon="ri-file-list-3-line" label="文件列表" to="/files" />
|
||
<ShortcutItem icon="ri-list-check-2" 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="/doc-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="/files" size="small">查看全部</Button>}
|
||
className="mt-6"
|
||
>
|
||
<div className="doc-list">
|
||
{recentFiles.map((file: RecentFile) => (
|
||
<div key={file.id} className="doc-item">
|
||
<div className="doc-info">
|
||
<i className={`doc-icon ${file.name.endsWith('.pdf') ? 'ri-file-pdf-line' : 'ri-file-word-line'}`}></i>
|
||
<div>
|
||
<div className="doc-name">{file.name}</div>
|
||
<div className="doc-meta">
|
||
{file.type} · {file.updatedAt}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="doc-status">
|
||
<StatusBadge status={file.reviewStatus} />
|
||
</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>
|
||
);
|
||
}
|
||
|
||
// 状态标签组件
|
||
interface StatusBadgeProps {
|
||
status: string;
|
||
}
|
||
|
||
function StatusBadge({ status }: StatusBadgeProps) {
|
||
const statusMap: Record<string, { label: string, className: string, icon: string }> = {
|
||
pass: {
|
||
label: '通过',
|
||
className: 'status-badge status-success',
|
||
icon: 'ri-checkbox-circle-line'
|
||
},
|
||
warning: {
|
||
label: '警告',
|
||
className: 'status-badge status-warning',
|
||
icon: 'ri-error-warning-line'
|
||
},
|
||
fail: {
|
||
label: '不通过',
|
||
className: 'status-badge status-error',
|
||
icon: 'ri-close-circle-line'
|
||
},
|
||
pending: {
|
||
label: '待确认',
|
||
className: 'status-badge',
|
||
icon: 'ri-time-line'
|
||
}
|
||
};
|
||
|
||
const { label, className, icon } = statusMap[status] || statusMap.pending;
|
||
|
||
return (
|
||
<span className={className}>
|
||
<i className={`${icon} mr-1`}></i>
|
||
{label}
|
||
</span>
|
||
);
|
||
}
|