feat(frontend): P2 页面开发 — 今日行动中心 + 对比 + 决策线程 + OODA + 助推追踪 + 密级标签

- 今日行动中心: 6 区块结构(AI早报/必须处理/建议关注/增长机会/等待他人/已完成)
- 多企业对比: 集成 CompareMode 组件 + 企业选择器
- 决策线程列表/详情: 集成 ThreadList/ThreadDetail 组件
- OODA 决策循环: 四阶段可视化 + 决策延迟追踪
- 助推效果追踪: 采纳率/行动率/疲劳度指标面板
- 信息分级标识: 4 级密级标签组件(公开/内部/机密/绝密)
This commit is contained in:
selfrelease
2026-07-19 19:02:32 +08:00
parent 129210405d
commit a4044baa31
8 changed files with 442 additions and 185 deletions
+67 -30
View File
@@ -1,9 +1,57 @@
/** 多企业对比工作区 — 2-5 家企业并排对比。 */
"use client";
import { GitCompareArrows } from "lucide-react";
import { CompareMode, type CompareColumn } from "@/components/workbench/CompareMode";
import { useState } from "react";
/** 多企业对比页面。 */
export default function ComparePage() {
const [selected, setSelected] = useState<string[]>(["智链科技", "云栈数据", "深瞳智能"]);
const allCompanies = ["智链科技", "云栈数据", "深瞳智能", "量子芯微", "光合生物"];
const companyData: Record<string, { healthScore: number; runway: number; highRisks: number }> = {
"智链科技": { healthScore: 78.5, runway: 18, highRisks: 0 },
"云栈数据": { healthScore: 62.0, runway: 15, highRisks: 1 },
"深瞳智能": { healthScore: 85.5, runway: 24, highRisks: 0 },
"量子芯微": { healthScore: 48.0, runway: 7, highRisks: 2 },
"光合生物": { healthScore: 68.5, runway: 20, highRisks: 0 },
};
const columns: CompareColumn[] = selected.map((name) => ({
id: name,
name,
...companyData[name],
content: (
<div className="space-y-2 text-xs">
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">800</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium text-emerald-600">+15%</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">AI </span>
<span className="font-medium">3 PoC</span>
</div>
</div>
),
}));
function toggleCompany(name: string) {
setSelected((prev) => {
if (prev.includes(name)) {
return prev.filter((n) => n !== name);
}
if (prev.length >= 5) return prev;
return [...prev, name];
});
}
return (
<div className="space-y-6">
<div className="flex items-center gap-2">
@@ -11,38 +59,27 @@ export default function ComparePage() {
<h1 className="text-2xl font-bold"></h1>
</div>
<div className="rounded-lg border bg-white p-4 shadow-sm">
<p className="text-sm text-muted-foreground">
2-5
</p>
</div>
{/* 对比卡片占位 */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
{["智链科技", "云栈数据", "深瞳智能"].map((name) => (
<div key={name} className="rounded-lg border bg-white p-4 shadow-sm">
<h3 className="mb-3 font-medium">{name}</h3>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">78.5</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">800</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Runway</span>
<span className="font-medium">12</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span className="font-medium">0</span>
</div>
</div>
</div>
{/* 企业选择器 */}
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-muted-foreground">2-5 </span>
{allCompanies.map((name) => (
<button
key={name}
type="button"
onClick={() => toggleCompany(name)}
className={`rounded-md border px-3 py-1 text-sm transition-colors ${
selected.includes(name)
? "border-indigo-300 bg-indigo-50 text-indigo-600"
: "border-gray-200 bg-white text-gray-600 hover:bg-gray-50"
}`}
>
{name}
</button>
))}
</div>
{/* 对比卡片 */}
<CompareMode columns={columns} />
</div>
);
}
+33 -1
View File
@@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
import { apiFetch } from "@/lib/api";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
import { EmptyState } from "@/components/shared/EmptyState";
import { Lightbulb, Sparkles, Check, X } from "lucide-react";
import { Lightbulb, Sparkles, Check, X, TrendingUp, BarChart3 } from "lucide-react";
/** 行为助推引擎页面。 */
export default function NudgesPage() {
@@ -117,6 +117,38 @@ export default function NudgesPage() {
<EmptyState title="暂无助推记录" />
)}
</div>
{/* 助推效果追踪 */}
<div className="rounded-lg border border-[var(--border)] bg-white p-4">
<div className="mb-3 flex items-center gap-2">
<BarChart3 size={18} className="text-[var(--investor-primary)]" />
<h2 className="text-sm font-medium"></h2>
</div>
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
<div className="rounded-md border p-3 text-center">
<div className="text-xs text-muted-foreground"></div>
<div className="mt-1 text-2xl font-bold">{nudges.length}</div>
</div>
<div className="rounded-md border p-3 text-center">
<div className="text-xs text-muted-foreground"></div>
<div className="mt-1 text-2xl font-bold text-emerald-600">
{nudges.length > 0 ? Math.round((nudges.filter((n) => n.accepted).length / nudges.length) * 100) : 0}%
</div>
</div>
<div className="rounded-md border p-3 text-center">
<div className="text-xs text-muted-foreground"></div>
<div className="mt-1 text-2xl font-bold text-indigo-600">67%</div>
</div>
<div className="rounded-md border p-3 text-center">
<div className="text-xs text-muted-foreground"></div>
<div className="mt-1 text-2xl font-bold text-amber-600"></div>
</div>
</div>
<div className="mt-3 flex items-center gap-1.5 text-xs text-muted-foreground">
<TrendingUp size={12} aria-hidden="true" />
12%"默认选项"
</div>
</div>
</div>
);
}
+104
View File
@@ -0,0 +1,104 @@
/** OODA 决策循环可视化页面 — Observe/Orient/Decide/Act 循环图 + 决策延迟追踪。 */
"use client";
import { Eye, Compass, Brain, Zap, Clock } from "lucide-react";
/** OODA 阶段定义。 */
interface OODAStage {
key: string;
label: string;
icon: typeof Eye;
color: string;
items: string[];
avgDelay: string;
}
/** OODA 决策循环可视化页面。 */
export default function OODAPage() {
const stages: OODAStage[] = [
{
key: "observe",
label: "Observe — 观察",
icon: Eye,
color: "border-blue-300 bg-blue-50",
items: ["健康度下降 5 分", "Runway 降至 7 月", "客户流失率上升"],
avgDelay: "实时",
},
{
key: "orient",
label: "Orient — 定向",
icon: Compass,
color: "border-indigo-300 bg-indigo-50",
items: ["行业对标分析", "融资环境评估", "风险等级判定"],
avgDelay: "2.3 天",
},
{
key: "decide",
label: "Decide — 决策",
icon: Brain,
color: "border-amber-300 bg-amber-50",
items: ["天使+ vs Pre-A 方案对比", "估值区间确定", "投资条款协商"],
avgDelay: "5.7 天",
},
{
key: "act",
label: "Act — 行动",
icon: Zap,
color: "border-emerald-300 bg-emerald-50",
items: ["BP 准备", "投资人接触", "尽调配合"],
avgDelay: "12.4 天",
},
];
return (
<div className="space-y-6">
<div className="flex items-center gap-2">
<Compass className="text-[var(--investor-primary)]" size={24} />
<h1 className="text-2xl font-bold">OODA </h1>
</div>
{/* 决策延迟摘要 */}
<div className="rounded-lg border bg-white p-4 shadow-sm">
<div className="mb-3 flex items-center gap-2">
<Clock className="text-amber-500" size={18} aria-hidden="true" />
<h2 className="font-medium"></h2>
</div>
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
{stages.map((stage) => (
<div key={stage.key} className="rounded-md border p-3 text-center">
<div className="text-xs text-muted-foreground">{stage.label.split(" — ")[0]}</div>
<div className="mt-1 text-lg font-bold text-indigo-600">{stage.avgDelay}</div>
</div>
))}
</div>
<p className="mt-3 text-xs text-muted-foreground">
20.4 3.2
</p>
</div>
{/* OODA 循环图 */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
{stages.map((stage, i) => (
<div key={stage.key} className={`rounded-lg border-2 p-4 ${stage.color}`}>
<div className="mb-3 flex items-center gap-2">
<stage.icon size={18} aria-hidden="true" />
<h3 className="text-sm font-medium">{stage.label}</h3>
</div>
<ul className="space-y-1.5 text-xs text-gray-600">
{stage.items.map((item, j) => (
<li key={j} className="flex items-center gap-1.5">
<span className="h-1.5 w-1.5 rounded-full bg-current opacity-40" />
{item}
</li>
))}
</ul>
{i < stages.length - 1 && (
<div className="mt-3 text-center text-gray-300"></div>
)}
</div>
))}
</div>
</div>
);
}
@@ -1,52 +1,34 @@
/** 决策线程详情页 — 状态机时间线 + 关联事件 + AAR 链接。 */
import { GitBranch, ArrowLeft } from "lucide-react";
import Link from "next/link";
"use client";
import { ThreadDetail } from "@/components/threads/ThreadDetail";
import type { TimelineNode } from "@/components/threads/ThreadDetail";
import type { RelatedEvent } from "@/components/threads/ThreadDetail";
/** 决策线程详情页面。 */
export default function ThreadDetailPage({ params }: { params: { id: string } }) {
const timeline: TimelineNode[] = [
{ status: "identified", label: "已识别", date: "2026-07-09", description: "Runway 不足 7 月,弱信号触发", active: true },
{ status: "analyzing", label: "分析中", date: "2026-07-12", description: "对比天使+ vs Pre-A 方案", active: true },
{ status: "acted", label: "已行动", date: "—", active: false },
{ status: "closed", label: "已关闭", date: "—", active: false },
];
const relatedEvents: RelatedEvent[] = [
{ type: "risk", title: "现金 Runway 不足 7 月", href: "/risks" },
{ type: "report", title: "2026 年 6 月月报", href: "/reports" },
{ type: "task", title: "准备天使+轮 BP", href: "/tasks" },
];
return (
<div className="space-y-6">
<Link href="/threads" className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
<ArrowLeft size={16} />
线
</Link>
<div className="flex items-center gap-2">
<GitBranch className="text-[var(--investor-primary)]" size={24} />
<h1 className="text-2xl font-bold">线</h1>
</div>
<div className="rounded-lg border bg-white p-4 shadow-sm">
<div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-medium">线 #{params.id}</h2>
<span className="rounded-full bg-indigo-50 px-3 py-1 text-xs text-indigo-600"></span>
</div>
<p className="text-sm text-muted-foreground">
线 AAR P1 #17
</p>
</div>
{/* 时间线占位 */}
<div className="rounded-lg border bg-white p-4 shadow-sm">
<h3 className="mb-4 font-medium">线</h3>
<div className="space-y-4">
{[
{ label: "已识别", date: "2026-07-09", active: true },
{ label: "分析中", date: "2026-07-12", active: true },
{ label: "已行动", date: "—", active: false },
{ label: "已关闭", date: "—", active: false },
].map((step, i) => (
<div key={i} className="flex items-center gap-3">
<div className={`h-3 w-3 rounded-full ${step.active ? "bg-indigo-500" : "bg-gray-300"}`} />
<span className={`text-sm ${step.active ? "font-medium" : "text-muted-foreground"}`}>
{step.label}
</span>
<span className="text-xs text-muted-foreground">{step.date}</span>
</div>
))}
</div>
</div>
</div>
<ThreadDetail
threadId={params.id}
title="量子芯微 融资策略:天使+ vs Pre-A"
company="量子芯微"
status="analyzing"
timeline={timeline}
relatedEvents={relatedEvents}
/>
);
}
+11 -20
View File
@@ -1,9 +1,19 @@
/** 决策线程列表页 — 按状态/企业筛选 + 线程卡片。 */
"use client";
import { GitBranch } from "lucide-react";
import { ThreadList, type ThreadItem } from "@/components/threads/ThreadList";
/** 决策线程列表页面。 */
export default function ThreadsPage() {
const threads: ThreadItem[] = [
{ id: "thread-001", title: "云栈数据 A+轮融资时机选择", company: "云栈数据", status: "analyzing", updatedAt: "2026-07-12", riskCount: 2 },
{ id: "thread-002", title: "深瞳智能 CTO retention 策略", company: "深瞳智能", status: "identified", updatedAt: "2026-07-10", riskCount: 1 },
{ id: "thread-003", title: "量子芯微 融资策略:天使+ vs Pre-A", company: "量子芯微", status: "acted", updatedAt: "2026-07-14", riskCount: 3 },
{ id: "thread-004", title: "智链科技 客户流失干预方案", company: "智链科技", status: "closed", updatedAt: "2026-06-28", riskCount: 1 },
];
return (
<div className="space-y-6">
<div className="flex items-center gap-2">
@@ -17,26 +27,7 @@ export default function ThreadsPage() {
</p>
</div>
{/* 线程卡片占位 */}
<div className="space-y-3">
{[
{ title: "云栈数据 A+轮融资时机选择", status: "分析中", company: "云栈数据" },
{ title: "深瞳智能 CTO retention 策略", status: "已识别", company: "深瞳智能" },
{ title: "量子芯微 融资策略:天使+ vs Pre-A", status: "已行动", company: "量子芯微" },
].map((thread) => (
<div key={thread.title} className="rounded-lg border bg-white p-4 shadow-sm">
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium">{thread.title}</h3>
<p className="mt-1 text-sm text-muted-foreground">{thread.company}</p>
</div>
<span className="rounded-full bg-indigo-50 px-3 py-1 text-xs text-indigo-600">
{thread.status}
</span>
</div>
</div>
))}
</div>
<ThreadList threads={threads} />
</div>
);
}
+153 -88
View File
@@ -1,109 +1,174 @@
/** 今日行动中心 — 投后负责人/投资经理默认首页。 */
import { CalendarClock, AlertTriangle, FileText, ListTodo, TrendingUp } from "lucide-react";
"use client";
import { CalendarClock, Sparkles, AlertTriangle, Eye, TrendingUp, Clock, CheckCircle2, ArrowRight } from "lucide-react";
import Link from "next/link";
/** 优先级行动项。 */
interface ActionItem {
title: string;
company: string;
priority: "critical" | "high" | "medium" | "low";
confidence: number;
dueDate?: string;
href: string;
}
/** 优先级颜色映射。 */
const PRIORITY_COLORS: Record<ActionItem["priority"], string> = {
critical: "bg-red-50 text-red-600 border-red-200",
high: "bg-amber-50 text-amber-600 border-amber-200",
medium: "bg-blue-50 text-blue-600 border-blue-200",
low: "bg-gray-50 text-gray-600 border-gray-200",
};
const PRIORITY_LABELS: Record<ActionItem["priority"], string> = {
critical: "紧急",
high: "高",
medium: "中",
low: "低",
};
/** 今日行动中心页面。 */
export default function TodayPage() {
const mustHandle: ActionItem[] = [
{ title: "现金 Runway 不足 7 月,需启动融资", company: "量子芯微", priority: "critical", confidence: 0.92, dueDate: "3 天内", href: "/risks" },
{ title: "客户流失率连续 2 月上升", company: "智链科技", priority: "high", confidence: 0.85, href: "/risks" },
{ title: "营收增长停滞,需突破策略", company: "云栈数据", priority: "high", confidence: 0.78, href: "/risks" },
{ title: "月报逾期未提交", company: "量子芯微", priority: "high", confidence: 1.0, dueDate: "已逾期 3 天", href: "/reports" },
];
const watchItems: ActionItem[] = [
{ title: "竞品发布类似产品,市场份额可能受挤压", company: "深瞳智能", priority: "medium", confidence: 0.65, href: "/weak-signals" },
{ title: "CTO 技术路线分歧弱信号", company: "深瞳智能", priority: "medium", confidence: 0.58, href: "/weak-signals" },
];
const growthItems: ActionItem[] = [
{ title: "智链科技 × 云栈数据供应链协同机会", company: "智链科技", priority: "medium", confidence: 0.72, href: "/synergies" },
{ title: "光合生物半导体验证进展,可加速商业化", company: "光合生物", priority: "low", confidence: 0.68, href: "/milestones" },
];
const waitingItems: ActionItem[] = [
{ title: "等待创始人提交修正后月报", company: "云栈数据", priority: "medium", confidence: 1.0, href: "/reports" },
{ title: "等待董事会决议确认", company: "智链科技", priority: "low", confidence: 1.0, href: "/board" },
];
const completedItems: ActionItem[] = [
{ title: "审阅智链科技月报", company: "智链科技", priority: "low", confidence: 1.0, href: "/reports" },
];
return (
<div className="space-y-6">
<div className="flex items-center gap-2">
<CalendarClock className="text-[var(--investor-primary)]" size={24} />
<h1 className="text-2xl font-bold"></h1>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<CalendarClock className="text-[var(--investor-primary)]" size={24} />
<h1 className="text-2xl font-bold"></h1>
</div>
<Link
href="/risks"
className="flex items-center gap-1 rounded-md bg-indigo-50 px-3 py-1.5 text-sm text-indigo-600 transition-colors hover:bg-indigo-100"
>
{mustHandle.length + watchItems.length}
<ArrowRight size={14} aria-hidden="true" />
</Link>
</div>
{/* AI 报 */}
<div className="rounded-lg border bg-white p-4 shadow-sm">
<h2 className="mb-2 text-sm font-medium text-muted-foreground">AI </h2>
{/* 1. AI 报 */}
<div className="rounded-lg border bg-gradient-to-r from-indigo-50 to-blue-50 p-4 shadow-sm">
<div className="mb-2 flex items-center gap-2">
<Sparkles className="text-indigo-500" size={18} aria-hidden="true" />
<h2 className="text-sm font-medium text-indigo-700">AI </h2>
</div>
<p className="text-sm text-gray-700">
3 2 5 Runway 7
5 Runway 7 2
CTO 线
</p>
</div>
{/* 四象限概览 */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="rounded-lg border bg-white p-4 shadow-sm">
<div className="mb-3 flex items-center gap-2">
<AlertTriangle className="text-amber-500" size={18} />
<h3 className="font-medium"></h3>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between rounded bg-red-50 px-3 py-2 text-sm">
<span> Runway不足7月</span>
<span className="text-red-600"></span>
</div>
<div className="flex items-center justify-between rounded bg-amber-50 px-3 py-2 text-sm">
<span> </span>
<span className="text-amber-600"></span>
</div>
<div className="flex items-center justify-between rounded bg-blue-50 px-3 py-2 text-sm">
<span> </span>
<span className="text-blue-600"></span>
</div>
</div>
</div>
{/* 2. 必须处理 */}
<ActionSection
title="必须处理"
icon={AlertTriangle}
iconColor="text-red-500"
items={mustHandle}
/>
<div className="rounded-lg border bg-white p-4 shadow-sm">
<div className="mb-3 flex items-center gap-2">
<FileText className="text-blue-500" size={18} />
<h3 className="font-medium"></h3>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between rounded bg-green-50 px-3 py-2 text-sm">
<span> </span>
<span className="text-green-600">AI </span>
</div>
<div className="flex items-center justify-between rounded bg-green-50 px-3 py-2 text-sm">
<span> </span>
<span className="text-green-600"></span>
</div>
<div className="flex items-center justify-between rounded bg-amber-50 px-3 py-2 text-sm">
<span> 稿</span>
<span className="text-amber-600"></span>
</div>
</div>
</div>
{/* 3. 建议关注 */}
<ActionSection
title="建议关注"
icon={Eye}
iconColor="text-amber-500"
items={watchItems}
/>
<div className="rounded-lg border bg-white p-4 shadow-sm">
<div className="mb-3 flex items-center gap-2">
<ListTodo className="text-indigo-500" size={18} />
<h3 className="font-medium"></h3>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between rounded bg-indigo-50 px-3 py-2 text-sm">
<span>访</span>
<span className="text-indigo-600"></span>
</div>
<div className="flex items-center justify-between rounded bg-gray-50 px-3 py-2 text-sm">
<span>使+ BP</span>
<span className="text-gray-600"></span>
</div>
<div className="flex items-center justify-between rounded bg-green-50 px-3 py-2 text-sm">
<span></span>
<span className="text-green-600"></span>
</div>
</div>
</div>
{/* 4. 增长机会 */}
<ActionSection
title="增长机会"
icon={TrendingUp}
iconColor="text-emerald-500"
items={growthItems}
/>
<div className="rounded-lg border bg-white p-4 shadow-sm">
<div className="mb-3 flex items-center gap-2">
<TrendingUp className="text-emerald-500" size={18} />
<h3 className="font-medium"></h3>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between rounded bg-green-50 px-3 py-2 text-sm">
<span> 78.5 </span>
<span className="text-green-600">+2.3</span>
{/* 5. 等待他人 */}
<ActionSection
title="等待他人"
icon={Clock}
iconColor="text-blue-500"
items={waitingItems}
/>
{/* 6. 已完成待验证 */}
<ActionSection
title="已完成待验证"
icon={CheckCircle2}
iconColor="text-gray-400"
items={completedItems}
/>
</div>
);
}
/** 行动区块组件。 */
function ActionSection({
title,
icon: Icon,
iconColor,
items,
}: {
title: string;
icon: typeof AlertTriangle;
iconColor: string;
items: ActionItem[];
}) {
if (items.length === 0) return null;
return (
<div className="rounded-lg border bg-white p-4 shadow-sm">
<div className="mb-3 flex items-center gap-2">
<Icon className={iconColor} size={18} aria-hidden="true" />
<h2 className="font-medium">{title}</h2>
<span className="text-xs text-muted-foreground">({items.length})</span>
</div>
<div className="space-y-2">
{items.map((item, i) => (
<Link
key={i}
href={item.href}
className={`flex items-center justify-between rounded-md border px-3 py-2 text-sm transition-colors hover:bg-gray-50 ${PRIORITY_COLORS[item.priority]}`}
>
<div className="flex items-center gap-2">
<span className="font-medium">{item.title}</span>
<span className="text-xs opacity-70">{item.company}</span>
{item.confidence < 0.6 && (
<span className="rounded bg-amber-100 px-1.5 py-0.5 text-xs text-amber-700"></span>
)}
</div>
<div className="flex items-center justify-between rounded bg-red-50 px-3 py-2 text-sm">
<span> 48.0 </span>
<span className="text-red-600">-5.0</span>
<div className="flex items-center gap-2 text-xs">
{item.dueDate && <span className="opacity-70">{item.dueDate}</span>}
<span className="font-medium">{PRIORITY_LABELS[item.priority]}</span>
</div>
<div className="flex items-center justify-between rounded bg-gray-50 px-3 py-2 text-sm">
<span> 62.0 </span>
<span className="text-gray-600"></span>
</div>
</div>
</div>
</Link>
))}
</div>
</div>
);
@@ -0,0 +1,46 @@
/** 信息分级标识组件 — 4 级密级标签。 */
import { Shield, ShieldCheck, ShieldAlert, ShieldX } from "lucide-react";
/** 密级类型。 */
export type ClassificationLevel = "public" | "internal" | "confidential" | "secret";
/** 密级配置。 */
const LEVEL_CONFIG: Record<ClassificationLevel, {
label: string;
icon: typeof Shield;
color: string;
description: string;
}> = {
public: { label: "公开", icon: Shield, color: "text-gray-500 bg-gray-50 border-gray-200", description: "可对外公开" },
internal: { label: "内部", icon: ShieldCheck, color: "text-blue-600 bg-blue-50 border-blue-200", description: "仅限内部使用" },
confidential: { label: "机密", icon: ShieldAlert, color: "text-amber-600 bg-amber-50 border-amber-200", description: "限授权人员查看" },
secret: { label: "绝密", icon: ShieldX, color: "text-rose-600 bg-rose-50 border-rose-200", description: "最高密级,查看留痕" },
};
/** ClassificationBadge Props。 */
interface ClassificationBadgeProps {
level: ClassificationLevel;
showDescription?: boolean;
}
/**
* 信息分级标识组件 — 4 级密级标签。
*
* 密级从低到高:公开 → 内部 → 机密 → 绝密。
*/
export function ClassificationBadge({ level, showDescription = false }: ClassificationBadgeProps) {
const config = LEVEL_CONFIG[level];
return (
<span
className={`inline-flex items-center gap-1 rounded border px-2 py-0.5 text-xs font-medium ${config.color}`}
title={config.description}
>
<config.icon size={12} aria-hidden="true" />
{config.label}
{showDescription && (
<span className="font-normal opacity-70">· {config.description}</span>
)}
</span>
);
}
@@ -7,7 +7,7 @@ import Link from "next/link";
import type { ThreadStatus } from "./ThreadList";
/** 时间线节点定义。 */
interface TimelineNode {
export interface TimelineNode {
status: ThreadStatus;
label: string;
date: string;
@@ -16,7 +16,7 @@ interface TimelineNode {
}
/** 关联事件定义。 */
interface RelatedEvent {
export interface RelatedEvent {
type: "risk" | "report" | "task" | "aar";
title: string;
href: string;