448 lines
25 KiB
TypeScript
448 lines
25 KiB
TypeScript
"use client";
|
||
|
||
import Link from "next/link";
|
||
import { useEffect, useMemo, useState } from "react";
|
||
import { Empty, Space, Spin, Tag, message } from "antd";
|
||
|
||
import { listGeneratedContracts, type GeneratedContractListItem } from "@/services/contract";
|
||
import { getReviews, type ReviewListItem } from "@/services/review";
|
||
import { useAuthStore } from "@/stores/auth.store";
|
||
import {
|
||
formatAutoReviewStatus,
|
||
formatContractType,
|
||
formatDateTime,
|
||
formatReviewAs,
|
||
formatRisk,
|
||
formatStatus,
|
||
getRiskColor,
|
||
getStatusColor,
|
||
} from "@/lib/format";
|
||
|
||
const workspaceUtilityLinks = [
|
||
{ href: "/files", label: "合同仓库", description: "查看文件、预览材料并关联合同。", eyebrow: "Files" },
|
||
{ href: "/search", label: "统一搜索", description: "按合同、审查、文件和模板快速定位。", eyebrow: "Search" },
|
||
{ href: "/reviews", label: "审查记录", description: "查看任务状态、风险等级,并进入详情页。", eyebrow: "Review History" },
|
||
{ href: "/chat", label: "法律问答", description: "围绕法条、风险点和修改建议继续追问。", eyebrow: "Legal Chat" },
|
||
{ href: "/contract-ledger", label: "合同台账", description: "集中查看生命周期、到期和逾期事项。", eyebrow: "Ledger" },
|
||
];
|
||
|
||
const draftingFlowItems = ["选择模板", "填写关键内容", "生成合同", "自动预审"];
|
||
|
||
type WorkspaceNotificationItem = {
|
||
id: string;
|
||
href: string;
|
||
title: string;
|
||
contract_title?: string | null;
|
||
description?: string | null;
|
||
priority?: "critical" | "high" | "medium" | "low" | string;
|
||
is_mine?: boolean;
|
||
};
|
||
|
||
function isCurrentMonth(value: string) {
|
||
const date = new Date(value);
|
||
const now = new Date();
|
||
return date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth();
|
||
}
|
||
|
||
function buildStats(items: ReviewListItem[]) {
|
||
return {
|
||
totalReviews: items.length,
|
||
monthlyReviews: items.filter((item) => isCurrentMonth(item.created_at)).length,
|
||
highRisk: items.filter((item) => item.risk_level === "high").length,
|
||
mediumRisk: items.filter((item) => item.risk_level === "medium").length,
|
||
lowRisk: items.filter((item) => item.risk_level === "low").length,
|
||
pendingCount: items.filter((item) => item.status === "pending" || item.status === "processing").length,
|
||
failedCount: items.filter((item) => item.status === "failed" || item.status === "partial").length,
|
||
completedCount: items.filter((item) => item.status === "completed").length,
|
||
};
|
||
}
|
||
|
||
function buildContractStats(items: GeneratedContractListItem[]) {
|
||
return {
|
||
totalContracts: items.length,
|
||
monthlyContracts: items.filter((item) => isCurrentMonth(item.generated_at)).length,
|
||
autoReviewCompleted: items.filter((item) => item.auto_review?.status === "completed").length,
|
||
autoReviewProcessing: items.filter((item) => item.auto_review?.status === "processing" || item.auto_review?.status === "pending").length,
|
||
autoReviewFailed: items.filter((item) => item.auto_review?.status === "failed" || item.auto_review?.status === "partial").length,
|
||
highRiskContracts: items.filter((item) => item.auto_review?.risk_level === "high").length,
|
||
};
|
||
}
|
||
|
||
function buildPriorityTasks(items: ReviewListItem[]) {
|
||
const highRiskItems = items.filter((item) => item.risk_level === "high").slice(0, 2);
|
||
const blockedItems = items.filter((item) => item.status === "failed" || item.status === "partial").slice(0, 2);
|
||
const processingItems = items.filter((item) => item.status === "pending" || item.status === "processing").slice(0, 2);
|
||
return [
|
||
...highRiskItems.map((item) => ({ id: item.id, href: `/reviews/${item.id}`, label: "高风险优先处理", title: item.title, meta: `${formatContractType(item.contract_type)} · ${formatRisk(item.risk_level)}` })),
|
||
...blockedItems.map((item) => ({ id: item.id, href: `/reviews/${item.id}`, label: "异常任务待处理", title: item.title, meta: `${formatContractType(item.contract_type)} · ${formatStatus(item.status)}` })),
|
||
...processingItems.map((item) => ({ id: item.id, href: `/reviews/${item.id}`, label: "处理中任务跟进", title: item.title, meta: `${formatContractType(item.contract_type)} · ${formatStatus(item.status)}` })),
|
||
].slice(0, 4);
|
||
}
|
||
|
||
function buildContractPriorityTasks(items: GeneratedContractListItem[]) {
|
||
const highRiskItems = items.filter((item) => item.auto_review?.risk_level === "high").slice(0, 2);
|
||
const pendingItems = items
|
||
.filter((item) => item.auto_review?.status === "processing" || item.auto_review?.status === "pending")
|
||
.slice(0, 2);
|
||
const failedItems = items
|
||
.filter((item) => item.auto_review?.status === "failed" || item.auto_review?.status === "partial")
|
||
.slice(0, 2);
|
||
return [
|
||
...highRiskItems.map((item) => ({
|
||
id: item.id,
|
||
href: item.auto_review?.detail_url || (item.auto_review?.review_id ? `/reviews/${item.auto_review.review_id}` : `/contracts/${item.id}`),
|
||
label: "高风险合同待处理",
|
||
title: item.template_name || item.file_name,
|
||
meta: `${formatContractType(item.document_type_code || item.contract_type)} · ${formatRisk(item.auto_review?.risk_level)}`,
|
||
})),
|
||
...failedItems.map((item) => ({
|
||
id: item.id,
|
||
href: `/contracts/${item.id}`,
|
||
label: "预审异常待确认",
|
||
title: item.template_name || item.file_name,
|
||
meta: `${formatContractType(item.document_type_code || item.contract_type)} · ${formatAutoReviewStatus(item.auto_review?.status)}`,
|
||
})),
|
||
...pendingItems.map((item) => ({
|
||
id: item.id,
|
||
href: `/contracts/${item.id}`,
|
||
label: "自动预审处理中",
|
||
title: item.template_name || item.file_name,
|
||
meta: `${formatContractType(item.document_type_code || item.contract_type)} · ${formatAutoReviewStatus(item.auto_review?.status)}`,
|
||
})),
|
||
].slice(0, 4);
|
||
}
|
||
|
||
function buildWorkspaceInsights(items: ReviewListItem[]) {
|
||
const latestHighRisk = items.find((item) => item.risk_level === "high");
|
||
const latestFailed = items.find((item) => item.status === "failed" || item.status === "partial");
|
||
const latestProcessing = items.find((item) => item.status === "pending" || item.status === "processing");
|
||
return [
|
||
latestHighRisk ? { label: "最高优先级", title: latestHighRisk.title, description: "当前存在高风险任务,建议优先进入详情页处理。", href: `/reviews/${latestHighRisk.id}` } : null,
|
||
latestFailed ? { label: "异常任务", title: latestFailed.title, description: "当前存在失败/部分完成任务,建议先定位卡点。", href: `/reviews/${latestFailed.id}` } : null,
|
||
latestProcessing ? { label: "处理中任务", title: latestProcessing.title, description: "当前仍有任务处理中,可继续跟进阶段状态。", href: `/reviews/${latestProcessing.id}` } : null,
|
||
].filter((item): item is { label: string; title: string; description: string; href: string } => item !== null);
|
||
}
|
||
|
||
function buildGettingStartedLinks(role: string | undefined) {
|
||
if (role === "admin") return [
|
||
{ href: "/contracts", label: "先起草一份合同", description: "验证模板起草、生成和自动预审主链路。" },
|
||
{ href: "/review", label: "上传已有合同审查", description: "验证客户已有合同的审查与报告交付。" },
|
||
{ href: "/templates", label: "准备合同模板", description: "让生成链路进入可重复使用状态。" },
|
||
{ href: "/knowledge-bases", label: "上传首批知识文档", description: "为问答和审查补齐依据来源。" },
|
||
];
|
||
return [
|
||
{ href: "/contracts", label: "先起草一份合同", description: "选择模板、填写关键内容并生成合同。" },
|
||
{ href: "/review", label: "上传已有合同审查", description: "上传合同文件或文本,完成第一次审查。" },
|
||
{ href: "/reviews", label: "查看审查记录", description: "从结果详情进入后续处理工作流。" },
|
||
{ href: "/chat", label: "继续使用审查助手", description: "围绕风险项、条款和建议继续追问。" },
|
||
];
|
||
}
|
||
|
||
export default function WorkspacePage() {
|
||
const [messageApi, contextHolder] = message.useMessage();
|
||
const currentUser = useAuthStore((state) => state.currentUser);
|
||
const [loading, setLoading] = useState(true);
|
||
const [reviews, setReviews] = useState<ReviewListItem[]>([]);
|
||
const [contracts, setContracts] = useState<GeneratedContractListItem[]>([]);
|
||
|
||
useEffect(() => {
|
||
const timer = setTimeout(async () => {
|
||
if (!currentUser) { setLoading(false); return; }
|
||
try {
|
||
const [reviewResult, contractResult] = await Promise.all([
|
||
getReviews(),
|
||
listGeneratedContracts({ page: 1, page_size: 100 }),
|
||
]);
|
||
if (reviewResult.code !== 0) { messageApi.error(reviewResult.message || "审查任务加载失败"); return; }
|
||
if (contractResult.code !== 0) { messageApi.error(contractResult.message || "合同起草记录加载失败"); return; }
|
||
setReviews(reviewResult.data.items);
|
||
setContracts(contractResult.data.items);
|
||
} catch { messageApi.error("工作台数据加载失败,请稍后重试"); }
|
||
finally { setLoading(false); }
|
||
}, 0);
|
||
return () => clearTimeout(timer);
|
||
}, [currentUser, messageApi]);
|
||
|
||
const stats = useMemo(() => buildStats(reviews), [reviews]);
|
||
const contractStats = useMemo(() => buildContractStats(contracts), [contracts]);
|
||
const contractReminderItems = useMemo(() => {
|
||
return contracts
|
||
.filter((item) => item.ledger_attention?.is_mine || item.lifecycle?.due_status === "due_soon" || item.lifecycle?.due_status === "overdue")
|
||
.sort((a, b) => Number(Boolean(b.ledger_attention?.is_mine)) - Number(Boolean(a.ledger_attention?.is_mine)))
|
||
.slice(0, 4);
|
||
}, [contracts]);
|
||
const notifications: WorkspaceNotificationItem[] = [];
|
||
const myContractTodoCount = contracts.filter((item) => item.ledger_attention?.is_mine).length;
|
||
const recentReviews = useMemo(() => reviews.slice(0, 5), [reviews]);
|
||
const recentContracts = useMemo(() => contracts.slice(0, 5), [contracts]);
|
||
const latestReview = recentReviews[0];
|
||
const latestContract = recentContracts[0];
|
||
const hasReviews = reviews.length > 0;
|
||
const hasContracts = contracts.length > 0;
|
||
const priorityTasks = useMemo(() => buildPriorityTasks(reviews), [reviews]);
|
||
const contractPriorityTasks = useMemo(() => buildContractPriorityTasks(contracts), [contracts]);
|
||
const adminWorkbenchLinks = useMemo(() => {
|
||
if (currentUser?.role !== "admin") return [];
|
||
return [
|
||
{ href: "/admin", label: "管理工作台", description: "统一进入后台治理、知识库、规则、模板与试点交付模块。" },
|
||
{ href: "/governance", label: "治理中心", description: "查看复核、证据、上线批准与门禁汇总。" },
|
||
{ href: "/knowledge-bases", label: "知识库管理", description: "维护文档生命周期和问答资料。" },
|
||
{ href: "/model-settings", label: "大模型设置", description: "维护模型供应商、密钥和运行时部署参数。" },
|
||
{ href: "/rules", label: "审查规则管理", description: "维护规则与风险识别逻辑。" },
|
||
{ href: "/templates", label: "模板管理", description: "维护合同模板与默认模板集。" },
|
||
{ href: "/audit-logs", label: "审计日志", description: "查看关键后台动作留痕。" },
|
||
{ href: "/pilot-readiness", label: "试点交付准备", description: "复查默认账号、演示链路和验收门禁。" },
|
||
];
|
||
}, [currentUser?.role]);
|
||
const workspaceInsights = useMemo(() => buildWorkspaceInsights(reviews), [reviews]);
|
||
const gettingStartedLinks = useMemo(() => buildGettingStartedLinks(currentUser?.role), [currentUser?.role]);
|
||
const activeTask = contractPriorityTasks[0] ?? priorityTasks[0] ?? null;
|
||
const dashboardHighlights = useMemo(() => [
|
||
...contractPriorityTasks.map((item) => ({ href: item.href, eyebrow: item.label, title: item.title, description: item.meta })),
|
||
...priorityTasks.map((item) => ({ href: item.href, eyebrow: item.label, title: item.title, description: item.meta })),
|
||
...workspaceInsights.map((item) => ({ href: item.href, eyebrow: item.label, title: item.title, description: item.description })),
|
||
].slice(0, activeTask ? 3 : 4), [activeTask, contractPriorityTasks, priorityTasks, workspaceInsights]);
|
||
const workflowLinks = useMemo(() => {
|
||
if (hasContracts || hasReviews) {
|
||
return workspaceUtilityLinks.slice(0, 4);
|
||
}
|
||
return [
|
||
...gettingStartedLinks.slice(2).map((item) => ({ ...item, eyebrow: "Next Step" })),
|
||
...workspaceUtilityLinks.slice(0, 2),
|
||
].slice(0, 4);
|
||
}, [gettingStartedLinks, hasContracts, hasReviews]);
|
||
const primaryAction = { href: "/contracts", label: "起草合同", description: "从模板选择、关键内容填写开始,生成后自动预审。" };
|
||
const secondaryAction = hasReviews || hasContracts
|
||
? { href: "/review", label: "上传已有合同审查", description: "直接上传 PDF / DOCX / TXT 或粘贴正文,适合审核客户现有合同。" }
|
||
: { href: "/review", label: "上传已有合同审查", description: "直接上传 PDF / DOCX / TXT 或粘贴正文,先完成一次审查闭环。" };
|
||
const workspaceHeadline = activeTask
|
||
? `你好,${currentUser?.display_name}。先处理待办,再快速发起。`
|
||
: `你好,${currentUser?.display_name}。先起草一份合同并启动自动预审。`;
|
||
const workspaceSubtitle = activeTask
|
||
? `当前优先任务:${activeTask.title}。也可以直接新建合同,完成选择模板、生成、自动预审和报告交付。`
|
||
: "工作台优先引导合同起草;如果客户已经有合同原文,也可以直接上传 PDF / DOCX / TXT 完成审查。";
|
||
const workspacePrimaryActions = useMemo(() => [
|
||
{ href: "/contracts", label: "合同起草", description: "选择模板、填写关键内容并生成合同。" },
|
||
{ href: "/review", label: "发起审查", description: "上传已有合同或粘贴正文进入审查。" },
|
||
], []);
|
||
const workspaceFocusItems = activeTask
|
||
? [
|
||
`优先任务:${activeTask.title}`,
|
||
`最近起草:${latestContract?.template_name || latestContract?.file_name || "暂无"}`,
|
||
`处理中任务:${stats.pendingCount} 个`,
|
||
]
|
||
: [
|
||
"先选择模板起草合同,验证生成和自动预审是否顺畅",
|
||
"如已有客户合同,直接上传审查并下载报告",
|
||
"生成结果可进入合同详情、审查详情和问答复核",
|
||
];
|
||
|
||
return (
|
||
<main className="landing-page">
|
||
{contextHolder}
|
||
<section className="landing-hero-shell">
|
||
<div className="landing-hero">
|
||
<span className="landing-badge">Contract Workspace</span>
|
||
<h1>{workspaceHeadline}</h1>
|
||
<p>{workspaceSubtitle}</p>
|
||
<div className="landing-actions">
|
||
<Link className="landing-primary-action" href={primaryAction.href}>{primaryAction.label}</Link>
|
||
<Link className="landing-secondary-action" href={secondaryAction.href}>{secondaryAction.label}</Link>
|
||
</div>
|
||
<div className="landing-command-visual" aria-label="合同审查智能流程">
|
||
<div className="landing-command-header">
|
||
<span>Draft & Review Pipeline</span>
|
||
<strong>{contractStats.autoReviewCompleted}/{contractStats.totalContracts || 0}</strong>
|
||
</div>
|
||
<div className="landing-command-flow">
|
||
{draftingFlowItems.map((item, index) => (
|
||
<div className="landing-command-step" key={item}>
|
||
<span>{String(index + 1).padStart(2, "0")}</span>
|
||
<strong>{item}</strong>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="landing-command-alerts">
|
||
<div><span>已起草</span><strong>{contractStats.totalContracts}</strong></div>
|
||
<div><span>预审中</span><strong>{contractStats.autoReviewProcessing}</strong></div>
|
||
<div><span>高风险</span><strong>{contractStats.highRiskContracts}</strong></div>
|
||
</div>
|
||
</div>
|
||
<div className="landing-stats">
|
||
<div className="landing-stat-card"><strong>{contractStats.totalContracts}</strong><span>累计起草合同</span></div>
|
||
<div className="landing-stat-card"><strong>{contractStats.monthlyContracts}</strong><span>本月起草次数</span></div>
|
||
<div className="landing-stat-card"><strong>{contractStats.autoReviewCompleted}</strong><span>预审完成</span></div>
|
||
<div className="landing-stat-card"><strong>{stats.totalReviews}</strong><span>审查任务</span></div>
|
||
</div>
|
||
<div className="workspace-focus-strip">
|
||
{workspaceFocusItems.slice(0, 3).map((item) => (
|
||
<span key={item}>{item}</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="landing-hero-panel">
|
||
<div className="landing-panel-card landing-panel-card-primary">
|
||
<span className="landing-panel-eyebrow">Drafting Overview</span>
|
||
<strong>起草与预审统计</strong>
|
||
<ul className="landing-panel-list">
|
||
<li>累计起草:{contractStats.totalContracts}</li>
|
||
<li>自动预审完成:{contractStats.autoReviewCompleted}</li>
|
||
<li>预审异常:{contractStats.autoReviewFailed}</li>
|
||
</ul>
|
||
</div>
|
||
<div className="landing-panel-card">
|
||
<span className="landing-panel-eyebrow">Recent Contract</span>
|
||
<strong>最近起草合同</strong>
|
||
<p>{latestContract ? `${latestContract.template_name || latestContract.file_name} · ${formatDateTime(latestContract.generated_at)}` : "暂无最近起草合同"}</p>
|
||
{latestContract ? <Link href={`/contracts/${latestContract.id}`}>进入合同详情</Link> : <Link href="/contracts">新建合同</Link>}
|
||
</div>
|
||
<div className="landing-panel-card landing-panel-card-soft">
|
||
<span className="landing-panel-eyebrow">Session</span>
|
||
<strong>{currentUser?.role === "admin" ? "管理员视角" : "业务视角"}</strong>
|
||
<ul className="landing-panel-list landing-panel-list-soft">
|
||
<li>合同待办:{myContractTodoCount}</li>
|
||
<li>已完成任务:{stats.completedCount}</li>
|
||
<li>最近风险:{latestReview?.risk_level ? formatRisk(latestReview.risk_level) : "-"}</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="workspace-today-panel">
|
||
<div className="workspace-panel-head">
|
||
<div>
|
||
<h2>今日工作</h2>
|
||
<p>先处理待办,再进入起草或上传审查。</p>
|
||
</div>
|
||
<Link href="/contract-ledger">查看合同台账</Link>
|
||
</div>
|
||
<div className="workspace-today-grid">
|
||
<div className="workspace-task-column">
|
||
<div className="workspace-column-head">
|
||
<strong>待处理</strong>
|
||
<Tag color={myContractTodoCount ? "orange" : "default"}>{myContractTodoCount} 个待办</Tag>
|
||
</div>
|
||
{notifications.length === 0 && contractReminderItems.length === 0 && dashboardHighlights.length === 0 ? (
|
||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="当前没有需要优先处理的任务。" />
|
||
) : (
|
||
<div className="workspace-compact-list">
|
||
{notifications.slice(0, 4).map((item) => (
|
||
<Link className="workspace-compact-row" href={item.href} key={item.id}>
|
||
<span>{item.title}</span>
|
||
<strong>{item.contract_title ?? item.title}</strong>
|
||
<small>{item.description ?? "请进入详情继续处理。"}</small>
|
||
<Space wrap size={6}>
|
||
<Tag color={item.priority === "critical" ? "red" : item.priority === "high" ? "orange" : "blue"}>
|
||
{item.priority === "critical" ? "紧急" : item.priority === "high" ? "高优先级" : "提醒"}
|
||
</Tag>
|
||
{item.is_mine ? <Tag color="orange">待我处理</Tag> : null}
|
||
</Space>
|
||
</Link>
|
||
))}
|
||
{notifications.length === 0 ? contractReminderItems.slice(0, 3).map((item) => (
|
||
<Link className="workspace-compact-row" href={`/contracts/${item.id}`} key={item.id}>
|
||
<span>{item.ledger_attention?.primary?.label ?? "合同提醒"}</span>
|
||
<strong>{item.template_name}</strong>
|
||
<small>{item.ledger_attention?.primary?.description ?? "请进入合同详情继续处理生命周期事项。"}</small>
|
||
<Space wrap size={6}>
|
||
{item.ledger_attention?.is_mine ? <Tag color="orange">待我处理</Tag> : null}
|
||
<Tag>{item.lifecycle?.due_status === "overdue" ? "已逾期" : item.lifecycle?.due_status === "due_soon" ? "即将到期" : "生命周期"}</Tag>
|
||
</Space>
|
||
</Link>
|
||
)) : null}
|
||
{dashboardHighlights.slice(0, notifications.length || contractReminderItems.length ? 1 : 3).map((item) => (
|
||
<Link className="workspace-compact-row" href={item.href} key={`${item.eyebrow}-${item.href}`}>
|
||
<span>{item.eyebrow}</span>
|
||
<strong>{item.title}</strong>
|
||
<small>{item.description}</small>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="workspace-flow-column">
|
||
<div className="workspace-column-head">
|
||
<strong>快速发起</strong>
|
||
<Tag color="blue">{workspacePrimaryActions.length} 个入口</Tag>
|
||
</div>
|
||
<div className="workspace-flow-list">
|
||
{workspacePrimaryActions.map((item, index) => (
|
||
<Link className="workspace-flow-row" href={item.href} key={item.href}>
|
||
<span>{index + 1}</span>
|
||
<div>
|
||
<strong>{item.label}</strong>
|
||
<small>{item.description}</small>
|
||
</div>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="landing-section workspace-tools-section">
|
||
<div className="landing-section-header workspace-section-header-compact">
|
||
<h2>常用入口</h2>
|
||
<p>快速发起之外的资料、搜索和问答集中放在这里。</p>
|
||
</div>
|
||
<div className="workspace-tool-grid">
|
||
{workflowLinks.map((item) => (
|
||
<Link className="workspace-tool-link" href={item.href} key={`${item.eyebrow}-${item.href}`}>
|
||
<span className="landing-card-eyebrow">{item.eyebrow}</span>
|
||
<strong>{item.label}</strong>
|
||
<small>{item.description}</small>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{adminWorkbenchLinks.length ? (
|
||
<section className="landing-section workspace-admin-section">
|
||
<div className="landing-section-header workspace-section-header-compact">
|
||
<h2>管理员入口</h2>
|
||
<p>后台治理入口保持可达,但不抢占日常业务操作。</p>
|
||
</div>
|
||
<div className="workspace-admin-strip">
|
||
{adminWorkbenchLinks.map((item) => (
|
||
<Link className="workspace-admin-link" href={item.href} key={item.href}>
|
||
<strong>{item.label}</strong>
|
||
<small>{item.description}</small>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</section>
|
||
) : null}
|
||
|
||
<section className="landing-section">
|
||
<div className="landing-section-header">
|
||
<h2>最近审查记录</h2>
|
||
<p>直接给出最近审查任务,减少反复跳去历史页的成本。</p>
|
||
</div>
|
||
{loading ? (
|
||
<div style={{ padding: 32, textAlign: "center" }}><Spin /></div>
|
||
) : recentReviews.length === 0 ? (
|
||
<Empty description="还没有审查记录,先发起第一份合同审查。" />
|
||
) : (
|
||
<div className="landing-grid">
|
||
{recentReviews.slice(0, 3).map((item) => (
|
||
<Link className="landing-card" href={`/reviews/${item.id}`} key={item.id}>
|
||
<span className="landing-card-eyebrow">{formatDateTime(item.created_at)}</span>
|
||
<strong>{item.title}</strong>
|
||
<span>{formatContractType(item.contract_type)} · {formatReviewAs(item.review_as)}</span>
|
||
<Space wrap>
|
||
<Tag color={getStatusColor(item.status)}>{formatStatus(item.status)}</Tag>
|
||
<Tag color={item.risk_level ? getRiskColor(item.risk_level) : "default"}>
|
||
{item.risk_level ? formatRisk(item.risk_level) : "未评级"}
|
||
</Tag>
|
||
</Space>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
)}
|
||
</section>
|
||
</main>
|
||
);
|
||
}
|