- 去掉顶栏与「生成于」时间标签;无数据时仅显示「暂无数据」、隐藏导航与标题 - 战情各页 + 导航 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>
210 lines
9.5 KiB
TypeScript
210 lines
9.5 KiB
TypeScript
/**
|
||
* @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>
|
||
);
|
||
}
|