diff --git a/frontend-web/.env.development b/frontend-web/.env.development index b78f859a..3837c075 100644 --- a/frontend-web/.env.development +++ b/frontend-web/.env.development @@ -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 diff --git a/frontend-web/src/roundtable-planning/dashboard-agent/Decision.tsx b/frontend-web/src/roundtable-planning/dashboard-agent/Decision.tsx new file mode 100644 index 00000000..fcf8d0a0 --- /dev/null +++ b/frontend-web/src/roundtable-planning/dashboard-agent/Decision.tsx @@ -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 ? : ; +} + +function DecisionPlaceholder() { + return ( +
+ {/* 1. 任务信息 + 作战主张 */} +
+ + +
+
+ {TASK_META.map((m) => ( +
+
{m.label}
+
{m.value}
+
+ ))} +
+
+
任务简介
+

{TASK_INTRO}

+
+
+
+ + + +
+ {PROPOSALS.map((p) => ( +
+
+ + {p.no} + +

{p.title}

+
+
    + {p.bullets.map((b) => ( +
  • + + {b} +
  • + ))} +
+
+ 推荐程度 + {p.rec} +
+
+ ))} +
+
+
+ + {/* 2. 打击目标 + 实施要点 */} +
+ + +
+ + {TARGETS.map((t) => ( + + + + + + + + + + + ))} +
{t.no}{t.name}{t.type} + {t.importance} + + {t.priority} + {t.effect}{t.method}{t.timing}
+
+
+ + + +
    + {KEY_POINTS.map((p, i) => ( +
  1. + + {i + 1} + + {p} +
  2. + ))} +
+
+
+ + {/* 3. 决心建议总结 */} + + +
+

+ {SUMMARY} +

+
+ {SUMMARY_STATS.map((s) => ( +
+
+ + {s.label} +
+
{s.value}
+
+ ))} +
+
+
+
+ ); +} + +// ── 真实数据渲染 ────────────────────────────────────────────────────────────── +function DecisionReal({ data }: { data: DecisionData }) { + return ( +
+ {/* 任务信息 + 作战目的 */} +
+ + +
+ {data.taskTitle && ( +
+
任务标题
+

{data.taskTitle}

+
+ )} + {data.taskIntro && ( +
+
任务简介
+

{data.taskIntro}

+
+ )} +
+
+ + {data.objectives.length > 0 && ( + + +
    + {data.objectives.map((o, i) => ( +
  1. + {i + 1} + {o} +
  2. + ))} +
+
+ )} +
+ + {/* 打击目标 + 实施要点 */} +
+ {data.targets.length > 0 && ( + + +
+ + {data.targets.map((t, i) => ( + + + + + + + + ))} +
{t.name || "—"}{t.desc || "—"}{t.reason || "—"}{t.impact || "—"}{t.basis || "—"}
+
+
+ )} + + {data.keyPoints.length > 0 && ( + + +
    + {data.keyPoints.map((p, i) => ( +
  1. + {i + 1} + {p} +
  2. + ))} +
+
+ )} +
+ + {/* 决心建议总结 */} + {data.summary && ( + + +
+

+ {data.summary} +

+
+
+ )} +
+ ); +} diff --git a/frontend-web/src/roundtable-planning/dashboard-agent/Disposal.tsx b/frontend-web/src/roundtable-planning/dashboard-agent/Disposal.tsx new file mode 100644 index 00000000..b7440f2c --- /dev/null +++ b/frontend-web/src/roundtable-planning/dashboard-agent/Disposal.tsx @@ -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 ? : ; +} + +function DisposalPlaceholder() { + return ( +
+ {/* 1. 处置建议总结 + 目标 */} +
+ {/* 处置建议总结 */} + + +
    + {SUMMARY_ITEMS.map((s) => ( +
  • + +

    + {s.label}: + {s.text} +

    +
  • + ))} +
+
+
+ 综合处置建议 + 稳健(取义) +
+
+ 建议执行优先级 + + {Array.from({ length: 5 }).map((_, i) => ( + + ))} + +
+
+
+ + {/* 目标 */} + + +
+ {GOALS.map((g) => ( +
+ + + +

{g.title}

+

{g.text}

+
+ ))} +
+
+
+ + {/* 2. 处置建议表 + 关键事态 */} +
+ {/* 处置建议 */} + + +
+ + {DISPOSALS.map((d) => ( + + + + + + + + + + + ))} +
{d.no}{d.content}{d.timing}{d.subject}{d.caliber}{d.method}{d.effect}{d.basis}
+
+ + 上述处置应在法治框架内规范实施,统筹线上线下,确保快速反应、综合施策。 + +
+ + {/* 关键事态 */} + + +
    + {EVENTS.map((e) => ( +
  • + + {e.no} + +
    +
    {e.title}
    +
    + {e.time} + 影响 + {e.impact} +
    +
    +
  • + ))} +
+ 关键事态需密切跟踪,及时研判处置。 +
+
+
+ ); +} + +// ── 真实数据渲染 ────────────────────────────────────────────────────────────── +function DisposalReal({ data }: { data: DisposalData }) { + return ( +
+ {/* 目标 + 处置总结 */} + {(data.goal || data.summary) && ( +
+ {data.goal && ( + + +
+

{data.goal}

+
+
+ )} + {data.summary && ( + + +
+

+ {data.summary} +

+
+
+ )} +
+ )} + + {/* 处置建议表 */} + {data.items.length > 0 && ( + + +
+ + {data.items.map((d, i) => ( + + + + + + + + + + ))} +
{i + 1}{d.content || "—"}{d.method || "—"}{d.tone || "—"}{d.effect || "—"}{d.subject || "—"}{d.basis || "—"}
+
+
+ )} +
+ ); +} diff --git a/frontend-web/src/roundtable-planning/dashboard-agent/EnemyDetail.tsx b/frontend-web/src/roundtable-planning/dashboard-agent/EnemyDetail.tsx new file mode 100644 index 00000000..89dfff1d --- /dev/null +++ b/frontend-web/src/roundtable-planning/dashboard-agent/EnemyDetail.tsx @@ -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 ? : ; +} + +function EnemyPlaceholder() { + return ( +
+ {/* 1. 上排:敌方目标 + 关键事件 */} +
+ {/* 敌方目标 */} + + + + + + } + /> +
+ + {TARGETS.map((t) => ( + + + + + + + + ))} +
{t.no}{t.name}{t.count} + {t.gap} + {t.note}
+
+
+ + {/* 关键事件 */} + + +
+ + {EVENTS.map((e) => ( + + + + + + + ))} +
{e.time}{e.title}{e.brief} + {e.impact} +
+
+ + 近期事件以网络空间动作与军事活动配合为主,整体风险趋升,需持续跟踪重点节点。 + +
+
+ + {/* 2. 对手叙事(整行) */} + + +
+ + {NARRATIVES.map((n) => ( + + + + + + + + + + ))} +
{n.name}{n.view}{n.intent}{n.channel}{n.audience} + {n.heat} + + {n.trend} +
+
+ + 对手叙事以「去式化行动」「外部归因」为核心,须以精准事实与权威信息及时反制,压缩其传播空间。 + +
+ + {/* 3. 下排:重点关注对象 + 传播渠道分析 + 风险热度研判 */} +
+ {/* 重点关注对象 */} + + +
+ + {FOCUS.map((f) => ( + + + + + + + + ))} +
{f.no}{f.name}{f.category} + + {f.relevance}% + + + {f.status} +
+
+
+ + {/* 传播渠道分析 */} + + +
    + {CHANNELS.map((c) => ( +
  • +
    + {c.name} + {c.pct}% +
    +
    +
    +
    +
    {c.desc}
    +
  • + ))} +
+
+ + {/* 风险热度研判 */} + + +
+
+ 总体风险等级 + + + 上升 1 级 + +
+
主要风险点
+
    + {RISK_POINTS.map((p) => ( +
  • + + {p} +
  • + ))} +
+
+ {RISK_CONCLUSION} +
+
+
+ ); +} + +// ── 真实数据渲染 ────────────────────────────────────────────────────────────── +function EnemyReal({ data }: { data: EnemyData }) { + return ( +
+
+ {data.targets.length > 0 && ( + + } /> +
+ + {data.targets.map((t, i) => ( + + + + + + ))} +
{t.name}{t.category || "—"}{t.note || "—"}
+
+
+ )} + + {data.events.length > 0 && ( + + +
+ {data.events.map((g, i) => ( +
+
+ {g.topic || "事件"} + {g.count} +
+
    + {g.items.map((e, j) => ( +
  • + {e.time} +
    +
    {e.title}
    + {e.brief &&
    {e.brief}
    } +
    +
  • + ))} +
+
+ ))} +
+
+ )} +
+ + {data.narratives.length > 0 && ( + + +
+ + {data.narratives.map((n, i) => ( + + + + + + + + ))} +
{n.topic || "—"}{n.narrator || "—"}{n.intention || "—"}{n.type || "—"}{n.summary || "—"}
+
+
+ )} +
+ ); +} diff --git a/frontend-web/src/roundtable-planning/dashboard-agent/EnvironmentDetail.tsx b/frontend-web/src/roundtable-planning/dashboard-agent/EnvironmentDetail.tsx new file mode 100644 index 00000000..5fd94717 --- /dev/null +++ b/frontend-web/src/roundtable-planning/dashboard-agent/EnvironmentDetail.tsx @@ -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 = { up: TrendingUp, down: TrendingDown, flat: Minus }; +const TREND_COLOR: Record = { up: "text-red-600", down: "text-emerald-600", flat: "text-slate-400" }; +const VAL_COLOR: Record = { + 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 ? : ; +} + +function EnvPlaceholder() { + return ( +
+ {/* 1. 顶部 5 个环境指数卡 */} +
+ {KPIS.map((k) => { + const TrendIco = TREND_ICON[k.trend]; + return ( + +
+ + + + {k.label} +
+
+ {k.value} +
+
+ 较昨日 + + + {k.delta} + +
+
+ ); + })} +
+ + {/* 2. 信息环境(5 栏) */} + 信息环境 +
+ {INFO_ENV.map((f) => ( + + ))} +
+ + {/* 3. 人文环境(4 栏) */} + 人文环境 +
+ {HUMAN_ENV.map((f) => ( + + ))} +
+ + {/* 4. 环境综合研判 + 信息来源与参考 */} +
+ + +
    + {REVIEW.map((r, i) => ( +
  1. + + {i + 1} + + {r} +
  2. + ))} +
+
+ + + +
+ + {SOURCES.map((s) => ( + + + + + + + ))} +
{s.name}{s.type} + {s.trust} + {s.updated}
+
+ 可信度分为高 / 中 / 低,时效性以最新更新时间为准,综合研判时按权重折算。 +
+
+
+ ); +} + +/** 环境维度卡(红色标题 + 键值行 + 评估脚注)。 */ +function EnvCard({ field }: { field: EnvField }) { + return ( + + +
+
+ {field.rows.map((r) => ( +
+
{r.label}
+
{r.value}
+
+ ))} +
+
+ {field.assessment} +
+ ); +} + +/** 行级区段标题(红色竖条 + 文字)。 */ +function SectionLabel({ children }: { children: React.ReactNode }) { + return ( +
+ + {children} +
+ ); +} + +// ── 真实数据渲染 ────────────────────────────────────────────────────────────── +function EnvReal({ data }: { data: EnvData }) { + return ( +
+ {data.info.length > 0 && ( + <> + 信息环境 +
+ {data.info.map((s, i) => ( + + ))} +
+ + )} + {data.human.length > 0 && ( + <> + 人文环境 +
+ {data.human.map((s, i) => ( + + ))} +
+ + )} +
+ ); +} + +/** 环境文本维度卡(标题 + 段落要点)。 */ +function EnvTextCard({ section }: { section: EnvSection }) { + return ( + + +
    + {section.paragraphs.map((p, i) => ( +
  • + + {p} +
  • + ))} +
+
+ ); +} diff --git a/frontend-web/src/roundtable-planning/dashboard-agent/Judgement.tsx b/frontend-web/src/roundtable-planning/dashboard-agent/Judgement.tsx new file mode 100644 index 00000000..0642bc62 --- /dev/null +++ b/frontend-web/src/roundtable-planning/dashboard-agent/Judgement.tsx @@ -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 ? : ; +} + +function JudgePlaceholder() { + return ( +
+ {/* 1. 敌我优劣势分析 */} + 敌我优劣势分析 +
+ {SWOT.map((c) => ( + + +
    + {c.items.map((it) => ( +
  • + + {it} +
  • + ))} +
+
+ ))} +
+ + {/* 2. 条件与限制 */} + 条件与限制 +
+ {CONSTRAINTS.map((c) => ( + + +
+
+ {c.rows.map((r) => ( +
+
{r.label}
+
+ {r.value} +
+
+ ))} +
+
+ {c.footer} +
+ ))} +
+ + {/* 3. 风险与应对 + 判断结论 */} + 风险与应对 +
+ {/* 风险与应对表 */} + + +
+ + {RISKS.map((r) => ( + + + + + + + ))} +
{r.item} + {r.level} + {r.impact}{r.plan}
+
+
+ + {/* 判断结论(综合研判) */} + + +
+
+ {CONCLUSION_STATS.map((s) => ( +
+
{s.value}
+
{s.caption}
+
+ ))} +
+
    + {CONCLUSION_POINTS.map((p) => ( +
  • + + {p} +
  • + ))} +
+
+ {CONCLUSION} +
+
+
+ ); +} + +/** 红色编号区段标题(① 敌我优劣势分析 …)。 */ +function NumberedHeading({ n, children }: { n: number; children: React.ReactNode }) { + return ( +
+ + {n} + +

{children}

+
+ ); +} + +// ── 真实数据渲染 ────────────────────────────────────────────────────────────── +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 ( + + +
    + {items.map((it, i) => ( +
  • + + + {it.title && {it.title}:} + {it.content} + +
  • + ))} +
