-
- {data.narratives.map((n, i) => (
-
- | {n.topic || "—"} |
- {n.narrator || "—"} |
- {n.intention || "—"} |
- {n.type || "—"} |
- {n.summary || "—"} |
-
- ))}
-
+
+ {/* 时间线样式(参考示例图):左侧一条竖线,每个事件一个红点节点;时间在节点旁,
+ 标题/摘要放进右侧浅底小卡。仅用已有字段(时间/标题/摘要),不加图片等无数据元素。 */}
+
+ {data.events.map((g, i) => (
+
+
{g.topic || "事件"}
+
+ {g.items.map((e, j) => (
+ -
+
+ {e.time && (
+
{e.time}
+ )}
+
+
{e.title}
+ {e.brief &&
{e.brief}
}
+
+
+ ))}
+
+
+ ))}
)}
diff --git a/frontend-web/src/roundtable-planning/dashboard-agent/EnvironmentDetail.tsx b/frontend-web/src/roundtable-planning/dashboard-agent/EnvironmentDetail.tsx
index 5fd94717..96e7c91a 100644
--- a/frontend-web/src/roundtable-planning/dashboard-agent/EnvironmentDetail.tsx
+++ b/frontend-web/src/roundtable-planning/dashboard-agent/EnvironmentDetail.tsx
@@ -32,7 +32,7 @@ import {
type LucideIcon,
} from "lucide-react";
-import { Card, CardTitle, FooterNote, Table, Td, Tag, NoData, type Tone } from "@/roundtable-planning/dashboard-agent/ui";
+import { Card, CardTitle, FooterNote, Table, Td, Tag, NoData, useDesensitize, type Tone } from "@/roundtable-planning/dashboard-agent/ui";
import type { EnvData, EnvSection } from "@/roundtable-planning/dashboard-agent/sq-report";
// ── 占位数据 ──────────────────────────────────────────────────────────────────
@@ -329,10 +329,11 @@ function EnvCard({ field }: { field: EnvField }) {
/** 行级区段标题(红色竖条 + 文字)。 */
function SectionLabel({ children }: { children: React.ReactNode }) {
+ const ds = useDesensitize();
return (
- {children}
+ {typeof children === "string" ? ds(children) : children}
);
}
diff --git a/frontend-web/src/roundtable-planning/dashboard-agent/Judgement.tsx b/frontend-web/src/roundtable-planning/dashboard-agent/Judgement.tsx
index 0642bc62..0d8ba8aa 100644
--- a/frontend-web/src/roundtable-planning/dashboard-agent/Judgement.tsx
+++ b/frontend-web/src/roundtable-planning/dashboard-agent/Judgement.tsx
@@ -11,7 +11,7 @@
*/
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 { Card, CardTitle, FooterNote, Table, Td, Tag, Val, NoData, MarkdownText, useDesensitize, type Tone } from "@/roundtable-planning/dashboard-agent/ui";
import type { JudgeData } from "@/roundtable-planning/dashboard-agent/sq-report";
// ── 占位数据 ──────────────────────────────────────────────────────────────────
@@ -255,12 +255,15 @@ function JudgePlaceholder() {
/** 红色编号区段标题(① 敌我优劣势分析 …)。 */
function NumberedHeading({ n, children }: { n: number; children: React.ReactNode }) {
+ const ds = useDesensitize();
return (
{n}
-
{children}
+
+ {typeof children === "string" ? ds(children) : children}
+
);
}
@@ -345,8 +348,10 @@ function JudgeReal({ data }: { data: JudgeData }) {
{data.responses.map((r, i) => (
-
- {i + 1}
- {r}
+ {i + 1}
+
+ {r}
+
))}
diff --git a/frontend-web/src/roundtable-planning/dashboard-agent/OurSituation.tsx b/frontend-web/src/roundtable-planning/dashboard-agent/OurSituation.tsx
index c5b3ff34..d39d75c8 100644
--- a/frontend-web/src/roundtable-planning/dashboard-agent/OurSituation.tsx
+++ b/frontend-web/src/roundtable-planning/dashboard-agent/OurSituation.tsx
@@ -17,6 +17,7 @@ import {
Table,
Td,
NoData,
+ useDesensitize,
type Tone,
} from "@/roundtable-planning/dashboard-agent/ui";
import type { OursData } from "@/roundtable-planning/dashboard-agent/sq-report";
@@ -250,6 +251,7 @@ function CalendarSection({ title, items }: { title: string; items: CalendarItem[
// ── 真实数据渲染 ──────────────────────────────────────────────────────────────
function OursReal({ data }: { data: OursData }) {
+ const ds = useDesensitize();
const keywords = Array.from(new Set(data.narratives.flatMap((n) => n.tags))).slice(0, 24);
return (
@@ -268,7 +270,7 @@ function OursReal({ data }: { data: OursData }) {
{s.summary &&
{s.summary}
}
{s.direction && (
- 导向:
+ {ds("导向")}:
{s.direction}
)}
diff --git a/frontend-web/src/roundtable-planning/dashboard-agent/SituationOverview.tsx b/frontend-web/src/roundtable-planning/dashboard-agent/SituationOverview.tsx
index 2e908899..ef72b5d8 100644
--- a/frontend-web/src/roundtable-planning/dashboard-agent/SituationOverview.tsx
+++ b/frontend-web/src/roundtable-planning/dashboard-agent/SituationOverview.tsx
@@ -24,7 +24,7 @@ import {
type LucideIcon,
} from "lucide-react";
-import { Card, CardTitle, FooterNote, NoData } from "@/roundtable-planning/dashboard-agent/ui";
+import { Card, CardTitle, FooterNote, NoData, useDesensitize } from "@/roundtable-planning/dashboard-agent/ui";
import type { SqReport } from "@/roundtable-planning/dashboard-agent/sq-report";
// ── 聚合 ──────────────────────────────────────────────────────────────────────
@@ -44,6 +44,7 @@ interface OverviewColumn {
// ── 主组件 ────────────────────────────────────────────────────────────────────
export function SituationOverview({ data }: { data?: SqReport | null }) {
+ const ds = useDesensitize();
const r = data;
const hasAny =
!!r && (!!r.enemy || !!r.ours || !!r.environment || !!r.judgement || !!r.decision || !!r.disposal);
@@ -123,7 +124,7 @@ export function SituationOverview({ data }: { data?: SqReport | null }) {
-
{m.label}
+
{ds(m.label)}
{m.value}
@@ -139,7 +140,7 @@ export function SituationOverview({ data }: { data?: SqReport | null }) {
- {s.label}:
+ {ds(s.label)}:
{s.text}
@@ -158,14 +159,14 @@ export function SituationOverview({ data }: { data?: SqReport | null }) {
{col.rows.map((row) => (
-
- {row.label}
+ - {ds(row.label)}
- {row.value}
))}
{col.focus.length > 0 && (
-
{col.focusTitle}
+
{ds(col.focusTitle)}
{col.focus.map((f, i) => (
-
diff --git a/frontend-web/src/roundtable-planning/dashboard-agent/sq-report.ts b/frontend-web/src/roundtable-planning/dashboard-agent/sq-report.ts
index b0d7bff8..8e1e947a 100644
--- a/frontend-web/src/roundtable-planning/dashboard-agent/sq-report.ts
+++ b/frontend-web/src/roundtable-planning/dashboard-agent/sq-report.ts
@@ -28,6 +28,38 @@ function strArr(v: unknown): string[] {
return asArray(v).map(asStr).filter((s) => s.trim().length > 0);
}
+/**
+ * 过滤掉自由文本里的「引用角标」标记,做得**通用**:
+ * - 独立角标:`[[1]]`、`[[12]]`、`[[1,3]]` 等(方括号里任意非括号内容);
+ * - 带中文标签:`资料[[1]]`、`来源[[2]]`、`参考[[3]]` …;
+ * - 括号包裹:`(资料[[1]])`、`(来源[[12]])`、`([[3]])` …(中英文括号)。
+ * 删除后再清理残留的空括号与多余空白。用于所有展示文本(解析末尾整树清洗)。
+ */
+function stripCitations(s: string): string {
+ if (!s) return s;
+ return s
+ // 1) 括号包裹且内部含 [[..]] 的整体删除(不跨越其它括号)
+ .replace(/[((][^(())]*?\[\[[^[\]]+\]\][^(())]*?[))]/g, "")
+ // 2) 可选中文标签 + [[..]],或独立 [[..]]
+ .replace(/(?:资料|来源|参考|出处|引用|文献|注|脚注)?\s*\[\[[^[\]]+\]\]/g, "")
+ // 3) 残留空括号 + 多余空白
+ .replace(/[((]\s*[))]/g, "")
+ .replace(/[ \t]{2,}/g, " ")
+ .trim();
+}
+
+/** 深度清洗:递归把对象/数组里所有字符串过一遍 {@link stripCitations},返回新值。 */
+function deepStripCitations(value: T): T {
+ if (typeof value === "string") return stripCitations(value) as unknown as T;
+ if (Array.isArray(value)) return value.map(deepStripCitations) as unknown as T;
+ if (value && typeof value === "object") {
+ const out: Record = {};
+ for (const [k, v] of Object.entries(value as Record)) out[k] = deepStripCitations(v);
+ return out as T;
+ }
+ return value;
+}
+
/** 取 categories 容器(输入即 fetchDashboardInputJson 的返回)。 */
export function getSqCategories(input: unknown): Record | null {
const o = asObj(input);
@@ -185,7 +217,11 @@ 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) }))
+ // 段落里过滤掉「引用角标」([[1]] / 资料[[1]] / (资料[[1]]) 等),过滤后为空的段落丢弃
+ .map(([title, v]) => ({
+ title: stripCitations(title),
+ paragraphs: strArr(v).map(stripCitations).filter((p) => p.length > 0),
+ }))
.filter((s) => s.paragraphs.length > 0);
const info = toSections(cats["信息环境"]);
const human = toSections(cats["人文环境"]);
@@ -278,12 +314,13 @@ export function parseSqReport(input: unknown): SqReport {
if (!cats) {
return { enemy: null, ours: null, environment: null, judgement: null, decision: null, disposal: null };
}
- return {
+ // 整树清洗:所有类目的所有字符串字段统一过滤引用角标(通用,不止战场环境)。
+ return deepStripCitations({
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
index 5e20b994..7be18190 100644
--- a/frontend-web/src/roundtable-planning/dashboard-agent/ui.tsx
+++ b/frontend-web/src/roundtable-planning/dashboard-agent/ui.tsx
@@ -4,14 +4,39 @@
* **dashboard-agent 独占**:仅供战情系列静态页(SituationOverview / EnemyDetail / …)
* 复用,不影响会商共享的 DashboardApp / dashboard 组件群。配色对齐设计图(白底 + 红色强调)。
*/
+import { createContext, useContext, type ReactNode } from "react";
import { ChevronRight, Inbox, type LucideIcon } from "lucide-react";
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+
+import { desensitize } from "@/lib/desensitize";
+import { markdownComponents } from "@/roundtable-planning/components/markdown-components";
+
+/**
+ * 战情页**展示层脱敏函数**的 Context。
+ *
+ * 由 `DashboardAgentPage` 注入「后台敏感词(/api/sensitive-words)」版本(与「敏感词管理」
+ * 配置页同源),让本文件所有公共 UI 片段渲染的**静态文案**(卡片标题 / 表头 / 徽标 / 脚注 等,
+ * 内含 目标→MB、叙事→XS 这类映射词)也统一脱敏;缺省回退全局种子 {@link desensitize}。
+ * 数据文本已在数据层脱敏,这里再过一遍是幂等的(替换词为拉丁码,不会二次命中中文原词)。
+ */
+const DesensitizeContext = createContext<(s: string) => string>(desensitize);
+export const DesensitizeProvider = DesensitizeContext.Provider;
+export function useDesensitize() {
+ return useContext(DesensitizeContext);
+}
+/** children 若是纯字符串则脱敏,否则原样返回(元素/数字等不动)。 */
+function dsNode(fn: (s: string) => string, node: ReactNode): ReactNode {
+ return typeof node === "string" ? fn(node) : node;
+}
/** 空数据占位(接口返回为空 / 该类目缺失时显示)。 */
export function NoData({ label = "暂无数据" }: { label?: string }) {
+ const ds = useDesensitize();
return (
- {label}
+ {ds(label)}
);
}
@@ -39,6 +64,7 @@ export function CardTitle({
badges?: React.ReactNode;
action?: React.ReactNode;
}) {
+ const ds = useDesensitize();
return (
@@ -46,7 +72,7 @@ export function CardTitle({
- {title}
+ {ds(title)}
{badges &&
{badges}
}
@@ -57,23 +83,25 @@ export function CardTitle({
/** 标题徽标药丸(如「组织目标 12 个」,数字红色高亮)。 */
export function HeadBadge({ label, value }: { label: string; value: string }) {
+ const ds = useDesensitize();
return (
- {label}
- {value}
+ {ds(label)}
+ {ds(value)}
);
}
/** 「更多 >」操作按钮(标题右侧,可自定义文案)。 */
export function MoreLink({ label = "更多", onClick }: { label?: string; onClick?: () => void }) {
+ const ds = useDesensitize();
return (
);
@@ -81,6 +109,7 @@ export function MoreLink({ label = "更多", onClick }: { label?: string; onClic
/** 卡片底部「查看完整… >」链接条(顶部分隔线 + 红色链接)。 */
export function CardFooterLink({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) {
+ const ds = useDesensitize();
return (
@@ -107,9 +136,10 @@ const TONE_CLASS: Record
= {
/** 语义色块徽标(高/中/低、上升/持平/下降、偏高 等)。 */
export function Tag({ tone, children }: { tone: Tone; children: React.ReactNode }) {
+ const ds = useDesensitize();
return (
- {children}
+ {dsNode(ds, children)}
);
}
@@ -124,37 +154,44 @@ const VAL_TEXT: Record = {
/** 语义色取值文字(键值行右侧的 高/中/充足/紧张 等,以颜色表强弱)。 */
export function Val({ tone, children }: { tone: Tone; children: React.ReactNode }) {
- return {children};
+ const ds = useDesensitize();
+ return {dsNode(ds, children)};
}
/** 紧凑表格(表头灰字 + 行细分隔线,对齐设计图密集表格)。 */
export function Table({ head, children }: { head: string[]; children: React.ReactNode }) {
+ const ds = useDesensitize();
+ // 外层 overflow-x-auto:字号抬到 16px 后,多列密集表(如 8 列)超宽时横向滚动而非撑破卡片,保证布局不错乱。
+ // 不加负 margin:表格左右须与卡片标题/正文同在 px-4 基线上,否则整张表相对标题左移看着错位。
return (
-
-
-
- {head.map((h) => (
- |
- {h}
- |
- ))}
-
-
- {children}
-
+
+
+
+
+ {head.map((h) => (
+ |
+ {ds(h)}
+ |
+ ))}
+
+
+ {children}
+
+
);
}
/** 表格数据单元(统一内边距 + 细分隔线 + 顶对齐)。 */
export function Td({ className = "", children }: { className?: string; children?: React.ReactNode }) {
+ const ds = useDesensitize();
return (
- {children}
+ {dsNode(ds, children)}
|
);
}
@@ -178,10 +215,28 @@ export function FooterNote({
: "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";
+ const ds = useDesensitize();
return (
- {label}:
- {children}
+ {ds(label)}:
+ {dsNode(ds, children)}
+
+ );
+}
+
+/**
+ * 富文本(Markdown)渲染——战情数据里部分字段(如「应对方案」)带 Markdown 标记,
+ * 直接当纯文本渲染会露出 `**`/`-`/`#` 等源码。这里用与圆桌报告同一套
+ * {@link markdownComponents}(react-markdown + remark-gfm)渲染,并先脱敏。
+ * 纯文本传入也能正常渲染(按段落输出),可安全套用。
+ */
+export function MarkdownText({ children }: { children: string }) {
+ const ds = useDesensitize();
+ return (
+
+
+ {ds(children ?? "")}
+
);
}
diff --git a/frontend-web/src/roundtable-planning/dashboard/DashboardApp.tsx b/frontend-web/src/roundtable-planning/dashboard/DashboardApp.tsx
index 91816cd3..c636df23 100644
--- a/frontend-web/src/roundtable-planning/dashboard/DashboardApp.tsx
+++ b/frontend-web/src/roundtable-planning/dashboard/DashboardApp.tsx
@@ -101,12 +101,15 @@ export function DashboardApp({
)}
-
-
-
-
生成于 {data.meta.generatedAt}
+ {/* 战情多页(dashboard-agent)不展示「生成于」时间标签;会商 Step3 大屏保留。 */}
+ {!usingPages && (
+
+
+
+ 生成于 {data.meta.generatedAt}
+
-
+ )}
{/* 导航 Tab */}
diff --git a/frontend-web/src/roundtable-planning/pages/DashboardAgentPage.tsx b/frontend-web/src/roundtable-planning/pages/DashboardAgentPage.tsx
index a7374625..bbafd103 100644
--- a/frontend-web/src/roundtable-planning/pages/DashboardAgentPage.tsx
+++ b/frontend-web/src/roundtable-planning/pages/DashboardAgentPage.tsx
@@ -15,21 +15,12 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useLocation, useSearchParams } from "react-router-dom";
import { toast } from "sonner";
-import {
- ArrowLeft,
- Boxes,
- Code2,
- ExternalLink,
- LayoutPanelTop,
- Loader2,
- Pencil,
- RefreshCw,
- Sparkles,
-} from "lucide-react";
+import { Loader2, RefreshCw, Sparkles } from "lucide-react";
import { fetchDashboardInputJson } from "@/core/auth/dashboard-input";
import { readCopTaskDetail } from "@/core/auth/task-deeplink";
-import { desensitizeDeep } from "@/lib/desensitize";
+import { desensitize, makeDesensitizer } from "@/lib/desensitize";
+import { useSensitiveWords, wordsToMap } from "@/strategy-components/api/sensitive-words";
import { useModels } from "@/core/models/hooks";
import { useTheme } from "@/contexts/ThemeContext";
import { Compass, Crosshair, Shield, Mountain, Lightbulb, Flag, FileText } from "lucide-react";
@@ -43,6 +34,7 @@ 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 { DesensitizeProvider } from "@/roundtable-planning/dashboard-agent/ui";
import { adaptReportData } from "@/roundtable-planning/dashboard/data-context";
import { DashboardChatPanel } from "@/roundtable-planning/components/DashboardChatPanel";
import { Step2SandboxLayout } from "@/roundtable-planning/components/Step2SandboxLayout";
@@ -66,6 +58,22 @@ function formatNow(): string {
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
+/**
+ * 用**指定脱敏函数**深度遍历对象/数组(仅替换字符串值),返回新值(不改入参)。
+ * 与 `desensitizeDeep` 的差别:脱敏函数由调用方传入——这里直接用 `/api/sensitive-words`
+ * 查询结果构造(见组件内 `desensitizeText`),避免依赖全局 `DESENSITIZE_MAP` 的异步回灌时序。
+ */
+function deepDesensitize
(value: T, fn: (s: string) => string): T {
+ if (typeof value === "string") return fn(value) as unknown as T;
+ if (Array.isArray(value)) return value.map((v) => deepDesensitize(v, fn)) as unknown as T;
+ if (value && typeof value === "object") {
+ const out: Record = {};
+ for (const [k, v] of Object.entries(value as Record)) out[k] = deepDesensitize(v, fn);
+ return out as T;
+ }
+ return value;
+}
+
type DashView = "chat" | "page";
type RenderMode = "app" | "html";
@@ -175,8 +183,14 @@ export default function DashboardAgentPage() {
inputRef.current = input;
// 解析战情报告 → 各页面展示模型(容错:异常/无数据均不抛错,页面回退占位)。
try {
- setSqReport(parseSqReport(input));
- } catch {
+ const parsed = parseSqReport(input);
+ // 调试:在浏览器控制台打印「接口原始 JSON」与「解析后的战情数据」,便于核对解析是否正确。
+ // eslint-disable-next-line no-console
+ console.log("[大屏绘制] taskId=%s\n原始输入 JSON:", taskId, input, "\n解析后 sqReport:", parsed);
+ setSqReport(parsed);
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.warn("[大屏绘制] parseSqReport 解析失败:", err, "\n原始输入:", input);
setSqReport(null);
}
const savedDialogues = (session?.transcript as Step2Dialogue[] | undefined) ?? [];
@@ -239,8 +253,17 @@ export default function DashboardAgentPage() {
return () => setPreviewEnabled(false);
}, [view, setPreviewEnabled]);
+ // 展示层脱敏函数:**直接用 `/api/sensitive-words` 查询结果**构造(不走全局 DESENSITIZE_MAP),
+ // 避免与 `replaceDesensitizeMap` 的 effect 时序竞态——与侧边栏/导航栏菜单脱敏同一手法。
+ // 后台为空(或未加载)时回退到全局种子 `desensitize`。供大屏所有 react 页面 + 导航 Tab 文本共用。
+ const { data: sensitiveWords } = useSensitiveWords();
+ const desensitizeText = useMemo(() => {
+ const map = wordsToMap(sensitiveWords ?? []);
+ return Object.keys(map).length > 0 ? makeDesensitizer(map) : desensitize;
+ }, [sensitiveWords]);
+
// ── 大屏渲染数据(深度脱敏后渲染,覆盖大屏所有展示文本) ──
- const report = useMemo(() => (data ? desensitizeDeep(data) : null), [data]);
+ const report = useMemo(() => (data ? deepDesensitize(data, desensitizeText) : null), [data, desensitizeText]);
const dashboardData = useMemo(
() => (report ? adaptReportData(report, formatNow()) : undefined),
[report],
@@ -251,28 +274,33 @@ export default function DashboardAgentPage() {
);
const hasResult = !!dashboardData || !!dashboardHtml;
- // 展示层脱敏:所有页面文本经 desensitizeDeep 深度替换后再渲染(与会商大屏一致)。
+ // 展示层脱敏:所有页面文本经深度替换后再渲染(与会商大屏一致),脱敏函数来自后台敏感词查询。
const safeReport = useMemo(() => {
if (!sqReport) return null;
try {
- return desensitizeDeep(sqReport) as SqReport;
+ return deepDesensitize(sqReport, desensitizeText) as SqReport;
} catch {
return sqReport; // 脱敏异常不影响展示
}
- }, [sqReport]);
+ }, [sqReport, desensitizeText]);
+
+ // 是否有可展示的真实数据:战情报告(safeReport)或已生成的大屏(report-json / HTML)。
+ // 三者皆无 → 不渲染大屏的导航栏与标题,仅显示「暂无数据」。
+ const hasData = !!safeReport || !!dashboardData || !!dashboardHtml;
// 战情多页:总览 + 六个详情页;详情页传入对应的(脱敏后)真实数据(无则页面回退占位)。
+ // 导航 Tab 文本(label)也经 desensitizeText 脱敏后再传给 DashboardApp 渲染导航栏。
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: },
+ { id: "overview", label: desensitizeText("总览"), icon: , content: },
+ { id: "enemy", label: desensitizeText("敌情"), icon: , content: },
+ { id: "ours", label: desensitizeText("我情"), icon: , content: },
+ { id: "field", label: desensitizeText("战场环境"), icon: , content: },
+ { id: "judge", label: desensitizeText("情况判断"), icon: , content: },
+ { id: "decision", label: desensitizeText("决心建议"), icon: , content: },
+ { id: "disposal", label: desensitizeText("处置建议"), icon: , content: },
],
- [safeReport],
+ [safeReport, desensitizeText],
);
// 网页渲染:blob URL 作 iframe src(:target 导航需独立文档 URL,见 Step3DashboardMode 注释)。
@@ -368,85 +396,8 @@ export default function DashboardAgentPage() {
}
return (
-
- {/* 顶栏 */}
-
-
-
-
大屏绘制智能体
-
- {(phase === "creating" || phase === "streaming") && }
- {view === "chat" ? "生成过程" : "大屏页面"}
-
-
-
-
- {view === "page" ? (
- <>
-
-
-
-
-
-
- >
- ) : (
- hasResult && (
-
- )
- )}
-
-
-
+ // dashboard-agent-fontscale:本页字号下限 16px(见 globals.css,仅作用于本页)。
+
{/* 主体 */}
{loading ? (
// 加载框:请求战情报告期间显示,拿到数据后再渲染大屏(避免先闪占位再跳真实数据)。
@@ -458,7 +409,12 @@ export default function DashboardAgentPage() {
) : view === "page" ? (
- renderMode === "html" && htmlBlobUrl ? (
+ !hasData ? (
+ // 无数据:隐藏大屏导航栏与顶部标题,仅显示「暂无数据」。
+
+ 暂无数据
+
+ ) : renderMode === "html" && htmlBlobUrl ? (
) : (
-
+ {/* 注入后台敏感词脱敏函数:让战情各页(含静态卡片标题/表头/脚注等)统一脱敏。 */}
+
+
+
)
) : inputError && dialogues.length === 0 ? (
diff --git a/frontend-web/src/styles/globals.css b/frontend-web/src/styles/globals.css
index 5a563267..9a5fe4d6 100644
--- a/frontend-web/src/styles/globals.css
+++ b/frontend-web/src/styles/globals.css
@@ -34,6 +34,12 @@
@source inline("border-l-{2,4}");
@source inline("border-l-{slate,gray,primary}-{300,400,500}");
+/* 战情大屏(dashboard-agent):时间线圆圈/虚线、FooterNote 左侧强调条等用到的
+ * 语义色边框——本项目边框色需显式 safelist,否则只生成宽度、颜色回退成灰色。
+ * 明、暗两套各一行。 */
+@source inline("border-{red,emerald,sky,amber}-{200,300,400,500,600,700}");
+@source inline("dark:border-{red,emerald,sky,amber}-{200,300,400,500,600,700}");
+
/* Link */
@source inline("text-{blue,primary,accent}-{500,600,700}");
@source inline("hover:text-{blue,primary,accent}-{600,700,800}");
@@ -656,6 +662,33 @@
border-color: #1a2f5a !important;
}
+ /* 「大屏绘制智能体」战情大屏:**全页最小字号 16px**(仅命中本页 wrapper
+ * .dashboard-agent-fontscale —— 加在 DashboardAgentPage 根容器上,会包住其下的
+ * DashboardApp 顶部标题/导航 Tab/页脚 + 七个战情子页)。把 10/11/12/13px 统一抬到 16px、
+ * 14px(text-sm) 抬到 18px,保留「正文 16 / 卡片标题 18(再叠 bold) / 指标大数 ≥20」的层级,
+ * 看上去自然不错乱;text-xl/2xl/3xl 本就 ≥16,保持原样不动。
+ * 作用域严格限定本页:会商共享的 DashboardApp(无此 wrapper)与其它页面零影响。 */
+ .dashboard-agent-fontscale
+ :is(.text-\[10px\], .text-\[11px\], .text-xs, .text-\[13px\]) {
+ font-size: 16px !important;
+ line-height: 1.75 !important;
+ }
+ .dashboard-agent-fontscale .text-sm {
+ font-size: 18px !important;
+ line-height: 1.65 !important;
+ }
+
+ /* 语义色边框兜底:本项目 border-red/emerald/sky 等颜色类未被 Tailwind 生成时只剩宽度、
+ * 颜色回退成灰色(时间线圆圈/虚线、FooterNote 左侧强调条)。这里在本页作用域内**直接定义**
+ * 这些类的 border-color(用 Tailwind v4 颜色变量,带 hex 兜底),不依赖 @source 安全列表的
+ * 重新生成,改完即时生效。仅命中 .dashboard-agent-fontscale 子树,不影响其它页面。 */
+ .dashboard-agent-fontscale .border-red-200 { border-color: var(--color-red-200, #fecaca) !important; }
+ .dashboard-agent-fontscale .border-red-300 { border-color: var(--color-red-300, #fca5a5) !important; }
+ .dashboard-agent-fontscale .border-red-400 { border-color: var(--color-red-400, #f87171) !important; }
+ .dashboard-agent-fontscale .border-red-500 { border-color: var(--color-red-500, #ef4444) !important; }
+ .dashboard-agent-fontscale .border-emerald-500 { border-color: var(--color-emerald-500, #10b981) !important; }
+ .dashboard-agent-fontscale .border-sky-500 { border-color: var(--color-sky-500, #0ea5e9) !important; }
+
.workspace-panel {
border: 1px solid #f0d7d0;
background: #ffffff;