Files
deerflow-code/frontend-web/src/roundtable-planning/dashboard/DashboardApp.tsx
T
董宏凯andClaude Opus 4.8 1a2ef0a537 feat(大屏绘制): 战情大屏样式优化与脱敏全覆盖
- 去掉顶栏与「生成于」时间标签;无数据时仅显示「暂无数据」、隐藏导航与标题
- 战情各页 + 导航 Tab 文本全部脱敏(响应式取后台敏感词,避免 replaceDesensitizeMap 时序竞态)
- 全页最小字号 16px,调整行高/对齐;表格加横向滚动防溢出
- 关键事件改时间线样式(灰色虚线 + 浅红空心圆圈节点),敌情三块改单行全宽
- 应对方案渲染 Markdown;处置建议由 7 列大表改为单条卡片(字段标签改 #222222)
- 通用过滤引用角标([[1]] / 资料[[1]] / (资料[[1]]) 等,解析末尾整树清洗)
- safelist + 本页兜底 CSS 修复 border 语义色回退成灰色(时间线圆圈/FooterNote 强调条)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:36:36 +08:00

210 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*
* 大屏主视图(源自参考 App.tsx)。2026-06-13 改造:
* - 接受 `data` prop(圆桌真实数据,adaptReportData 产出),用 DashboardDataProvider 注入;
* - 标题/副标题/导航 Tab 由数据动态生成(原为写死的 6 个固定席位);
* - 不传 data 时回落参考 demo(DEMO_DATA)。
*/
import './dashboard.css';
import { useEffect, useState, type ReactNode } from 'react';
import { TabType } from './types';
import {
DashboardDataProvider,
DEMO_DATA,
type DashboardDataValue,
} from './data-context';
import { MetricsHeader } from './components/MetricsHeader';
import { ConsensusPanel } from './components/ConsensusPanel';
import { BrandScores } from './components/BrandScores';
import { RiskDonut } from './components/RiskDonut';
import { SeatDeliveryOverview } from './components/SeatDeliveryOverview';
import { Roadmap } from './components/Roadmap';
import { AnalyticsCenter } from './components/AnalyticsCenter';
import { DetailView } from './components/DetailView';
import { motion, AnimatePresence } from 'motion/react';
import {
Compass, LayoutGrid, Users, Megaphone, ShieldAlert, FileText,
Clock, Activity,
} from 'lucide-react';
const TAB_ICONS = [LayoutGrid, Users, Megaphone, ShieldAlert, FileText, Activity];
/** 自定义页(dashboard-agent 战情多页用):一个 Tab = id + 标签 + 图标 + 正文。 */
export interface DashboardPage {
id: string;
label: string;
icon?: ReactNode;
content: ReactNode;
}
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(firstTab);
}, [data, firstTab]);
// 导航 Tab:自定义页集 → 各页 id;否则总览 + 各席位(由数据动态生成)。
const tabs: TabType[] = usingPages
? pages!.map((p) => p.id)
: ['overview', ...data.SEAT_CARDS.map((s) => s.id)];
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];
return <Ico className={cls} />;
};
return (
<DashboardDataProvider value={data}>
<div className="dash-root min-h-screen bg-white dark:bg-[var(--dash-page)] font-sans text-slate-800 dark:text-[var(--dash-text)] relative pb-16 [background-image:radial-gradient(#f1f5f9_1.2px,transparent_1.2px)] dark:[background-image:radial-gradient(#1a2f5a_1.2px,transparent_1.2px)] [background-size:20px_20px]">
{/* 顶部彩条 */}
<div className="h-1 bg-gradient-to-r from-rose-600 via-amber-500 to-sky-600 dark:from-[var(--dash-primary)] dark:via-[var(--dash-accent)] dark:to-[#4096ff] w-full" />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-5 space-y-6">
{/* Header */}
<header className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 pb-4 border-b border-rose-100 dark:border-[var(--dash-border)]">
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-3">
<h1 className="text-xl sm:text-2xl font-black tracking-tight font-display flex items-center gap-1.5 text-rose-900 dark:text-[var(--dash-text-strong)]">
{data.meta.title}
</h1>
</div>
{data.meta.subtitle && (
<p className="text-xs sm:text-sm text-slate-500 dark:text-[var(--dash-text-sec)] font-sans tracking-wide">
{data.meta.subtitle}
</p>
)}
</div>
{/* 战情多页(dashboard-agent)不展示「生成于」时间标签;会商 Step3 大屏保留。 */}
{!usingPages && (
<div className="flex flex-wrap items-center gap-2.5 self-start lg:self-center text-xs text-slate-500 dark:text-[var(--dash-text-sec)]">
<div className="flex items-center gap-1.5 font-mono px-3 py-1.5 bg-white dark:bg-[var(--dash-card)] border border-slate-200 dark:border-[var(--dash-border)] shadow-sm rounded-sm">
<Clock className="w-3.5 h-3.5 text-rose-600 dark:text-[var(--dash-primary)] animate-pulse" />
<span>生成于 {data.meta.generatedAt}</span>
</div>
</div>
)}
</header>
{/* 导航 Tab */}
<nav className="flex items-center justify-start overflow-x-auto pb-1 -mx-4 px-4 sm:mx-0 sm:px-0">
<div className="flex space-x-1.5 bg-slate-100/80 dark:bg-[var(--dash-elev)] backdrop-blur-xs p-1 border border-slate-200 dark:border-[var(--dash-border)] rounded-md shrink-0">
{tabs.map((tab) => {
const active = activeTab === tab;
return (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold rounded-sm transition-all duration-200 cursor-pointer whitespace-nowrap ${
active
? 'bg-white dark:bg-[var(--dash-card)] text-rose-700 dark:text-[var(--dash-primary)] shadow-sm border border-rose-100 dark:border-[var(--dash-primary)] font-bold scale-[1.01]'
: 'text-slate-600 dark:text-[var(--dash-text-sec)] hover:text-slate-900 dark:hover:text-[var(--dash-text-strong)] hover:bg-slate-50 dark:hover:bg-[var(--dash-card)]'
}`}
>
{getTabIcon(tab)}
<span>{getTabLabel(tab)}</span>
</button>
);
})}
</div>
</nav>
{/* Body */}
<AnimatePresence mode="wait">
{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 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.25 }}
className="space-y-6"
>
<MetricsHeader />
{/* 上半部分:总控最终共识 + 多维核心竞争力排行(核心风险分布下移到数据研判中心之后) */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-5">
<div className="lg:col-span-7 h-full">
<ConsensusPanel onExploreClick={() => setActiveTab(tabs[tabs.length - 1] ?? 'overview')} />
</div>
<div className="lg:col-span-5 h-full">
<BrandScores />
</div>
</div>
<AnalyticsCenter />
{/* 核心风险分类分布:移到数据研判中心下方,整行展示 */}
<RiskDonut />
<SeatDeliveryOverview onSeatSelect={setActiveTab} activeSeatId={activeTab} />
<Roadmap />
</motion.div>
) : (
<motion.div
key="seat-details"
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.25 }}
>
<DetailView
activeTab={activeTab}
onBackToOverview={() => setActiveTab('overview')}
onNavigate={setActiveTab}
/>
</motion.div>
)}
</AnimatePresence>
<footer className="pt-8 border-t border-slate-200/60 dark:border-[var(--dash-border)] mt-12 text-center text-xs text-slate-400 dark:text-[var(--dash-text-dim)] font-sans flex flex-col sm:flex-row items-center justify-between gap-4">
<span>圆桌多智能体研讨 · 方案大屏</span>
<div className="flex gap-4 font-mono text-[10px] tracking-wider uppercase">
<span>Engineering Grid: True</span>
<span>Roundtable Console</span>
</div>
</footer>
</div>
</div>
</DashboardDataProvider>
);
}