+
+ ); +} + +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 ( +
+ {hasSwot && ( + <> + 优劣势分析 +
+ {data.strengths.length > 0 && } + {data.weaknesses.length > 0 && } +
+ + )} + + {data.constraints.length > 0 && ( + <> + 条件与限制 +
+ {data.constraints.map((c, i) => ( + + +
    + {c.items.map((it, j) => ( +
  • + + {it} +
  • + ))} +
+
+ ))} +
+ + )} + + {hasRisk && ( + <> + 风险与应对 +
+ {data.risks.length > 0 && ( + + +
    + {data.risks.map((r, i) => ( +
  • + + {r} +
  • + ))} +
+
+ )} + {data.responses.length > 0 && ( + + +
    + {data.responses.map((r, i) => ( +
  1. + {i + 1} + {r} +
  2. + ))} +
+
+ )} +
+ + )} +
+ ); +} diff --git a/frontend-web/src/roundtable-planning/dashboard-agent/OurSituation.tsx b/frontend-web/src/roundtable-planning/dashboard-agent/OurSituation.tsx new file mode 100644 index 00000000..c5b3ff34 --- /dev/null +++ b/frontend-web/src/roundtable-planning/dashboard-agent/OurSituation.tsx @@ -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 ? : ; +} + +function OursPlaceholder() { + return ( +
+ {/* 1. 上排:口径 + 叙事 + 上级要求 */} +
+ {/* 口径 */} + + +
+
+ {TONE_META.map((m) => ( +
+
{m.label}
+
{m.value}
+
+ ))} +
+
    + {TONE_POINTS.map((p) => ( +
  • + + {p} +
  • + ))} +
+
+ 查看完整口径 +
+ + {/* 叙事 */} + + +
+ + {NARRATIVES.map((n) => ( + + + + + + + ))} +
{n.no}{n.title}{n.point}{n.time}
+
+ 查看更多叙事 +
+ + {/* 上级要求 */} + + +
    + {ORDERS.map((o, i) => ( +
  1. + + {i + 1} + + {o} +
  2. + ))} +
+
+ {ORDER_META.map((m) => ( +
+
{m.label}
+
{m.value}
+
+ ))} +
+
+
+ + {/* 2. 下排:重点发布日历 + 口径关键词 + 传播任务分工速览 */} +
+ {/* 重点发布日历 */} + + +
+ + +
+ 查看完整日历 +
+ + {/* 口径关键词 */} + + +
+ {KEYWORDS.map((g) => ( +
+
+ {g.name} +
+
+ {g.words.map((w) => ( + + {w} + + ))} +
+
+ ))} +
+ 查看更多关键词 +
+ + {/* 传播任务分工速览 */} + + +
+ + {TASKS.map((t) => ( + + + + + + ))} +
{t.dept}{t.task}{t.deadline}
+
+ 查看完整分工 +
+
+
+ ); +} + +/** 发布日历小节(标题 + 时间/事件/状态行)。 */ +function CalendarSection({ title, items }: { title: string; items: CalendarItem[] }) { + return ( +
+
{title}
+
    + {items.map((it) => ( +
  • + {it.time} + {it.event} + {it.status} +
  • + ))} +
+
+ ); +} + +// ── 真实数据渲染 ────────────────────────────────────────────────────────────── +function OursReal({ data }: { data: OursData }) { + const keywords = Array.from(new Set(data.narratives.flatMap((n) => n.tags))).slice(0, 24); + return ( +
+
+ {/* 口径 */} + {data.speeches.length > 0 && ( + + +
    + {data.speeches.map((s, i) => ( +
  • +
    + {s.title || "口径"} + {s.time && {s.time}} +
    + {s.summary &&

    {s.summary}

    } + {s.direction && ( +

    + 导向: + {s.direction} +

    + )} +
  • + ))} +
+
+ )} + + {/* 上级要求 */} + {data.orders.length > 0 && ( + + +
    + {data.orders.map((o, i) => ( +
  1. + {i + 1} + {o} +
  2. + ))} +
+
+ )} +
+ + {/* 叙事 */} + {data.narratives.length > 0 && ( + + +
    + {data.narratives.map((n, i) => ( +
  • +
    + {n.title || "叙事"} + {n.tags.map((t) => ( + {t} + ))} +
    + {n.content &&

    {n.content}

    } +
  • + ))} +
+
+ )} + + {/* 口径关键词(取自叙事标签) */} + {keywords.length > 0 && ( + + +
+ {keywords.map((w) => ( + {w} + ))} +
+
+ )} +
+ ); +} diff --git a/frontend-web/src/roundtable-planning/dashboard-agent/SituationOverview.tsx b/frontend-web/src/roundtable-planning/dashboard-agent/SituationOverview.tsx new file mode 100644 index 00000000..2e908899 --- /dev/null +++ b/frontend-web/src/roundtable-planning/dashboard-agent/SituationOverview.tsx @@ -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 ; + + // 顶部指标(各类目数量) + 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 ( +
+ {/* 1. 顶部指标 */} +
+ {metrics.map((m) => ( + +
+ + + + {m.label} +
+
{m.value}
+
+ ))} +
+ + {/* 2. 关键战情摘要 */} + {summary.length > 0 && ( + + +
    + {summary.map((s) => ( +
  • + +

    + {s.label}: + {s.text} +

    +
  • + ))} +
+
+ )} + + {/* 3. 三栏概览 */} + {columns.length > 0 && ( +
+ {columns.map((col) => ( + + +
+
+ {col.rows.map((row) => ( +
+
{row.label}
+
{row.value}
+
+ ))} +
+ {col.focus.length > 0 && ( +
+
{col.focusTitle}
+
    + {col.focus.map((f, i) => ( +
  • + + {f} +
  • + ))} +
+
+ )} +
+
+ ))} +
+ )} + + {/* 4. 情况判断 / 决策建议 / 处置建议 */} + {(judgePoints.length > 0 || decisionPoints.length > 0 || disposalPoints.length > 0) && ( +
+ {judgePoints.length > 0 && ( + + )} + {decisionPoints.length > 0 && ( + + )} + {disposalPoints.length > 0 && ( + + )} +
+ )} +
+ ); +} + +/** 底部要点卡(编号要点 + 可选脚注)。 */ +function SummaryCard({ + icon, + title, + points, + footerLabel, + footer, +}: { + icon: LucideIcon; + title: string; + points: string[]; + footerLabel: string; + footer?: string; +}) { + return ( + + +
    + {points.map((p, i) => ( +
  1. + {i + 1} + {p} +
  2. + ))} +
+ {footer && {footer}} +
+ ); +} diff --git a/frontend-web/src/roundtable-planning/dashboard-agent/sq-report.ts b/frontend-web/src/roundtable-planning/dashboard-agent/sq-report.ts new file mode 100644 index 00000000..b0d7bff8 --- /dev/null +++ b/frontend-web/src/roundtable-planning/dashboard-agent/sq-report.ts @@ -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 { + return v && typeof v === "object" && !Array.isArray(v) ? (v as Record) : {}; +} +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 | 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): 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): 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): 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): 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): 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): 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), + }; +} diff --git a/frontend-web/src/roundtable-planning/dashboard-agent/ui.tsx b/frontend-web/src/roundtable-planning/dashboard-agent/ui.tsx new file mode 100644 index 00000000..5e20b994 --- /dev/null +++ b/frontend-web/src/roundtable-planning/dashboard-agent/ui.tsx @@ -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 ( +
+ + {label} +
+ ); +} + +/** 卡片外框(白底 + slate 细边 + 轻阴影)。 */ +export function Card({ className = "", children }: { className?: string; children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} + +/** 卡片标题(红色图标 + 标题 + 可选徽标 + 可选右侧操作 + 底部分隔线)。 */ +export function CardTitle({ + icon: Icon, + title, + badges, + action, +}: { + icon: LucideIcon; + title: string; + badges?: React.ReactNode; + action?: React.ReactNode; +}) { + return ( +
+
+ + + +

+ {title} +

+ {badges &&
{badges}
} +
+ {action} +
+ ); +} + +/** 标题徽标药丸(如「组织目标 12 个」,数字红色高亮)。 */ +export function HeadBadge({ label, value }: { label: string; value: string }) { + return ( + + {label} + {value} + + ); +} + +/** 「更多 >」操作按钮(标题右侧,可自定义文案)。 */ +export function MoreLink({ label = "更多", onClick }: { label?: string; onClick?: () => void }) { + return ( + + ); +} + +/** 卡片底部「查看完整… >」链接条(顶部分隔线 + 红色链接)。 */ +export function CardFooterLink({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) { + return ( +
+ +
+ ); +} + +export type Tone = "red" | "amber" | "green" | "blue" | "slate"; + +const TONE_CLASS: Record = { + 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 ( + + {children} + + ); +} + +const VAL_TEXT: Record = { + 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 {children}; +} + +/** 紧凑表格(表头灰字 + 行细分隔线,对齐设计图密集表格)。 */ +export function Table({ head, children }: { head: string[]; children: React.ReactNode }) { + return ( + + + + {head.map((h) => ( + + ))} + + + {children} +
+ {h} +
+ ); +} + +/** 表格数据单元(统一内边距 + 细分隔线 + 顶对齐)。 */ +export function Td({ className = "", children }: { className?: string; children?: React.ReactNode }) { + return ( + + {children} + + ); +} + +/** 卡片底部「说明 / 动态」提示条(左侧色条 + 浅底)。 */ +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 ( +
+ {label}: + {children} +
+ ); +} diff --git a/frontend-web/src/roundtable-planning/dashboard/DashboardApp.tsx b/frontend-web/src/roundtable-planning/dashboard/DashboardApp.tsx index 5a003174..91816cd3 100644 --- a/frontend-web/src/roundtable-planning/dashboard/DashboardApp.tsx +++ b/frontend-web/src/roundtable-planning/dashboard/DashboardApp.tsx @@ -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('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(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 ?? ; if (tab === 'overview') return ; 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 */} - {activeTab === 'overview' ? ( + {usingPages ? ( + + {pages!.find((p) => p.id === activeTab)?.content} + + ) : activeTab === 'overview' ? ( (null); + // 战情报告解析结果(selectSqReport → 各页面展示模型);解析失败/无数据 → null(页面回退占位)。 + const [sqReport, setSqReport] = useState(null); const dashboard = useStep3Dashboard({ intentReady: null, @@ -113,7 +133,8 @@ export default function DashboardAgentPage() { } = dashboard; const [loading, setLoading] = useState(true); - const [view, setView] = useState("chat"); + // 静态阶段默认进「大屏页面」,直接展示静态版式(见 STATIC_PREVIEW)。 + const [view, setView] = useState(STATIC_PREVIEW ? "page" : "chat"); const [renderMode, setRenderMode] = useState("app"); // 真实接口请求失败/无数据时的提示(真实模式不拿假数据兜底,改为提示+重试)。 const [inputError, setInputError] = useState(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(() => { + if (!sqReport) return null; + try { + return desensitizeDeep(sqReport) as SqReport; + } catch { + return sqReport; // 脱敏异常不影响展示 + } + }, [sqReport]); + + // 战情多页:总览 + 六个详情页;详情页传入对应的(脱敏后)真实数据(无则页面回退占位)。 + const dashPages: DashboardPage[] = useMemo( + () => [ + { id: "overview", label: "总览", icon: , content: }, + { id: "enemy", label: "敌情", icon: , content: }, + { id: "ours", label: "我情", icon: , content: }, + { id: "field", label: "战场环境", icon: , content: }, + { id: "judge", label: "情况判断", icon: , content: }, + { id: "decision", label: "决心建议", icon: , content: }, + { id: "disposal", label: "处置建议", icon: , content: }, + ], + [safeReport], + ); + // 网页渲染:blob URL 作 iframe src(:target 导航需独立文档 URL,见 Step3DashboardMode 注释)。 const [htmlBlobUrl, setHtmlBlobUrl] = useState(null); useEffect(() => { @@ -392,13 +449,27 @@ export default function DashboardAgentPage() { {/* 主体 */} {loading ? ( + // 加载框:请求战情报告期间显示,拿到数据后再渲染大屏(避免先闪占位再跳真实数据)。
正在请求任务数据…
-
拉取接口返回后将自动开始智能体抽取绘制
+
拉取接口返回后展示大屏
+ ) : view === "page" ? ( + renderMode === "html" && htmlBlobUrl ? ( +