feat(大屏绘制): 战情七页版式 + 对接 selectSqReport 真实数据
- 新增 dashboard-agent 独占战情多页(总览/敌情/我情/战场环境/情况判断/决心建议/处置建议), 经 DashboardApp 的可选 pages 插槽注入,会商 Step3 大屏多页样式零影响 - 总览由六类数据聚合;六个详情页对接 selectSqReport(防御式解析,容错不抛错, 接口为空显示「暂无数据」) - 展示层 desensitizeDeep 脱敏 + 请求加载框;去掉所有「更多」按钮 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
adabfa2fe3
commit
fc632f26e9
12 files changed
+2805
-34
No files matched your search
@@ -8,7 +8,7 @@ VITE_USE_REAL_DATA=true
|
||||
VITE_MOCK_COP_TASK_DETAIL=true
|
||||
|
||||
# 研判情报报告(大屏绘制智能体深链 selectSqReport)数据源:true 走本地样例假数据(真实接口未就绪时调试用);联调通过后置 false。
|
||||
VITE_MOCK_SQ_REPORT=false
|
||||
VITE_MOCK_SQ_REPORT=true
|
||||
|
||||
# 深链任务详情(cop-task-detail:任务名称/描述)数据源:true 走本地假数据(真实接口未就绪时调试用);联调通过后置 false。
|
||||
VITE_MOCK_COP_TASK_DETAIL=true
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* 「大屏绘制智能体」决心建议页 —— 任务信息 / 作战主张 / 打击目标 / 实施要点 / 决心总结
|
||||
* (按设计图 dp-6.png 一比一实现)。
|
||||
*
|
||||
* **dashboard-agent 独占**:作为 DashboardApp `pages` 插槽里「决心建议」Tab 的内容,
|
||||
* 不影响多智能体会商(Step3)大屏多页的样式。当前为「静态页面」,文案为占位内容,
|
||||
* 动态数据后续接入。版式包含:
|
||||
* 1. 任务信息 + 任务行动方向 / 作战主张(3 个编号方案)
|
||||
* 2. 打击目标 / 目标建议(表)+ 实施要点
|
||||
* 3. 决心建议总结(总结段 + 统计块)
|
||||
*/
|
||||
import { ClipboardList, Crosshair, Target, ListChecks, Flag, Clock, ShieldCheck, AlertTriangle, type LucideIcon } from "lucide-react";
|
||||
|
||||
import { Card, CardTitle, Table, Td, Tag, NoData, type Tone } from "@/roundtable-planning/dashboard-agent/ui";
|
||||
import type { DecisionData } from "@/roundtable-planning/dashboard-agent/sq-report";
|
||||
|
||||
// ── 占位数据 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const TASK_META: Array<{ label: string; value: string }> = [
|
||||
{ label: "任务标题", value: "夺控 XX 关键地域,压制敌防御体系" },
|
||||
{ label: "任务类型", value: "进攻作战" },
|
||||
{ label: "任务时限", value: "5 小时内" },
|
||||
{ label: "任务方向", value: "XX 方向" },
|
||||
];
|
||||
const TASK_INTRO =
|
||||
"根据上级指示,我方对敌防御体系实施进攻,夺控 XX 关键地域,破坏敌指挥控制能力与防御部署,压制敌纵深支援能力,为后续行动创造有利条件。";
|
||||
|
||||
interface Proposal {
|
||||
no: number;
|
||||
title: string;
|
||||
bullets: string[];
|
||||
rec: string;
|
||||
recTone: Tone;
|
||||
}
|
||||
const PROPOSALS: Proposal[] = [
|
||||
{
|
||||
no: 1,
|
||||
title: "主攻突破,速战速决",
|
||||
bullets: ["集中优势兵力突击敌防御薄弱点", "快速突破并夺控要点", "压制敌纵深火力支援能力"],
|
||||
rec: "高",
|
||||
recTone: "red",
|
||||
},
|
||||
{
|
||||
no: 2,
|
||||
title: "稳固推进,逐步压制",
|
||||
bullets: ["先期压制敌火力与指挥体系", "分段推进,扩大战果", "稳固圈占,巩固防御"],
|
||||
rec: "中",
|
||||
recTone: "amber",
|
||||
},
|
||||
{
|
||||
no: 3,
|
||||
title: "迂回包抄,分割合围",
|
||||
bullets: ["迂回敌防御侧翼实施包抄", "切割敌防御部署", "围点打援,歼敌纵深兵力"],
|
||||
rec: "中",
|
||||
recTone: "amber",
|
||||
},
|
||||
];
|
||||
|
||||
interface TargetRow {
|
||||
no: number;
|
||||
name: string;
|
||||
type: string;
|
||||
importance: string;
|
||||
impTone: Tone;
|
||||
priority: string;
|
||||
priTone: Tone;
|
||||
effect: string;
|
||||
method: string;
|
||||
timing: string;
|
||||
}
|
||||
const TARGETS: TargetRow[] = [
|
||||
{ no: 1, name: "敌前沿防御阵地", type: "防御工事", importance: "高", impTone: "red", priority: "高", priTone: "red", effect: "摧毁瘫痪,降低防御能力", method: "精确打击", timing: "D+0~0+3h" },
|
||||
{ no: 2, name: "敌指挥控制节点", type: "指挥所", importance: "高", impTone: "red", priority: "高", priTone: "red", effect: "压制致瘫", method: "火力打击", timing: "D+0~0+5h" },
|
||||
{ no: 3, name: "敌炮兵阵地", type: "火力群", importance: "高", impTone: "red", priority: "中", priTone: "amber", effect: "压制摧毁", method: "远程火力", timing: "D+0~0+4h" },
|
||||
{ no: 4, name: "敌防空火力点", type: "防空阵地", importance: "中", impTone: "amber", priority: "高", priTone: "red", effect: "前期压制夺取制空", method: "电子+火力", timing: "D+0~0+2h" },
|
||||
{ no: 5, name: "敌纵深兵力", type: "增援部队", importance: "中", impTone: "amber", priority: "中", priTone: "amber", effect: "迟滞阻断", method: "火力封锁", timing: "D+0~0+18h" },
|
||||
];
|
||||
|
||||
const KEY_POINTS = [
|
||||
"集中优势兵力与火力,确保主攻方向突破能力。",
|
||||
"先期制电、压制网络,为突击与夺控创造信息优势。",
|
||||
"强化诸军兵种协同,提升整体作战效能。",
|
||||
"严格协同空地火力与地面行动,形成体系打击合力。",
|
||||
"做好预备兵力与增援应对预案,保持持续作战能力。",
|
||||
"注重战场舆情与目标善后,及时修订完善方案。",
|
||||
];
|
||||
|
||||
const SUMMARY =
|
||||
"建议采取「主攻突破,速战速决」的作战行动方向,集中优势兵力与火力,优先打击敌防御阵地、敌指挥控制节点与敌纵深兵力,迅速压制敌防御体系,夺控 XX 关键地域,并同步破坏敌纵深支援能力,形成战役全局有利态势。";
|
||||
|
||||
interface SummaryStat {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
const SUMMARY_STATS: SummaryStat[] = [
|
||||
{ icon: Crosshair, label: "推荐行动方向", value: "主攻突破,速战速决" },
|
||||
{ icon: Clock, label: "建议处置时机", value: "D+0 ~ D+6h" },
|
||||
{ icon: Target, label: "关键目标数量", value: "5 个" },
|
||||
{ icon: ShieldCheck, label: "重点保障", value: "火力压制 / 空中协同 / 机动补给" },
|
||||
{ icon: AlertTriangle, label: "风险提示", value: "敌防空反制 / 增援压力 / 地形限制" },
|
||||
];
|
||||
|
||||
// ── 主组件(有真实数据走真实渲染,否则回退静态占位) ─────────────────────────────
|
||||
|
||||
export function Decision({ data }: { data?: DecisionData | null }) {
|
||||
return data ? <DecisionReal data={data} /> : <NoData />;
|
||||
}
|
||||
|
||||
function DecisionPlaceholder() {
|
||||
return (
|
||||
<div className="space-y-4 text-slate-700 dark:text-[var(--dash-text)]">
|
||||
{/* 1. 任务信息 + 作战主张 */}
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-12">
|
||||
<Card className="flex flex-col lg:col-span-4">
|
||||
<CardTitle icon={ClipboardList} title="任务信息" />
|
||||
<div className="flex-1 px-4 py-3.5">
|
||||
<dl className="space-y-2">
|
||||
{TASK_META.map((m) => (
|
||||
<div key={m.label} className="flex gap-2 text-xs">
|
||||
<dt className="w-16 shrink-0 text-slate-400 dark:text-[var(--dash-text-dim)]">{m.label}</dt>
|
||||
<dd className="font-medium text-slate-700 dark:text-[var(--dash-text-strong)]">{m.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
<div className="mt-3 border-t border-slate-100 pt-3 dark:border-[var(--dash-border)]">
|
||||
<div className="mb-1 text-[11px] text-slate-400 dark:text-[var(--dash-text-dim)]">任务简介</div>
|
||||
<p className="text-xs leading-relaxed text-slate-600 dark:text-[var(--dash-text)]">{TASK_INTRO}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="flex flex-col lg:col-span-8">
|
||||
<CardTitle icon={Flag} title="任务行动方向 / 作战主张" />
|
||||
<div className="grid flex-1 grid-cols-1 gap-3 px-4 py-3.5 sm:grid-cols-3">
|
||||
{PROPOSALS.map((p) => (
|
||||
<div
|
||||
key={p.no}
|
||||
className="flex flex-col rounded-md border border-slate-200 p-3 dark:border-[var(--dash-border)]"
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-red-600 text-[11px] font-bold text-white">
|
||||
{p.no}
|
||||
</span>
|
||||
<h4 className="text-xs font-bold text-slate-900 dark:text-[var(--dash-text-strong)]">{p.title}</h4>
|
||||
</div>
|
||||
<ul className="flex-1 space-y-1.5">
|
||||
{p.bullets.map((b) => (
|
||||
<li key={b} className="flex gap-1.5 text-[11px] leading-snug text-slate-600 dark:text-[var(--dash-text)]">
|
||||
<span className="mt-1.5 h-1 w-1 shrink-0 rounded-full bg-red-500" />
|
||||
{b}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-2 flex items-center gap-1.5 border-t border-slate-100 pt-2 text-[11px] text-slate-400 dark:border-[var(--dash-border)]">
|
||||
推荐程度
|
||||
<Tag tone={p.recTone}>{p.rec}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 2. 打击目标 + 实施要点 */}
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-12">
|
||||
<Card className="flex flex-col lg:col-span-8">
|
||||
<CardTitle icon={Target} title="打击目标 / 目标建议" />
|
||||
<div className="flex-1 px-4 py-3.5">
|
||||
<Table head={["序号", "目标名称", "类型", "重要性", "优先级", "达成效果", "建议方式", "建议时机"]}>
|
||||
{TARGETS.map((t) => (
|
||||
<tr key={t.no}>
|
||||
<Td className="font-mono text-slate-400">{t.no}</Td>
|
||||
<Td className="whitespace-nowrap font-semibold text-slate-800 dark:text-[var(--dash-text-strong)]">{t.name}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{t.type}</Td>
|
||||
<Td>
|
||||
<Tag tone={t.impTone}>{t.importance}</Tag>
|
||||
</Td>
|
||||
<Td>
|
||||
<Tag tone={t.priTone}>{t.priority}</Tag>
|
||||
</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{t.effect}</Td>
|
||||
<Td className="whitespace-nowrap text-slate-500 dark:text-[var(--dash-text-sec)]">{t.method}</Td>
|
||||
<Td className="whitespace-nowrap font-mono text-slate-400">{t.timing}</Td>
|
||||
</tr>
|
||||
))}
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="flex flex-col lg:col-span-4">
|
||||
<CardTitle icon={ListChecks} title="实施要点" />
|
||||
<ol className="flex-1 space-y-2.5 px-4 py-3.5">
|
||||
{KEY_POINTS.map((p, i) => (
|
||||
<li key={p} className="flex gap-2 text-xs leading-relaxed">
|
||||
<span className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-red-600 text-[10px] font-bold text-white">
|
||||
{i + 1}
|
||||
</span>
|
||||
<span className="text-slate-600 dark:text-[var(--dash-text)]">{p}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 3. 决心建议总结 */}
|
||||
<Card>
|
||||
<CardTitle icon={Flag} title="决心建议总结" />
|
||||
<div className="px-4 py-3.5">
|
||||
<p className="rounded border-l-2 border-red-500 bg-red-50 px-3 py-2.5 text-[13px] leading-relaxed text-slate-700 dark:bg-red-500/10 dark:text-[var(--dash-text)]">
|
||||
{SUMMARY}
|
||||
</p>
|
||||
<div className="mt-4 grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
|
||||
{SUMMARY_STATS.map((s) => (
|
||||
<div key={s.label} className="rounded-md bg-slate-50 px-3 py-3 dark:bg-white/5">
|
||||
<div className="mb-1.5 flex items-center gap-1.5 text-[11px] text-slate-500 dark:text-[var(--dash-text-sec)]">
|
||||
<s.icon className="h-3.5 w-3.5 text-red-600" />
|
||||
{s.label}
|
||||
</div>
|
||||
<div className="text-xs font-bold text-slate-900 dark:text-[var(--dash-text-strong)]">{s.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 真实数据渲染 ──────────────────────────────────────────────────────────────
|
||||
function DecisionReal({ data }: { data: DecisionData }) {
|
||||
return (
|
||||
<div className="space-y-4 text-slate-700 dark:text-[var(--dash-text)]">
|
||||
{/* 任务信息 + 作战目的 */}
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-12">
|
||||
<Card className="flex flex-col lg:col-span-5">
|
||||
<CardTitle icon={ClipboardList} title="任务信息" />
|
||||
<div className="flex-1 px-4 py-3.5">
|
||||
{data.taskTitle && (
|
||||
<div className="mb-2">
|
||||
<div className="mb-1 text-[11px] text-slate-400 dark:text-[var(--dash-text-dim)]">任务标题</div>
|
||||
<p className="text-sm font-bold text-slate-800 dark:text-[var(--dash-text-strong)]">{data.taskTitle}</p>
|
||||
</div>
|
||||
)}
|
||||
{data.taskIntro && (
|
||||
<div className="border-t border-slate-100 pt-2.5 dark:border-[var(--dash-border)]">
|
||||
<div className="mb-1 text-[11px] text-slate-400 dark:text-[var(--dash-text-dim)]">任务简介</div>
|
||||
<p className="text-xs leading-relaxed text-slate-600 dark:text-[var(--dash-text)]">{data.taskIntro}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{data.objectives.length > 0 && (
|
||||
<Card className="flex flex-col lg:col-span-7">
|
||||
<CardTitle icon={Crosshair} title="作战目的" />
|
||||
<ol className="flex-1 space-y-2.5 px-4 py-3.5">
|
||||
{data.objectives.map((o, i) => (
|
||||
<li key={i} className="flex gap-2 text-xs leading-relaxed">
|
||||
<span className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-red-600 text-[10px] font-bold text-white">{i + 1}</span>
|
||||
<span className="text-slate-600 dark:text-[var(--dash-text)]">{o}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 打击目标 + 实施要点 */}
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-12">
|
||||
{data.targets.length > 0 && (
|
||||
<Card className="flex flex-col lg:col-span-8">
|
||||
<CardTitle icon={Target} title="打击目标 / 目标建议" />
|
||||
<div className="flex-1 px-4 py-3.5">
|
||||
<Table head={["目标名称", "目标描述", "选择理由", "潜在影响", "推荐依据"]}>
|
||||
{data.targets.map((t, i) => (
|
||||
<tr key={i}>
|
||||
<Td className="whitespace-nowrap font-semibold text-slate-800 dark:text-[var(--dash-text-strong)]">{t.name || "—"}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{t.desc || "—"}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{t.reason || "—"}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{t.impact || "—"}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{t.basis || "—"}</Td>
|
||||
</tr>
|
||||
))}
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{data.keyPoints.length > 0 && (
|
||||
<Card className="flex flex-col lg:col-span-4">
|
||||
<CardTitle icon={ListChecks} title="实施要点" />
|
||||
<ol className="flex-1 space-y-2.5 px-4 py-3.5">
|
||||
{data.keyPoints.map((p, i) => (
|
||||
<li key={i} className="flex gap-2 text-xs leading-relaxed">
|
||||
<span className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-red-600 text-[10px] font-bold text-white">{i + 1}</span>
|
||||
<span className="text-slate-600 dark:text-[var(--dash-text)]">{p}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 决心建议总结 */}
|
||||
{data.summary && (
|
||||
<Card>
|
||||
<CardTitle icon={Flag} title="决心建议总结" />
|
||||
<div className="px-4 py-3.5">
|
||||
<p className="rounded border-l-2 border-red-500 bg-red-50 px-3 py-2.5 text-[13px] leading-relaxed text-slate-700 dark:bg-red-500/10 dark:text-[var(--dash-text)]">
|
||||
{data.summary}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* 「大屏绘制智能体」处置建议页 —— 处置总结 / 目标 / 处置建议表 / 关键事态
|
||||
* (按设计图 dp-7.png 一比一实现)。
|
||||
*
|
||||
* **dashboard-agent 独占**:作为 DashboardApp `pages` 插槽里「处置建议」Tab 的内容,
|
||||
* 不影响多智能体会商(Step3)大屏多页的样式。当前为「静态页面」,文案为占位内容,
|
||||
* 动态数据后续接入。版式包含:
|
||||
* 1. 处置建议总结 + 目标(5 个处置目标卡)
|
||||
* 2. 处置建议(大表)+ 关键事态(编号事件列表)
|
||||
*/
|
||||
import {
|
||||
ClipboardCheck,
|
||||
ClipboardList,
|
||||
Flag,
|
||||
ShieldAlert,
|
||||
Megaphone,
|
||||
Users,
|
||||
Gavel,
|
||||
Handshake,
|
||||
AlertTriangle,
|
||||
Star,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Card, CardTitle, FooterNote, Table, Td, Tag, NoData, type Tone } from "@/roundtable-planning/dashboard-agent/ui";
|
||||
import type { DisposalData } from "@/roundtable-planning/dashboard-agent/sq-report";
|
||||
|
||||
// ── 占位数据 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const SUMMARY_ITEMS: Array<{ label: string; text: string }> = [
|
||||
{ label: "总体判断", text: "当前事件可控但需密切关注,舆情存在扩散与升级风险,需快速反应、综合施策。" },
|
||||
{ label: "主体策略", text: "以澄清事实、权威发声为主,依法依规处置,统筹线上线下,压缩谣言空间。" },
|
||||
{ label: "处置重点", text: "统一对外口径,加强舆论引导,强化协同联动,做好重点群体与重点平台工作。" },
|
||||
{ label: "风险防控", text: "警惕谣言扩散、情绪对立与境外炒作,防范局部聚集与事态升级,守牢安全底线。" },
|
||||
];
|
||||
|
||||
interface GoalCard {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
const GOALS: GoalCard[] = [
|
||||
{ icon: ShieldAlert, title: "阻止威胁扩张", text: "及时防御与压制扩张,减少不实信息蔓延与影响扩散。" },
|
||||
{ icon: Megaphone, title: "澄清事实,统一口径", text: "权威发布、统一口径,及时回应社会关切。" },
|
||||
{ icon: Users, title: "维护社会稳定", text: "引导社会情绪,防范对立与局部聚集,维护大局稳定。" },
|
||||
{ icon: Gavel, title: "依法严正应对", text: "对违法违规行为依法处置,严正立场、坚守底线。" },
|
||||
{ icon: Handshake, title: "提升协同效能", text: "强化多部门协同联动,形成处置合力与快速反应能力。" },
|
||||
];
|
||||
|
||||
interface DisposalRow {
|
||||
no: number;
|
||||
content: string;
|
||||
timing: string;
|
||||
subject: string;
|
||||
caliber: string;
|
||||
method: string;
|
||||
effect: string;
|
||||
basis: string;
|
||||
}
|
||||
const DISPOSALS: DisposalRow[] = [
|
||||
{ no: 1, content: "澄清事实,发布权威信息", timing: "立即", subject: "宣传部门 / 权威发布机构", caliber: "事实 / 立场", method: "权威发布、多渠道同步", effect: "统一口径,稳定舆论", basis: "公开透明" },
|
||||
{ no: 2, content: "管控舆情,做强引导", timing: "持续", subject: "网信部门 / 平台", caliber: "引导 / 疏导", method: "加强议题设置与正向引导", effect: "压制负面,提升正能量", basis: "依法管理" },
|
||||
{ no: 3, content: "防范网络,加强溯源处置", timing: "快速", subject: "公安部门 / 平台", caliber: "溯源 / 处置", method: "监测溯源、依法处置", effect: "切断扩散链条", basis: "合法合规" },
|
||||
{ no: 4, content: "协同处置,打击违法行为", timing: "视情", subject: "多部门联动", caliber: "联动 / 执法", method: "联合执法、分级处置", effect: "形成有效震慑", basis: "严正应对" },
|
||||
{ no: 5, content: "防范升级,维护稳定", timing: "持续", subject: "综合部门", caliber: "稳控 / 防控", method: "重点防控、预案处置", effect: "守牢稳定底线", basis: "安全第一" },
|
||||
];
|
||||
|
||||
interface KeyEvent {
|
||||
no: number;
|
||||
title: string;
|
||||
time: string;
|
||||
impact: string;
|
||||
impactTone: Tone;
|
||||
}
|
||||
const EVENTS: KeyEvent[] = [
|
||||
{ no: 1, title: "不实信息在社交平台扩散", time: "05-16 09:30", impact: "高", impactTone: "red" },
|
||||
{ no: 2, title: "部分舆情持续发酵引发讨论", time: "05-16 08:40", impact: "中", impactTone: "amber" },
|
||||
{ no: 3, title: "相关话题持续热度上升", time: "05-16 10:05", impact: "中", impactTone: "amber" },
|
||||
{ no: 4, title: "线下出现小范围聚集活动", time: "05-16 10:20", impact: "高", impactTone: "red" },
|
||||
{ no: 5, title: "涉事相关单位回应迟缓", time: "05-16 09:50", impact: "中", impactTone: "amber" },
|
||||
];
|
||||
|
||||
// ── 主组件(有真实数据走真实渲染,否则回退静态占位) ─────────────────────────────
|
||||
|
||||
export function Disposal({ data }: { data?: DisposalData | null }) {
|
||||
return data ? <DisposalReal data={data} /> : <NoData />;
|
||||
}
|
||||
|
||||
function DisposalPlaceholder() {
|
||||
return (
|
||||
<div className="space-y-4 text-slate-700 dark:text-[var(--dash-text)]">
|
||||
{/* 1. 处置建议总结 + 目标 */}
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-12">
|
||||
{/* 处置建议总结 */}
|
||||
<Card className="flex flex-col lg:col-span-4">
|
||||
<CardTitle icon={ClipboardCheck} title="处置建议总结" />
|
||||
<ul className="flex-1 space-y-3 px-4 py-3.5">
|
||||
{SUMMARY_ITEMS.map((s) => (
|
||||
<li key={s.label} className="flex gap-2.5 text-xs leading-relaxed">
|
||||
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-red-600" />
|
||||
<p className="text-slate-600 dark:text-[var(--dash-text)]">
|
||||
<span className="font-bold text-red-600">{s.label}:</span>
|
||||
{s.text}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="mx-4 mb-3.5 flex flex-wrap items-center gap-x-4 gap-y-2 rounded bg-slate-50 px-3 py-2.5 dark:bg-white/5">
|
||||
<div className="flex items-center gap-1.5 text-[11px] text-slate-500 dark:text-[var(--dash-text-sec)]">
|
||||
综合处置建议
|
||||
<Tag tone="red">稳健(取义)</Tag>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-[11px] text-slate-500 dark:text-[var(--dash-text-sec)]">
|
||||
建议执行优先级
|
||||
<span className="flex items-center gap-0.5 text-red-600">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<Star key={i} className="h-3.5 w-3.5 fill-current" />
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 目标 */}
|
||||
<Card className="flex flex-col lg:col-span-8">
|
||||
<CardTitle icon={Flag} title="目标" />
|
||||
<div className="grid flex-1 grid-cols-1 gap-3 px-4 py-3.5 sm:grid-cols-2 lg:grid-cols-5">
|
||||
{GOALS.map((g) => (
|
||||
<div key={g.title} className="flex flex-col rounded-md border border-slate-200 p-3 dark:border-[var(--dash-border)]">
|
||||
<span className="mb-2 flex h-8 w-8 items-center justify-center rounded-md bg-red-50 text-red-600 dark:bg-red-500/15">
|
||||
<g.icon className="h-4 w-4" />
|
||||
</span>
|
||||
<h4 className="mb-1.5 text-xs font-bold text-slate-900 dark:text-[var(--dash-text-strong)]">{g.title}</h4>
|
||||
<p className="text-[11px] leading-snug text-slate-500 dark:text-[var(--dash-text-sec)]">{g.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 2. 处置建议表 + 关键事态 */}
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-12">
|
||||
{/* 处置建议 */}
|
||||
<Card className="flex flex-col lg:col-span-8">
|
||||
<CardTitle icon={ClipboardList} title="处置建议" />
|
||||
<div className="flex-1 px-4 py-3.5">
|
||||
<Table head={["序号", "建议内容", "时序", "处置主体", "口径要素", "处置方式", "预期效果", "立场依据"]}>
|
||||
{DISPOSALS.map((d) => (
|
||||
<tr key={d.no}>
|
||||
<Td className="font-mono text-slate-400">{d.no}</Td>
|
||||
<Td className="whitespace-nowrap font-semibold text-slate-800 dark:text-[var(--dash-text-strong)]">{d.content}</Td>
|
||||
<Td className="whitespace-nowrap text-slate-500 dark:text-[var(--dash-text-sec)]">{d.timing}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{d.subject}</Td>
|
||||
<Td className="whitespace-nowrap text-slate-500 dark:text-[var(--dash-text-sec)]">{d.caliber}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{d.method}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{d.effect}</Td>
|
||||
<Td className="whitespace-nowrap text-slate-500 dark:text-[var(--dash-text-sec)]">{d.basis}</Td>
|
||||
</tr>
|
||||
))}
|
||||
</Table>
|
||||
</div>
|
||||
<FooterNote label="说明">
|
||||
上述处置应在法治框架内规范实施,统筹线上线下,确保快速反应、综合施策。
|
||||
</FooterNote>
|
||||
</Card>
|
||||
|
||||
{/* 关键事态 */}
|
||||
<Card className="flex flex-col lg:col-span-4">
|
||||
<CardTitle icon={AlertTriangle} title="关键事态" />
|
||||
<ul className="flex-1 space-y-3 px-4 py-3.5">
|
||||
{EVENTS.map((e) => (
|
||||
<li key={e.no} className="flex gap-2.5">
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-red-600 text-[11px] font-bold text-white">
|
||||
{e.no}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-xs font-semibold text-slate-800 dark:text-[var(--dash-text-strong)]">{e.title}</div>
|
||||
<div className="mt-1 flex items-center gap-2 text-[11px] text-slate-400 dark:text-[var(--dash-text-dim)]">
|
||||
<span className="font-mono">{e.time}</span>
|
||||
<span>影响</span>
|
||||
<Tag tone={e.impactTone}>{e.impact}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<FooterNote label="提示">关键事态需密切跟踪,及时研判处置。</FooterNote>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 真实数据渲染 ──────────────────────────────────────────────────────────────
|
||||
function DisposalReal({ data }: { data: DisposalData }) {
|
||||
return (
|
||||
<div className="space-y-4 text-slate-700 dark:text-[var(--dash-text)]">
|
||||
{/* 目标 + 处置总结 */}
|
||||
{(data.goal || data.summary) && (
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
|
||||
{data.goal && (
|
||||
<Card className="flex flex-col">
|
||||
<CardTitle icon={Flag} title="目标" />
|
||||
<div className="flex-1 px-4 py-3.5">
|
||||
<p className="text-xs leading-relaxed text-slate-600 dark:text-[var(--dash-text)]">{data.goal}</p>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
{data.summary && (
|
||||
<Card className="flex flex-col">
|
||||
<CardTitle icon={ClipboardCheck} title="处置建议总结" />
|
||||
<div className="flex-1 px-4 py-3.5">
|
||||
<p className="rounded border-l-2 border-red-500 bg-red-50 px-3 py-2.5 text-xs leading-relaxed text-slate-700 dark:bg-red-500/10 dark:text-[var(--dash-text)]">
|
||||
{data.summary}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 处置建议表 */}
|
||||
{data.items.length > 0 && (
|
||||
<Card>
|
||||
<CardTitle icon={ClipboardList} title="处置建议" />
|
||||
<div className="px-4 py-3.5">
|
||||
<Table head={["序号", "建议内容", "处置方式与步骤", "口径基调", "预期效果", "主责单位", "立场与原则"]}>
|
||||
{data.items.map((d, i) => (
|
||||
<tr key={i}>
|
||||
<Td className="font-mono text-slate-400">{i + 1}</Td>
|
||||
<Td className="font-semibold text-slate-800 dark:text-[var(--dash-text-strong)]">{d.content || "—"}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{d.method || "—"}</Td>
|
||||
<Td className="whitespace-nowrap text-slate-500 dark:text-[var(--dash-text-sec)]">{d.tone || "—"}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{d.effect || "—"}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{d.subject || "—"}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{d.basis || "—"}</Td>
|
||||
</tr>
|
||||
))}
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* 「大屏绘制智能体」敌情页 —— 敌情详情版式(按设计图 dp-2.png 一比一实现)。
|
||||
*
|
||||
* **dashboard-agent 独占**:作为 DashboardApp `pages` 插槽里「敌情」Tab 的内容,
|
||||
* 不影响多智能体会商(Step3)大屏多页的样式。当前为「静态页面」,文案为占位内容,
|
||||
* 动态数据后续接入。版式包含:
|
||||
* 1. 敌方目标表 + 关键事件表(上排两栏)
|
||||
* 2. 对手叙事表(整行)
|
||||
* 3. 重点关注对象表 + 传播渠道分析 + 风险热度研判(下排三栏)
|
||||
*/
|
||||
import { Target, CalendarClock, Users, Crosshair, Share2, Activity, ArrowUp } from "lucide-react";
|
||||
|
||||
import { Card, CardTitle, HeadBadge, Tag, Table, Td, FooterNote, NoData, type Tone } from "@/roundtable-planning/dashboard-agent/ui";
|
||||
import type { EnemyData } from "@/roundtable-planning/dashboard-agent/sq-report";
|
||||
|
||||
// ── 占位数据 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
interface EnemyTarget {
|
||||
no: number;
|
||||
name: string;
|
||||
count: string;
|
||||
gap: string;
|
||||
gapTone: Tone;
|
||||
note: string;
|
||||
}
|
||||
const TARGETS: EnemyTarget[] = [
|
||||
{ no: 1, name: "战略级", count: "2 个", gap: "+2", gapTone: "red", note: "查实战略意义重大的目标节点" },
|
||||
{ no: 2, name: "战役级", count: "3 个", gap: "+1", gapTone: "red", note: "重要枢纽与指挥控制节点" },
|
||||
{ no: 3, name: "战术级", count: "5 个", gap: "-2", gapTone: "green", note: "前沿火力点 / 机动单元" },
|
||||
{ no: 4, name: "支援级", count: "4 个", gap: "持平", gapTone: "slate", note: "后勤与通信保障目标" },
|
||||
{ no: 5, name: "关键人员", count: "78 人", gap: "+3", gapTone: "red", note: "专家与高级指挥人员,重点关注" },
|
||||
];
|
||||
|
||||
interface KeyEvent {
|
||||
time: string;
|
||||
title: string;
|
||||
brief: string;
|
||||
impact: string;
|
||||
impactTone: Tone;
|
||||
}
|
||||
const EVENTS: KeyEvent[] = [
|
||||
{ time: "05-16 09:30", title: "对我方侦察活动加强", brief: "敌多个 APT 组织对我重点系统进行扫描探测", impact: "高", impactTone: "red" },
|
||||
{ time: "05-16 08:45", title: "北部方向兵力调动", brief: "北部出现兵力机动迹象,向前沿地域集结", impact: "高", impactTone: "red" },
|
||||
{ time: "05-15 21:10", title: "增设通信节点", brief: "增设若干临时通信节点,提升指挥保障", impact: "中", impactTone: "amber" },
|
||||
{ time: "05-15 13:05", title: "网络渗透扫描", brief: "针对我政务与能源系统的渗透扫描频度上升", impact: "高", impactTone: "red" },
|
||||
];
|
||||
|
||||
interface Narrative {
|
||||
name: string;
|
||||
view: string;
|
||||
intent: string;
|
||||
channel: string;
|
||||
audience: string;
|
||||
heat: string;
|
||||
trend: string;
|
||||
trendTone: Tone;
|
||||
}
|
||||
const NARRATIVES: Narrative[] = [
|
||||
{ name: "主导叙事", view: "渲染我方为地区紧张根源", intent: "争夺道义高地、动员国际舆论", channel: "境外主流媒体 / 社交平台", audience: "国际受众 / 中立国家", heat: "高", trend: "上升", trendTone: "red" },
|
||||
{ name: "配套舆论", view: "夸大我方军事活动威胁", intent: "制造焦虑、离间区域关系", channel: "本地社交媒体矩阵", audience: "周边民众", heat: "中", trend: "上升", trendTone: "red" },
|
||||
{ name: "情绪动员", view: "煽动对立与不满情绪", intent: "削弱我方内部凝聚力", channel: "短视频 / 论坛", audience: "青年群体", heat: "中", trend: "持平", trendTone: "slate" },
|
||||
];
|
||||
|
||||
interface FocusObject {
|
||||
no: number;
|
||||
name: string;
|
||||
category: string;
|
||||
relevance: number;
|
||||
relTone: Tone;
|
||||
status: string;
|
||||
statusTone: Tone;
|
||||
}
|
||||
const FOCUS: FocusObject[] = [
|
||||
{ no: 1, name: "红方网络安全组织", category: "组织", relevance: 88, relTone: "red", status: "高度活跃", statusTone: "red" },
|
||||
{ no: 2, name: "东方某无人机部队", category: "部队", relevance: 82, relTone: "red", status: "活跃", statusTone: "amber" },
|
||||
{ no: 3, name: "北部战区通信枢纽", category: "设施", relevance: 76, relTone: "amber", status: "监控中", statusTone: "amber" },
|
||||
{ no: 4, name: "某舆论账号矩阵", category: "媒体", relevance: 70, relTone: "amber", status: "活跃", statusTone: "amber" },
|
||||
{ no: 5, name: "海外某情报机构", category: "机构", relevance: 65, relTone: "slate", status: "持续观察", statusTone: "slate" },
|
||||
];
|
||||
|
||||
interface Channel {
|
||||
name: string;
|
||||
pct: number;
|
||||
desc: string;
|
||||
}
|
||||
const CHANNELS: Channel[] = [
|
||||
{ name: "即时通讯类", pct: 38, desc: "封闭式扩散,溯源难度大" },
|
||||
{ name: "社交媒体类", pct: 27, desc: "扩散速度快、覆盖面广" },
|
||||
{ name: "论坛 / 贴吧", pct: 16, desc: "话题沉淀、舆论发酵" },
|
||||
{ name: "视频平台", pct: 12, desc: "情绪煽动性强" },
|
||||
{ name: "其他渠道", pct: 7, desc: "小范围定向传播" },
|
||||
];
|
||||
|
||||
const RISK_POINTS = [
|
||||
"网络攻击与渗透风险偏高,关键系统面临持续探测。",
|
||||
"信息渗透与认知作战风险上升,舆论环境复杂。",
|
||||
"前沿方向摩擦风险存在,需防范局部突发事件。",
|
||||
];
|
||||
const RISK_CONCLUSION = "当前风险整体呈上升态势,须重点防范网络与认知域,强化预警与协同处置。";
|
||||
|
||||
// ── 主组件(有真实数据走真实渲染,否则回退静态占位) ─────────────────────────────
|
||||
|
||||
export function EnemyDetail({ data }: { data?: EnemyData | null }) {
|
||||
return data ? <EnemyReal data={data} /> : <NoData />;
|
||||
}
|
||||
|
||||
function EnemyPlaceholder() {
|
||||
return (
|
||||
<div className="space-y-4 text-slate-700 dark:text-[var(--dash-text)]">
|
||||
{/* 1. 上排:敌方目标 + 关键事件 */}
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-12">
|
||||
{/* 敌方目标 */}
|
||||
<Card className="flex flex-col lg:col-span-5">
|
||||
<CardTitle
|
||||
icon={Target}
|
||||
title="敌方目标"
|
||||
badges={
|
||||
<>
|
||||
<HeadBadge label="组织目标" value="12 个" />
|
||||
<HeadBadge label="人员目标" value="78 人" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div className="flex-1 px-4 py-3.5">
|
||||
<Table head={["编号", "名称", "数量", "缺口", "状态说明"]}>
|
||||
{TARGETS.map((t) => (
|
||||
<tr key={t.no}>
|
||||
<Td className="font-mono text-slate-400">{t.no}</Td>
|
||||
<Td className="font-semibold text-slate-800 dark:text-[var(--dash-text-strong)]">{t.name}</Td>
|
||||
<Td className="font-mono">{t.count}</Td>
|
||||
<Td>
|
||||
<Tag tone={t.gapTone}>{t.gap}</Tag>
|
||||
</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{t.note}</Td>
|
||||
</tr>
|
||||
))}
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 关键事件 */}
|
||||
<Card className="flex flex-col lg:col-span-7">
|
||||
<CardTitle icon={CalendarClock} title="关键事件" />
|
||||
<div className="flex-1 px-4 py-3.5">
|
||||
<Table head={["时间", "标题", "内容摘要", "影响评估"]}>
|
||||
{EVENTS.map((e) => (
|
||||
<tr key={e.time}>
|
||||
<Td className="whitespace-nowrap font-mono text-slate-400">{e.time}</Td>
|
||||
<Td className="font-semibold text-slate-800 dark:text-[var(--dash-text-strong)]">{e.title}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{e.brief}</Td>
|
||||
<Td>
|
||||
<Tag tone={e.impactTone}>{e.impact}</Tag>
|
||||
</Td>
|
||||
</tr>
|
||||
))}
|
||||
</Table>
|
||||
</div>
|
||||
<FooterNote label="事件说明">
|
||||
近期事件以网络空间动作与军事活动配合为主,整体风险趋升,需持续跟踪重点节点。
|
||||
</FooterNote>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 2. 对手叙事(整行) */}
|
||||
<Card>
|
||||
<CardTitle icon={Users} title="对手叙事" />
|
||||
<div className="px-4 py-3.5">
|
||||
<Table head={["名称", "主要观点", "叙事意图", "使用渠道", "受众群体", "热度", "影响烈度"]}>
|
||||
{NARRATIVES.map((n) => (
|
||||
<tr key={n.name}>
|
||||
<Td className="whitespace-nowrap font-semibold text-slate-800 dark:text-[var(--dash-text-strong)]">{n.name}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{n.view}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{n.intent}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{n.channel}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{n.audience}</Td>
|
||||
<Td>
|
||||
<Tag tone={n.heat === "高" ? "red" : n.heat === "中" ? "amber" : "slate"}>{n.heat}</Tag>
|
||||
</Td>
|
||||
<Td>
|
||||
<Tag tone={n.trendTone}>{n.trend}</Tag>
|
||||
</Td>
|
||||
</tr>
|
||||
))}
|
||||
</Table>
|
||||
</div>
|
||||
<FooterNote label="叙事说明">
|
||||
对手叙事以「去式化行动」「外部归因」为核心,须以精准事实与权威信息及时反制,压缩其传播空间。
|
||||
</FooterNote>
|
||||
</Card>
|
||||
|
||||
{/* 3. 下排:重点关注对象 + 传播渠道分析 + 风险热度研判 */}
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-3">
|
||||
{/* 重点关注对象 */}
|
||||
<Card className="flex flex-col">
|
||||
<CardTitle icon={Crosshair} title="重点关注对象" />
|
||||
<div className="flex-1 px-4 py-3.5">
|
||||
<Table head={["编号", "名称", "类别", "相关度", "状态"]}>
|
||||
{FOCUS.map((f) => (
|
||||
<tr key={f.no}>
|
||||
<Td className="font-mono text-slate-400">{f.no}</Td>
|
||||
<Td className="font-semibold text-slate-800 dark:text-[var(--dash-text-strong)]">{f.name}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{f.category}</Td>
|
||||
<Td>
|
||||
<span className={`font-mono font-bold ${f.relTone === "red" ? "text-red-600" : f.relTone === "amber" ? "text-amber-600" : "text-slate-500"}`}>
|
||||
{f.relevance}%
|
||||
</span>
|
||||
</Td>
|
||||
<Td>
|
||||
<Tag tone={f.statusTone}>{f.status}</Tag>
|
||||
</Td>
|
||||
</tr>
|
||||
))}
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 传播渠道分析 */}
|
||||
<Card className="flex flex-col">
|
||||
<CardTitle icon={Share2} title="传播渠道分析(文字化)" />
|
||||
<ul className="flex-1 space-y-3 px-4 py-3.5">
|
||||
{CHANNELS.map((c) => (
|
||||
<li key={c.name}>
|
||||
<div className="mb-1 flex items-baseline justify-between gap-2 text-xs">
|
||||
<span className="font-semibold text-slate-700 dark:text-[var(--dash-text-strong)]">{c.name}</span>
|
||||
<span className="font-mono font-bold text-red-600">{c.pct}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 overflow-hidden rounded-full bg-slate-100 dark:bg-white/10">
|
||||
<div className="h-full rounded-full bg-red-500" style={{ width: `${c.pct}%` }} />
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] leading-snug text-slate-400 dark:text-[var(--dash-text-dim)]">{c.desc}</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
|
||||
{/* 风险热度研判 */}
|
||||
<Card className="flex flex-col">
|
||||
<CardTitle icon={Activity} title="风险热度研判(文字化)" />
|
||||
<div className="flex-1 px-4 py-3.5">
|
||||
<div className="mb-3 flex items-center justify-between rounded-md bg-red-50 px-3 py-2.5 dark:bg-red-500/10">
|
||||
<span className="text-xs font-semibold text-slate-700 dark:text-[var(--dash-text-strong)]">总体风险等级</span>
|
||||
<span className="inline-flex items-center gap-1 text-sm font-bold text-red-600">
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
上升 1 级
|
||||
</span>
|
||||
</div>
|
||||
<div className="mb-2 text-[11px] font-semibold text-red-600">主要风险点</div>
|
||||
<ul className="space-y-2">
|
||||
{RISK_POINTS.map((p) => (
|
||||
<li key={p} className="flex gap-1.5 text-xs leading-snug text-slate-600 dark:text-[var(--dash-text)]">
|
||||
<span className="mt-1.5 h-1 w-1 shrink-0 rounded-full bg-red-500" />
|
||||
{p}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<FooterNote label="研判结论">{RISK_CONCLUSION}</FooterNote>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 真实数据渲染 ──────────────────────────────────────────────────────────────
|
||||
function EnemyReal({ data }: { data: EnemyData }) {
|
||||
return (
|
||||
<div className="space-y-4 text-slate-700 dark:text-[var(--dash-text)]">
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-12">
|
||||
{data.targets.length > 0 && (
|
||||
<Card className="flex flex-col lg:col-span-5">
|
||||
<CardTitle icon={Target} title="敌方目标" badges={<HeadBadge label="目标数" value={`${data.targets.length}`} />} />
|
||||
<div className="flex-1 px-4 py-3.5">
|
||||
<Table head={["名称", "类别", "说明"]}>
|
||||
{data.targets.map((t, i) => (
|
||||
<tr key={i}>
|
||||
<Td className="whitespace-nowrap font-semibold text-slate-800 dark:text-[var(--dash-text-strong)]">{t.name}</Td>
|
||||
<Td className="whitespace-nowrap text-slate-500 dark:text-[var(--dash-text-sec)]">{t.category || "—"}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{t.note || "—"}</Td>
|
||||
</tr>
|
||||
))}
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{data.events.length > 0 && (
|
||||
<Card className="flex flex-col lg:col-span-7">
|
||||
<CardTitle icon={CalendarClock} title="关键事件" />
|
||||
<div className="flex-1 space-y-3.5 px-4 py-3.5">
|
||||
{data.events.map((g, i) => (
|
||||
<div key={i}>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span className="text-xs font-bold text-slate-800 dark:text-[var(--dash-text-strong)]">{g.topic || "事件"}</span>
|
||||
<Tag tone="red">{g.count}</Tag>
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{g.items.map((e, j) => (
|
||||
<li key={j} className="flex gap-2.5 text-xs">
|
||||
<span className="w-24 shrink-0 whitespace-nowrap font-mono text-[11px] text-slate-400 dark:text-[var(--dash-text-dim)]">{e.time}</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-semibold text-slate-800 dark:text-[var(--dash-text-strong)]">{e.title}</div>
|
||||
{e.brief && <div className="mt-0.5 line-clamp-2 leading-snug text-slate-500 dark:text-[var(--dash-text-sec)]">{e.brief}</div>}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{data.narratives.length > 0 && (
|
||||
<Card>
|
||||
<CardTitle icon={Users} title="对手叙事" />
|
||||
<div className="px-4 py-3.5">
|
||||
<Table head={["主题", "叙事方", "叙事意图", "类型", "概述"]}>
|
||||
{data.narratives.map((n, i) => (
|
||||
<tr key={i}>
|
||||
<Td className="whitespace-nowrap font-semibold text-slate-800 dark:text-[var(--dash-text-strong)]">{n.topic || "—"}</Td>
|
||||
<Td className="whitespace-nowrap text-slate-500 dark:text-[var(--dash-text-sec)]">{n.narrator || "—"}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{n.intention || "—"}</Td>
|
||||
<Td className="whitespace-nowrap text-slate-500 dark:text-[var(--dash-text-sec)]">{n.type || "—"}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{n.summary || "—"}</Td>
|
||||
</tr>
|
||||
))}
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* 「大屏绘制智能体」战场环境页 —— 信息环境 / 人文环境 / 综合研判(按设计图 dp-4.png 一比一实现)。
|
||||
*
|
||||
* **dashboard-agent 独占**:作为 DashboardApp `pages` 插槽里「战场环境」Tab 的内容,
|
||||
* 不影响多智能体会商(Step3)大屏多页的样式。当前为「静态页面」,文案为占位内容,
|
||||
* 动态数据后续接入。版式包含:
|
||||
* 1. 顶部 5 个环境指数卡
|
||||
* 2. 信息环境(5 栏维度卡:媒体生态 / 平台分布 / 基础设施 / 监管治理 / 受众习惯)
|
||||
* 3. 人文环境(4 栏维度卡:社会 / 经济 / 舆论 / 文化)
|
||||
* 4. 环境综合研判 + 信息来源与参考
|
||||
*/
|
||||
import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
Network,
|
||||
AlertOctagon,
|
||||
BarChart3,
|
||||
Tv,
|
||||
LayoutGrid,
|
||||
Server,
|
||||
ShieldCheck,
|
||||
Users,
|
||||
Building2,
|
||||
Coins,
|
||||
MessageSquare,
|
||||
BookOpen,
|
||||
Lightbulb,
|
||||
FileText,
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Minus,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Card, CardTitle, FooterNote, Table, Td, Tag, NoData, type Tone } from "@/roundtable-planning/dashboard-agent/ui";
|
||||
import type { EnvData, EnvSection } from "@/roundtable-planning/dashboard-agent/sq-report";
|
||||
|
||||
// ── 占位数据 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
type Trend = "up" | "down" | "flat";
|
||||
|
||||
interface Kpi {
|
||||
label: string;
|
||||
value: string;
|
||||
icon: LucideIcon;
|
||||
delta: string;
|
||||
trend: Trend;
|
||||
}
|
||||
const KPIS: Kpi[] = [
|
||||
{ label: "信息活跃度指数", value: "78.6", icon: Activity, delta: "+3.4", trend: "up" },
|
||||
{ label: "舆论风险指数", value: "64.2", icon: AlertTriangle, delta: "+5.1", trend: "up" },
|
||||
{ label: "网络连接通畅率", value: "58.7%", icon: Network, delta: "+2.3", trend: "up" },
|
||||
{ label: "虚假信息占比", value: "23.6%", icon: AlertOctagon, delta: "-1.8", trend: "down" },
|
||||
{ label: "关键平台覆盖率", value: "92.4%", icon: BarChart3, delta: "+0.6", trend: "up" },
|
||||
];
|
||||
|
||||
interface EnvRow {
|
||||
label: string;
|
||||
value: string;
|
||||
tone: Tone;
|
||||
}
|
||||
interface EnvField {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
rows: EnvRow[];
|
||||
assessLabel: string;
|
||||
assessment: string;
|
||||
}
|
||||
|
||||
const INFO_ENV: EnvField[] = [
|
||||
{
|
||||
icon: Tv,
|
||||
title: "媒体生态分布",
|
||||
rows: [
|
||||
{ label: "主流媒体", value: "高", tone: "red" },
|
||||
{ label: "自媒体", value: "高", tone: "red" },
|
||||
{ label: "社交平台", value: "高", tone: "red" },
|
||||
{ label: "境外媒体", value: "中", tone: "amber" },
|
||||
{ label: "传统媒体", value: "中", tone: "amber" },
|
||||
],
|
||||
assessLabel: "总体评估",
|
||||
assessment: "媒体生态多元,信息传播活跃。",
|
||||
},
|
||||
{
|
||||
icon: LayoutGrid,
|
||||
title: "平台分布",
|
||||
rows: [
|
||||
{ label: "即时通讯", value: "高", tone: "red" },
|
||||
{ label: "社交媒体", value: "高", tone: "red" },
|
||||
{ label: "视频平台", value: "高", tone: "red" },
|
||||
{ label: "论坛社区", value: "中", tone: "amber" },
|
||||
{ label: "新闻门户", value: "中", tone: "amber" },
|
||||
],
|
||||
assessLabel: "总体评估",
|
||||
assessment: "平台覆盖广泛,传播路径多样。",
|
||||
},
|
||||
{
|
||||
icon: Server,
|
||||
title: "基础设施状况",
|
||||
rows: [
|
||||
{ label: "网络覆盖", value: "良好", tone: "green" },
|
||||
{ label: "带宽容量", value: "充足", tone: "green" },
|
||||
{ label: "移动网络", value: "良好", tone: "green" },
|
||||
{ label: "数据中心", value: "稳定", tone: "green" },
|
||||
{ label: "电力保障", value: "稳定", tone: "green" },
|
||||
],
|
||||
assessLabel: "总体评估",
|
||||
assessment: "基础设施完备,保障能力强。",
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
title: "信息监管与治理",
|
||||
rows: [
|
||||
{ label: "内容审核", value: "较强", tone: "green" },
|
||||
{ label: "谣言治理", value: "较强", tone: "green" },
|
||||
{ label: "平台自律", value: "一般", tone: "slate" },
|
||||
{ label: "跨境监管", value: "较弱", tone: "amber" },
|
||||
],
|
||||
assessLabel: "总体评估",
|
||||
assessment: "治理体系较完善,执行力度持续提升。",
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: "受众信息接触习惯",
|
||||
rows: [
|
||||
{ label: "移动端为主", value: "高", tone: "red" },
|
||||
{ label: "短视频主导", value: "高", tone: "red" },
|
||||
{ label: "信息甄别能力", value: "中", tone: "amber" },
|
||||
{ label: "权威信息依赖", value: "中", tone: "amber" },
|
||||
],
|
||||
assessLabel: "总体评估",
|
||||
assessment: "触达渠道广泛,注意力相对分散。",
|
||||
},
|
||||
];
|
||||
|
||||
const HUMAN_ENV: EnvField[] = [
|
||||
{
|
||||
icon: Building2,
|
||||
title: "社会环境",
|
||||
rows: [
|
||||
{ label: "社会稳定度", value: "良好", tone: "green" },
|
||||
{ label: "社会矛盾", value: "可控", tone: "amber" },
|
||||
{ label: "民族宗教", value: "稳定", tone: "green" },
|
||||
{ label: "城乡发展", value: "一般", tone: "slate" },
|
||||
],
|
||||
assessLabel: "评估结论",
|
||||
assessment: "社会大局稳定,整体可控。",
|
||||
},
|
||||
{
|
||||
icon: Coins,
|
||||
title: "经济基础",
|
||||
rows: [
|
||||
{ label: "经济发展水平", value: "良好", tone: "green" },
|
||||
{ label: "产业结构", value: "一般", tone: "slate" },
|
||||
{ label: "就业形势", value: "一般", tone: "slate" },
|
||||
{ label: "居民收入", value: "一般", tone: "slate" },
|
||||
],
|
||||
assessLabel: "评估结论",
|
||||
assessment: "经济运行平稳,韧性较强。",
|
||||
},
|
||||
{
|
||||
icon: MessageSquare,
|
||||
title: "社会舆论",
|
||||
rows: [
|
||||
{ label: "主流舆论引导", value: "有效", tone: "green" },
|
||||
{ label: "网络情绪", value: "一般", tone: "amber" },
|
||||
{ label: "热点话题", value: "活跃", tone: "red" },
|
||||
{ label: "意见多样性", value: "较强", tone: "amber" },
|
||||
],
|
||||
assessLabel: "评估结论",
|
||||
assessment: "舆论总体平稳,热点多发。",
|
||||
},
|
||||
{
|
||||
icon: BookOpen,
|
||||
title: "文化环境",
|
||||
rows: [
|
||||
{ label: "文化认同", value: "较强", tone: "green" },
|
||||
{ label: "价值观主流", value: "稳定", tone: "green" },
|
||||
{ label: "文化多元", value: "一般", tone: "slate" },
|
||||
{ label: "文化渗透风险", value: "中", tone: "amber" },
|
||||
],
|
||||
assessLabel: "评估结论",
|
||||
assessment: "文化主流稳定,需防外来渗透。",
|
||||
},
|
||||
];
|
||||
|
||||
const REVIEW = [
|
||||
"信息环境高度活跃,传播速度快、覆盖面广,主流舆论引导总体有效,但虚假信息与噪声并存。",
|
||||
"网络基础设施完备、平台多元,为信息传播与舆论引导提供良好支撑,需防范境外渗透。",
|
||||
"社会大局稳定,经济运行平稳,人文环境总体向好,为我方舆论工作提供有利条件。",
|
||||
"综合研判:当前战场信息环境总体有利,须强化内容供给与精准引导,压缩虚假信息空间。",
|
||||
];
|
||||
|
||||
interface SourceRow {
|
||||
name: string;
|
||||
type: string;
|
||||
trust: string;
|
||||
trustTone: Tone;
|
||||
updated: string;
|
||||
}
|
||||
const SOURCES: SourceRow[] = [
|
||||
{ name: "全网舆情监测平台", type: "舆情", trust: "高", trustTone: "green", updated: "2025-05-16 10:20" },
|
||||
{ name: "媒体监测系统", type: "媒体", trust: "高", trustTone: "green", updated: "2025-05-16 10:00" },
|
||||
{ name: "社交平台数据接口", type: "社交", trust: "中", trustTone: "amber", updated: "2025-05-16 09:40" },
|
||||
{ name: "行业研究报告", type: "第三方", trust: "中", trustTone: "amber", updated: "2025-05-15 18:00" },
|
||||
{ name: "公开统计数据", type: "官方", trust: "高", trustTone: "green", updated: "2025-05-14 16:00" },
|
||||
];
|
||||
|
||||
const TREND_ICON: Record<Trend, LucideIcon> = { up: TrendingUp, down: TrendingDown, flat: Minus };
|
||||
const TREND_COLOR: Record<Trend, string> = { up: "text-red-600", down: "text-emerald-600", flat: "text-slate-400" };
|
||||
const VAL_COLOR: Record<Tone, string> = {
|
||||
red: "text-red-600",
|
||||
amber: "text-amber-600",
|
||||
green: "text-emerald-600",
|
||||
blue: "text-sky-600",
|
||||
slate: "text-slate-400 dark:text-[var(--dash-text-dim)]",
|
||||
};
|
||||
|
||||
// ── 主组件(有真实数据走真实渲染,否则回退静态占位) ─────────────────────────────
|
||||
|
||||
export function EnvironmentDetail({ data }: { data?: EnvData | null }) {
|
||||
return data ? <EnvReal data={data} /> : <NoData />;
|
||||
}
|
||||
|
||||
function EnvPlaceholder() {
|
||||
return (
|
||||
<div className="space-y-4 text-slate-700 dark:text-[var(--dash-text)]">
|
||||
{/* 1. 顶部 5 个环境指数卡 */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
|
||||
{KPIS.map((k) => {
|
||||
const TrendIco = TREND_ICON[k.trend];
|
||||
return (
|
||||
<Card key={k.label} className="p-4">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-md bg-red-50 text-red-600 dark:bg-red-500/15">
|
||||
<k.icon className="h-4 w-4" />
|
||||
</span>
|
||||
<span className="text-xs font-medium text-slate-500 dark:text-[var(--dash-text-sec)]">{k.label}</span>
|
||||
</div>
|
||||
<div className="font-mono text-3xl font-black leading-none text-slate-900 dark:text-[var(--dash-text-strong)]">
|
||||
{k.value}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-1 text-[11px] text-slate-400 dark:text-[var(--dash-text-dim)]">
|
||||
<span>较昨日</span>
|
||||
<span className={`inline-flex items-center gap-0.5 font-semibold ${TREND_COLOR[k.trend]}`}>
|
||||
<TrendIco className="h-3 w-3" />
|
||||
{k.delta}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 2. 信息环境(5 栏) */}
|
||||
<SectionLabel>信息环境</SectionLabel>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-5">
|
||||
{INFO_ENV.map((f) => (
|
||||
<EnvCard key={f.title} field={f} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 3. 人文环境(4 栏) */}
|
||||
<SectionLabel>人文环境</SectionLabel>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{HUMAN_ENV.map((f) => (
|
||||
<EnvCard key={f.title} field={f} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 4. 环境综合研判 + 信息来源与参考 */}
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-12">
|
||||
<Card className="flex flex-col lg:col-span-7">
|
||||
<CardTitle icon={Lightbulb} title="环境综合研判" />
|
||||
<ol className="flex-1 space-y-3 px-4 py-3.5">
|
||||
{REVIEW.map((r, i) => (
|
||||
<li key={r} className="flex gap-2.5 text-[13px] leading-relaxed">
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-red-600 text-[11px] font-bold text-white">
|
||||
{i + 1}
|
||||
</span>
|
||||
<span className="text-slate-600 dark:text-[var(--dash-text)]">{r}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</Card>
|
||||
|
||||
<Card className="flex flex-col lg:col-span-5">
|
||||
<CardTitle icon={FileText} title="信息来源与参考" />
|
||||
<div className="flex-1 px-4 py-3.5">
|
||||
<Table head={["来源名称", "类型", "可信度", "更新时间"]}>
|
||||
{SOURCES.map((s) => (
|
||||
<tr key={s.name}>
|
||||
<Td className="font-semibold text-slate-800 dark:text-[var(--dash-text-strong)]">{s.name}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{s.type}</Td>
|
||||
<Td>
|
||||
<Tag tone={s.trustTone}>{s.trust}</Tag>
|
||||
</Td>
|
||||
<Td className="whitespace-nowrap font-mono text-slate-400">{s.updated}</Td>
|
||||
</tr>
|
||||
))}
|
||||
</Table>
|
||||
</div>
|
||||
<FooterNote label="说明">可信度分为高 / 中 / 低,时效性以最新更新时间为准,综合研判时按权重折算。</FooterNote>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 环境维度卡(红色标题 + 键值行 + 评估脚注)。 */
|
||||
function EnvCard({ field }: { field: EnvField }) {
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<CardTitle icon={field.icon} title={field.title} />
|
||||
<div className="flex-1 px-4 py-3.5">
|
||||
<dl className="space-y-2">
|
||||
{field.rows.map((r) => (
|
||||
<div key={r.label} className="flex items-center justify-between gap-2 text-xs">
|
||||
<dt className="text-slate-400 dark:text-[var(--dash-text-dim)]">{r.label}</dt>
|
||||
<dd className={`font-semibold ${VAL_COLOR[r.tone]}`}>{r.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
<FooterNote label={field.assessLabel}>{field.assessment}</FooterNote>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/** 行级区段标题(红色竖条 + 文字)。 */
|
||||
function SectionLabel({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 pt-1 text-sm font-bold text-slate-900 dark:text-[var(--dash-text-strong)]">
|
||||
<span className="h-3.5 w-1 rounded-full bg-red-600" />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 真实数据渲染 ──────────────────────────────────────────────────────────────
|
||||
function EnvReal({ data }: { data: EnvData }) {
|
||||
return (
|
||||
<div className="space-y-4 text-slate-700 dark:text-[var(--dash-text)]">
|
||||
{data.info.length > 0 && (
|
||||
<>
|
||||
<SectionLabel>信息环境</SectionLabel>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{data.info.map((s, i) => (
|
||||
<EnvTextCard key={i} section={s} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{data.human.length > 0 && (
|
||||
<>
|
||||
<SectionLabel>人文环境</SectionLabel>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{data.human.map((s, i) => (
|
||||
<EnvTextCard key={i} section={s} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 环境文本维度卡(标题 + 段落要点)。 */
|
||||
function EnvTextCard({ section }: { section: EnvSection }) {
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<CardTitle icon={FileText} title={section.title} />
|
||||
<ul className="flex-1 space-y-2 px-4 py-3.5">
|
||||
{section.paragraphs.map((p, i) => (
|
||||
<li key={i} className="flex gap-2 text-xs leading-relaxed">
|
||||
<span className="mt-1.5 h-1 w-1 shrink-0 rounded-full bg-red-500" />
|
||||
<span className="text-slate-600 dark:text-[var(--dash-text)]">{p}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* 「大屏绘制智能体」情况判断页 —— 敌我优劣 / 条件限制 / 风险应对 / 综合研判
|
||||
* (按设计图 dp-5.png 一比一实现)。
|
||||
*
|
||||
* **dashboard-agent 独占**:作为 DashboardApp `pages` 插槽里「情况判断」Tab 的内容,
|
||||
* 不影响多智能体会商(Step3)大屏多页的样式。当前为「静态页面」,文案为占位内容,
|
||||
* 动态数据后续接入。版式包含:
|
||||
* 1. 敌我优劣势分析(敌方优势/劣势 + 我方优势/劣势 四张要点卡)
|
||||
* 2. 条件与限制(资源/法理/政治/时间 四张约束键值卡)
|
||||
* 3. 风险与应对(风险表)+ 判断结论(综合研判)
|
||||
*/
|
||||
import { Swords, ShieldX, ShieldCheck, ShieldAlert, Package, Scale, Landmark, Clock, AlertTriangle, Gavel, type LucideIcon } from "lucide-react";
|
||||
|
||||
import { Card, CardTitle, FooterNote, Table, Td, Tag, Val, NoData, type Tone } from "@/roundtable-planning/dashboard-agent/ui";
|
||||
import type { JudgeData } from "@/roundtable-planning/dashboard-agent/sq-report";
|
||||
|
||||
// ── 占位数据 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
interface BulletCardData {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
items: string[];
|
||||
}
|
||||
const SWOT: BulletCardData[] = [
|
||||
{
|
||||
icon: Swords,
|
||||
title: "敌方优势",
|
||||
items: [
|
||||
"火力密度大,远程打击能力强",
|
||||
"信息系统较完善,情报获取渠道多",
|
||||
"机动能力强,可快速集结兵力",
|
||||
"体系作战能力较强,协同较成熟",
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: ShieldX,
|
||||
title: "敌方劣势",
|
||||
items: [
|
||||
"地形适应性差,山地作战能力不足",
|
||||
"部队协同较弱,指挥链路较长",
|
||||
"兵力部署分散,防御纵深不足",
|
||||
"后勤补给线长,维护保障压力大",
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
title: "我方优势",
|
||||
items: [
|
||||
"占据有利地形,防御体系完备",
|
||||
"群众基础好,保障支援能力强",
|
||||
"部队士气高昂,战斗意志坚定",
|
||||
"熟悉地形地貌,机动灵活",
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: ShieldAlert,
|
||||
title: "我方劣势",
|
||||
items: [
|
||||
"远程火力相对不足,打击体系待完善",
|
||||
"部分装备老化,信息化程度待提升",
|
||||
"防空反导能力有限,制空压力较大",
|
||||
"特定方向兵力薄弱,需重点加强",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
interface KvRow {
|
||||
label: string;
|
||||
value: string;
|
||||
tone: Tone;
|
||||
}
|
||||
interface ConstraintData {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
rows: KvRow[];
|
||||
footer: string;
|
||||
}
|
||||
const CONSTRAINTS: ConstraintData[] = [
|
||||
{
|
||||
icon: Package,
|
||||
title: "资源约束",
|
||||
rows: [
|
||||
{ label: "弹药储备", value: "充足", tone: "green" },
|
||||
{ label: "燃油保障", value: "适中", tone: "amber" },
|
||||
{ label: "医疗资源", value: "紧张", tone: "red" },
|
||||
{ label: "兵员补充", value: "适中", tone: "amber" },
|
||||
{ label: "通信物资", value: "适中", tone: "amber" },
|
||||
],
|
||||
footer: "资源总体可支撑当前作战,医疗与持续保障需重点关注。",
|
||||
},
|
||||
{
|
||||
icon: Scale,
|
||||
title: "法理约束",
|
||||
rows: [
|
||||
{ label: "作战法理依据", value: "符合", tone: "green" },
|
||||
{ label: "国际人道法", value: "合规", tone: "green" },
|
||||
{ label: "平民保护责任", value: "严格", tone: "red" },
|
||||
{ label: "舆论道义争取", value: "有利", tone: "green" },
|
||||
],
|
||||
footer: "行动总体合法合规,应在法理框架内规范实施。",
|
||||
},
|
||||
{
|
||||
icon: Landmark,
|
||||
title: "政治约束",
|
||||
rows: [
|
||||
{ label: "政治敏感度", value: "高", tone: "red" },
|
||||
{ label: "对外关系影响", value: "中", tone: "amber" },
|
||||
{ label: "国内舆论支持", value: "较高", tone: "green" },
|
||||
{ label: "国际舆论环境", value: "一般", tone: "slate" },
|
||||
],
|
||||
footer: "政治考量需贯穿全程,注重舆论与外交配合。",
|
||||
},
|
||||
{
|
||||
icon: Clock,
|
||||
title: "时间约束",
|
||||
rows: [
|
||||
{ label: "现有作战窗口", value: "36 小时内", tone: "red" },
|
||||
{ label: "兵力集结时限", value: "48 小时内", tone: "amber" },
|
||||
{ label: "后勤展开时间", value: "72 小时内", tone: "amber" },
|
||||
{ label: "上级决心时限", value: "60 小时内", tone: "amber" },
|
||||
],
|
||||
footer: "时间窗口偏紧,须加快决策与力量展开。",
|
||||
},
|
||||
];
|
||||
|
||||
interface RiskRow {
|
||||
item: string;
|
||||
level: string;
|
||||
levelTone: Tone;
|
||||
impact: string;
|
||||
plan: string;
|
||||
}
|
||||
const RISKS: RiskRow[] = [
|
||||
{ item: "敌火力突袭风险", level: "高", levelTone: "red", impact: "前沿阵地与人员装备损失", plan: "强化伪装防护与机动疏散" },
|
||||
{ item: "敌增援兵力介入", level: "中", levelTone: "amber", impact: "局部兵力对比变化", plan: "预置阻援力量,封控通道" },
|
||||
{ item: "通信干扰破坏", level: "中", levelTone: "amber", impact: "指挥效率下降", plan: "启用备份链路与抗扰手段" },
|
||||
{ item: "网络攻击渗透", level: "高", levelTone: "red", impact: "信息系统受损", plan: "加固防护,分区隔离" },
|
||||
{ item: "舆论造谣攻击", level: "低", levelTone: "green", impact: "局部认知影响", plan: "及时澄清,权威发声" },
|
||||
];
|
||||
|
||||
interface StatBlock {
|
||||
value: string;
|
||||
caption: string;
|
||||
}
|
||||
const CONCLUSION_STATS: StatBlock[] = [
|
||||
{ value: "我方占优", caption: "敌我对比" },
|
||||
{ value: "中高", caption: "整体风险" },
|
||||
{ value: "积极进取", caption: "决策倾向" },
|
||||
];
|
||||
const CONCLUSION_POINTS = [
|
||||
"当前总体态势有利,敌方兵力行动受牵制。",
|
||||
"我方占据地利与人和,具备掌握主动的条件。",
|
||||
];
|
||||
const CONCLUSION = "总体态势有利,敌方行动受牵制;建议积极把握主动,稳中求进、择机而动。";
|
||||
|
||||
// ── 主组件(有真实数据走真实渲染,否则回退静态占位) ─────────────────────────────
|
||||
|
||||
export function Judgement({ data }: { data?: JudgeData | null }) {
|
||||
return data ? <JudgeReal data={data} /> : <NoData />;
|
||||
}
|
||||
|
||||
function JudgePlaceholder() {
|
||||
return (
|
||||
<div className="space-y-4 text-slate-700 dark:text-[var(--dash-text)]">
|
||||
{/* 1. 敌我优劣势分析 */}
|
||||
<NumberedHeading n={1}>敌我优劣势分析</NumberedHeading>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{SWOT.map((c) => (
|
||||
<Card key={c.title} className="flex flex-col">
|
||||
<CardTitle icon={c.icon} title={c.title} />
|
||||
<ul className="flex-1 space-y-2 px-4 py-3.5">
|
||||
{c.items.map((it) => (
|
||||
<li key={it} className="flex gap-2 text-xs leading-relaxed">
|
||||
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-red-600" />
|
||||
<span className="text-slate-600 dark:text-[var(--dash-text)]">{it}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 2. 条件与限制 */}
|
||||
<NumberedHeading n={2}>条件与限制</NumberedHeading>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{CONSTRAINTS.map((c) => (
|
||||
<Card key={c.title} className="flex flex-col">
|
||||
<CardTitle icon={c.icon} title={c.title} />
|
||||
<div className="flex-1 px-4 py-3.5">
|
||||
<dl className="space-y-2">
|
||||
{c.rows.map((r) => (
|
||||
<div key={r.label} className="flex items-center justify-between gap-2 text-xs">
|
||||
<dt className="text-slate-400 dark:text-[var(--dash-text-dim)]">{r.label}</dt>
|
||||
<dd>
|
||||
<Val tone={r.tone}>{r.value}</Val>
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
<FooterNote label="评估">{c.footer}</FooterNote>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 3. 风险与应对 + 判断结论 */}
|
||||
<NumberedHeading n={3}>风险与应对</NumberedHeading>
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-12">
|
||||
{/* 风险与应对表 */}
|
||||
<Card className="flex flex-col lg:col-span-7">
|
||||
<CardTitle icon={AlertTriangle} title="风险与应对" />
|
||||
<div className="flex-1 px-4 py-3.5">
|
||||
<Table head={["风险事项", "等级", "可能影响", "应对计划"]}>
|
||||
{RISKS.map((r) => (
|
||||
<tr key={r.item}>
|
||||
<Td className="whitespace-nowrap font-semibold text-slate-800 dark:text-[var(--dash-text-strong)]">{r.item}</Td>
|
||||
<Td>
|
||||
<Tag tone={r.levelTone}>{r.level}</Tag>
|
||||
</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{r.impact}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{r.plan}</Td>
|
||||
</tr>
|
||||
))}
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 判断结论(综合研判) */}
|
||||
<Card className="flex flex-col lg:col-span-5">
|
||||
<CardTitle icon={Gavel} title="判断结论(综合研判)" />
|
||||
<div className="flex-1 px-4 py-3.5">
|
||||
<div className="mb-3 grid grid-cols-3 gap-2">
|
||||
{CONCLUSION_STATS.map((s) => (
|
||||
<div key={s.caption} className="rounded-md bg-red-50 px-2 py-3 text-center dark:bg-red-500/10">
|
||||
<div className="text-sm font-black text-red-600">{s.value}</div>
|
||||
<div className="mt-1 text-[10px] text-slate-400 dark:text-[var(--dash-text-dim)]">{s.caption}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{CONCLUSION_POINTS.map((p) => (
|
||||
<li key={p} className="flex gap-2 text-xs leading-relaxed">
|
||||
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-red-600" />
|
||||
<span className="text-slate-600 dark:text-[var(--dash-text)]">{p}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<FooterNote label="结论">{CONCLUSION}</FooterNote>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 红色编号区段标题(① 敌我优劣势分析 …)。 */
|
||||
function NumberedHeading({ n, children }: { n: number; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-red-600 text-[11px] font-bold text-white">
|
||||
{n}
|
||||
</span>
|
||||
<h2 className="text-sm font-bold text-slate-900 dark:text-[var(--dash-text-strong)]">{children}</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 真实数据渲染 ──────────────────────────────────────────────────────────────
|
||||
function SwotCard({ title, icon, items, dot }: { title: string; icon: LucideIcon; items: Array<{ title: string; content: string }>; dot: Tone }) {
|
||||
const dotColor = dot === "green" ? "bg-emerald-500" : "bg-red-600";
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<CardTitle icon={icon} title={title} />
|
||||
<ul className="flex-1 space-y-2.5 px-4 py-3.5">
|
||||
{items.map((it, i) => (
|
||||
<li key={i} className="flex gap-2 text-xs leading-relaxed">
|
||||
<span className={`mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full ${dotColor}`} />
|
||||
<span className="text-slate-600 dark:text-[var(--dash-text)]">
|
||||
{it.title && <span className="font-bold text-slate-800 dark:text-[var(--dash-text-strong)]">{it.title}:</span>}
|
||||
{it.content}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function JudgeReal({ data }: { data: JudgeData }) {
|
||||
const hasSwot = data.strengths.length > 0 || data.weaknesses.length > 0;
|
||||
const hasRisk = data.risks.length > 0 || data.responses.length > 0;
|
||||
return (
|
||||
<div className="space-y-4 text-slate-700 dark:text-[var(--dash-text)]">
|
||||
{hasSwot && (
|
||||
<>
|
||||
<NumberedHeading n={1}>优劣势分析</NumberedHeading>
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
|
||||
{data.strengths.length > 0 && <SwotCard title="优势" icon={ShieldCheck} items={data.strengths} dot="green" />}
|
||||
{data.weaknesses.length > 0 && <SwotCard title="劣势" icon={ShieldAlert} items={data.weaknesses} dot="red" />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{data.constraints.length > 0 && (
|
||||
<>
|
||||
<NumberedHeading n={2}>条件与限制</NumberedHeading>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{data.constraints.map((c, i) => (
|
||||
<Card key={i} className="flex flex-col">
|
||||
<CardTitle icon={Scale} title={c.title} />
|
||||
<ul className="flex-1 space-y-2 px-4 py-3.5">
|
||||
{c.items.map((it, j) => (
|
||||
<li key={j} className="flex gap-2 text-xs leading-relaxed">
|
||||
<span className="mt-1.5 h-1 w-1 shrink-0 rounded-full bg-red-500" />
|
||||
<span className="text-slate-600 dark:text-[var(--dash-text)]">{it}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasRisk && (
|
||||
<>
|
||||
<NumberedHeading n={3}>风险与应对</NumberedHeading>
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
|
||||
{data.risks.length > 0 && (
|
||||
<Card className="flex flex-col">
|
||||
<CardTitle icon={AlertTriangle} title="风险分析" />
|
||||
<ul className="flex-1 space-y-2.5 px-4 py-3.5">
|
||||
{data.risks.map((r, i) => (
|
||||
<li key={i} className="flex gap-2 text-xs leading-relaxed">
|
||||
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-red-600" />
|
||||
<span className="text-slate-600 dark:text-[var(--dash-text)]">{r}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
)}
|
||||
{data.responses.length > 0 && (
|
||||
<Card className="flex flex-col">
|
||||
<CardTitle icon={Gavel} title="应对方案" />
|
||||
<ol className="flex-1 space-y-2.5 px-4 py-3.5">
|
||||
{data.responses.map((r, i) => (
|
||||
<li key={i} className="flex gap-2 text-xs leading-relaxed">
|
||||
<span className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-red-600 text-[10px] font-bold text-white">{i + 1}</span>
|
||||
<span className="text-slate-600 dark:text-[var(--dash-text)]">{r}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* 「大屏绘制智能体」我情页 —— 我方口径 / 叙事 / 任务版式(按设计图 dp-3.png 一比一实现)。
|
||||
*
|
||||
* **dashboard-agent 独占**:作为 DashboardApp `pages` 插槽里「我情」Tab 的内容,
|
||||
* 不影响多智能体会商(Step3)大屏多页的样式。当前为「静态页面」,文案为占位内容,
|
||||
* 动态数据后续接入。版式包含:
|
||||
* 1. 口径 + 叙事 + 上级要求(上排三栏)
|
||||
* 2. 重点发布日历 + 口径关键词 + 传播任务分工速览(下排三栏)
|
||||
*/
|
||||
import { MessageSquareQuote, BookText, Star, CalendarDays, Tags, Megaphone } from "lucide-react";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardTitle,
|
||||
CardFooterLink,
|
||||
Tag,
|
||||
Table,
|
||||
Td,
|
||||
NoData,
|
||||
type Tone,
|
||||
} from "@/roundtable-planning/dashboard-agent/ui";
|
||||
import type { OursData } from "@/roundtable-planning/dashboard-agent/sq-report";
|
||||
|
||||
// ── 占位数据 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const TONE_META: Array<{ label: string; value: string }> = [
|
||||
{ label: "创建时间", value: "2025-05-16 10:00" },
|
||||
{ label: "适用范围", value: "对外宣传、媒体回应、公开发布" },
|
||||
{ label: "发布层级", value: "公开" },
|
||||
];
|
||||
const TONE_POINTS = [
|
||||
"坚定维护国家主权、安全与领土完整,立场坚定不移。",
|
||||
"我方始终致力于维护地区和平稳定与共同发展。",
|
||||
"任何外部势力干涉地区事务的行径都不得人心。",
|
||||
"呼吁有关各方保持克制,通过对话协商解决分歧。",
|
||||
];
|
||||
|
||||
interface NarrativeRow {
|
||||
no: number;
|
||||
title: string;
|
||||
point: string;
|
||||
time: string;
|
||||
}
|
||||
const NARRATIVES: NarrativeRow[] = [
|
||||
{ no: 1, title: "维护正当权益", point: "正当维权行为合法合理", time: "05-16 10:00" },
|
||||
{ no: 2, title: "致力地区和平", point: "始终是和平稳定的建设者", time: "05-16 09:00" },
|
||||
{ no: 3, title: "倡导对话合作", point: "反对集团对抗与零和博弈", time: "05-15 18:30" },
|
||||
{ no: 4, title: "揭批外部干涉", point: "外部势力是紧张根源", time: "05-15 14:00" },
|
||||
];
|
||||
|
||||
const ORDERS = [
|
||||
"坚定政治方向,主动发声,紧贴宣传节奏。",
|
||||
"强化媒体引导,及时回应社会关切。",
|
||||
"严守宣传纪律,确保信息口径统一。",
|
||||
"加强协同联动,形成对外宣传合力。",
|
||||
];
|
||||
const ORDER_META: Array<{ label: string; value: string }> = [
|
||||
{ label: "下发时间", value: "2025-05-16 08:00" },
|
||||
{ label: "下发单位", value: "联合作战指挥部宣传工作组" },
|
||||
];
|
||||
|
||||
interface CalendarItem {
|
||||
time: string;
|
||||
event: string;
|
||||
status: string;
|
||||
tone: Tone;
|
||||
}
|
||||
const TODAY_PLAN: CalendarItem[] = [
|
||||
{ time: "09:00", event: "例行新闻发布会", status: "已发布", tone: "green" },
|
||||
{ time: "14:00", event: "重点新闻通稿", status: "待发布", tone: "amber" },
|
||||
{ time: "18:00", event: "对外答问口径", status: "待发布", tone: "amber" },
|
||||
];
|
||||
const UPCOMING: CalendarItem[] = [
|
||||
{ time: "05-17 09:00", event: "专题新闻发布会", status: "待发布", tone: "amber" },
|
||||
{ time: "05-18 10:00", event: "白皮书发布", status: "筹备中", tone: "slate" },
|
||||
];
|
||||
|
||||
interface KeywordGroup {
|
||||
name: string;
|
||||
tone: Tone;
|
||||
words: string[];
|
||||
}
|
||||
const KEYWORDS: KeywordGroup[] = [
|
||||
{ name: "主权安全", tone: "red", words: ["主权", "领土完整", "国家安全", "核心利益"] },
|
||||
{ name: "和平发展", tone: "green", words: ["和平", "稳定", "合作", "共同发展"] },
|
||||
{ name: "立场态度", tone: "blue", words: ["坚决", "克制", "正当", "合法合理"] },
|
||||
];
|
||||
|
||||
interface TaskRow {
|
||||
dept: string;
|
||||
task: string;
|
||||
deadline: string;
|
||||
}
|
||||
const TASKS: TaskRow[] = [
|
||||
{ dept: "新闻发布组", task: "组织发布会与新闻通稿", deadline: "当日" },
|
||||
{ dept: "媒体协调组", task: "协调媒体报道口径", deadline: "持续" },
|
||||
{ dept: "网络宣传组", task: "网络平台内容推送与监看", deadline: "实时" },
|
||||
{ dept: "对外传播组", task: "多语种对外发声", deadline: "当日" },
|
||||
];
|
||||
|
||||
// ── 主组件(有真实数据走真实渲染,否则回退静态占位) ─────────────────────────────
|
||||
|
||||
export function OurSituation({ data }: { data?: OursData | null }) {
|
||||
return data ? <OursReal data={data} /> : <NoData />;
|
||||
}
|
||||
|
||||
function OursPlaceholder() {
|
||||
return (
|
||||
<div className="space-y-4 text-slate-700 dark:text-[var(--dash-text)]">
|
||||
{/* 1. 上排:口径 + 叙事 + 上级要求 */}
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-3">
|
||||
{/* 口径 */}
|
||||
<Card className="flex flex-col">
|
||||
<CardTitle icon={MessageSquareQuote} title="口径" />
|
||||
<div className="flex-1 px-4 py-3.5">
|
||||
<dl className="mb-3 space-y-1.5">
|
||||
{TONE_META.map((m) => (
|
||||
<div key={m.label} className="flex gap-2 text-[11px]">
|
||||
<dt className="shrink-0 text-slate-400 dark:text-[var(--dash-text-dim)]">{m.label}</dt>
|
||||
<dd className="text-slate-600 dark:text-[var(--dash-text)]">{m.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
<ul className="space-y-2 border-t border-slate-100 pt-3 dark:border-[var(--dash-border)]">
|
||||
{TONE_POINTS.map((p) => (
|
||||
<li key={p} className="flex gap-2 text-xs leading-relaxed">
|
||||
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-red-600" />
|
||||
<span className="text-slate-600 dark:text-[var(--dash-text)]">{p}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<CardFooterLink>查看完整口径</CardFooterLink>
|
||||
</Card>
|
||||
|
||||
{/* 叙事 */}
|
||||
<Card className="flex flex-col">
|
||||
<CardTitle icon={BookText} title="叙事" />
|
||||
<div className="flex-1 px-4 py-3.5">
|
||||
<Table head={["编号", "标题", "核心要点", "更新时间"]}>
|
||||
{NARRATIVES.map((n) => (
|
||||
<tr key={n.no}>
|
||||
<Td className="font-mono text-slate-400">{n.no}</Td>
|
||||
<Td className="whitespace-nowrap font-semibold text-slate-800 dark:text-[var(--dash-text-strong)]">{n.title}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{n.point}</Td>
|
||||
<Td className="whitespace-nowrap font-mono text-slate-400">{n.time}</Td>
|
||||
</tr>
|
||||
))}
|
||||
</Table>
|
||||
</div>
|
||||
<CardFooterLink>查看更多叙事</CardFooterLink>
|
||||
</Card>
|
||||
|
||||
{/* 上级要求 */}
|
||||
<Card className="flex flex-col">
|
||||
<CardTitle icon={Star} title="上级要求" />
|
||||
<ol className="flex-1 space-y-2.5 px-4 py-3.5">
|
||||
{ORDERS.map((o, i) => (
|
||||
<li key={o} className="flex gap-2 text-xs leading-relaxed">
|
||||
<span className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-red-600 text-[10px] font-bold text-white">
|
||||
{i + 1}
|
||||
</span>
|
||||
<span className="text-slate-600 dark:text-[var(--dash-text)]">{o}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<dl className="mx-4 mb-3.5 space-y-1.5 rounded bg-slate-50 px-3 py-2.5 dark:bg-white/5">
|
||||
{ORDER_META.map((m) => (
|
||||
<div key={m.label} className="flex gap-2 text-[11px]">
|
||||
<dt className="shrink-0 text-slate-400 dark:text-[var(--dash-text-dim)]">{m.label}</dt>
|
||||
<dd className="text-slate-600 dark:text-[var(--dash-text)]">{m.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 2. 下排:重点发布日历 + 口径关键词 + 传播任务分工速览 */}
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-3">
|
||||
{/* 重点发布日历 */}
|
||||
<Card className="flex flex-col">
|
||||
<CardTitle icon={CalendarDays} title="重点发布日历(05-16)" />
|
||||
<div className="flex-1 space-y-3 px-4 py-3.5">
|
||||
<CalendarSection title="今日重点发布安排" items={TODAY_PLAN} />
|
||||
<CalendarSection title="近期重要安排" items={UPCOMING} />
|
||||
</div>
|
||||
<CardFooterLink>查看完整日历</CardFooterLink>
|
||||
</Card>
|
||||
|
||||
{/* 口径关键词 */}
|
||||
<Card className="flex flex-col">
|
||||
<CardTitle icon={Tags} title="口径关键词" />
|
||||
<div className="flex-1 space-y-3 px-4 py-3.5">
|
||||
{KEYWORDS.map((g) => (
|
||||
<div key={g.name}>
|
||||
<div className="mb-1.5 text-[11px] font-semibold text-slate-400 dark:text-[var(--dash-text-dim)]">
|
||||
{g.name}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{g.words.map((w) => (
|
||||
<Tag key={w} tone={g.tone}>
|
||||
{w}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<CardFooterLink>查看更多关键词</CardFooterLink>
|
||||
</Card>
|
||||
|
||||
{/* 传播任务分工速览 */}
|
||||
<Card className="flex flex-col">
|
||||
<CardTitle icon={Megaphone} title="传播任务分工速览" />
|
||||
<div className="flex-1 px-4 py-3.5">
|
||||
<Table head={["部门", "主要任务", "完成时限"]}>
|
||||
{TASKS.map((t) => (
|
||||
<tr key={t.dept}>
|
||||
<Td className="whitespace-nowrap font-semibold text-slate-800 dark:text-[var(--dash-text-strong)]">{t.dept}</Td>
|
||||
<Td className="text-slate-500 dark:text-[var(--dash-text-sec)]">{t.task}</Td>
|
||||
<Td className="whitespace-nowrap text-slate-500 dark:text-[var(--dash-text-sec)]">{t.deadline}</Td>
|
||||
</tr>
|
||||
))}
|
||||
</Table>
|
||||
</div>
|
||||
<CardFooterLink>查看完整分工</CardFooterLink>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 发布日历小节(标题 + 时间/事件/状态行)。 */
|
||||
function CalendarSection({ title, items }: { title: string; items: CalendarItem[] }) {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-1.5 text-[11px] font-semibold text-red-600">{title}</div>
|
||||
<ul className="space-y-1.5">
|
||||
{items.map((it) => (
|
||||
<li key={`${it.time}-${it.event}`} className="flex items-center gap-2 text-xs">
|
||||
<span className="w-20 shrink-0 font-mono text-slate-400 dark:text-[var(--dash-text-dim)]">{it.time}</span>
|
||||
<span className="flex-1 truncate text-slate-600 dark:text-[var(--dash-text)]">{it.event}</span>
|
||||
<Tag tone={it.tone}>{it.status}</Tag>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 真实数据渲染 ──────────────────────────────────────────────────────────────
|
||||
function OursReal({ data }: { data: OursData }) {
|
||||
const keywords = Array.from(new Set(data.narratives.flatMap((n) => n.tags))).slice(0, 24);
|
||||
return (
|
||||
<div className="space-y-4 text-slate-700 dark:text-[var(--dash-text)]">
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-12">
|
||||
{/* 口径 */}
|
||||
{data.speeches.length > 0 && (
|
||||
<Card className="flex flex-col lg:col-span-8">
|
||||
<CardTitle icon={MessageSquareQuote} title="口径" />
|
||||
<ul className="flex-1 space-y-3 px-4 py-3.5">
|
||||
{data.speeches.map((s, i) => (
|
||||
<li key={i} className="border-b border-slate-50 pb-3 last:border-0 last:pb-0 dark:border-[var(--dash-border)]">
|
||||
<div className="mb-1 flex items-start justify-between gap-2">
|
||||
<span className="text-xs font-bold text-slate-800 dark:text-[var(--dash-text-strong)]">{s.title || "口径"}</span>
|
||||
{s.time && <span className="shrink-0 whitespace-nowrap font-mono text-[10px] text-slate-400 dark:text-[var(--dash-text-dim)]">{s.time}</span>}
|
||||
</div>
|
||||
{s.summary && <p className="line-clamp-3 text-xs leading-relaxed text-slate-600 dark:text-[var(--dash-text)]">{s.summary}</p>}
|
||||
{s.direction && (
|
||||
<p className="mt-1 line-clamp-2 text-[11px] leading-snug text-slate-400 dark:text-[var(--dash-text-dim)]">
|
||||
<span className="font-semibold text-red-600">导向:</span>
|
||||
{s.direction}
|
||||
</p>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 上级要求 */}
|
||||
{data.orders.length > 0 && (
|
||||
<Card className="flex flex-col lg:col-span-4">
|
||||
<CardTitle icon={Star} title="上级要求" />
|
||||
<ol className="flex-1 space-y-2.5 px-4 py-3.5">
|
||||
{data.orders.map((o, i) => (
|
||||
<li key={i} className="flex gap-2 text-xs leading-relaxed">
|
||||
<span className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-red-600 text-[10px] font-bold text-white">{i + 1}</span>
|
||||
<span className="text-slate-600 dark:text-[var(--dash-text)]">{o}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 叙事 */}
|
||||
{data.narratives.length > 0 && (
|
||||
<Card>
|
||||
<CardTitle icon={BookText} title="叙事" />
|
||||
<ul className="space-y-3 px-4 py-3.5">
|
||||
{data.narratives.map((n, i) => (
|
||||
<li key={i} className="border-b border-slate-50 pb-3 last:border-0 last:pb-0 dark:border-[var(--dash-border)]">
|
||||
<div className="mb-1 flex flex-wrap items-center gap-1.5">
|
||||
<span className="text-xs font-bold text-slate-800 dark:text-[var(--dash-text-strong)]">{n.title || "叙事"}</span>
|
||||
{n.tags.map((t) => (
|
||||
<Tag key={t} tone="blue">{t}</Tag>
|
||||
))}
|
||||
</div>
|
||||
{n.content && <p className="text-xs leading-relaxed text-slate-600 dark:text-[var(--dash-text)]">{n.content}</p>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 口径关键词(取自叙事标签) */}
|
||||
{keywords.length > 0 && (
|
||||
<Card>
|
||||
<CardTitle icon={Tags} title="口径关键词" />
|
||||
<div className="flex flex-wrap gap-1.5 px-4 py-3.5">
|
||||
{keywords.map((w) => (
|
||||
<Tag key={w} tone="red">{w}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* 「大屏绘制智能体」总览页 —— **由六类战情数据聚合**而成(敌情/我情/战场环境/
|
||||
* 情况判断/决心建议/处置建议),版式参考设计图 dp-1.png。
|
||||
*
|
||||
* **dashboard-agent 独占组件**:作为 DashboardApp `pages` 插槽里「总览」Tab 的内容,
|
||||
* **不影响多智能体会商(Step3)大屏多页的样式**。
|
||||
*
|
||||
* 数据来自 `parseSqReport` 的整份 `SqReport`:
|
||||
* - 顶部指标 = 各类目数量;关键战情摘要 / 三栏概览 / 判断·决策·处置 = 各类目要点提炼。
|
||||
* - 接口返回为空(无任何类目)→ 显示「暂无数据」。
|
||||
*/
|
||||
import {
|
||||
Target,
|
||||
Zap,
|
||||
Megaphone,
|
||||
BookText,
|
||||
ShieldAlert,
|
||||
FileText,
|
||||
Crosshair,
|
||||
Shield,
|
||||
Mountain,
|
||||
Lightbulb,
|
||||
Flag,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Card, CardTitle, FooterNote, NoData } from "@/roundtable-planning/dashboard-agent/ui";
|
||||
import type { SqReport } from "@/roundtable-planning/dashboard-agent/sq-report";
|
||||
|
||||
// ── 聚合 ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
function eventCount(r: SqReport): number {
|
||||
return (r.enemy?.events ?? []).reduce((s, g) => s + (g.count || g.items.length || 0), 0);
|
||||
}
|
||||
|
||||
interface OverviewColumn {
|
||||
title: string;
|
||||
icon: LucideIcon;
|
||||
rows: Array<{ label: string; value: string }>;
|
||||
focusTitle: string;
|
||||
focus: string[];
|
||||
}
|
||||
|
||||
// ── 主组件 ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function SituationOverview({ data }: { data?: SqReport | null }) {
|
||||
const r = data;
|
||||
const hasAny =
|
||||
!!r && (!!r.enemy || !!r.ours || !!r.environment || !!r.judgement || !!r.decision || !!r.disposal);
|
||||
if (!hasAny) return <NoData />;
|
||||
|
||||
// 顶部指标(各类目数量)
|
||||
const metrics: Array<{ label: string; value: number; icon: LucideIcon }> = [
|
||||
{ label: "敌方目标", value: r!.enemy?.targets.length ?? 0, icon: Target },
|
||||
{ label: "关键事件", value: eventCount(r!), icon: Zap },
|
||||
{ label: "对手叙事", value: r!.enemy?.narratives.length ?? 0, icon: Megaphone },
|
||||
{ label: "我方口径", value: r!.ours?.speeches.length ?? 0, icon: BookText },
|
||||
{ label: "风险项", value: r!.judgement?.risks.length ?? 0, icon: ShieldAlert },
|
||||
{ label: "处置建议", value: r!.disposal?.items.length ?? 0, icon: FileText },
|
||||
];
|
||||
|
||||
// 关键战情摘要(取各类目最有代表性的一句)
|
||||
const summary: Array<{ label: string; text: string }> = [
|
||||
r!.decision?.summary && { label: "决心研判", text: r!.decision.summary },
|
||||
r!.disposal?.summary && { label: "处置研判", text: r!.disposal.summary },
|
||||
r!.enemy?.narratives[0]?.summary && { label: "对手叙事", text: r!.enemy!.narratives[0].summary },
|
||||
r!.judgement?.risks[0] && { label: "主要风险", text: r!.judgement!.risks[0] },
|
||||
r!.ours?.speeches[0]?.summary && { label: "我方口径", text: r!.ours!.speeches[0].summary },
|
||||
].filter(Boolean) as Array<{ label: string; text: string }>;
|
||||
|
||||
// 三栏概览
|
||||
const columns: OverviewColumn[] = [];
|
||||
if (r!.enemy) {
|
||||
columns.push({
|
||||
title: "敌情概览",
|
||||
icon: Crosshair,
|
||||
rows: [
|
||||
{ label: "敌方目标", value: `${r!.enemy.targets.length} 个` },
|
||||
{ label: "关键事件", value: `${eventCount(r!)} 起` },
|
||||
{ label: "对手叙事", value: `${r!.enemy.narratives.length} 条` },
|
||||
],
|
||||
focusTitle: "重点事件",
|
||||
focus: r!.enemy.events.map((e) => e.topic).filter(Boolean).slice(0, 3),
|
||||
});
|
||||
}
|
||||
if (r!.ours) {
|
||||
columns.push({
|
||||
title: "我情概览",
|
||||
icon: Shield,
|
||||
rows: [
|
||||
{ label: "口径", value: `${r!.ours.speeches.length} 条` },
|
||||
{ label: "叙事", value: `${r!.ours.narratives.length} 条` },
|
||||
{ label: "上级要求", value: `${r!.ours.orders.length} 项` },
|
||||
],
|
||||
focusTitle: "口径要点",
|
||||
focus: r!.ours.speeches.map((s) => s.title).filter(Boolean).slice(0, 3),
|
||||
});
|
||||
}
|
||||
if (r!.environment) {
|
||||
columns.push({
|
||||
title: "战场环境概览",
|
||||
icon: Mountain,
|
||||
rows: [
|
||||
{ label: "信息环境", value: `${r!.environment.info.length} 维度` },
|
||||
{ label: "人文环境", value: `${r!.environment.human.length} 维度` },
|
||||
],
|
||||
focusTitle: "环境维度",
|
||||
focus: [...r!.environment.info, ...r!.environment.human].map((s) => s.title).slice(0, 4),
|
||||
});
|
||||
}
|
||||
|
||||
const judgePoints = (r!.judgement?.risks ?? []).slice(0, 3);
|
||||
const decisionPoints = (r!.decision?.objectives ?? []).slice(0, 3);
|
||||
const disposalPoints = (r!.disposal?.items ?? []).map((i) => i.content).filter(Boolean).slice(0, 3);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 text-slate-700 dark:text-[var(--dash-text)]">
|
||||
{/* 1. 顶部指标 */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
|
||||
{metrics.map((m) => (
|
||||
<Card key={m.label} className="p-4">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-md bg-red-50 text-red-600 dark:bg-red-500/15">
|
||||
<m.icon className="h-4 w-4" />
|
||||
</span>
|
||||
<span className="text-xs font-medium text-slate-500 dark:text-[var(--dash-text-sec)]">{m.label}</span>
|
||||
</div>
|
||||
<div className="font-mono text-3xl font-black leading-none text-slate-900 dark:text-[var(--dash-text-strong)]">{m.value}</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 2. 关键战情摘要 */}
|
||||
{summary.length > 0 && (
|
||||
<Card>
|
||||
<CardTitle icon={Megaphone} title="关键战情摘要" />
|
||||
<ul className="space-y-3 px-4 py-3.5">
|
||||
{summary.map((s) => (
|
||||
<li key={s.label} className="flex gap-2.5 text-[13px] leading-relaxed">
|
||||
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-red-600" />
|
||||
<p className="text-slate-600 dark:text-[var(--dash-text)]">
|
||||
<span className="font-bold text-red-600">{s.label}:</span>
|
||||
{s.text}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 3. 三栏概览 */}
|
||||
{columns.length > 0 && (
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-3">
|
||||
{columns.map((col) => (
|
||||
<Card key={col.title} className="flex flex-col">
|
||||
<CardTitle icon={col.icon} title={col.title} />
|
||||
<div className="grid flex-1 grid-cols-1 gap-x-4 gap-y-3 px-4 py-3.5 sm:grid-cols-2">
|
||||
<dl className="space-y-2">
|
||||
{col.rows.map((row) => (
|
||||
<div key={row.label} className="flex items-baseline justify-between gap-2 text-xs">
|
||||
<dt className="shrink-0 text-slate-400 dark:text-[var(--dash-text-dim)]">{row.label}</dt>
|
||||
<dd className="text-right font-medium text-slate-800 dark:text-[var(--dash-text-strong)]">{row.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
{col.focus.length > 0 && (
|
||||
<div className="border-t border-dashed border-slate-100 pt-3 dark:border-[var(--dash-border)] sm:border-l sm:border-t-0 sm:pl-4 sm:pt-0">
|
||||
<div className="mb-2 text-[11px] font-semibold text-red-600">{col.focusTitle}</div>
|
||||
<ul className="space-y-1.5">
|
||||
{col.focus.map((f, i) => (
|
||||
<li key={i} className="flex gap-1.5 text-xs leading-snug text-slate-600 dark:text-[var(--dash-text)]">
|
||||
<span className="mt-1.5 h-1 w-1 shrink-0 rounded-full bg-red-500" />
|
||||
{f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 4. 情况判断 / 决策建议 / 处置建议 */}
|
||||
{(judgePoints.length > 0 || decisionPoints.length > 0 || disposalPoints.length > 0) && (
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-3">
|
||||
{judgePoints.length > 0 && (
|
||||
<SummaryCard icon={Lightbulb} title="情况判断" points={judgePoints} footerLabel="风险研判" footer={r!.judgement?.responses[0]} />
|
||||
)}
|
||||
{decisionPoints.length > 0 && (
|
||||
<SummaryCard icon={Flag} title="决策建议" points={decisionPoints} footerLabel="决心重点" footer={r!.decision?.taskTitle} />
|
||||
)}
|
||||
{disposalPoints.length > 0 && (
|
||||
<SummaryCard icon={FileText} title="处置建议" points={disposalPoints} footerLabel="处置目标" footer={r!.disposal?.goal} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 底部要点卡(编号要点 + 可选脚注)。 */
|
||||
function SummaryCard({
|
||||
icon,
|
||||
title,
|
||||
points,
|
||||
footerLabel,
|
||||
footer,
|
||||
}: {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
points: string[];
|
||||
footerLabel: string;
|
||||
footer?: string;
|
||||
}) {
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<CardTitle icon={icon} title={title} />
|
||||
<ol className="flex-1 space-y-2.5 px-4 py-3.5">
|
||||
{points.map((p, i) => (
|
||||
<li key={i} className="flex gap-2 text-xs leading-relaxed">
|
||||
<span className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-red-600 text-[10px] font-bold text-white">{i + 1}</span>
|
||||
<span className="text-slate-600 dark:text-[var(--dash-text)]">{p}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
{footer && <FooterNote label={footerLabel}>{footer}</FooterNote>}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* 「大屏绘制智能体」战情报告(selectSqReport)→ 各页面展示模型 的适配器。
|
||||
*
|
||||
* 接口经 `fetchDashboardInputJson` → `extractSqReport` 后得到:
|
||||
* `{ taskId, source, categories: { [类目]: 反序列化内容 } }`
|
||||
* 类目键(categoryType 去掉「 JSON」后缀):敌情目标 / 敌情关键事件 / 敌情对手叙事 /
|
||||
* 我情口径 / 我情叙事 / 我情上级要求 / 信息环境 / 人文环境 / 优劣势分析 /
|
||||
* 条件与限制 / 风险及应对 / 决心建议 / 处置建议 / 行动方案。
|
||||
*
|
||||
* 本模块把这些**异构、自由文本**的内容防御式地适配成每个页面要用的结构化模型;
|
||||
* 任一类目缺失/为空 → 对应页面 data 为 null,页面回退到静态占位(先做对接,编辑后续再说)。
|
||||
*/
|
||||
|
||||
// ── 防御式取值 ────────────────────────────────────────────────────────────────
|
||||
function asArray(v: unknown): unknown[] {
|
||||
return Array.isArray(v) ? v : [];
|
||||
}
|
||||
function asObj(v: unknown): Record<string, unknown> {
|
||||
return v && typeof v === "object" && !Array.isArray(v) ? (v as Record<string, unknown>) : {};
|
||||
}
|
||||
function asStr(v: unknown): string {
|
||||
if (typeof v === "string") return v;
|
||||
if (v == null) return "";
|
||||
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
||||
return "";
|
||||
}
|
||||
function strArr(v: unknown): string[] {
|
||||
return asArray(v).map(asStr).filter((s) => s.trim().length > 0);
|
||||
}
|
||||
|
||||
/** 取 categories 容器(输入即 fetchDashboardInputJson 的返回)。 */
|
||||
export function getSqCategories(input: unknown): Record<string, unknown> | null {
|
||||
const o = asObj(input);
|
||||
const cats = asObj(o.categories);
|
||||
return Object.keys(cats).length > 0 ? cats : null;
|
||||
}
|
||||
|
||||
// ── 各页面展示模型 ────────────────────────────────────────────────────────────
|
||||
|
||||
export interface EnemyData {
|
||||
targets: Array<{ name: string; category: string; note: string }>;
|
||||
events: Array<{ topic: string; count: number; items: Array<{ time: string; title: string; brief: string }> }>;
|
||||
narratives: Array<{ topic: string; narrator: string; intention: string; type: string; summary: string }>;
|
||||
}
|
||||
|
||||
export interface OursData {
|
||||
speeches: Array<{ title: string; summary: string; direction: string; time: string }>;
|
||||
narratives: Array<{ title: string; intent: string; tags: string[]; content: string }>;
|
||||
orders: string[];
|
||||
}
|
||||
|
||||
export interface EnvSection {
|
||||
title: string;
|
||||
paragraphs: string[];
|
||||
}
|
||||
export interface EnvData {
|
||||
info: EnvSection[];
|
||||
human: EnvSection[];
|
||||
}
|
||||
|
||||
export interface JudgeData {
|
||||
strengths: Array<{ title: string; content: string }>;
|
||||
weaknesses: Array<{ title: string; content: string }>;
|
||||
constraints: Array<{ title: string; items: string[] }>;
|
||||
risks: string[];
|
||||
responses: string[];
|
||||
}
|
||||
|
||||
export interface DecisionData {
|
||||
taskTitle: string;
|
||||
taskIntro: string;
|
||||
objectives: string[];
|
||||
targets: Array<{ name: string; desc: string; reason: string; impact: string; basis: string }>;
|
||||
keyPoints: string[];
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface DisposalData {
|
||||
goal: string;
|
||||
summary: string;
|
||||
items: Array<{ content: string; method: string; tone: string; effect: string; subject: string; basis: string }>;
|
||||
}
|
||||
|
||||
export interface SqReport {
|
||||
enemy: EnemyData | null;
|
||||
ours: OursData | null;
|
||||
environment: EnvData | null;
|
||||
judgement: JudgeData | null;
|
||||
decision: DecisionData | null;
|
||||
disposal: DisposalData | null;
|
||||
}
|
||||
|
||||
// ── 适配函数(每个返回 null 表示该类目无可用数据) ─────────────────────────────
|
||||
|
||||
function adaptEnemy(cats: Record<string, unknown>): EnemyData | null {
|
||||
// 敌方目标:组织 + 人物实体
|
||||
const tgt = asObj(cats["敌情目标"]);
|
||||
const targets = [...asArray(tgt.organization), ...asArray(tgt.human)]
|
||||
.map((e) => {
|
||||
const o = asObj(e);
|
||||
return { name: asStr(o.name), category: asStr(o.text) || asStr(o.etype), note: asStr(o.reason) };
|
||||
})
|
||||
.filter((t) => t.name);
|
||||
|
||||
// 关键事件:按 topic 分组
|
||||
const events = asArray(cats["敌情关键事件"])
|
||||
.map((g) => {
|
||||
const o = asObj(g);
|
||||
const items = asArray(o.events)
|
||||
.map((ev) => {
|
||||
const e = asObj(ev);
|
||||
return { time: asStr(e.date) || asStr(e.publish_time), title: asStr(e.title), brief: asStr(e.content) };
|
||||
})
|
||||
.filter((i) => i.title);
|
||||
return { topic: asStr(o.topic), count: typeof o.count === "number" ? o.count : items.length, items };
|
||||
})
|
||||
.filter((g) => g.topic || g.items.length > 0);
|
||||
|
||||
// 对手叙事
|
||||
const narratives = asArray(cats["敌情对手叙事"])
|
||||
.map((n) => {
|
||||
const o = asObj(n);
|
||||
return {
|
||||
topic: asStr(o.topic) || asStr(o.event_name),
|
||||
narrator: asStr(o.narrator) || asStr(o.entity),
|
||||
intention: asStr(o.intention) || asStr(o.intent_of_the_opponent),
|
||||
type: asStr(o.topicType) || asStr(o.narrative_topic_types),
|
||||
summary: asStr(o.summary) || asStr(o.event_summary),
|
||||
};
|
||||
})
|
||||
.filter((n) => n.topic || n.summary);
|
||||
|
||||
if (targets.length === 0 && events.length === 0 && narratives.length === 0) return null;
|
||||
return { targets, events, narratives };
|
||||
}
|
||||
|
||||
function adaptOurs(cats: Record<string, unknown>): OursData | null {
|
||||
const speech = asObj(cats["我情口径"]);
|
||||
const speeches = [
|
||||
...asArray(speech.selfSpeech).map((s) => {
|
||||
const o = asObj(s);
|
||||
return {
|
||||
title: asStr(o.trendSpeechTitle) || asStr(o.trendSpeechTitleMini),
|
||||
summary: asStr(o.trendSpeechLike) || asStr(o.trendSpeechContent),
|
||||
direction: asStr(o.narrativeDirection),
|
||||
time: asStr(o.trendSpeechTime),
|
||||
};
|
||||
}),
|
||||
...asArray(speech.selfConsistents).map((s) => {
|
||||
const o = asObj(s);
|
||||
return {
|
||||
title: asStr(o.trendConsistentTitleMini) || asStr(o.trendConsistentTitle),
|
||||
summary: asStr(o.trendConsistentLike) || asStr(o.trendConsistentContent),
|
||||
direction: asStr(o.narrativeDirection),
|
||||
time: asStr(o.trendConsistentTime),
|
||||
};
|
||||
}),
|
||||
].filter((s) => s.title || s.summary);
|
||||
|
||||
const narratives = asArray(cats["我情叙事"])
|
||||
.map((n) => {
|
||||
const o = asObj(n);
|
||||
return {
|
||||
title: asStr(o["标题"]),
|
||||
intent: asStr(o["意图"]),
|
||||
tags: strArr(o["标签"]),
|
||||
content: asStr(o["内容"]),
|
||||
};
|
||||
})
|
||||
.filter((n) => n.title || n.content);
|
||||
|
||||
// 上级要求:字符串(含编号),按行拆分、去掉行首编号
|
||||
const orderRaw = cats["我情上级要求"];
|
||||
const orderText = typeof orderRaw === "string" ? orderRaw : asStr(orderRaw);
|
||||
const orders = orderText
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.trim().replace(/^[0-9]+[.、)]\s*/, ""))
|
||||
.filter((l) => l.length > 0);
|
||||
|
||||
if (speeches.length === 0 && narratives.length === 0 && orders.length === 0) return null;
|
||||
return { speeches, narratives, orders };
|
||||
}
|
||||
|
||||
function adaptEnv(cats: Record<string, unknown>): EnvData | null {
|
||||
const toSections = (raw: unknown): EnvSection[] =>
|
||||
Object.entries(asObj(raw))
|
||||
.filter(([k]) => k !== "type")
|
||||
.map(([title, v]) => ({ title, paragraphs: strArr(v) }))
|
||||
.filter((s) => s.paragraphs.length > 0);
|
||||
const info = toSections(cats["信息环境"]);
|
||||
const human = toSections(cats["人文环境"]);
|
||||
if (info.length === 0 && human.length === 0) return null;
|
||||
return { info, human };
|
||||
}
|
||||
|
||||
function adaptJudge(cats: Record<string, unknown>): JudgeData | null {
|
||||
const sw = asObj(cats["优劣势分析"]);
|
||||
const mapTC = (v: unknown) =>
|
||||
asArray(v)
|
||||
.map((x) => {
|
||||
const o = asObj(x);
|
||||
return { title: asStr(o["title"]), content: asStr(o["content"]) };
|
||||
})
|
||||
.filter((x) => x.title || x.content);
|
||||
const strengths = mapTC(sw["优势"]);
|
||||
const weaknesses = mapTC(sw["劣势"]);
|
||||
|
||||
const con = asObj(cats["条件与限制"]);
|
||||
const constraints = Object.entries(con)
|
||||
.map(([title, v]) => ({ title: title.replace(/分析与?/, ""), items: strArr(v) }))
|
||||
.filter((c) => c.items.length > 0);
|
||||
|
||||
const risk = asObj(cats["风险及应对"]);
|
||||
const risks = strArr(risk["风险分析"]);
|
||||
const responses = strArr(risk["应对方案"]);
|
||||
|
||||
if (strengths.length === 0 && weaknesses.length === 0 && constraints.length === 0 && risks.length === 0 && responses.length === 0)
|
||||
return null;
|
||||
return { strengths, weaknesses, constraints, risks, responses };
|
||||
}
|
||||
|
||||
function adaptDecision(cats: Record<string, unknown>): DecisionData | null {
|
||||
const root = asObj(cats["决心建议"]);
|
||||
const core = asObj(root["决心建议"]);
|
||||
const targets = asArray(core["打击目标"])
|
||||
.map((t) => {
|
||||
const o = asObj(t);
|
||||
return {
|
||||
name: asStr(o["目标名称"]),
|
||||
desc: asStr(o["目标描述"]),
|
||||
reason: asStr(o["选择理由"]),
|
||||
impact: asStr(o["潜在影响"]),
|
||||
basis: asStr(o["推荐依据"]),
|
||||
};
|
||||
})
|
||||
.filter((t) => t.name || t.desc);
|
||||
const data: DecisionData = {
|
||||
taskTitle: asStr(core["任务标题"]),
|
||||
taskIntro: asStr(core["任务简介"]),
|
||||
objectives: strArr(core["作战目的"]),
|
||||
targets,
|
||||
keyPoints: strArr(root["实施要点"]),
|
||||
summary: asStr(root["决心建议总结"]),
|
||||
};
|
||||
if (!data.taskTitle && !data.taskIntro && data.objectives.length === 0 && targets.length === 0 && data.keyPoints.length === 0 && !data.summary)
|
||||
return null;
|
||||
return data;
|
||||
}
|
||||
|
||||
function adaptDisposal(cats: Record<string, unknown>): DisposalData | null {
|
||||
const root = asObj(cats["处置建议"]);
|
||||
const items = asArray(root["处置建议"])
|
||||
.map((it) => {
|
||||
const o = asObj(it);
|
||||
const subj = asObj(o["处置主体"]);
|
||||
return {
|
||||
content: asStr(o["建议内容"]),
|
||||
method: asStr(o["处置方式与步骤"]),
|
||||
tone: asStr(o["口径基调"]),
|
||||
effect: asStr(o["预期效果"]),
|
||||
subject: asStr(subj["主责单位"]),
|
||||
basis: asStr(o["立场与原则"]),
|
||||
};
|
||||
})
|
||||
.filter((i) => i.content || i.method);
|
||||
const data: DisposalData = {
|
||||
goal: asStr(root["目标"]),
|
||||
summary: asStr(root["处置建议总结"]),
|
||||
items,
|
||||
};
|
||||
if (!data.goal && !data.summary && items.length === 0) return null;
|
||||
return data;
|
||||
}
|
||||
|
||||
/** 把 fetchDashboardInputJson 的返回解析成各页面展示模型(无 categories → 全 null)。 */
|
||||
export function parseSqReport(input: unknown): SqReport {
|
||||
const cats = getSqCategories(input);
|
||||
if (!cats) {
|
||||
return { enemy: null, ours: null, environment: null, judgement: null, decision: null, disposal: null };
|
||||
}
|
||||
return {
|
||||
enemy: adaptEnemy(cats),
|
||||
ours: adaptOurs(cats),
|
||||
environment: adaptEnv(cats),
|
||||
judgement: adaptJudge(cats),
|
||||
decision: adaptDecision(cats),
|
||||
disposal: adaptDisposal(cats),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* 「大屏绘制智能体」战情页公共 UI 片段(卡片 / 标题 / 更多按钮 / 色块徽标 / 表格)。
|
||||
*
|
||||
* **dashboard-agent 独占**:仅供战情系列静态页(SituationOverview / EnemyDetail / …)
|
||||
* 复用,不影响会商共享的 DashboardApp / dashboard 组件群。配色对齐设计图(白底 + 红色强调)。
|
||||
*/
|
||||
import { ChevronRight, Inbox, type LucideIcon } from "lucide-react";
|
||||
|
||||
/** 空数据占位(接口返回为空 / 该类目缺失时显示)。 */
|
||||
export function NoData({ label = "暂无数据" }: { label?: string }) {
|
||||
return (
|
||||
<div className="flex min-h-[320px] flex-1 flex-col items-center justify-center gap-2 py-20 text-slate-400 dark:text-[var(--dash-text-dim)]">
|
||||
<Inbox className="h-10 w-10 opacity-40" />
|
||||
<span className="text-sm">{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 卡片外框(白底 + slate 细边 + 轻阴影)。 */
|
||||
export function Card({ className = "", children }: { className?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
className={`rounded-md border border-slate-200 bg-white shadow-sm dark:border-[var(--dash-border)] dark:bg-[var(--dash-card)] ${className}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 卡片标题(红色图标 + 标题 + 可选徽标 + 可选右侧操作 + 底部分隔线)。 */
|
||||
export function CardTitle({
|
||||
icon: Icon,
|
||||
title,
|
||||
badges,
|
||||
action,
|
||||
}: {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
badges?: React.ReactNode;
|
||||
action?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2 border-b border-slate-100 px-4 py-2.5 dark:border-[var(--dash-border)]">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded bg-red-50 text-red-600 dark:bg-red-500/15">
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
<h3 className="shrink-0 text-sm font-bold tracking-tight text-slate-900 dark:text-[var(--dash-text-strong)]">
|
||||
{title}
|
||||
</h3>
|
||||
{badges && <div className="flex min-w-0 flex-wrap items-center gap-1.5">{badges}</div>}
|
||||
</div>
|
||||
{action}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 标题徽标药丸(如「组织目标 12 个」,数字红色高亮)。 */
|
||||
export function HeadBadge({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 rounded bg-slate-50 px-1.5 py-0.5 text-[11px] text-slate-500 dark:bg-white/5 dark:text-[var(--dash-text-sec)]">
|
||||
{label}
|
||||
<b className="font-bold text-red-600">{value}</b>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** 「更多 >」操作按钮(标题右侧,可自定义文案)。 */
|
||||
export function MoreLink({ label = "更多", onClick }: { label?: string; onClick?: () => void }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="inline-flex shrink-0 items-center gap-0.5 text-xs font-medium text-red-600 hover:underline"
|
||||
>
|
||||
{label}
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/** 卡片底部「查看完整… >」链接条(顶部分隔线 + 红色链接)。 */
|
||||
export function CardFooterLink({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) {
|
||||
return (
|
||||
<div className="mt-auto border-t border-slate-100 px-4 py-2.5 dark:border-[var(--dash-border)]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="inline-flex items-center gap-0.5 text-xs font-medium text-red-600 hover:underline"
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type Tone = "red" | "amber" | "green" | "blue" | "slate";
|
||||
|
||||
const TONE_CLASS: Record<Tone, string> = {
|
||||
red: "bg-red-50 text-red-600 dark:bg-red-500/15",
|
||||
amber: "bg-amber-50 text-amber-600 dark:bg-amber-500/15",
|
||||
green: "bg-emerald-50 text-emerald-600 dark:bg-emerald-500/15",
|
||||
blue: "bg-sky-50 text-sky-600 dark:bg-sky-500/15",
|
||||
slate: "bg-slate-100 text-slate-500 dark:bg-white/10 dark:text-[var(--dash-text-sec)]",
|
||||
};
|
||||
|
||||
/** 语义色块徽标(高/中/低、上升/持平/下降、偏高 等)。 */
|
||||
export function Tag({ tone, children }: { tone: Tone; children: React.ReactNode }) {
|
||||
return (
|
||||
<span className={`inline-flex items-center rounded px-1.5 py-0.5 text-[11px] font-semibold ${TONE_CLASS[tone]}`}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const VAL_TEXT: Record<Tone, string> = {
|
||||
red: "text-red-600",
|
||||
amber: "text-amber-600",
|
||||
green: "text-emerald-600",
|
||||
blue: "text-sky-600",
|
||||
slate: "text-slate-400 dark:text-[var(--dash-text-dim)]",
|
||||
};
|
||||
|
||||
/** 语义色取值文字(键值行右侧的 高/中/充足/紧张 等,以颜色表强弱)。 */
|
||||
export function Val({ tone, children }: { tone: Tone; children: React.ReactNode }) {
|
||||
return <span className={`font-semibold ${VAL_TEXT[tone]}`}>{children}</span>;
|
||||
}
|
||||
|
||||
/** 紧凑表格(表头灰字 + 行细分隔线,对齐设计图密集表格)。 */
|
||||
export function Table({ head, children }: { head: string[]; children: React.ReactNode }) {
|
||||
return (
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr className="text-left text-[11px] text-slate-400 dark:text-[var(--dash-text-dim)]">
|
||||
{head.map((h) => (
|
||||
<th
|
||||
key={h}
|
||||
className="border-b border-slate-100 px-1.5 pb-2 font-semibold first:pl-0 last:pr-0 dark:border-[var(--dash-border)]"
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{children}</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
/** 表格数据单元(统一内边距 + 细分隔线 + 顶对齐)。 */
|
||||
export function Td({ className = "", children }: { className?: string; children?: React.ReactNode }) {
|
||||
return (
|
||||
<td
|
||||
className={`border-b border-slate-50 px-1.5 py-2 align-top leading-relaxed first:pl-0 last:pr-0 dark:border-[var(--dash-border)] ${className}`}
|
||||
>
|
||||
{children}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
/** 卡片底部「说明 / 动态」提示条(左侧色条 + 浅底)。 */
|
||||
export function FooterNote({
|
||||
label,
|
||||
tone = "red",
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
tone?: Tone;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const border = tone === "green" ? "border-emerald-500" : tone === "blue" ? "border-sky-500" : "border-red-500";
|
||||
const labelColor =
|
||||
tone === "green"
|
||||
? "text-emerald-700 dark:text-emerald-400"
|
||||
: tone === "blue"
|
||||
? "text-sky-700 dark:text-sky-400"
|
||||
: "text-red-600";
|
||||
const bg =
|
||||
tone === "green" ? "bg-emerald-50 dark:bg-emerald-500/10" : tone === "blue" ? "bg-sky-50 dark:bg-sky-500/10" : "bg-red-50 dark:bg-red-500/10";
|
||||
return (
|
||||
<div className={`mx-4 mb-3.5 rounded border-l-2 ${border} ${bg} px-3 py-2 text-[11px] leading-relaxed`}>
|
||||
<span className={`font-semibold ${labelColor}`}>{label}:</span>
|
||||
<span className="text-slate-600 dark:text-[var(--dash-text)]">{children}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
* - 不传 data 时回落参考 demo(DEMO_DATA)。
|
||||
*/
|
||||
import './dashboard.css';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, type ReactNode } from 'react';
|
||||
import { TabType } from './types';
|
||||
import {
|
||||
DashboardDataProvider,
|
||||
@@ -31,23 +31,48 @@ import {
|
||||
|
||||
const TAB_ICONS = [LayoutGrid, Users, Megaphone, ShieldAlert, FileText, Activity];
|
||||
|
||||
export function DashboardApp({ data = DEMO_DATA }: { data?: DashboardDataValue }) {
|
||||
const [activeTab, setActiveTab] = useState<TabType>('overview');
|
||||
/** 自定义页(dashboard-agent 战情多页用):一个 Tab = id + 标签 + 图标 + 正文。 */
|
||||
export interface DashboardPage {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: ReactNode;
|
||||
content: ReactNode;
|
||||
}
|
||||
|
||||
// 数据切换(切历史记录/重绘 → data 引用变化)时复位到总览:避免停留在上一份数据
|
||||
// 钻入的某个席位详情页,而新数据里无同名席位导致「加载席位数据失败」。
|
||||
export function DashboardApp({
|
||||
data = DEMO_DATA,
|
||||
pages,
|
||||
}: {
|
||||
data?: DashboardDataValue;
|
||||
/**
|
||||
* 可选自定义页集:传入时整套 Tab(标签/图标)与正文都由它驱动,替换默认的
|
||||
* 「总览 + 各席位」(dashboard-agent 用它注入战情多页)。会商 Step3 不传 → 行为零变化。
|
||||
*/
|
||||
pages?: DashboardPage[];
|
||||
}) {
|
||||
const usingPages = !!pages && pages.length > 0;
|
||||
const firstTab: TabType = usingPages ? pages![0].id : 'overview';
|
||||
const [activeTab, setActiveTab] = useState<TabType>(firstTab);
|
||||
|
||||
// 数据/页集切换(切历史记录/重绘 → data 引用变化)时复位到首个 Tab:避免停留在上一份
|
||||
// 数据钻入的某个席位详情页,而新数据里无同名席位导致「加载席位数据失败」。
|
||||
useEffect(() => {
|
||||
setActiveTab('overview');
|
||||
}, [data]);
|
||||
setActiveTab(firstTab);
|
||||
}, [data, firstTab]);
|
||||
|
||||
// 导航 Tab:总览 + 各席位(由数据动态生成,原参考为写死的固定 6 项)。
|
||||
const tabs: TabType[] = ['overview', ...data.SEAT_CARDS.map((s) => s.id)];
|
||||
// 导航 Tab:自定义页集 → 各页 id;否则总览 + 各席位(由数据动态生成)。
|
||||
const tabs: TabType[] = usingPages
|
||||
? pages!.map((p) => p.id)
|
||||
: ['overview', ...data.SEAT_CARDS.map((s) => s.id)];
|
||||
|
||||
const getTabLabel = (tab: TabType) =>
|
||||
tab === 'overview' ? '总览' : data.SEAT_CARDS.find((c) => c.id === tab)?.title ?? tab;
|
||||
const getTabLabel = (tab: TabType) => {
|
||||
if (usingPages) return pages!.find((p) => p.id === tab)?.label ?? tab;
|
||||
return tab === 'overview' ? '总览' : data.SEAT_CARDS.find((c) => c.id === tab)?.title ?? tab;
|
||||
};
|
||||
|
||||
const getTabIcon = (tab: TabType) => {
|
||||
const cls = 'w-4 h-4';
|
||||
if (usingPages) return pages!.find((p) => p.id === tab)?.icon ?? <Compass className={cls} />;
|
||||
if (tab === 'overview') return <Compass className={cls} />;
|
||||
const idx = data.SEAT_CARDS.findIndex((c) => c.id === tab);
|
||||
const Ico = TAB_ICONS[(idx >= 0 ? idx : 0) % TAB_ICONS.length];
|
||||
@@ -109,7 +134,18 @@ export function DashboardApp({ data = DEMO_DATA }: { data?: DashboardDataValue }
|
||||
|
||||
{/* Body */}
|
||||
<AnimatePresence mode="wait">
|
||||
{activeTab === 'overview' ? (
|
||||
{usingPages ? (
|
||||
<motion.div
|
||||
key={activeTab}
|
||||
initial={{ opacity: 0, y: 5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -5 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="space-y-6"
|
||||
>
|
||||
{pages!.find((p) => p.id === activeTab)?.content}
|
||||
</motion.div>
|
||||
) : activeTab === 'overview' ? (
|
||||
<motion.div
|
||||
key="overview-dashboard"
|
||||
initial={{ opacity: 0, y: 5 }}
|
||||
|
||||
@@ -32,7 +32,17 @@ import { readCopTaskDetail } from "@/core/auth/task-deeplink";
|
||||
import { desensitizeDeep } from "@/lib/desensitize";
|
||||
import { useModels } from "@/core/models/hooks";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { DashboardApp } from "@/roundtable-planning/dashboard/DashboardApp";
|
||||
import { Compass, Crosshair, Shield, Mountain, Lightbulb, Flag, FileText } from "lucide-react";
|
||||
|
||||
import { DashboardApp, type DashboardPage } from "@/roundtable-planning/dashboard/DashboardApp";
|
||||
import { SituationOverview } from "@/roundtable-planning/dashboard-agent/SituationOverview";
|
||||
import { EnemyDetail } from "@/roundtable-planning/dashboard-agent/EnemyDetail";
|
||||
import { OurSituation } from "@/roundtable-planning/dashboard-agent/OurSituation";
|
||||
import { EnvironmentDetail } from "@/roundtable-planning/dashboard-agent/EnvironmentDetail";
|
||||
import { Judgement } from "@/roundtable-planning/dashboard-agent/Judgement";
|
||||
import { Decision } from "@/roundtable-planning/dashboard-agent/Decision";
|
||||
import { Disposal } from "@/roundtable-planning/dashboard-agent/Disposal";
|
||||
import { parseSqReport, type SqReport } from "@/roundtable-planning/dashboard-agent/sq-report";
|
||||
import { adaptReportData } from "@/roundtable-planning/dashboard/data-context";
|
||||
import { DashboardChatPanel } from "@/roundtable-planning/components/DashboardChatPanel";
|
||||
import { Step2SandboxLayout } from "@/roundtable-planning/components/Step2SandboxLayout";
|
||||
@@ -59,6 +69,14 @@ function formatNow(): string {
|
||||
type DashView = "chat" | "page";
|
||||
type RenderMode = "app" | "html";
|
||||
|
||||
/**
|
||||
* 静态页面阶段开关(先按设计图搭总览版式,动态数据后续再接):
|
||||
* - 默认落在「大屏页面」视图,直接展示 SituationOverview 静态版式;
|
||||
* - 不自动调用智能体抽取(避免静态阶段空跑)。
|
||||
* 接入动态数据后置为 false 即恢复原有「请求数据→智能体抽取→查看大屏」流程。
|
||||
*/
|
||||
const STATIC_PREVIEW = true;
|
||||
|
||||
/** 从 URL / 路由 state / 登录暂存任务详情解析 taskId。 */
|
||||
function useTaskId(): string {
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -83,6 +101,8 @@ export default function DashboardAgentPage() {
|
||||
|
||||
// 输入 JSON(登录拉取/占位)——auto-start 时由 buildInitialPrompt 闭包读取。
|
||||
const inputRef = useRef<unknown>(null);
|
||||
// 战情报告解析结果(selectSqReport → 各页面展示模型);解析失败/无数据 → null(页面回退占位)。
|
||||
const [sqReport, setSqReport] = useState<SqReport | null>(null);
|
||||
|
||||
const dashboard = useStep3Dashboard({
|
||||
intentReady: null,
|
||||
@@ -113,7 +133,8 @@ export default function DashboardAgentPage() {
|
||||
} = dashboard;
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [view, setView] = useState<DashView>("chat");
|
||||
// 静态阶段默认进「大屏页面」,直接展示静态版式(见 STATIC_PREVIEW)。
|
||||
const [view, setView] = useState<DashView>(STATIC_PREVIEW ? "page" : "chat");
|
||||
const [renderMode, setRenderMode] = useState<RenderMode>("app");
|
||||
// 真实接口请求失败/无数据时的提示(真实模式不拿假数据兜底,改为提示+重试)。
|
||||
const [inputError, setInputError] = useState<string | null>(null);
|
||||
@@ -152,6 +173,12 @@ export default function DashboardAgentPage() {
|
||||
fetchDashboardInputJson(taskId).catch(() => null),
|
||||
]);
|
||||
inputRef.current = input;
|
||||
// 解析战情报告 → 各页面展示模型(容错:异常/无数据均不抛错,页面回退占位)。
|
||||
try {
|
||||
setSqReport(parseSqReport(input));
|
||||
} catch {
|
||||
setSqReport(null);
|
||||
}
|
||||
const savedDialogues = (session?.transcript as Step2Dialogue[] | undefined) ?? [];
|
||||
const validReport = session?.reportJson && parseReportJson(session.reportJson);
|
||||
if (validReport) {
|
||||
@@ -161,14 +188,20 @@ export default function DashboardAgentPage() {
|
||||
} else if (savedDialogues.length > 0) {
|
||||
// 有历史聊天但无有效结果 → 进生成过程展示历史,不自动重跑(点「重新绘制」可重生成)。
|
||||
hydrateTranscriptRef.current(savedDialogues, session?.reportJson ?? null);
|
||||
setView("chat");
|
||||
// 静态阶段仍停在「大屏页面」看版式(历史对话可点「编辑」查看)。
|
||||
setView(STATIC_PREVIEW ? "page" : "chat");
|
||||
} else {
|
||||
// 无任何记录 → 接口返回了真实数据才启动模型抽取;否则提示(真实模式不拿假数据兜底)。
|
||||
setView("chat");
|
||||
if (input) {
|
||||
startRef.current();
|
||||
// 静态阶段(STATIC_PREVIEW):不自动抽取,停在「大屏页面」展示静态版式。
|
||||
if (STATIC_PREVIEW) {
|
||||
setView("page");
|
||||
} else {
|
||||
setInputError("未获取到任务数据:接口请求失败或未返回内容,请稍后重试。");
|
||||
setView("chat");
|
||||
if (input) {
|
||||
startRef.current();
|
||||
} else {
|
||||
setInputError("未获取到任务数据:接口请求失败或未返回内容,请稍后重试。");
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -218,6 +251,30 @@ export default function DashboardAgentPage() {
|
||||
);
|
||||
const hasResult = !!dashboardData || !!dashboardHtml;
|
||||
|
||||
// 展示层脱敏:所有页面文本经 desensitizeDeep 深度替换后再渲染(与会商大屏一致)。
|
||||
const safeReport = useMemo<SqReport | null>(() => {
|
||||
if (!sqReport) return null;
|
||||
try {
|
||||
return desensitizeDeep(sqReport) as SqReport;
|
||||
} catch {
|
||||
return sqReport; // 脱敏异常不影响展示
|
||||
}
|
||||
}, [sqReport]);
|
||||
|
||||
// 战情多页:总览 + 六个详情页;详情页传入对应的(脱敏后)真实数据(无则页面回退占位)。
|
||||
const dashPages: DashboardPage[] = useMemo(
|
||||
() => [
|
||||
{ id: "overview", label: "总览", icon: <Compass className="h-4 w-4" />, content: <SituationOverview data={safeReport} /> },
|
||||
{ id: "enemy", label: "敌情", icon: <Crosshair className="h-4 w-4" />, content: <EnemyDetail data={safeReport?.enemy} /> },
|
||||
{ id: "ours", label: "我情", icon: <Shield className="h-4 w-4" />, content: <OurSituation data={safeReport?.ours} /> },
|
||||
{ id: "field", label: "战场环境", icon: <Mountain className="h-4 w-4" />, content: <EnvironmentDetail data={safeReport?.environment} /> },
|
||||
{ id: "judge", label: "情况判断", icon: <Lightbulb className="h-4 w-4" />, content: <Judgement data={safeReport?.judgement} /> },
|
||||
{ id: "decision", label: "决心建议", icon: <Flag className="h-4 w-4" />, content: <Decision data={safeReport?.decision} /> },
|
||||
{ id: "disposal", label: "处置建议", icon: <FileText className="h-4 w-4" />, content: <Disposal data={safeReport?.disposal} /> },
|
||||
],
|
||||
[safeReport],
|
||||
);
|
||||
|
||||
// 网页渲染:blob URL 作 iframe src(:target 导航需独立文档 URL,见 Step3DashboardMode 注释)。
|
||||
const [htmlBlobUrl, setHtmlBlobUrl] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
@@ -392,13 +449,27 @@ export default function DashboardAgentPage() {
|
||||
|
||||
{/* 主体 */}
|
||||
{loading ? (
|
||||
// 加载框:请求战情报告期间显示,拿到数据后再渲染大屏(避免先闪占位再跳真实数据)。
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-3 text-sm text-slate-500">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
||||
<div className="text-center">
|
||||
<div className="font-semibold text-slate-700">正在请求任务数据…</div>
|
||||
<div className="mt-1 text-xs text-slate-400">拉取接口返回后将自动开始智能体抽取绘制</div>
|
||||
<div className="mt-1 text-xs text-slate-400">拉取接口返回后展示大屏</div>
|
||||
</div>
|
||||
</div>
|
||||
) : view === "page" ? (
|
||||
renderMode === "html" && htmlBlobUrl ? (
|
||||
<iframe
|
||||
title="任务研讨大屏"
|
||||
src={htmlBlobUrl}
|
||||
className="min-h-0 w-full flex-1 border-0 bg-white"
|
||||
sandbox="allow-scripts allow-popups allow-downloads allow-same-origin"
|
||||
/>
|
||||
) : (
|
||||
<div className="min-h-0 flex-1 overflow-auto">
|
||||
<DashboardApp data={dashboardData} pages={dashPages} />
|
||||
</div>
|
||||
)
|
||||
) : inputError && dialogues.length === 0 ? (
|
||||
<div className="flex flex-1 items-center justify-center p-8 text-center text-sm text-slate-500">
|
||||
<div className="max-w-sm">
|
||||
@@ -414,19 +485,6 @@ export default function DashboardAgentPage() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : view === "page" ? (
|
||||
renderMode === "html" && htmlBlobUrl ? (
|
||||
<iframe
|
||||
title="任务研讨大屏"
|
||||
src={htmlBlobUrl}
|
||||
className="min-h-0 w-full flex-1 border-0 bg-white"
|
||||
sandbox="allow-scripts allow-popups allow-downloads allow-same-origin"
|
||||
/>
|
||||
) : (
|
||||
<div className="min-h-0 flex-1 overflow-auto">
|
||||
<DashboardApp data={dashboardData} />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="min-h-0 flex-1 p-3">
|
||||
<section className="h-full min-h-0 overflow-hidden rounded-2xl border border-slate-200/60 bg-white">
|
||||
|
||||
Reference in new issue
Block a user