feat(圆桌): 后台挂起按钮移至顶部暂停按钮右侧 + 实时研讨复位历史闸门

- Step2 顶部「暂停/恢复会商」右侧新增「后台挂起」按钮(查看历史时隐藏),移除底部干预栏里的旧入口
- handleConfirmAgents 从第一步开启实时研讨时复位 loadedFromDraftRef,修复看过历史后再开新讨论按钮不出现的问题
- 大屏 dashboard 多组件样式与业务链条编辑页细节调整

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
董宏凯andClaude Opus 4.8 committed 2026-06-13 17:37:14 +08:00
1 parent 634cb1413b
commit 61754acb87
18 files changed
+672 -368

No files matched your search

+255 -52
View File
@@ -1,27 +1,71 @@
/**
* 应用列表页 —— 以卡片网格展示「新页跳转(window.open)」类型的轻应用。
*
* 支持搜索 + 分页。点击卡片用 buildLightAppUrl 构造最终地址(追加解析后的路由参数)
* 并 window.open 新开页跳转。仅展示已启用的应用;iframe 类型不在此处(它们注入侧边栏)。
* 只读启动器:搜索 + 分页;每张卡片预览「拼接后最终路由」(buildLightAppUrl 追加解析后的
* 登录态 / 主题路由参数),并提供「一键复制URL」「进入应用」(window.open 新开页) 两个动作。
* 增删改不在本页,走独立的「应用管理」页(admin);iframe 类型也不在此(它们注入侧边栏)。
*
* 配色(与「业务链条配置」页统一):
* - 浅色:品牌红 `#CF1322`(hover `#A8071A`)。
* - 深蓝:按《深蓝色 UI 规范》——页面 `#0C1836`、卡片 `#122347`、边框 `#1A2F5A`、
* 正文 `#C5CFE0`、次级 `#6E88B0`;主操作改用规范主色蓝 `#1677FF`(hover `#0958D9`)。
*
* 后端 LightApp 无「分类」字段,故不做分类筛选;卡片图标 / 主题色按 appId 哈希派生
* (`icon` 字段当前未用),仅作视觉区分,不承载语义。
*/
import { useEffect, useMemo, useState } from "react";
import { ExternalLink, Search } from "lucide-react";
import {
ArrowUpRight,
Bot,
Check,
Code,
Copy,
FileText,
Globe,
Layers,
Notebook,
Search,
SlidersHorizontal,
Terminal,
X,
type LucideIcon,
} from "lucide-react";
import { toast } from "sonner";
import { Input } from "@/components/ui/input";
import { Pagination } from "@/components/ui/pagination";
import { useTheme } from "@/contexts/ThemeContext";
import { cn } from "@/lib/utils";
import { useLightApps, type LightApp } from "@/strategy-components/api/light-apps";
import { buildLightAppUrl } from "@/strategy-components/lib/light-app";
const PAGE_SIZE = 12;
export default function LightAppListPage() {
const { theme } = useTheme();
const isDark = theme === "dark";
/** 卡片图标池(按 appId 哈希取一个,仅作视觉区分)。 */
const ICON_POOL: LucideIcon[] = [Bot, Notebook, Terminal, SlidersHorizontal, Globe, Code, Layers, FileText];
/** 卡片主题色池:浅色用对应色阶,深蓝统一落到 `#1A2F5A` 容器底 + 规范强调色。 */
const THEME_POOL = [
{ iconBg: "bg-rose-100 text-rose-600 dark:bg-[#1A2F5A] dark:text-[#FF7875]" },
{ iconBg: "bg-indigo-100 text-indigo-600 dark:bg-[#1A2F5A] dark:text-[#6388FF]" },
{ iconBg: "bg-emerald-100 text-emerald-600 dark:bg-[#1A2F5A] dark:text-[#52C41A]" },
{ iconBg: "bg-amber-100 text-amber-600 dark:bg-[#1A2F5A] dark:text-[#FAAD14]" },
{ iconBg: "bg-blue-100 text-blue-600 dark:bg-[#1A2F5A] dark:text-[#4096FF]" },
{ iconBg: "bg-purple-100 text-purple-600 dark:bg-[#1A2F5A] dark:text-[#9C7BFF]" },
] as const;
/** 稳定字符串哈希 → [0, mod) 下标。 */
function hashIndex(str: string, mod: number): number {
let h = 0;
for (let i = 0; i < str.length; i += 1) {
h = (h * 31 + str.charCodeAt(i)) >>> 0;
}
return mod > 0 ? h % mod : 0;
}
export default function LightAppListPage() {
const [search, setSearch] = useState("");
const [page, setPage] = useState(1);
const [copiedId, setCopiedId] = useState<string | null>(null);
const { data: apps = [], isLoading } = useLightApps({
appType: "window_open",
enabledOnly: true,
@@ -47,67 +91,226 @@ export default function LightAppListPage() {
window.open(url, "_blank", "noopener,noreferrer");
};
const shellClass = isDark
? "h-full overflow-y-auto bg-[linear-gradient(180deg,#081322_0%,#0d1d33_42%,#0a1628_100%)] text-slate-100"
: "h-full overflow-y-auto bg-[linear-gradient(180deg,#fffdfd_0%,#fff7f7_42%,#fafafa_100%)] text-[#222222]";
const cardClass = isDark
? "group cursor-pointer rounded-2xl border border-[rgba(67,89,124,0.46)] bg-[rgba(12,29,54,0.94)] p-4 transition-all hover:-translate-y-0.5 hover:border-[rgba(77,141,255,0.6)] hover:shadow-[0_18px_38px_rgba(3,10,22,0.35)]"
: "group cursor-pointer rounded-2xl border border-[#E8E8E8] bg-white p-4 transition-all hover:-translate-y-0.5 hover:border-[#FFA39E] hover:shadow-[0_16px_32px_rgba(245,34,45,0.1)]";
const mutedText = isDark ? "text-slate-500" : "text-[#A6A6A6]";
const iconWrap = isDark
? "bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]"
: "bg-[#FFF1F0] text-[#CF1322]";
const copyUrl = async (app: LightApp) => {
const url = buildLightAppUrl(app);
if (!url) {
toast.error("该应用地址为空,无法复制");
return;
}
try {
await navigator.clipboard.writeText(url);
setCopiedId(app.appId);
setTimeout(() => setCopiedId((id) => (id === app.appId ? null : id)), 2000);
} catch {
toast.error("复制失败,请手动选择地址");
}
};
return (
<div className={shellClass}>
<div className="mx-auto flex min-h-full max-w-[1280px] flex-col gap-5 px-6 py-6 lg:px-8">
<div className="h-full overflow-y-auto bg-zinc-50/60 font-sans text-zinc-900 antialiased dark:bg-[#0C1836] dark:text-[#C5CFE0]">
<div className="mx-auto flex min-h-full w-full max-w-7xl flex-col gap-6 px-4 py-7 sm:px-6 lg:px-8">
{/* 标题 */}
<div className="space-y-1">
<h1 className={`text-[26px] font-semibold tracking-[0.02em] ${isDark ? "text-slate-50" : "text-[#222222]"}`}>
应用列表
</h1>
<p className={`text-sm ${isDark ? "text-slate-300/80" : "text-slate-500"}`}>
点击卡片在新标签页打开应用。地址会自动附带配置的路由参数(登录态 / 主题)。
<div className="flex items-center gap-2.5">
<span className="relative flex size-3">
<span className="absolute inline-flex size-full animate-ping rounded-full bg-[#CF1322] opacity-75 dark:bg-[#1677FF]" />
<span className="relative inline-flex size-3 rounded-full bg-[#CF1322] dark:bg-[#1677FF]" />
</span>
<h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-[#E8EDF5]">
应用列表
</h1>
<span className="rounded-full border border-[#CF1322]/15 bg-[#FFF1F0] px-2 py-0.5 font-mono text-xs font-medium text-[#CF1322] dark:border-[#1677FF]/30 dark:bg-[#1677FF]/15 dark:text-[#4096FF]">
导航工作台
</span>
</div>
<p className="max-w-2xl text-sm leading-relaxed text-zinc-500 dark:text-[#6E88B0]">
点击卡片将在新标签页中一键直达应用。地址会自动、智能地拼接路由参数(跨域登录态、个性化主题等)。
</p>
</div>
<div className="relative max-w-sm">
<Search className={`pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 ${mutedText}`} />
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="搜索应用名称 / 描述"
className="pl-9"
/>
{/* 搜索 */}
<div className="rounded-2xl border border-zinc-200/80 bg-white p-5 shadow-sm dark:border-[#1A2F5A] dark:bg-[#122347]">
<div className="relative w-full">
<Search className="pointer-events-none absolute left-3.5 top-1/2 size-4.5 -translate-y-1/2 text-zinc-400 dark:text-[#6E88B0]" />
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="键入应用名称、目标地址、或描述,实时过滤检索…"
className="w-full rounded-xl border border-zinc-200 bg-zinc-50/50 py-3 pl-10 pr-10 text-sm transition focus:border-[#CF1322]/60 focus:bg-white focus:outline-none focus:ring-2 focus:ring-[#CF1322]/20 dark:border-[#1A2F5A] dark:bg-[#0C1836] dark:text-[#C5CFE0] dark:placeholder-[#4A6491] dark:focus:border-[#1677FF]/60 dark:focus:bg-[#122347] dark:focus:ring-[#1677FF]/25"
/>
{search && (
<button
type="button"
onClick={() => setSearch("")}
title="清除搜索"
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-zinc-400 transition hover:text-zinc-600 dark:hover:text-[#C5CFE0]"
>
<X className="size-4" />
</button>
)}
</div>
</div>
{/* 结果计数 */}
<div className="-mt-2 flex items-center justify-between">
<div className="text-xs font-medium text-zinc-500 dark:text-[#6E88B0]">
检索到最终结果:{" "}
<span className="font-bold text-[#CF1322] dark:text-[#4096FF]">{apps.length}</span> 个关联应用
</div>
{search && (
<button
type="button"
onClick={() => setSearch("")}
className="text-xs font-medium text-[#CF1322] underline hover:text-[#A8071A] dark:text-[#4096FF] dark:hover:text-[#1677FF]"
>
重置过滤条件
</button>
)}
</div>
{/* 卡片网格 / 空态 */}
{apps.length === 0 ? (
<div className={`rounded-3xl border py-20 text-center ${isDark ? "border-[rgba(67,89,124,0.4)] text-slate-500" : "border-[#F0F0F0] text-[#A6A6A6]"}`}>
{isLoading ? "加载中…" : "暂无可用应用"}
<div className="rounded-2xl border border-dashed border-zinc-200 bg-white p-16 text-center shadow-sm dark:border-[#1A2F5A] dark:bg-[#122347]">
<div className="mx-auto mb-4 flex size-16 items-center justify-center rounded-full border border-zinc-100 bg-zinc-50 text-zinc-300 dark:border-[#1A2F5A] dark:bg-[#0C1836] dark:text-[#6E88B0]">
<Search className="size-6" />
</div>
<h4 className="mb-1 text-base font-medium text-zinc-800 dark:text-[#C5CFE0]">
{isLoading ? "正在加载应用…" : search ? "未找到匹配的应用" : "暂无可用应用"}
</h4>
<p className="mx-auto max-w-md text-sm text-zinc-400 dark:text-[#6E88B0]">
{isLoading
? "请稍候。"
: search
? "尝试更换关键词,或清除搜索查看全部应用。"
: "应用由管理员在「应用管理」中登记后会出现在这里。"}
</p>
</div>
) : (
<>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{pageApps.map((app) => (
<button key={app.appId} type="button" onClick={() => openApp(app)} className={`${cardClass} text-left`}>
<div className="flex items-start gap-3">
<span className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl ${iconWrap}`}>
<ExternalLink className="h-5 w-5" />
</span>
<div className="min-w-0 flex-1">
<div className={`truncate text-[15px] font-semibold ${isDark ? "text-white" : "text-[#222222]"}`}>
{app.name}
</div>
<div className={`mt-1 line-clamp-2 text-xs leading-relaxed ${isDark ? "text-slate-400" : "text-slate-500"}`}>
{app.description || app.url}
</div>
</div>
</div>
</button>
<AppCard
key={app.appId}
app={app}
copied={copiedId === app.appId}
onCopy={() => void copyUrl(app)}
onOpen={() => openApp(app)}
/>
))}
</div>
<Pagination page={page} pageSize={PAGE_SIZE} total={apps.length} onPageChange={setPage} className="pt-2" />
{apps.length > PAGE_SIZE && (
<Pagination
page={page}
pageSize={PAGE_SIZE}
total={apps.length}
onPageChange={setPage}
className="pt-2"
/>
)}
</>
)}
{/* 页脚 */}
<footer className="mt-auto pt-8 text-center text-xs text-zinc-400 dark:text-[#4A6491]">
应用导航工作台 • 优雅可靠的任务中转入口
</footer>
</div>
</div>
);
}
// ── 单张应用卡 ───────────────────────────────────────────────────────────────
function AppCard({
app,
copied,
onCopy,
onOpen,
}: {
app: LightApp;
copied: boolean;
onCopy: () => void;
onOpen: () => void;
}) {
const Icon = ICON_POOL[hashIndex(app.appId, ICON_POOL.length)];
const theme = THEME_POOL[hashIndex(app.appId, THEME_POOL.length)];
const parsedUrl = buildLightAppUrl(app);
const hasParams = (app.routeParams ?? []).some((p) => p.key) && parsedUrl !== app.url;
return (
<div className="group relative flex flex-col justify-between overflow-hidden rounded-2xl border border-zinc-100 bg-white p-5 shadow-[0_2px_8px_rgba(0,0,0,0.01)] transition-all duration-300 hover:-translate-y-1 hover:border-zinc-200/90 hover:shadow-[0_12px_24px_rgba(0,0,0,0.04)] dark:border-[#1A2F5A] dark:bg-[#122347] dark:shadow-none dark:hover:border-[#2D4473]">
{/* 上:图标 + 标题 + 描述 */}
<div>
<div className="mb-3.5 flex items-start gap-4">
<div className={cn("rounded-xl border border-white/60 p-3 shadow-sm dark:border-transparent", theme.iconBg)}>
<Icon className="size-5.5" />
</div>
</div>
<h4 className="mb-2 line-clamp-1 text-base font-bold leading-snug text-zinc-900 transition-colors group-hover:text-[#CF1322] dark:text-[#E8EDF5] dark:group-hover:text-[#4096FF]">
{app.name}
</h4>
<p className="mb-4 line-clamp-2 min-h-[2.5rem] text-xs leading-relaxed text-zinc-500 dark:text-[#6E88B0]">
{app.description || "暂无应用详细描述说明,点击进入下方链接查看详情。"}
</p>
</div>
{/* 下:URL 预览 + 动作 */}
<div className="mt-auto border-t border-zinc-100 pt-4 dark:border-[#1A2F5A]">
<div className="mb-4 rounded-xl border border-zinc-100 bg-zinc-50/70 p-3 dark:border-[#1A2F5A] dark:bg-[#0C1836]">
<div className="mb-1 flex items-center justify-between text-[10px] font-semibold text-zinc-400 dark:text-[#6E88B0]">
<span>拼接后目标路由终端:</span>
<span className="text-[9px] font-bold uppercase tracking-wider text-[#CF1322] dark:text-[#4096FF]">
自动拼接已激活参
</span>
</div>
<div className="line-clamp-1 select-all break-all font-mono text-[11px] font-medium text-zinc-600 dark:text-[#C5CFE0]">
{parsedUrl || "(地址为空)"}
</div>
{hasParams && (
<div className="mt-1 flex items-center gap-1 text-[9px] text-zinc-400 dark:text-[#6E88B0]">
<span className="inline-block size-1 rounded-full bg-amber-400 dark:bg-[#FAAD14]" />
<span>包含动态拼接参数</span>
</div>
)}
</div>
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={onCopy}
className={cn(
"flex items-center justify-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-semibold transition",
copied
? "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-[#52C41A]/30 dark:bg-[#52C41A]/15 dark:text-[#52C41A]"
: "border-zinc-200 bg-white text-zinc-600 hover:bg-zinc-50 hover:text-zinc-800 dark:border-[#1A2F5A] dark:bg-[#122347] dark:text-[#C5CFE0] dark:hover:bg-[#1A2F5A]",
)}
>
{copied ? (
<>
<Check className="size-3.5" />
<span>已复制!</span>
</>
) : (
<>
<Copy className="size-3.5" />
<span>一键复制URL</span>
</>
)}
</button>
<button
type="button"
onClick={onOpen}
className="flex items-center justify-center gap-1.5 rounded-lg bg-[#CF1322] px-3 py-2 text-xs font-semibold text-white shadow-[0_1px_3px_rgba(207,19,34,0.2)] transition hover:bg-[#A8071A] active:scale-[0.98] dark:bg-[#1677FF] dark:shadow-none dark:hover:bg-[#0958D9]"
>
<span>进入应用</span>
<ArrowUpRight className="size-3.5" />
</button>
</div>
</div>
</div>
);
@@ -528,7 +528,7 @@ export default function LightAppManagementPage() {
className="flex h-7 cursor-pointer items-center space-x-1 rounded-[2px] border border-[#DBDBDB] bg-white px-2.5 text-[12px] font-bold text-[#575757] transition-colors hover:border-[#F5222D] hover:text-[#F5222D]"
>
<Plus className="h-3.5 w-3.5" />
<span>+ 添加路由参数</span>
<span>添加路由参数</span>
</button>
</div>
@@ -47,13 +47,26 @@ export interface ChainFlowDiagramProps {
}
// ── 布局常量(世界坐标,单位 px)─────────────────────────────────────────────
const NODE_W = 188;
const NODE_H = 66;
const NODE_W = 206;
const NODE_H = 84;
const COL_GAP = 116;
const ROW_GAP = 22;
const PAD = 52;
const COORDINATOR_ID = "__coordinator__";
// 节点卡左侧描边(hex,内联上色避免被 border-border 覆盖)/ 角色行配色(按头像配色)。
// 参考「业务链条配置器」设计:彩色左描边 + 同色角色行。
const NODE_COLORS: Record<string, { hex: string; role: string }> = {
blue: { hex: "#3b82f6", role: "text-blue-600 dark:text-blue-300" },
purple: { hex: "#a855f7", role: "text-purple-600 dark:text-purple-300" },
emerald: { hex: "#10b981", role: "text-emerald-600 dark:text-emerald-300" },
amber: { hex: "#f59e0b", role: "text-amber-600 dark:text-amber-300" },
pink: { hex: "#ec4899", role: "text-pink-600 dark:text-pink-300" },
human: { hex: "#8b5cf6", role: "text-violet-600 dark:text-violet-300" },
};
const nodeColorOf = (t: string) =>
NODE_COLORS[t] ?? { hex: "#94a3b8", role: "text-muted-foreground" };
const MIN_SCALE = 0.25;
const MAX_SCALE = 2.6;
@@ -119,6 +132,7 @@ export function ChainFlowDiagram({ stages, onNodeClick, className }: ChainFlowDi
for (const nb of b.nodes) {
edges.push({
id: `${k}:${na.id}->${nb.id}`,
// 直角折线:右出 → 到中线 → 竖直 → 进左侧(尖角,不抹圆)。
d: `M ${na.rightX} ${na.cy} H ${midX} V ${nb.cy} H ${nb.leftX}`,
});
}
@@ -232,13 +246,13 @@ export function ChainFlowDiagram({ stages, onNodeClick, className }: ChainFlowDi
<div
ref={viewportRef}
className={cn(
"relative h-full w-full overflow-hidden bg-muted/20 outline-none",
"relative h-full w-full overflow-hidden bg-white outline-none dark:bg-[#0C1836]",
grabbing ? "cursor-grabbing" : "cursor-grab",
)}
style={{
backgroundImage:
"radial-gradient(circle, rgba(100,116,139,0.13) 1px, transparent 1px)",
backgroundSize: "22px 22px",
"radial-gradient(circle, rgba(148,163,184,0.18) 1px, transparent 1px)",
backgroundSize: "24px 24px",
}}
onPointerDown={(e) => {
if (e.button !== 0) return;
@@ -272,7 +286,7 @@ export function ChainFlowDiagram({ stages, onNodeClick, className }: ChainFlowDi
<svg
width={layout.worldW}
height={layout.worldH}
className="pointer-events-none absolute inset-0 overflow-visible text-muted-foreground/50"
className="pointer-events-none absolute inset-0 overflow-visible text-slate-300 dark:text-[#3a4d72]"
>
<defs>
<marker
@@ -293,16 +307,18 @@ export function ChainFlowDiagram({ stages, onNodeClick, className }: ChainFlowDi
d={edge.d}
fill="none"
stroke="currentColor"
strokeWidth={1.6}
strokeWidth={1.75}
markerEnd="url(#cfd-arrow)"
/>
))}
</svg>
{/* 节点卡片 */}
{layout.cols.flatMap((col) =>
{/* 节点卡片(参考「业务链条配置器」样式:左彩色描边 + 头像 + 名称 +
L-阶段徽标 + 同色角色行 + 底部 Ready 心跳条;图标仍用我方 getAvatar)。 */}
{layout.cols.flatMap((col, colIdx) =>
col.nodes.map((n) => {
const isCoordinator = n.id === COORDINATOR_ID;
const nc = nodeColorOf(n.avatarType);
return (
<div
key={n.id}
@@ -318,19 +334,38 @@ export function ChainFlowDiagram({ stages, onNodeClick, className }: ChainFlowDi
>
<div
className={cn(
"flex h-full w-full items-center gap-2.5 rounded-xl border bg-card px-3 shadow-sm transition",
"flex h-full w-full flex-col justify-between rounded-[5px] border bg-card p-3 shadow-[0_2px_10px_rgba(15,23,42,0.05)] transition",
isCoordinator
? "border-primary/40 bg-primary/[0.06]"
: "border-border hover:-translate-y-0.5 hover:border-primary hover:shadow-md",
? "border-border bg-primary/[0.04]"
: "border-border hover:-translate-y-0.5 hover:shadow-[0_8px_22px_rgba(15,23,42,0.10)]",
)}
// 左侧 4px 彩色描边:内联上色,避免被 border-border 的四边色覆盖。
style={{ borderLeftColor: nc.hex, borderLeftWidth: 4 }}
>
<div className="shrink-0">{getAvatar(n.avatarType, "h-9 w-9")}</div>
<div className="min-w-0 flex-1">
<div className="truncate text-xs font-bold text-foreground">{n.name}</div>
<div className="truncate text-[10px] text-muted-foreground">
{isCoordinator ? "按阶段派活 · 综合收口" : n.description || n.id}
{/* 上:头像 + 名称 + 阶段徽标 + 角色行 */}
<div className="flex items-start gap-2 overflow-hidden">
<div className="shrink-0">{getAvatar(n.avatarType, "h-7 w-7")}</div>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-1">
<span className="truncate text-[11px] font-bold text-foreground">{n.name}</span>
<span className="shrink-0 rounded border border-border bg-muted px-1 py-0.5 text-[7.5px] font-bold uppercase leading-none text-muted-foreground">
{isCoordinator ? "核心" : `L-${colIdx}`}
</span>
</div>
<p className={cn("mt-0.5 truncate text-[9px] font-medium", nc.role)}>
{isCoordinator ? "按阶段派活 · 综合收口" : n.description || n.id}
</p>
</div>
</div>
{/* 下:身份 + Ready 心跳条 */}
<div className="flex items-center justify-between border-t border-border/60 pt-1 text-[8px] text-muted-foreground">
<span className="truncate">{isCoordinator ? "总控协调核心" : "智能体席位"}</span>
<span className="ml-1.5 flex shrink-0 items-center gap-1 rounded bg-muted px-1 py-0.5 font-medium text-primary">
<span className="size-1 rounded-full bg-emerald-500" />
Ready
</span>
</div>
</div>
</div>
);
@@ -354,6 +389,16 @@ export function ChainFlowDiagram({ stages, onNodeClick, className }: ChainFlowDi
))}
</div>
{/* 左上状态条(参考设计的「拓扑画布」浮条)。全屏时让位给退出按钮。 */}
{!fullscreen && (
<div className="absolute left-3 top-3 flex items-center gap-2 rounded-full border border-border bg-card/90 px-3 py-1 text-[11px] font-medium text-muted-foreground shadow-sm backdrop-blur">
<span className="size-1.5 animate-pulse rounded-full bg-primary" />
<span className="font-semibold text-foreground">拓扑画布</span>
<span className="h-3 w-px bg-border" />
<span>节点 {layout.allNodes.length}</span>
</div>
)}
{/* 工具条:缩小 / 放大 / 适应 / 全屏 */}
<div className="absolute right-3 top-3 flex flex-col gap-1.5">
<ToolButton title="放大" onClick={() => zoomBy(1.2)}>
@@ -2,7 +2,7 @@
* Step 2 主面板 —— 多智能体圆桌研讨。
*
* 包含 4 块:
* 1) 顶部 Headline + 推理模型下拉 + 挂起/恢复按钮
* 1) 顶部 Headline + 推理模型下拉 + 暂停/恢复按钮 + 后台挂起按钮(查看历史时隐藏)
* 2) Persona 抽屉(点击席位头像后展开,见 `PersonaDrawer.tsx`)
* 3) 对话流(初始化占位 + 气泡列表 + ChainOfThought 折叠卡 + markdown)
* 4) 底部澄清条 + 用户干预输入 + 「达成共识」入口
@@ -149,6 +149,8 @@ export interface Step2PanelProps {
onProceedToStep3: () => void;
/** 「后台挂起」:把当前研讨转入后端自动跑到结果绘制完成。 */
onBackgroundSuspend: () => void;
/** 是否在「查看历史记录」(从草稿/后台作业加载)—— 为 true 时隐藏「后台挂起」按钮。 */
isViewingHistory?: boolean;
onTriggerToast: (msg: string) => void;
}
@@ -194,6 +196,7 @@ export function Step2Panel(props: Step2PanelProps) {
onSavePersona,
onProceedToStep3,
onBackgroundSuspend,
isViewingHistory,
onTriggerToast,
} = props;
@@ -462,6 +465,19 @@ export function Step2Panel(props: Step2PanelProps) {
暂停会商
</button>
)}
{/* 后台挂起:放在暂停/恢复按钮右侧。查看历史记录时隐藏(isViewingHistory),
重新从第一步进入第二步研讨时(isViewingHistory 为假)正常显示。 */}
{SHOW_BACKGROUND_SUSPEND && !isViewingHistory && (
<button
type="button"
onClick={onBackgroundSuspend}
title="把当前研讨转入后台自动跑到结果绘制完成;可离开页面,进度在历史记录里查看"
className="bg-slate-100 hover:bg-slate-200 text-slate-700 text-[calc(10px*var(--ui-font-scale,1))] font-bold px-2.5 py-1 rounded-xl flex items-center gap-1 transition border border-slate-200/60 shadow-sm dark:bg-secondary dark:text-foreground dark:border-border"
>
<Network className="w-3.5 h-3.5" />
后台挂起
</button>
)}
</div>
</div>
@@ -765,21 +781,6 @@ export function Step2Panel(props: Step2PanelProps) {
干预并发送
</button>
{SHOW_BACKGROUND_SUSPEND && (
<>
<div className="h-6 w-[1px] bg-slate-200 shrink-0" />
<button
type="button"
onClick={onBackgroundSuspend}
title="把当前研讨转入后台自动跑到结果绘制完成;可离开页面,进度在历史记录里查看"
className="bg-white border border-slate-200 hover:bg-slate-50 text-slate-700 text-xs font-bold px-4 py-2.5 rounded-xl transition shrink-0 flex items-center gap-1 active:scale-95 cursor-pointer dark:bg-secondary dark:text-foreground dark:border-border"
>
后台挂起
</button>
</>
)}
<button
type="button"
onClick={onProceedToStep3}
@@ -56,29 +56,29 @@ export function DashboardApp({ data = DEMO_DATA }: { data?: DashboardDataValue }
return (
<DashboardDataProvider value={data}>
<div className="min-h-screen bg-white font-sans text-slate-800 relative pb-16 [background-image:radial-gradient(#f1f5f9_1.2px,transparent_1.2px)] [background-size:20px_20px]">
<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 w-full" />
<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">
<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">
<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 font-sans tracking-wide">
<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>
<div className="flex flex-wrap items-center gap-2.5 self-start lg:self-center text-xs text-slate-500">
<div className="flex items-center gap-1.5 font-mono px-3 py-1.5 bg-white border border-slate-200 shadow-sm rounded-sm">
<Clock className="w-3.5 h-3.5 text-rose-600 animate-pulse" />
<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>
@@ -86,7 +86,7 @@ export function DashboardApp({ data = DEMO_DATA }: { data?: DashboardDataValue }
{/* 导航 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 backdrop-blur-xs p-1 border border-slate-200 rounded-md shrink-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 (
@@ -95,8 +95,8 @@ export function DashboardApp({ data = DEMO_DATA }: { data?: DashboardDataValue }
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 text-rose-700 shadow-sm border border-rose-100 font-bold scale-[1.01]'
: 'text-slate-600 hover:text-slate-900 hover:bg-slate-50'
? '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)}
@@ -156,7 +156,7 @@ export function DashboardApp({ data = DEMO_DATA }: { data?: DashboardDataValue }
)}
</AnimatePresence>
<footer className="pt-8 border-t border-slate-200/60 mt-12 text-center text-xs text-slate-400 font-sans flex flex-col sm:flex-row items-center justify-between gap-4">
<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>
@@ -31,7 +31,7 @@ function LineChartSvg({ items }: { items: DashChart["items"] }) {
const pts = data.map((it, i) => `${x(i).toFixed(1)},${y(it.value).toFixed(1)}`).join(" ");
return (
<div className="relative w-full h-[180px] bg-slate-50/30 rounded border border-slate-100 p-1 flex items-center justify-center">
<div className="relative w-full h-[180px] bg-slate-50/30 dark:bg-[var(--dash-inset)] rounded border border-slate-100 dark:border-[var(--dash-border)] p-1 flex items-center justify-center">
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-full">
{[0, 0.25, 0.5, 0.75, 1].map((k, i) => {
const gy = padT + k * (H - padT - padB);
@@ -134,7 +134,7 @@ function DonutChartSvg({ items }: { items: DashChart["items"] }) {
<div
key={item.label}
className={`flex items-center justify-between p-1 rounded-sm cursor-pointer transition-all border-l-[3px] ${
hover === idx ? "bg-slate-50 font-bold" : "border-transparent"
hover === idx ? "bg-slate-50 dark:bg-[var(--dash-elev)] font-bold" : "border-transparent"
}`}
style={{ borderLeftColor: hover === idx ? ACCENTS[idx % ACCENTS.length] : undefined }}
onMouseEnter={() => setHover(idx)}
@@ -142,9 +142,9 @@ function DonutChartSvg({ items }: { items: DashChart["items"] }) {
>
<div className="flex items-center gap-1.5 min-w-0">
<span className="w-2.5 h-2.5 rounded-sm shrink-0" style={{ backgroundColor: ACCENTS[idx % ACCENTS.length] }} />
<span className="text-[10px] text-slate-700 truncate">{item.label}</span>
<span className="text-[10px] text-slate-700 dark:text-[var(--dash-text)] truncate">{item.label}</span>
</div>
<span className="text-[10.5px] font-bold font-mono text-slate-900">{item.value}</span>
<span className="text-[10.5px] font-bold font-mono text-slate-900 dark:text-[var(--dash-text-strong)]">{item.value}</span>
</div>
))}
</div>
@@ -168,7 +168,7 @@ function RadarChartSvg({ items }: { items: DashChart["items"] }) {
return (
<div className="flex items-center gap-2">
<div className="relative w-40 h-40 flex items-center justify-center p-1 bg-slate-50/40 rounded border border-slate-100 shrink-0">
<div className="relative w-40 h-40 flex items-center justify-center p-1 bg-slate-50/40 dark:bg-[var(--dash-inset)] rounded border border-slate-100 dark:border-[var(--dash-border)] shrink-0">
<svg viewBox="0 0 200 200" className="w-40 h-40">
{[0.25, 0.5, 0.75, 1].map((k, i) => (
<polygon
@@ -197,9 +197,9 @@ function RadarChartSvg({ items }: { items: DashChart["items"] }) {
</div>
<div className="flex-1 space-y-1 text-[10px]">
{data.map((it, i) => (
<div key={i} className="flex justify-between items-center gap-1 border-b border-slate-50 pb-0.5">
<span className="text-slate-500 truncate">{it.label}</span>
<b className="font-mono text-slate-800">{it.value}</b>
<div key={i} className="flex justify-between items-center gap-1 border-b border-slate-50 dark:border-[var(--dash-border)] pb-0.5">
<span className="text-slate-500 dark:text-[var(--dash-text-sec)] truncate">{it.label}</span>
<b className="font-mono text-slate-800 dark:text-[var(--dash-text)]">{it.value}</b>
</div>
))}
</div>
@@ -216,10 +216,10 @@ function BarChartSvg({ items }: { items: DashChart["items"] }) {
{data.map((it, i) => (
<div key={i} className="space-y-1">
<div className="flex justify-between text-[10.5px]">
<span className="text-slate-600 truncate">{it.label}</span>
<b className="font-mono text-slate-900">{it.value}</b>
<span className="text-slate-600 dark:text-[var(--dash-text-sec)] truncate">{it.label}</span>
<b className="font-mono text-slate-900 dark:text-[var(--dash-text-strong)]">{it.value}</b>
</div>
<div className="w-full h-2 bg-slate-100 rounded-full overflow-hidden">
<div className="w-full h-2 bg-slate-100 dark:bg-[var(--dash-elev)] rounded-full overflow-hidden">
<motion.div
className="h-full rounded-full"
style={{ background: ACCENTS[i % ACCENTS.length] }}
@@ -239,11 +239,11 @@ function ChartCard({ chart }: { chart: DashChart }) {
return (
<DraftingCard className="flex flex-col">
<div className="space-y-4">
<div className="flex items-center gap-2 border-b border-slate-100 pb-2.5">
<div className="p-1.5 bg-rose-50/80 border border-rose-100 rounded-sm text-rose-600">
<div className="flex items-center gap-2 border-b border-slate-100 dark:border-[var(--dash-border)] pb-2.5">
<div className="p-1.5 bg-rose-50/80 dark:bg-[var(--dash-primary-soft)] border border-rose-100 dark:border-[var(--dash-border)] rounded-sm text-rose-600 dark:text-[var(--dash-primary)]">
<Icon className="w-3.5 h-3.5" />
</div>
<h4 className="text-xs font-black text-slate-800 tracking-wide font-sans">{chart.title || "图表"}</h4>
<h4 className="text-xs font-black text-slate-800 dark:text-[var(--dash-text)] tracking-wide font-sans">{chart.title || "图表"}</h4>
</div>
{chart.type === "line" ? (
<LineChartSvg items={chart.items} />
@@ -265,13 +265,13 @@ export function AnalyticsCenter() {
return (
<div className="space-y-5">
<div className="flex items-center justify-between pb-1 border-b border-rose-100/90">
<h3 className="text-base font-extrabold text-slate-800 tracking-tight font-sans flex items-center gap-2">
<Activity className="w-5 h-5 text-rose-600 animate-pulse" />
<div className="flex items-center justify-between pb-1 border-b border-rose-100/90 dark:border-[var(--dash-border)]">
<h3 className="text-base font-extrabold text-slate-800 dark:text-[var(--dash-text)] tracking-tight font-sans flex items-center gap-2">
<Activity className="w-5 h-5 text-rose-600 dark:text-[var(--dash-primary)] animate-pulse" />
数据研判中心
<span className="text-xs font-normal text-slate-400 font-sans hidden sm:inline">多维度数据可视化研判</span>
<span className="text-xs font-normal text-slate-400 dark:text-[var(--dash-text-dim)] font-sans hidden sm:inline">多维度数据可视化研判</span>
</h3>
<span className="text-[10px] px-2 py-0.5 bg-slate-50 border border-slate-200 text-slate-500 font-mono rounded">
<span className="text-[10px] px-2 py-0.5 bg-slate-50 dark:bg-[var(--dash-elev)] border border-slate-200 dark:border-[var(--dash-border)] text-slate-500 dark:text-[var(--dash-text-sec)] font-mono rounded">
STATUS: REAL-TIME CONSOLE
</span>
</div>
@@ -27,7 +27,7 @@ export function BrandScores({ onBrandSelect }: BrandScoresProps) {
if (BRAND_SCORES.length === 0) {
return (
<DraftingCard className="h-full flex items-center justify-center" id="brand-scores-card">
<span className="text-xs text-slate-400">暂无可对比的竞争力排行数据。</span>
<span className="text-xs text-slate-400 dark:text-[var(--dash-text-dim)]">暂无可对比的竞争力排行数据。</span>
</DraftingCard>
);
}
@@ -46,25 +46,25 @@ export function BrandScores({ onBrandSelect }: BrandScoresProps) {
const getRankBadge = (rankIndex: number) => {
if (rankIndex === 0)
return (
<div className="relative flex items-center justify-center w-7 h-7 rounded-sm bg-amber-50 text-amber-700 font-mono text-xs font-bold border border-amber-200 ring-2 ring-amber-100/50">
<div className="relative flex items-center justify-center w-7 h-7 rounded-sm bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-300 font-mono text-xs font-bold border border-amber-200 dark:border-amber-500/30 ring-2 ring-amber-100/50">
01
<Trophy className="absolute -top-1 -right-1 w-2.5 h-2.5 text-amber-500 fill-amber-500" />
</div>
);
if (rankIndex === 1)
return (
<div className="flex items-center justify-center w-7 h-7 rounded-sm bg-slate-50 text-slate-600 font-mono text-xs font-bold border border-slate-200 ring-2 ring-slate-100/50">
<div className="flex items-center justify-center w-7 h-7 rounded-sm bg-slate-50 dark:bg-[var(--dash-elev)] text-slate-600 dark:text-[var(--dash-text-sec)] font-mono text-xs font-bold border border-slate-200 dark:border-[var(--dash-border)] ring-2 ring-slate-100/50">
02
</div>
);
if (rankIndex === 2)
return (
<div className="flex items-center justify-center w-7 h-7 rounded-sm bg-rose-50 text-rose-700 font-mono text-xs font-bold border border-rose-200 ring-2 ring-rose-100/30">
<div className="flex items-center justify-center w-7 h-7 rounded-sm bg-rose-50 dark:bg-rose-500/10 text-rose-700 dark:text-rose-300 font-mono text-xs font-bold border border-rose-200 dark:border-rose-500/30 ring-2 ring-rose-100/30">
03
</div>
);
return (
<div className="flex items-center justify-center w-7 h-7 rounded-sm bg-slate-50/80 text-slate-400 font-mono text-xs font-medium border border-slate-200">
<div className="flex items-center justify-center w-7 h-7 rounded-sm bg-slate-50/80 dark:bg-[var(--dash-elev)] text-slate-400 dark:text-[var(--dash-text-dim)] font-mono text-xs font-medium border border-slate-200 dark:border-[var(--dash-border)]">
{String(rankIndex + 1).padStart(2, "0")}
</div>
);
@@ -73,25 +73,25 @@ export function BrandScores({ onBrandSelect }: BrandScoresProps) {
return (
<DraftingCard className="h-full flex flex-col justify-between" id="brand-scores-card">
<div>
<div className="flex flex-col md:flex-row md:items-center justify-between pb-4 border-b border-rose-100/85 mb-5 gap-3">
<div className="flex flex-col md:flex-row md:items-center justify-between pb-4 border-b border-rose-100/85 dark:border-[var(--dash-border)] mb-5 gap-3">
<div className="flex items-center gap-2">
<div className="p-1 px-2 bg-rose-50 border border-rose-200 rounded text-rose-700">
<div className="p-1 px-2 bg-rose-50 dark:bg-[var(--dash-primary-soft)] border border-rose-200 dark:border-[var(--dash-primary)] rounded text-rose-700 dark:text-[var(--dash-primary)]">
<Trophy className="w-4 h-4" />
</div>
<div>
<h3 className="text-sm font-black text-slate-900 tracking-tight font-sans">多维核心竞争力排行榜</h3>
<p className="text-[10px] text-slate-400 font-sans tracking-wide">多角度研讨加权估算指数</p>
<h3 className="text-sm font-black text-slate-900 dark:text-[var(--dash-text-strong)] tracking-tight font-sans">多维核心竞争力排行榜</h3>
<p className="text-[10px] text-slate-400 dark:text-[var(--dash-text-dim)] font-sans tracking-wide">多角度研讨加权估算指数</p>
</div>
</div>
{hasShare && (
<div className="flex bg-slate-50 border border-slate-200/60 p-0.5 rounded-sm select-none shrink-0 self-start md:self-auto">
<div className="flex bg-slate-50 dark:bg-[var(--dash-elev)] border border-slate-200/60 dark:border-[var(--dash-border)] p-0.5 rounded-sm select-none shrink-0 self-start md:self-auto">
{(["score", "share"] as const).map((vt) => (
<button
key={vt}
onClick={() => setViewType(vt)}
className={`px-2.5 py-1 text-[11px] font-bold rounded-xs cursor-pointer transition-all ${
viewType === vt ? "bg-white text-rose-700 border border-slate-200/40" : "text-slate-500 hover:text-slate-800"
viewType === vt ? "bg-white dark:bg-[var(--dash-card)] text-rose-700 dark:text-[var(--dash-primary)] border border-slate-200/40 dark:border-[var(--dash-border)]" : "text-slate-500 dark:text-[var(--dash-text-sec)] hover:text-slate-800 dark:hover:text-[var(--dash-text)]"
}`}
>
{vt === "score" ? "综合评分" : "市场份额"}
@@ -111,14 +111,14 @@ export function BrandScores({ onBrandSelect }: BrandScoresProps) {
key={brand.name}
className={`group rounded-sm border transition-all duration-300 p-2.5 cursor-pointer relative ${
isSelected
? "bg-gradient-to-b from-rose-50/15 to-white border-rose-300 shadow-sm ring-1 ring-rose-200/40"
: "bg-white border-slate-200 hover:bg-slate-50/40 hover:border-slate-300"
? "bg-gradient-to-b from-rose-50/15 to-white dark:from-[var(--dash-primary-soft)] dark:to-[var(--dash-card)] border-rose-300 dark:border-[var(--dash-primary)] shadow-sm ring-1 ring-rose-200/40 dark:ring-[var(--dash-primary)]/30"
: "bg-white dark:bg-[var(--dash-card)] border-slate-200 dark:border-[var(--dash-border)] hover:bg-slate-50/40 dark:hover:bg-[var(--dash-elev)] hover:border-slate-300 dark:hover:border-[var(--dash-border-soft)]"
}`}
onClick={() => handleBrandClick(brand)}
onMouseEnter={() => setHoveredBrand(brand.name)}
onMouseLeave={() => setHoveredBrand(null)}
>
{isSelected && <div className="absolute left-0 top-0 bottom-0 w-[3px] bg-rose-600 rounded-l-xs" />}
{isSelected && <div className="absolute left-0 top-0 bottom-0 w-[3px] bg-rose-600 dark:bg-[var(--dash-primary)] rounded-l-xs" />}
<div className="flex items-center gap-3">
{getRankBadge(index)}
@@ -132,30 +132,30 @@ export function BrandScores({ onBrandSelect }: BrandScoresProps) {
<div className="flex-1 min-w-0">
<div className="flex justify-between items-center mb-1 text-xs">
<span className="font-extrabold text-slate-800 flex items-center gap-1.5 group-hover:text-rose-950 transition-colors truncate">
<span className="font-extrabold text-slate-800 dark:text-[var(--dash-text)] flex items-center gap-1.5 group-hover:text-rose-950 dark:group-hover:text-[var(--dash-primary)] transition-colors truncate">
{brand.name}
{brand.score >= 85 && (
<span className="text-[9px] bg-amber-50 border border-amber-200 text-amber-700 px-1 rounded-xs font-mono shrink-0">
<span className="text-[9px] bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/30 text-amber-700 dark:text-amber-300 px-1 rounded-xs font-mono shrink-0">
LEADING
</span>
)}
</span>
<div className="flex items-center gap-1.5 shrink-0">
{hasShare && (
<span className="text-[10px] text-slate-400 font-mono">
<span className="text-[10px] text-slate-400 dark:text-[var(--dash-text-dim)] font-mono">
{effectiveView === "score" ? `份额: ${brand.marketShare ?? "—"}` : `指数: ${brand.score}`}
</span>
)}
<span className="font-bold text-slate-900 font-mono text-sm tracking-tight">
<span className="font-bold text-slate-900 dark:text-[var(--dash-text-strong)] font-mono text-sm tracking-tight">
{effectiveView === "score" ? brand.score : brand.marketShare}
</span>
</div>
</div>
<div className="w-full h-2.5 bg-slate-100 rounded-sm overflow-hidden relative flex items-center p-0.5 border border-slate-200/50">
<div className="w-full h-2.5 bg-slate-100 dark:bg-[var(--dash-inset)] rounded-sm overflow-hidden relative flex items-center p-0.5 border border-slate-200/50 dark:border-[var(--dash-border)]">
<div className="absolute inset-0 flex justify-between pointer-events-none px-1">
{[...Array(10)].map((_, i) => (
<div key={i} className="w-[1.2px] h-full bg-slate-200/80 last:hidden" />
<div key={i} className="w-[1.2px] h-full bg-slate-200/80 dark:bg-[var(--dash-border-soft)]/80 last:hidden" />
))}
</div>
<motion.div
@@ -171,8 +171,8 @@ export function BrandScores({ onBrandSelect }: BrandScoresProps) {
</div>
<ChevronRight
className={`w-3.5 h-3.5 text-slate-400 group-hover:text-rose-600 transition-all ${
isSelected ? "rotate-90 text-rose-500" : ""
className={`w-3.5 h-3.5 text-slate-400 dark:text-[var(--dash-text-dim)] group-hover:text-rose-600 dark:group-hover:text-[var(--dash-primary)] transition-all ${
isSelected ? "rotate-90 text-rose-500 dark:text-[var(--dash-primary)]" : ""
}`}
/>
</div>
@@ -186,45 +186,45 @@ export function BrandScores({ onBrandSelect }: BrandScoresProps) {
transition={{ duration: 0.25, ease: "easeInOut" }}
className="overflow-hidden"
>
<div className="pt-3.5 mt-2.5 border-t border-slate-100 grid grid-cols-1 md:grid-cols-2 gap-3.5">
<div className="bg-slate-50/80 p-2.5 rounded-sm border border-slate-200/50">
<div className="pt-3.5 mt-2.5 border-t border-slate-100 dark:border-[var(--dash-border)] grid grid-cols-1 md:grid-cols-2 gap-3.5">
<div className="bg-slate-50/80 dark:bg-[var(--dash-elev)] p-2.5 rounded-sm border border-slate-200/50 dark:border-[var(--dash-border)]">
<div className="flex items-center gap-1.5 mb-2">
<Zap className="w-3.5 h-3.5 text-amber-500" />
<span className="text-[10px] font-bold text-slate-700 tracking-wider">竞争优势 / 亮点</span>
<span className="text-[10px] font-bold text-slate-700 dark:text-[var(--dash-text)] tracking-wider">竞争优势 / 亮点</span>
</div>
{brand.strengths && brand.strengths.length > 0 ? (
<ul className="space-y-1.5">
{brand.strengths.map((str, i) => (
<li key={i} className="text-[11px] text-slate-600 flex items-start gap-1">
<span className="text-emerald-500 font-bold shrink-0 mt-0.5">✓</span>
<li key={i} className="text-[11px] text-slate-600 dark:text-[var(--dash-text-sec)] flex items-start gap-1">
<span className="text-emerald-500 dark:text-emerald-300 font-bold shrink-0 mt-0.5">✓</span>
<span className="leading-tight">{str}</span>
</li>
))}
</ul>
) : (
<span className="text-[10px] text-slate-400 italic">综合指数 {brand.score} 分</span>
<span className="text-[10px] text-slate-400 dark:text-[var(--dash-text-dim)] italic">综合指数 {brand.score} 分</span>
)}
</div>
<div className="bg-slate-50/80 p-2.5 rounded-sm border border-slate-200/50 flex flex-col justify-between">
<div className="bg-slate-50/80 dark:bg-[var(--dash-elev)] p-2.5 rounded-sm border border-slate-200/50 dark:border-[var(--dash-border)] flex flex-col justify-between">
<div>
<div className="flex items-center gap-1.5 mb-2">
<Car className="w-3.5 h-3.5 text-sky-500" />
<span className="text-[10px] font-bold text-slate-700 tracking-wider">代表性产物 / 车型</span>
<Car className="w-3.5 h-3.5 text-sky-500 dark:text-sky-300" />
<span className="text-[10px] font-bold text-slate-700 dark:text-[var(--dash-text)] tracking-wider">代表性产物 / 车型</span>
</div>
{brand.vehicles && brand.vehicles.length > 0 ? (
<div className="flex flex-wrap gap-1.5 mt-1">
{brand.vehicles.map((v, i) => (
<span
key={i}
className="text-[10px] px-2 py-0.5 bg-white border border-slate-200 text-slate-700 rounded-sm font-semibold"
className="text-[10px] px-2 py-0.5 bg-white dark:bg-[var(--dash-card)] border border-slate-200 dark:border-[var(--dash-border)] text-slate-700 dark:text-[var(--dash-text)] rounded-sm font-semibold"
>
{v}
</span>
))}
</div>
) : (
<span className="text-[10px] text-slate-400 italic block mt-1">详情见对应席位专页。</span>
<span className="text-[10px] text-slate-400 dark:text-[var(--dash-text-dim)] italic block mt-1">详情见对应席位专页。</span>
)}
</div>
</div>
@@ -238,9 +238,9 @@ export function BrandScores({ onBrandSelect }: BrandScoresProps) {
</div>
</div>
<div className="mt-6 border-t border-dashed border-slate-200/85 pt-3 text-[10px] text-slate-400 flex items-center justify-between gap-2">
<div className="mt-6 border-t border-dashed border-slate-200/85 dark:border-[var(--dash-border-soft)] pt-3 text-[10px] text-slate-400 dark:text-[var(--dash-text-dim)] flex items-center justify-between gap-2">
<span className="italic">* 排序基于多维综合指标评议。</span>
<span className="px-1.5 py-0.5 bg-slate-50 border border-slate-200 rounded-xs font-mono text-[9px] text-slate-500">
<span className="px-1.5 py-0.5 bg-slate-50 dark:bg-[var(--dash-elev)] border border-slate-200 dark:border-[var(--dash-border)] rounded-xs font-mono text-[9px] text-slate-500 dark:text-[var(--dash-text-sec)]">
AUTO CO-RANK
</span>
</div>
@@ -19,13 +19,13 @@ export function ConsensusPanel({ onExploreClick }: ConsensusPanelProps) {
return (
<DraftingCard className="h-full flex flex-col justify-between" id="consensus-panel">
<div>
<div className="flex items-center gap-2 pb-4 border-b border-rose-100">
<div className="w-2.5 h-2.5 bg-rose-600 rounded-xs animate-pulse" />
<h2 className="text-lg font-bold text-slate-800 tracking-tight font-sans">总控最终共识</h2>
<div className="flex items-center gap-2 pb-4 border-b border-rose-100 dark:border-[var(--dash-border)]">
<div className="w-2.5 h-2.5 bg-rose-600 dark:bg-[var(--dash-primary)] rounded-xs animate-pulse" />
<h2 className="text-lg font-bold text-slate-800 dark:text-[var(--dash-text)] tracking-tight font-sans">总控最终共识</h2>
</div>
{consensus.headline && (
<p className="mt-4 text-sm font-semibold text-slate-700 leading-relaxed bg-rose-50/40 p-3.5 rounded-sm border-l-2 border-rose-500">
<p className="mt-4 text-sm font-semibold text-slate-700 dark:text-[var(--dash-text)] leading-relaxed bg-rose-50/40 dark:bg-[var(--dash-primary-soft)] p-3.5 rounded-sm border-l-2 border-rose-500 dark:border-[var(--dash-primary)]">
{consensus.headline}
</p>
)}
@@ -36,20 +36,20 @@ export function ConsensusPanel({ onExploreClick }: ConsensusPanelProps) {
return (
<li key={idx} className="flex gap-3 text-xs leading-relaxed group">
<Icon className={`w-4.5 h-4.5 mt-0.5 shrink-0 ${POINT_COLORS[idx % POINT_COLORS.length]}`} />
<div className="text-slate-600 group-hover:text-slate-900 transition-colors">{pt}</div>
<div className="text-slate-600 dark:text-[var(--dash-text-sec)] group-hover:text-slate-900 dark:group-hover:text-[var(--dash-text-strong)] transition-colors">{pt}</div>
</li>
);
})}
</ul>
</div>
<div className="mt-6 pt-4 border-t border-slate-100 flex justify-between items-center bg-slate-50/50 -mx-5 -mb-5 px-5 py-4 rounded-b-sm">
<span className="text-[11px] text-slate-400 font-mono tracking-wider uppercase">
<div className="mt-6 pt-4 border-t border-slate-100 dark:border-[var(--dash-border)] flex justify-between items-center bg-slate-50/50 dark:bg-[var(--dash-elev)] -mx-5 -mb-5 px-5 py-4 rounded-b-sm">
<span className="text-[11px] text-slate-400 dark:text-[var(--dash-text-dim)] font-mono tracking-wider uppercase">
Roundtable Final Consensus
</span>
<button
onClick={onExploreClick}
className="text-xs text-rose-600 hover:text-rose-700 font-semibold cursor-pointer flex items-center gap-1 group transition-all"
className="text-xs text-rose-600 dark:text-[var(--dash-primary)] hover:text-rose-700 dark:hover:text-[var(--dash-primary)] font-semibold cursor-pointer flex items-center gap-1 group transition-all"
>
查看研讨详情
<span className="group-hover:translate-x-1 transition-transform">→</span>
@@ -54,28 +54,28 @@ export function DetailView({ activeTab, onBackToOverview, onNavigate }: DetailVi
return (
<div className="space-y-6">
{/* 顶栏 */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 pb-3 border-b border-slate-200">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 pb-3 border-b border-slate-200 dark:border-[var(--dash-border)]">
<div className="flex items-center gap-3">
<button
onClick={onBackToOverview}
className="text-xs text-slate-400 hover:text-rose-600 transition-colors font-medium cursor-pointer border border-slate-200 bg-white px-2.5 py-1.5 rounded-sm"
className="text-xs text-slate-400 hover:text-rose-600 transition-colors font-medium cursor-pointer border border-slate-200 bg-white px-2.5 py-1.5 rounded-sm dark:text-[var(--dash-text-dim)] dark:hover:text-[var(--dash-primary)] dark:border-[var(--dash-border)] dark:bg-[var(--dash-card)]"
>
← 返回总览研讨看盘
</button>
<div className="h-4 w-[1px] bg-slate-200 hidden sm:block" />
<div className="h-4 w-[1px] bg-slate-200 hidden sm:block dark:bg-[var(--dash-border)]" />
<div>
<div className="flex items-center gap-2">
<h2 className="text-base font-bold text-slate-900 tracking-tight font-sans">{seat.title} · 席位专页</h2>
<h2 className="text-base font-bold text-slate-900 tracking-tight font-sans dark:text-[var(--dash-text-strong)]">{seat.title} · 席位专页</h2>
{seat.completeness > 0 && (
<span className="text-[10px] font-bold text-emerald-600 bg-emerald-50 border border-emerald-200 px-1.5 rounded-sm font-sans">
<span className="text-[10px] font-bold text-emerald-600 bg-emerald-50 border border-emerald-200 px-1.5 rounded-sm font-sans dark:text-emerald-300 dark:bg-emerald-500/10 dark:border-emerald-500/30">
达成度: {seat.completeness}%
</span>
)}
</div>
{seat.analyst && <span className="text-xs text-slate-400 font-sans block mt-0.5">席任分析官:{seat.analyst}</span>}
{seat.analyst && <span className="text-xs text-slate-400 font-sans block mt-0.5 dark:text-[var(--dash-text-dim)]">席任分析官:{seat.analyst}</span>}
</div>
</div>
<span className="text-xs font-mono text-slate-400 uppercase tracking-widest hidden md:inline">
<span className="text-xs font-mono text-slate-400 uppercase tracking-widest hidden md:inline dark:text-[var(--dash-text-dim)]">
Seat node: {String(seat.id).toUpperCase()}
</span>
</div>
@@ -85,11 +85,11 @@ export function DetailView({ activeTab, onBackToOverview, onNavigate }: DetailVi
<div className="lg:col-span-2 space-y-6">
{variant !== "release" && (
<DraftingCard hoverEffect={false}>
<div className="flex items-center gap-2 pb-3.5 border-b border-rose-100 mb-4">
<Settings className="w-4 h-4 text-rose-600 animate-spin-slow" />
<h3 className="text-xs font-bold text-slate-800 uppercase tracking-wider">此席位核心交付 · 使命概述</h3>
<div className="flex items-center gap-2 pb-3.5 border-b border-rose-100 mb-4 dark:border-[var(--dash-border)]">
<Settings className="w-4 h-4 text-rose-600 animate-spin-slow dark:text-[var(--dash-primary)]" />
<h3 className="text-xs font-bold text-slate-800 uppercase tracking-wider dark:text-[var(--dash-text)]">此席位核心交付 · 使命概述</h3>
</div>
<p className="text-xs text-slate-600 leading-relaxed font-sans bg-slate-50 p-3.5 rounded-sm border border-slate-200/40">
<p className="text-xs text-slate-600 leading-relaxed font-sans bg-slate-50 p-3.5 rounded-sm border border-slate-200/40 dark:text-[var(--dash-text-sec)] dark:bg-[var(--dash-elev)] dark:border-[var(--dash-border)]">
{seat.description || "(无摘要)"}
</p>
</DraftingCard>
@@ -102,20 +102,20 @@ export function DetailView({ activeTab, onBackToOverview, onNavigate }: DetailVi
<div className="space-y-6">
{seat.metrics.length > 0 && (
<DraftingCard hoverEffect={false}>
<div className="pb-3 border-b border-rose-100 mb-4">
<h4 className="text-xs font-bold text-slate-800 uppercase tracking-wider">席位评定量化得分</h4>
<div className="pb-3 border-b border-rose-100 mb-4 dark:border-[var(--dash-border)]">
<h4 className="text-xs font-bold text-slate-800 uppercase tracking-wider dark:text-[var(--dash-text)]">席位评定量化得分</h4>
</div>
<div className="space-y-4">
{seat.metrics.map((met, i) => (
<div key={i} className="p-4 bg-slate-50 border border-slate-100 rounded-md">
<div key={i} className="p-4 bg-slate-50 border border-slate-100 rounded-md dark:bg-[var(--dash-elev)] dark:border-[var(--dash-border)]">
<div className="flex justify-between items-baseline mb-2">
<span className="text-[11px] text-slate-500">{met.label}</span>
<span className="text-[11px] text-slate-500 dark:text-[var(--dash-text-sec)]">{met.label}</span>
<span className="text-2xl font-black font-mono tracking-tight" style={{ color }}>
{met.value}
<small className="text-xs font-normal text-slate-400 ml-0.5">分</small>
</span>
</div>
<div className="w-full h-1.5 bg-slate-200/70 rounded-full overflow-hidden">
<div className="w-full h-1.5 bg-slate-200/70 rounded-full overflow-hidden dark:bg-[var(--dash-border)]">
<div className="h-full rounded-full" style={{ width: `${met.value}%`, backgroundColor: color }} />
</div>
</div>
@@ -125,10 +125,10 @@ export function DetailView({ activeTab, onBackToOverview, onNavigate }: DetailVi
)}
{seat.risks.length > 0 && (
<DraftingCard hoverEffect={false} className="border-amber-200 bg-amber-50/5">
<div className="flex items-center gap-1.5 pb-3 border-b border-amber-100 mb-4">
<ShieldAlert className="w-4 h-4 text-amber-600" />
<h4 className="text-xs font-bold text-amber-800 uppercase tracking-wider">此专项下属风险与关注点</h4>
<DraftingCard hoverEffect={false} className="border-amber-200 bg-amber-50/5 dark:border-amber-500/30 dark:bg-amber-500/5">
<div className="flex items-center gap-1.5 pb-3 border-b border-amber-100 mb-4 dark:border-amber-500/30">
<ShieldAlert className="w-4 h-4 text-amber-600 dark:text-amber-300" />
<h4 className="text-xs font-bold text-amber-800 uppercase tracking-wider dark:text-amber-300">此专项下属风险与关注点</h4>
</div>
<div className="space-y-3">
{seat.risks.map((risk) => (
@@ -150,15 +150,15 @@ export function DetailView({ activeTab, onBackToOverview, onNavigate }: DetailVi
{(deps.length > 0 || supports.length > 0) && (
<DraftingCard hoverEffect={false}>
<div className="pb-3 border-b border-rose-100 mb-4">
<h4 className="text-xs font-bold text-slate-800 uppercase tracking-wider">席位依赖关系</h4>
<div className="pb-3 border-b border-rose-100 mb-4 dark:border-[var(--dash-border)]">
<h4 className="text-xs font-bold text-slate-800 uppercase tracking-wider dark:text-[var(--dash-text)]">席位依赖关系</h4>
</div>
<div className="flex flex-wrap gap-2">
{deps.map((d) => (
<button
key={d.id}
onClick={() => go(d.id)}
className="text-[11px] font-semibold px-2.5 py-1 border border-dashed border-rose-400 text-rose-600 rounded-sm hover:bg-rose-50 transition-colors"
className="text-[11px] font-semibold px-2.5 py-1 border border-dashed border-rose-400 text-rose-600 rounded-sm hover:bg-rose-50 transition-colors dark:border-[var(--dash-primary)] dark:text-[var(--dash-primary)] dark:hover:bg-[var(--dash-primary-soft)]"
>
依赖 → {d.title}
</button>
@@ -167,7 +167,7 @@ export function DetailView({ activeTab, onBackToOverview, onNavigate }: DetailVi
<button
key={s.id}
onClick={() => go(s.id)}
className="text-[11px] font-semibold px-2.5 py-1 border border-dashed border-sky-400 text-sky-600 rounded-sm hover:bg-sky-50 transition-colors"
className="text-[11px] font-semibold px-2.5 py-1 border border-dashed border-sky-400 text-sky-600 rounded-sm hover:bg-sky-50 transition-colors dark:border-sky-500/50 dark:text-sky-300 dark:hover:bg-sky-500/10"
>
{s.title} ← 承接本席位
</button>
@@ -177,19 +177,19 @@ export function DetailView({ activeTab, onBackToOverview, onNavigate }: DetailVi
)}
{recommended.length > 0 && (
<DraftingCard hoverEffect={false} className="bg-slate-50/50">
<div className="pb-2.5 border-b border-slate-200 mb-3.5">
<h4 className="text-xs font-bold text-slate-700 uppercase tracking-wider">研讨协同席位推荐</h4>
<DraftingCard hoverEffect={false} className="bg-slate-50/50 dark:bg-[var(--dash-elev)]">
<div className="pb-2.5 border-b border-slate-200 mb-3.5 dark:border-[var(--dash-border)]">
<h4 className="text-xs font-bold text-slate-700 uppercase tracking-wider dark:text-[var(--dash-text)]">研讨协同席位推荐</h4>
</div>
<div className="space-y-2">
{recommended.map((other) => (
<button
key={other.id}
onClick={() => go(other.id)}
className="w-full p-2 bg-white hover:bg-slate-50 border border-slate-200 rounded cursor-pointer flex justify-between items-center text-xs text-slate-700 hover:text-rose-600 hover:border-slate-300 transition-all"
className="w-full p-2 bg-white hover:bg-slate-50 border border-slate-200 rounded cursor-pointer flex justify-between items-center text-xs text-slate-700 hover:text-rose-600 hover:border-slate-300 transition-all dark:bg-[var(--dash-card)] dark:hover:bg-[var(--dash-elev)] dark:border-[var(--dash-border)] dark:text-[var(--dash-text)] dark:hover:text-[var(--dash-primary)]"
>
<span className="font-semibold truncate">{other.title}</span>
<span className="text-[10px] text-slate-400 shrink-0">点击切出 →</span>
<span className="text-[10px] text-slate-400 shrink-0 dark:text-[var(--dash-text-dim)]">点击切出 →</span>
</button>
))}
</div>
@@ -22,22 +22,16 @@ export function DraftingCard({
<div
id={id || undefined}
onClick={onClick}
className={`relative bg-white rounded-sm p-5 border border-slate-200/80 transition-all duration-300 shadow-[0_3px_10px_rgba(15,23,42,0.04)] ${
className={`relative bg-white dark:bg-[var(--dash-card)] rounded-sm p-5 border border-slate-200/80 dark:border-[var(--dash-border)] transition-all duration-300 shadow-[0_3px_10px_rgba(15,23,42,0.04)] dark:shadow-[0_2px_8px_rgba(6,14,36,0.5)] ${
hoverEffect
? "hover:shadow-[0_8px_24px_rgba(15,23,42,0.08)] hover:-translate-y-[2px] hover:border-slate-300"
? "hover:shadow-[0_8px_24px_rgba(15,23,42,0.08)] dark:hover:shadow-[0_4px_16px_rgba(22,119,255,0.24)] hover:-translate-y-[2px] hover:border-slate-300 dark:hover:border-[var(--dash-border-soft)]"
: ""
} ${className}`}
>
{/* 左上角标(品牌正红 #F5222D,对齐设计原图) */}
<div
className="absolute top-0 left-0 w-3 h-3 border-t-2 border-l-2 -mt-[1px] -ml-[1px]"
style={{ borderColor: "#F5222D" }}
/>
{/* 左上角标(浅色品牌正红 #F5222D / 深色主蓝,取色见 dashboard.css 的 .dash-corner) */}
<div className="dash-corner absolute top-0 left-0 w-3 h-3 border-t-2 border-l-2 -mt-[1px] -ml-[1px]" />
{/* 右下角标 */}
<div
className="absolute bottom-0 right-0 w-3 h-3 border-b-2 border-r-2 -mb-[1px] -mr-[1px]"
style={{ borderColor: "#F5222D" }}
/>
<div className="dash-corner absolute bottom-0 right-0 w-3 h-3 border-b-2 border-r-2 -mb-[1px] -mr-[1px]" />
{/* 极淡绘图网格 overlay */}
<div className="absolute inset-0 bg-grid-slate-100 [mask-image:linear-gradient(0deg,white,rgba(255,255,255,0.6))] pointer-events-none rounded-sm opacity-30" />
<div className="relative z-10">{children}</div>
@@ -27,24 +27,24 @@ export function MetricsHeader({ onCardClick }: MetricsHeaderProps) {
onClick={() => onCardClick?.(metric.id)}
>
<div className="flex justify-between items-start gap-2">
<span className="text-xs text-slate-500 font-sans tracking-wide font-medium">
<span className="text-xs text-slate-500 dark:text-[var(--dash-text-sec)] font-sans tracking-wide font-medium">
{metric.label}
</span>
<Icon className="w-5 h-5 shrink-0" style={{ color: metric.color }} />
</div>
<div className="mt-2 flex items-baseline gap-1">
<span className="text-3xl font-bold text-slate-800 tracking-tight font-sans">
<span className="text-3xl font-bold text-slate-800 dark:text-[var(--dash-text)] tracking-tight font-sans">
{metric.value}
</span>
{metric.unit && <span className="text-xs text-slate-400 font-medium ml-0.5">{metric.unit}</span>}
{metric.unit && <span className="text-xs text-slate-400 dark:text-[var(--dash-text-dim)] font-medium ml-0.5">{metric.unit}</span>}
{metric.trend && (
<span
className={`inline-flex items-center text-xs font-semibold px-1 rounded-sm ml-2 ${
metric.trend.type === "up"
? "text-emerald-600 bg-emerald-50"
: "text-rose-600 bg-rose-50"
? "text-emerald-600 bg-emerald-50 dark:text-emerald-300 dark:bg-emerald-500/10"
: "text-rose-600 bg-rose-50 dark:text-rose-300 dark:bg-rose-500/10"
}`}
>
{metric.trend.type === "up" ? "▲" : "▼"}
@@ -30,13 +30,13 @@ export function RiskDonut({ onCategoryHover }: RiskDonutProps) {
return (
<DraftingCard className="h-full flex flex-col justify-between" id="risk-donut-card">
<div>
<div className="flex items-center gap-2 pb-4 border-b border-rose-100 mb-5">
<ShieldAlert className="w-5 h-5 text-rose-600" />
<h3 className="text-sm font-bold text-slate-800 tracking-tight font-sans">核心风险分类分布</h3>
<div className="flex items-center gap-2 pb-4 border-b border-rose-100 dark:border-[var(--dash-border)] mb-5">
<ShieldAlert className="w-5 h-5 text-rose-600 dark:text-[var(--dash-primary)]" />
<h3 className="text-sm font-bold text-slate-800 dark:text-[var(--dash-text)] tracking-tight font-sans">核心风险分类分布</h3>
</div>
{empty ? (
<div className="flex flex-col items-center justify-center gap-2 text-slate-400 py-12">
<div className="flex flex-col items-center justify-center gap-2 text-slate-400 dark:text-[var(--dash-text-dim)] py-12">
<ShieldAlert className="w-8 h-8 text-emerald-400" />
<span className="text-xs">本轮研讨未识别到显式风险项。</span>
</div>
@@ -44,11 +44,11 @@ export function RiskDonut({ onCategoryHover }: RiskDonutProps) {
<>
<div className="flex flex-row items-center gap-5 py-2">
{/* 风险总数(去掉百分比圆环,保留大数字,对齐设计原图) */}
<div className="flex flex-col items-center justify-center shrink-0 px-3 py-2 border-r border-slate-100">
<span className="text-4xl font-extrabold text-slate-800 font-mono leading-none">
<div className="flex flex-col items-center justify-center shrink-0 px-3 py-2 border-r border-slate-100 dark:border-[var(--dash-border)]">
<span className="text-4xl font-extrabold text-slate-800 dark:text-[var(--dash-text)] font-mono leading-none">
{activeCategory ? `${activeCategory.percentage}%` : riskTotal}
</span>
<span className="text-[10px] text-slate-400 font-medium tracking-wider mt-1.5 text-center max-w-[60px] leading-tight">
<span className="text-[10px] text-slate-400 dark:text-[var(--dash-text-dim)] font-medium tracking-wider mt-1.5 text-center max-w-[60px] leading-tight">
{activeCategory ? activeCategory.name : "项核心风险"}
</span>
</div>
@@ -61,7 +61,7 @@ export function RiskDonut({ onCategoryHover }: RiskDonutProps) {
<div
key={cat.name}
className={`flex items-start gap-2.5 p-1.5 rounded-xs transition-colors cursor-pointer ${
isSelected ? "bg-slate-50 border-l-2" : "border-l-2 border-transparent"
isSelected ? "bg-slate-50 dark:bg-[var(--dash-elev)] border-l-2" : "border-l-2 border-transparent"
}`}
style={{ borderLeftColor: isSelected ? cat.color : undefined }}
onMouseEnter={() => handleMouseEnter(cat)}
@@ -70,8 +70,8 @@ export function RiskDonut({ onCategoryHover }: RiskDonutProps) {
<div className="w-2.5 h-2.5 rounded-full mt-1 shrink-0" style={{ backgroundColor: cat.color }} />
<div className="text-left">
<div className="flex justify-between items-baseline gap-2">
<span className="text-xs font-semibold text-slate-800">{cat.name}</span>
<span className="text-xs font-bold text-slate-900 font-mono">{cat.value}%</span>
<span className="text-xs font-semibold text-slate-800 dark:text-[var(--dash-text)]">{cat.name}</span>
<span className="text-xs font-bold text-slate-900 dark:text-[var(--dash-text-strong)] font-mono">{cat.value}%</span>
</div>
</div>
</div>
@@ -81,20 +81,20 @@ export function RiskDonut({ onCategoryHover }: RiskDonutProps) {
</div>
{/* 悬停细项抽屉 */}
<div className="mt-4 min-h-[90px] bg-slate-50 border border-slate-100 rounded-sm p-3 flex flex-col justify-center">
<div className="mt-4 min-h-[90px] bg-slate-50 dark:bg-[var(--dash-elev)] border border-slate-100 dark:border-[var(--dash-border)] rounded-sm p-3 flex flex-col justify-center">
{activeCategory ? (
<div className="space-y-1">
<span className="text-[10px] font-bold uppercase tracking-wider block" style={{ color: activeCategory.color }}>
{activeCategory.name}包含细项:
</span>
<ul className="text-[11px] text-slate-600 list-disc list-inside space-y-0.5">
<ul className="text-[11px] text-slate-600 dark:text-[var(--dash-text-sec)] list-disc list-inside space-y-0.5">
{activeCategory.details.slice(0, 3).map((det, i) => (
<li key={i} className="truncate">{det}</li>
))}
</ul>
</div>
) : (
<div className="flex items-center gap-2.5 text-slate-400 justify-center">
<div className="flex items-center gap-2.5 text-slate-400 dark:text-[var(--dash-text-dim)] justify-center">
<ShieldAlert className="w-4 h-4" />
<span className="text-xs">悬停上方分类标签,查看各项底层细节。</span>
</div>
@@ -104,9 +104,9 @@ export function RiskDonut({ onCategoryHover }: RiskDonutProps) {
)}
</div>
<div className="mt-5 border-t border-dashed border-slate-100 pt-3 text-[10px] text-slate-400 flex items-center justify-between">
<div className="mt-5 border-t border-dashed border-slate-100 dark:border-[var(--dash-border-soft)] pt-3 text-[10px] text-slate-400 dark:text-[var(--dash-text-dim)] flex items-center justify-between">
<span>风险按高/中/低等级聚合占比</span>
<span className="font-semibold text-rose-600 px-1 bg-rose-50 border border-rose-200 rounded-xs">
<span className="font-semibold text-rose-600 dark:text-[var(--dash-primary)] px-1 bg-rose-50 dark:bg-[var(--dash-primary-soft)] border border-rose-200 dark:border-[var(--dash-border)] rounded-xs">
共 {riskTotal} 项
</span>
</div>
@@ -19,12 +19,12 @@ export function Roadmap() {
return (
<div className="space-y-5">
<div className="flex items-center justify-between pb-1 border-b border-rose-100">
<h3 className="text-base font-bold text-slate-800 tracking-tight font-sans flex items-center gap-2">
<div className="flex items-center justify-between pb-1 border-b border-rose-100 dark:border-[var(--dash-border)]">
<h3 className="text-base font-bold text-slate-800 dark:text-[var(--dash-text)] tracking-tight font-sans flex items-center gap-2">
执行路线
<span className="text-xs font-normal text-slate-400 font-sans">点击阶段卡片展开关键里程碑</span>
<span className="text-xs font-normal text-slate-400 dark:text-[var(--dash-text-dim)] font-sans">点击阶段卡片展开关键里程碑</span>
</h3>
<span className="text-[10px] text-rose-600 font-mono font-medium px-2 py-0.5 bg-rose-50 border border-rose-200/50">
<span className="text-[10px] text-rose-600 dark:text-[var(--dash-primary)] font-mono font-medium px-2 py-0.5 bg-rose-50 dark:bg-[var(--dash-primary-soft)] border border-rose-200/50 dark:border-[var(--dash-border)]">
共 {ROADMAP_PHASES.length} 个阶段
</span>
</div>
@@ -36,42 +36,42 @@ export function Roadmap() {
return (
<DraftingCard
key={phase.id}
className={`flex flex-col justify-between transition-all duration-300 text-left overflow-hidden group min-h-[160px] border-l-[5px] border-l-rose-600 hover:border-l-rose-700 ${
className={`flex flex-col justify-between transition-all duration-300 text-left overflow-hidden group min-h-[160px] border-l-[5px] border-l-rose-600 hover:border-l-rose-700 dark:border-l-[var(--dash-primary)] dark:hover:border-l-[var(--dash-primary)] ${
hasHighlights ? "cursor-pointer" : ""
} ${isExpanded ? "bg-slate-50 border-rose-400 shadow-md ring-1 ring-rose-300/40" : ""}`}
} ${isExpanded ? "bg-slate-50 border-rose-400 shadow-md ring-1 ring-rose-300/40 dark:bg-[var(--dash-elev)] dark:border-[var(--dash-primary)] dark:ring-[var(--dash-primary)]/30" : ""}`}
onClick={hasHighlights ? () => togglePhase(phase.id) : undefined}
>
<div>
<div className="flex justify-between items-start gap-2 mb-2">
<div className="space-y-0.5">
<span className="text-xs font-extrabold text-rose-600 font-sans block uppercase tracking-wide">
<span className="text-xs font-extrabold text-rose-600 dark:text-[var(--dash-primary)] font-sans block uppercase tracking-wide">
{phase.id}
</span>
<h4 className="text-sm font-extrabold text-slate-800 font-sans">{phase.title}</h4>
<h4 className="text-sm font-extrabold text-slate-800 dark:text-[var(--dash-text)] font-sans">{phase.title}</h4>
</div>
{phase.period && (
<span className="text-[11px] font-bold text-slate-400 font-mono bg-slate-100 border border-slate-200 px-2 py-0.5 rounded-sm shrink-0">
<span className="text-[11px] font-bold text-slate-400 dark:text-[var(--dash-text-dim)] font-mono bg-slate-100 dark:bg-[var(--dash-elev)] border border-slate-200 dark:border-[var(--dash-border)] px-2 py-0.5 rounded-sm shrink-0">
{phase.period}
</span>
)}
</div>
<p className="text-xs text-slate-600 leading-relaxed font-sans mt-2.5">{phase.description}</p>
<p className="text-xs text-slate-600 dark:text-[var(--dash-text-sec)] leading-relaxed font-sans mt-2.5">{phase.description}</p>
{hasHighlights && isExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="mt-4 pt-3.5 border-t border-slate-200/80 space-y-2 overflow-hidden"
className="mt-4 pt-3.5 border-t border-slate-200/80 dark:border-[var(--dash-border)] space-y-2 overflow-hidden"
>
<span className="text-[10px] font-bold text-rose-600 uppercase tracking-widest block mb-1">
<span className="text-[10px] font-bold text-rose-600 dark:text-[var(--dash-primary)] uppercase tracking-widest block mb-1">
此阶段关键里程碑:
</span>
<ul className="space-y-2">
{phase.highlights.map((hlt, idx) => (
<li key={idx} className="flex items-start gap-2 text-xs text-slate-700">
<CheckCircle2 className="w-3.5 h-3.5 text-rose-500 shrink-0 mt-0.5" />
<li key={idx} className="flex items-start gap-2 text-xs text-slate-700 dark:text-[var(--dash-text)]">
<CheckCircle2 className="w-3.5 h-3.5 text-rose-500 dark:text-[var(--dash-primary)] shrink-0 mt-0.5" />
<span>{hlt}</span>
</li>
))}
@@ -80,10 +80,10 @@ export function Roadmap() {
)}
</div>
<div className="mt-4 pt-2.5 border-t border-slate-100 flex items-center justify-between text-[10px] text-slate-400">
<div className="mt-4 pt-2.5 border-t border-slate-100 dark:border-[var(--dash-border)] flex items-center justify-between text-[10px] text-slate-400 dark:text-[var(--dash-text-dim)]">
<span className="font-mono uppercase tracking-wider">Timeline Segment {phase.id}</span>
{hasHighlights && (
<div className="flex items-center gap-1 font-semibold text-rose-600 hover:text-rose-700">
<div className="flex items-center gap-1 font-semibold text-rose-600 hover:text-rose-700 dark:text-[var(--dash-primary)] dark:hover:text-[var(--dash-primary)]">
<span>{isExpanded ? "收起" : "详细指标"}</span>
{isExpanded ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
</div>
@@ -22,9 +22,9 @@ export function SeatDeliveryOverview({ onSeatSelect, activeSeatId }: SeatDeliver
return (
<div className="space-y-6">
<div className="flex flex-col sm:flex-row items-baseline justify-between gap-2">
<h3 className="text-base font-bold text-slate-800 tracking-tight font-sans flex items-center gap-2">
<h3 className="text-base font-bold text-slate-800 dark:text-[var(--dash-text)] tracking-tight font-sans flex items-center gap-2">
席位交付总览
<span className="text-xs font-normal text-slate-400 font-sans">点击卡片快速切入对应专属席位页</span>
<span className="text-xs font-normal text-slate-400 dark:text-[var(--dash-text-dim)] font-sans">点击卡片快速切入对应专属席位页</span>
</h3>
</div>
@@ -36,39 +36,39 @@ export function SeatDeliveryOverview({ onSeatSelect, activeSeatId }: SeatDeliver
<DraftingCard
key={seat.id}
className={`flex flex-col justify-between h-full cursor-pointer relative ${
isActive ? "ring-2 ring-rose-500/70 bg-rose-50/5 border-rose-300" : ""
isActive ? "ring-2 ring-rose-500/70 bg-rose-50/5 border-rose-300 dark:ring-[var(--dash-primary)]/60 dark:bg-[var(--dash-primary)]/5 dark:border-[var(--dash-primary)]" : ""
}`}
onClick={() => onSeatSelect(seat.id)}
>
<div>
<div className="flex items-center justify-between pb-3 border-b border-slate-100">
<div className="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-[var(--dash-border)]">
<div className="flex items-center gap-2 min-w-0">
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: color }} />
<span className="text-xs font-bold text-slate-800 font-sans truncate">{seat.title}</span>
<span className="text-xs font-bold text-slate-800 dark:text-[var(--dash-text)] font-sans truncate">{seat.title}</span>
</div>
<div className="flex items-center gap-0.5 text-[10px] text-slate-400 font-sans hover:text-slate-600 shrink-0">
<div className="flex items-center gap-0.5 text-[10px] text-slate-400 dark:text-[var(--dash-text-dim)] font-sans hover:text-slate-600 dark:hover:text-[var(--dash-text-sec)] shrink-0">
<span>席位页</span>
<ArrowRight className="w-3 h-3" />
</div>
</div>
{seat.analyst && (
<div className="mt-3 text-slate-400 text-[10px] uppercase font-mono font-bold tracking-wider truncate">
<div className="mt-3 text-slate-400 dark:text-[var(--dash-text-dim)] text-[10px] uppercase font-mono font-bold tracking-wider truncate">
汇报人: {seat.analyst}
</div>
)}
<p className="mt-2 text-xs text-slate-600 leading-relaxed line-clamp-3">{seat.description}</p>
<p className="mt-2 text-xs text-slate-600 dark:text-[var(--dash-text-sec)] leading-relaxed line-clamp-3">{seat.description}</p>
{seat.metrics.length > 0 && (
<div className="mt-4 space-y-2.5">
{seat.metrics.slice(0, 2).map((met, i) => (
<div key={i} className="space-y-1">
<div className="flex justify-between text-[10px] font-medium text-slate-500">
<div className="flex justify-between text-[10px] font-medium text-slate-500 dark:text-[var(--dash-text-sec)]">
<span className="truncate">{met.label}</span>
<span className="font-bold text-slate-800 font-mono shrink-0">{met.value}</span>
<span className="font-bold text-slate-800 dark:text-[var(--dash-text)] font-mono shrink-0">{met.value}</span>
</div>
<div className="w-full h-1.5 bg-slate-100 rounded-full overflow-hidden">
<div className="w-full h-1.5 bg-slate-100 dark:bg-[var(--dash-inset)] rounded-full overflow-hidden">
<div
className="h-full rounded-full"
style={{ width: `${met.value}%`, backgroundColor: ACCENTS[i % ACCENTS.length] }}
@@ -81,16 +81,16 @@ export function SeatDeliveryOverview({ onSeatSelect, activeSeatId }: SeatDeliver
</div>
{seat.risks.length > 0 && (
<div className="mt-5 pt-3.5 border-t border-slate-100 space-y-1.5">
<div className="mt-5 pt-3.5 border-t border-slate-100 dark:border-[var(--dash-border)] space-y-1.5">
{seat.risks.slice(0, 3).map((risk) => (
<div
key={risk.id}
className={`text-[9px] px-2 py-0.5 rounded-sm flex items-center gap-1 border-l-2 ${
risk.level === "high"
? "bg-red-50 text-red-700 border-red-500 border border-red-200/50"
? "bg-red-50 text-red-700 border-red-500 border border-red-200/50 dark:bg-red-500/10 dark:text-red-300 dark:border-red-500/40"
: risk.level === "low"
? "bg-emerald-50 text-emerald-700 border-emerald-500 border border-emerald-200/50"
: "bg-amber-50 text-amber-700 border-amber-500 border border-amber-200/50"
? "bg-emerald-50 text-emerald-700 border-emerald-500 border border-emerald-200/50 dark:bg-emerald-500/10 dark:text-emerald-300 dark:border-emerald-500/40"
: "bg-amber-50 text-amber-700 border-amber-500 border border-amber-200/50 dark:bg-amber-500/10 dark:text-amber-300 dark:border-amber-500/40"
}`}
>
{risk.level === "high" ? (
@@ -110,27 +110,27 @@ export function SeatDeliveryOverview({ onSeatSelect, activeSeatId }: SeatDeliver
{summarySeat && (
<DraftingCard
className={`cursor-pointer ${activeSeatId === summarySeat.id ? "ring-2 ring-rose-500 bg-rose-50/10" : ""}`}
className={`cursor-pointer ${activeSeatId === summarySeat.id ? "ring-2 ring-rose-500 bg-rose-50/10 dark:ring-[var(--dash-primary)] dark:bg-[var(--dash-primary)]/10" : ""}`}
onClick={() => onSeatSelect(summarySeat.id)}
>
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<div className="flex items-start md:items-center gap-3">
<div className="p-2.5 bg-sky-100 border border-sky-300 rounded-sm">
<FileText className="w-5 h-5 text-sky-600" />
<div className="p-2.5 bg-sky-100 border border-sky-300 rounded-sm dark:bg-sky-500/15 dark:border-sky-500/40">
<FileText className="w-5 h-5 text-sky-600 dark:text-sky-300" />
</div>
<div>
<div className="flex items-center gap-2">
<span className="w-2.5 h-2.5 rounded-full bg-sky-500" />
<h4 className="text-sm font-bold text-slate-950 font-sans">{summarySeat.title}</h4>
<span className="text-[10px] px-2 py-0.5 bg-sky-100/50 text-sky-800 rounded-full font-mono border border-sky-200/40 font-semibold uppercase tracking-wider">
<h4 className="text-sm font-bold text-slate-950 dark:text-[var(--dash-text-strong)] font-sans">{summarySeat.title}</h4>
<span className="text-[10px] px-2 py-0.5 bg-sky-100/50 text-sky-800 rounded-full font-mono border border-sky-200/40 font-semibold uppercase tracking-wider dark:bg-sky-500/15 dark:text-sky-300 dark:border-sky-500/30">
决策中心终审
</span>
</div>
<p className="mt-1.5 text-xs text-slate-600 leading-relaxed max-w-4xl">{summarySeat.description}</p>
<p className="mt-1.5 text-xs text-slate-600 dark:text-[var(--dash-text-sec)] leading-relaxed max-w-4xl">{summarySeat.description}</p>
</div>
</div>
<div className="shrink-0 flex items-center gap-1 text-xs text-sky-600 hover:text-sky-700 font-semibold group bg-sky-50 px-3.5 py-2.5 rounded-sm border border-sky-100">
<div className="shrink-0 flex items-center gap-1 text-xs text-sky-600 hover:text-sky-700 font-semibold group bg-sky-50 px-3.5 py-2.5 rounded-sm border border-sky-100 dark:bg-sky-500/10 dark:text-sky-300 dark:border-sky-500/30">
<span>进入 {summarySeat.title} 专页</span>
<span className="group-hover:translate-x-1 transition-transform">→</span>
</div>
@@ -40,7 +40,7 @@ function allBullets(groups: SeatSection[]): string[] {
function EmptyShowcase() {
return (
<DraftingCard hoverEffect={false}>
<p className="py-6 text-center text-xs text-slate-400">该席位暂无结构化汇报内容。</p>
<p className="py-6 text-center text-xs text-slate-400 dark:text-[var(--dash-text-dim)]">该席位暂无结构化汇报内容。</p>
</DraftingCard>
);
}
@@ -82,7 +82,7 @@ function MatrixShowcase({ groups, color }: { groups: SeatSection[]; color: strin
return (
<div className="space-y-5">
<DraftingCard hoverEffect={false}>
<div className="mb-4 flex items-center gap-1.5 border-b border-rose-100 pb-3.5 text-xs font-bold uppercase tracking-widest text-rose-700">
<div className="mb-4 flex items-center gap-1.5 border-b border-rose-100 pb-3.5 text-xs font-bold uppercase tracking-widest text-rose-700 dark:border-[var(--dash-border)] dark:text-[var(--dash-primary)]">
<Building2 className="h-4 w-4 text-emerald-600" />
<span>结构化框架 · 维度拆解</span>
</div>
@@ -90,14 +90,14 @@ function MatrixShowcase({ groups, color }: { groups: SeatSection[]; color: strin
{groups.map((g, i) => (
<div
key={i}
className="rounded-sm border border-slate-200 bg-slate-50/60 p-3 text-center"
className="rounded-sm border border-slate-200 bg-slate-50/60 p-3 text-center dark:border-[var(--dash-border)] dark:bg-[var(--dash-elev)]"
style={{ borderTopColor: color, borderTopWidth: 2 }}
>
<div className="mb-1 text-[10px] font-bold uppercase tracking-wider text-slate-400">
<div className="mb-1 text-[10px] font-bold uppercase tracking-wider text-slate-400 dark:text-[var(--dash-text-dim)]">
维度 {i + 1}
</div>
<div className="truncate text-xs font-semibold text-slate-800">{g.heading || `小节 ${i + 1}`}</div>
<span className="mt-1.5 inline-block rounded-full border border-current bg-white/70 px-1.5 py-0.5 font-mono text-[9px] text-slate-500">
<div className="truncate text-xs font-semibold text-slate-800 dark:text-[var(--dash-text)]">{g.heading || `小节 ${i + 1}`}</div>
<span className="mt-1.5 inline-block rounded-full border border-current bg-white/70 px-1.5 py-0.5 font-mono text-[9px] text-slate-500 dark:bg-[var(--dash-card)] dark:text-[var(--dash-text-sec)]">
{g.bullets.length} 项要点
</span>
</div>
@@ -106,28 +106,28 @@ function MatrixShowcase({ groups, color }: { groups: SeatSection[]; color: strin
</DraftingCard>
<DraftingCard hoverEffect={false}>
<div className="overflow-hidden rounded-md border border-slate-200/60 bg-slate-50">
<div className="flex items-center justify-between border-b border-slate-200 bg-slate-100 p-3">
<span className="text-xs font-bold text-slate-700">各维度关键交付要点对标</span>
<span className="rounded-sm bg-slate-200/50 px-2 py-0.5 font-mono text-[10px] text-slate-600">
<div className="overflow-hidden rounded-md border border-slate-200/60 bg-slate-50 dark:border-[var(--dash-border)] dark:bg-[var(--dash-elev)]">
<div className="flex items-center justify-between border-b border-slate-200 bg-slate-100 p-3 dark:border-[var(--dash-border)] dark:bg-[var(--dash-inset)]">
<span className="text-xs font-bold text-slate-700 dark:text-[var(--dash-text)]">各维度关键交付要点对标</span>
<span className="rounded-sm bg-slate-200/50 px-2 py-0.5 font-mono text-[10px] text-slate-600 dark:bg-[var(--dash-inset)] dark:text-[var(--dash-text-sec)]">
共 {groups.reduce((n, g) => n + g.bullets.length, 0)} 项
</span>
</div>
<table className="w-full text-left text-xs">
<thead className="border-b border-slate-200 bg-slate-50 font-sans text-[10px] uppercase tracking-wider text-slate-500">
<thead className="border-b border-slate-200 bg-slate-50 font-sans text-[10px] uppercase tracking-wider text-slate-500 dark:border-[var(--dash-border)] dark:bg-[var(--dash-elev)] dark:text-[var(--dash-text-sec)]">
<tr>
<th className="px-3 py-2.5">序号</th>
<th className="px-4 py-2.5">所属维度</th>
<th className="px-4 py-2.5">关键交付要点</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 text-slate-700">
<tbody className="divide-y divide-slate-100 text-slate-700 dark:divide-[var(--dash-border)] dark:text-[var(--dash-text)]">
{groups.flatMap((g, gi) =>
g.bullets.map((b, bi) => {
rowNo += 1;
return (
<tr key={`${gi}-${bi}`} className={rowNo % 2 === 0 ? "bg-white/50" : ""}>
<td className="px-3 py-2.5 font-mono font-semibold text-slate-400">{String(rowNo).padStart(2, "0")}</td>
<tr key={`${gi}-${bi}`} className={rowNo % 2 === 0 ? "bg-white/50 dark:bg-[var(--dash-card)]" : ""}>
<td className="px-3 py-2.5 font-mono font-semibold text-slate-400 dark:text-[var(--dash-text-dim)]">{String(rowNo).padStart(2, "0")}</td>
<td className="px-4 py-2.5 font-semibold" style={{ color }}>
{bi === 0 ? g.heading || `维度 ${gi + 1}` : ""}
</td>
@@ -151,20 +151,20 @@ function GaugeShowcase({ seat, groups, color }: { seat: SeatCard; groups: SeatSe
<DraftingCard hoverEffect={false}>
<div className="mb-4 flex items-center gap-2">
<Sparkles className="h-5 w-5" style={{ color }} />
<span className="text-xs font-bold text-slate-800">核心洞察聚焦 · 关键论点拆解</span>
<span className="text-xs font-bold text-slate-800 dark:text-[var(--dash-text)]">核心洞察聚焦 · 关键论点拆解</span>
</div>
<div
className="mb-5 rounded-md border p-4"
style={{ backgroundColor: `${color}0d`, borderColor: `${color}33` }}
>
<span className="mb-1 block text-[10px] font-bold uppercase tracking-wider text-slate-400">本席位核心论断</span>
<p className="text-xs font-medium leading-relaxed text-slate-700">{seat.description || "(无摘要)"}</p>
<span className="mb-1 block text-[10px] font-bold uppercase tracking-wider text-slate-400 dark:text-[var(--dash-text-dim)]">本席位核心论断</span>
<p className="text-xs font-medium leading-relaxed text-slate-700 dark:text-[var(--dash-text)]">{seat.description || "(无摘要)"}</p>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{bullets.map((b, i) => (
<div key={i} className="overflow-hidden rounded-sm border border-slate-200 bg-white">
<div key={i} className="overflow-hidden rounded-sm border border-slate-200 bg-white dark:border-[var(--dash-border)] dark:bg-[var(--dash-card)]">
<div className="h-1 w-full" style={{ backgroundColor: color }} />
<div className="flex gap-2.5 p-3">
<span
@@ -173,7 +173,7 @@ function GaugeShowcase({ seat, groups, color }: { seat: SeatCard; groups: SeatSe
>
{i + 1}
</span>
<p className="text-[11px] leading-relaxed text-slate-600">{b}</p>
<p className="text-[11px] leading-relaxed text-slate-600 dark:text-[var(--dash-text-sec)]">{b}</p>
</div>
</div>
))}
@@ -218,7 +218,7 @@ function CompareShowcase({ groups, color }: { groups: SeatSection[]; color: stri
{p.bullets.map((b, j) => (
<li key={j} className="flex gap-2">
<span className={`mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full ${t.dot}`} />
<span className="text-slate-600">{b}</span>
<span className="text-slate-600 dark:text-[var(--dash-text-sec)]">{b}</span>
</li>
))}
</ul>
@@ -236,12 +236,12 @@ function AuditShowcase({ bullets, color }: { bullets: string[]; color: string })
return (
<DraftingCard hoverEffect={false}>
<div className="mb-4 flex items-baseline justify-between border-b border-slate-100 pb-3">
<span className="flex items-center gap-1.5 text-xs font-bold uppercase tracking-wider text-slate-800">
<div className="mb-4 flex items-baseline justify-between border-b border-slate-100 pb-3 dark:border-[var(--dash-border)]">
<span className="flex items-center gap-1.5 text-xs font-bold uppercase tracking-wider text-slate-800 dark:text-[var(--dash-text)]">
<ClipboardList className="h-4 w-4" style={{ color }} />
要点核对台
</span>
<span className="font-mono text-[10px] text-slate-400">
<span className="font-mono text-[10px] text-slate-400 dark:text-[var(--dash-text-dim)]">
已确认 {done.length}/{bullets.length}
</span>
</div>
@@ -254,19 +254,19 @@ function AuditShowcase({ bullets, color }: { bullets: string[]; color: string })
key={i}
onClick={() => toggle(i)}
className={`flex cursor-pointer select-none items-start gap-3 rounded border p-3 transition-all duration-300 ${
checked ? "border-emerald-300 bg-emerald-50/40" : "border-slate-200 bg-white hover:border-slate-300"
checked ? "border-emerald-300 bg-emerald-50/40 dark:border-emerald-500/40 dark:bg-emerald-500/10" : "border-slate-200 bg-white hover:border-slate-300 dark:border-[var(--dash-border)] dark:bg-[var(--dash-card)] dark:hover:border-[var(--dash-border-soft)]"
}`}
>
<div
className={`mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded border transition-colors ${
checked ? "border-emerald-600 bg-emerald-500 text-white" : "border-slate-300 bg-white"
checked ? "border-emerald-600 bg-emerald-500 text-white" : "border-slate-300 bg-white dark:border-[var(--dash-border-soft)] dark:bg-[var(--dash-card)]"
}`}
>
{checked && <Check className="h-3.5 w-3.5" />}
</div>
<span
className={`text-xs leading-relaxed ${
checked ? "text-slate-400 line-through" : "text-slate-700"
checked ? "text-slate-400 line-through dark:text-[var(--dash-text-dim)]" : "text-slate-700 dark:text-[var(--dash-text)]"
}`}
>
{b}
@@ -284,17 +284,17 @@ function ReleaseShowcase({ seat, bullets }: { seat: SeatCard; bullets: string[]
const [signed, setSigned] = useState(false);
return (
<div className="space-y-4">
<DraftingCard hoverEffect={false} className="border-sky-200/65 bg-sky-50/40">
<h4 className="mb-2.5 flex items-center gap-1.5 text-xs font-bold text-sky-800">
<Award className="h-4 w-4 text-sky-600" />
<DraftingCard hoverEffect={false} className="border-sky-200/65 bg-sky-50/40 dark:border-sky-500/30 dark:bg-sky-500/10">
<h4 className="mb-2.5 flex items-center gap-1.5 text-xs font-bold text-sky-800 dark:text-sky-300">
<Award className="h-4 w-4 text-sky-600 dark:text-sky-300" />
研讨结果最终发布声明书 (Executive Release)
</h4>
<p className="text-xs leading-relaxed text-slate-600">{seat.description || "(无总结陈述)"}</p>
<p className="text-xs leading-relaxed text-slate-600 dark:text-[var(--dash-text-sec)]">{seat.description || "(无总结陈述)"}</p>
{bullets.length > 0 && (
<div className="mt-4 space-y-2 border-t border-sky-100 pt-3">
<span className="block text-[10px] font-bold uppercase tracking-widest text-sky-700">关键结论与调和事项:</span>
<div className="grid grid-cols-1 gap-2 text-[11px] text-slate-600 md:grid-cols-2">
<div className="mt-4 space-y-2 border-t border-sky-100 pt-3 dark:border-sky-500/30">
<span className="block text-[10px] font-bold uppercase tracking-widest text-sky-700 dark:text-sky-300">关键结论与调和事项:</span>
<div className="grid grid-cols-1 gap-2 text-[11px] text-slate-600 md:grid-cols-2 dark:text-[var(--dash-text-sec)]">
{bullets.map((b, i) => (
<div key={i} className="flex items-start gap-1.5">
<Check className="mt-0.5 h-4 w-4 shrink-0 text-emerald-600" />
@@ -306,11 +306,11 @@ function ReleaseShowcase({ seat, bullets }: { seat: SeatCard; bullets: string[]
)}
</DraftingCard>
<div className="flex flex-col items-center justify-center space-y-4 rounded-lg border border-dashed border-slate-200 bg-white/50 p-5 text-center">
<div className="flex flex-col items-center justify-center space-y-4 rounded-lg border border-dashed border-slate-200 bg-white/50 p-5 text-center dark:border-[var(--dash-border-soft)] dark:bg-[var(--dash-card)]">
<div className="max-w-md">
<LayoutGrid className="mx-auto mb-2 h-8 w-8 text-sky-600" />
<h5 className="text-sm font-bold text-slate-800">总台最终数字签署确认栏</h5>
<p className="mt-1 text-xs text-slate-400">勾选此项即代表会议全席专家一致同意此交付终稿,封盘归档。</p>
<LayoutGrid className="mx-auto mb-2 h-8 w-8 text-sky-600 dark:text-sky-300" />
<h5 className="text-sm font-bold text-slate-800 dark:text-[var(--dash-text)]">总台最终数字签署确认栏</h5>
<p className="mt-1 text-xs text-slate-400 dark:text-[var(--dash-text-dim)]">勾选此项即代表会议全席专家一致同意此交付终稿,封盘归档。</p>
</div>
<button
type="button"
@@ -318,7 +318,7 @@ function ReleaseShowcase({ seat, bullets }: { seat: SeatCard; bullets: string[]
className={`flex items-center gap-2 rounded-sm border px-5 py-2.5 text-xs font-semibold transition-all ${
signed
? "border-emerald-700 bg-emerald-600 text-white hover:bg-emerald-700"
: "border-slate-300 bg-white text-slate-700 hover:border-rose-400 hover:text-rose-600"
: "border-slate-300 bg-white text-slate-700 hover:border-rose-400 hover:text-rose-600 dark:bg-[var(--dash-card)] dark:border-[var(--dash-border-soft)] dark:text-[var(--dash-text)] dark:hover:border-[var(--dash-primary)] dark:hover:text-[var(--dash-primary)]"
}`}
>
{signed ? (
@@ -4,6 +4,44 @@
* 仅在 DashboardApp 子树内通过 import 引入,作用域不污染全局。
*/
/* ─────────────────────────────────────────────────────────────────────────
* 深色主题 token(《深蓝色 UI 规范》§1 取值)—— 挂在 DashboardApp 根 `.dash-root`,
* 组件保留浅色 Tailwind 类、仅追加 `dark:bg-[var(--dash-*)]` 变体引用本表,
* 浅色模式不受影响、深色模式整体切到深蓝规范色(色值集中此处,组件不散落 HEX)。
* ──────────────────────────────────────────────────────────────────────── */
.dash-root {
--dash-page: #0c1836; /* 页面背景 zs-004-09 */
--dash-card: #122347; /* 卡片背景 zs-004-08 */
--dash-elev: #1a2f5a; /* 容器/内嵌面板 zs-004-07 */
--dash-inset: #0c1836; /* 更深内嵌块(表头/代码态) */
--dash-border: #1a2f5a; /* 常规边框/分隔 zs-004-07 */
--dash-border-soft: #2d4473; /* 次级/虚线/控件边框 zs-004-06 */
--dash-text-strong: #ffffff; /* 标题/强调 */
--dash-text: #c5cfe0; /* 正文 zs-004-02 */
--dash-text-sec: #6e88b0; /* 次级 zs-004-04 */
--dash-text-dim: #4a6491; /* 禁用/弱 zs-004-05 */
--dash-primary: #1677ff; /* 主操作/品牌强调(替代浅色 rose)zs-002-06 */
--dash-primary-soft: rgba(22, 119, 255, 0.16); /* 主色浅底 */
--dash-accent: #13c2c2; /* 辅色 zs-003-06 */
}
/* 深色下网格 overlay 改为极淡深蓝描线(浅色仍走上方 .bg-grid-slate-100)。 */
.dark .dash-root .bg-grid-slate-100 {
background-image:
linear-gradient(to right, rgba(45, 68, 115, 0.35) 1px, transparent 1px),
linear-gradient(to bottom, rgba(45, 68, 115, 0.35) 1px, transparent 1px);
}
/* DraftingCard 四角工程角标取色:浅色品牌正红 #F5222D,深色切深蓝规范主蓝。
* (角标元素只设单边 border 宽度,用专用类直接给 border-color,避开 Tailwind
* 任意值色类在单边宽度场景下的不生效问题。) */
.dash-corner {
border-color: #f5222d;
}
.dark .dash-root .dash-corner {
border-color: var(--dash-primary);
}
/* DraftingCard 的极淡绘图网格 overlay(参考 `bg-grid-slate-100`)。 */
.bg-grid-slate-100 {
background-image:
@@ -36,6 +36,7 @@ import {
Network,
Plus,
Save,
Sliders,
SplitSquareVertical,
Trash2,
UserPlus,
@@ -62,7 +63,6 @@ import {
import { CSS } from "@dnd-kit/utilities";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import {
Dialog,
DialogContent,
@@ -632,94 +632,113 @@ export default function BusinessChainEditorPage() {
// 步骤一左侧「基本信息」表单(标题 / 描述 / 总控提示词,描述与总控均为多行文本)。
// 抽成元素:无编排内容时居中单列,有内容时与右侧流程图并排(左列)。
// 样式按设计稿「配置参数面板」还原:label 在上、圆角输入;按钮配色沿用现有 primary。
const FIELD =
"w-full rounded-md border border-border bg-muted/30 px-3.5 py-2 text-sm text-foreground outline-none transition placeholder:text-muted-foreground/55 hover:bg-muted/50 focus:border-primary focus:bg-card";
const basicInfoForm = (
<div className="space-y-4">
{/* 标题 */}
<div
className={cn(
"flex items-stretch overflow-hidden border border-border bg-card transition focus-within:border-primary focus-within:ring-1 focus-within:ring-primary/30",
R_CTRL,
)}
>
<span className="flex min-w-[64px] select-none items-center justify-center border-r border-border bg-muted/50 px-4 text-sm font-medium text-muted-foreground">
标题
<div className="space-y-5">
{/* 面板头:配置参数面板 / 实时编译中 */}
<div className="flex items-center justify-between">
<span className="flex select-none items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-muted-foreground">
<Sliders className="size-3" />
配置参数面板
</span>
<Input
<span className="flex select-none items-center gap-1 text-[11px] font-medium text-primary">
实时编译中
<span className="ml-1 size-1.5 animate-pulse rounded-full bg-emerald-500" />
</span>
</div>
{/* 标题 */}
<div className="space-y-1.5">
<label htmlFor="chain-title" className="flex items-center gap-1 text-xs font-semibold text-foreground">
标题
<span className="text-destructive">*</span>
</label>
<input
id="chain-title"
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="如:应急方案三段链"
maxLength={120}
className="h-auto rounded-none border-0 bg-transparent px-4 py-2.5 text-sm font-medium shadow-none focus-visible:ring-0 dark:border-0 dark:bg-transparent"
className={cn(FIELD, "font-medium")}
/>
</div>
{/* 描述(多行文本,与总控提示词同款)。 */}
<div
className={cn(
"overflow-hidden border border-border bg-card transition focus-within:border-primary focus-within:ring-1 focus-within:ring-primary/30",
R_CTRL,
)}
>
<div className="flex items-center justify-between border-b border-border bg-muted/50 px-4 py-2">
<span className="select-none text-sm font-medium text-muted-foreground">描述</span>
<span className="select-none text-[10px] text-muted-foreground/70">
链条用途说明:解决什么问题、适用什么场景(可选)
</span>
</div>
{/* 描述 */}
<div className="space-y-1.5">
<label htmlFor="chain-desc" className="text-xs font-semibold text-foreground">
描述
</label>
<textarea
id="chain-desc"
value={description}
onChange={(e) => setDescription(e.target.value)}
maxLength={2000}
rows={3}
placeholder="这条链条解决什么问题、适用什么场景…"
className="w-full resize-y bg-card px-4 py-2.5 text-sm leading-relaxed text-foreground placeholder:text-muted-foreground/60 focus:outline-none"
rows={2}
placeholder="链条用途说明:解决什么问题、适用什么场景(可选)…"
className={cn(FIELD, "resize-none leading-relaxed")}
/>
</div>
{/* 总控编排提示:写给总控(协调)智能体的整体派活指引,仅分层 DAG 模式生效。 */}
<div
className={cn(
"overflow-hidden border border-border bg-card transition focus-within:border-primary focus-within:ring-1 focus-within:ring-primary/30",
R_CTRL,
)}
>
<div className="flex items-center justify-between border-b border-border bg-muted/50 px-4 py-2">
<span className="select-none text-sm font-medium text-muted-foreground">总控提示词</span>
<div className="flex items-center gap-2">
<span className="select-none text-[10px] text-muted-foreground/70">
写给总控:怎么派活、做什么(仅分层研讨生效,可选)
</span>
<button
type="button"
onClick={handleOptimizeCoordinatorPrompt}
disabled={optimizingCoordinator || !coordinatorPrompt.trim()}
title={
coordinatorPrompt.trim()
? "让 AI 按链条结构重写这段提示词(更明确的派活方式、交付口径与总结要求)"
: "先写一版提示词,AI 才能在它基础上优化"
}
className={cn(
"inline-flex shrink-0 items-center gap-1 border border-primary/30 bg-primary/5 px-2 py-0.5 text-[10px] font-semibold text-primary transition hover:bg-primary/10 disabled:cursor-not-allowed disabled:opacity-50",
R_CTRL,
)}
>
{optimizingCoordinator ? (
<Loader2 className="size-3 animate-spin" />
) : (
<Wand2 className="size-3" />
)}
{optimizingCoordinator ? "优化中…" : "AI 优化"}
</button>
{/* 总控提示词:label + AI 优化(配色沿用现有 primary)+ 大号等宽编辑区 */}
<div className="space-y-2">
<div className="flex items-center justify-between gap-2">
<label htmlFor="chain-coord" className="flex items-center gap-1 text-xs font-semibold text-foreground">
总控提示词
<span className="text-[10px] font-normal text-muted-foreground">(写给总控怎么分发、汇总)</span>
</label>
<button
type="button"
onClick={handleOptimizeCoordinatorPrompt}
disabled={optimizingCoordinator || !coordinatorPrompt.trim()}
title={
coordinatorPrompt.trim()
? "让 AI 按链条结构重写这段提示词(更明确的派活方式、交付口径与总结要求)"
: "先写一版提示词,AI 才能在它基础上优化"
}
className={cn(
"inline-flex shrink-0 items-center gap-1 border border-primary/30 bg-primary/5 px-2.5 py-1 text-[11px] font-semibold text-primary transition hover:bg-primary/10 active:scale-[0.97] disabled:cursor-not-allowed disabled:opacity-50",
R_CTRL,
)}
>
{optimizingCoordinator ? (
<Loader2 className="size-3 animate-spin" />
) : (
<Wand2 className="size-3" />
)}
{optimizingCoordinator ? "优化中…" : "AI 优化提示词"}
</button>
</div>
<div className="relative">
<textarea
id="chain-coord"
value={coordinatorPrompt}
onChange={(e) => setCoordinatorPrompt(e.target.value)}
maxLength={4000}
placeholder="例如:你是本次研讨的总控。请严格按阶段顺序派活,每阶段先明确产出目标再分配;要求各席位的交付聚焦本职、显式承接上一阶段结论;最终阶段做综合收口,给出可落地的全案。"
className={cn(
FIELD,
"h-[260px] resize-y p-4 font-mono text-[12px] leading-relaxed",
)}
/>
<div className="pointer-events-none absolute bottom-2 right-2 rounded-md border border-border bg-card/90 px-2 py-1 font-mono text-[9px] text-muted-foreground">
Markdown 格式可用
</div>
</div>
<textarea
value={coordinatorPrompt}
onChange={(e) => setCoordinatorPrompt(e.target.value)}
maxLength={4000}
rows={4}
placeholder="例如:你是本次研讨的总控。请严格按阶段顺序派活,每阶段先明确产出目标再分配;要求各席位的交付聚焦本职、显式承接上一阶段结论;最终阶段做综合收口,给出可落地的全案。"
className="w-full resize-y bg-card px-4 py-2.5 text-sm leading-relaxed text-foreground placeholder:text-muted-foreground/60 focus:outline-none"
/>
</div>
{/* 提示:分层并行调度规则 */}
<div className={cn("space-y-2 border border-primary/15 bg-primary/[0.05] p-4", R_CARD)}>
<div className="flex items-center gap-1.5 text-xs font-semibold text-primary">
<Info className="size-4" />
分层并行调度规则
</div>
<p className="text-[11px] leading-relaxed text-muted-foreground">
处于同一<strong className="font-semibold text-foreground">阶段(Stage)</strong>的席位会被并行下发任务以压缩时延;阶段之间串行,由总控在下一阶段承接并合并上一阶段产出。
</p>
</div>
</div>
);
@@ -732,6 +732,9 @@ export default function RoundtablePlanningPage() {
step2.resetStep2State();
step2.setSelectedAgents(normalized);
step2.resetInitGate();
// 这是从第一步确认席位、开启的一轮**实时**研讨(非查看历史/草稿) → 清掉
// “查看历史”闸门,否则之前看过历史记录留下的 true 会一直把「后台挂起」按钮藏掉。
drafts.loadedFromDraftRef.current = false;
// 新一轮研讨 → 允许共识达成后再次向父页面回传。
consensusEmittedRef.current = false;
// result.mode 决定 Step 2 编排:recommend = 总控自由派活;chain = 按链条顺序派活;
@@ -750,7 +753,7 @@ export default function RoundtablePlanningPage() {
: "成功确认!已切换至第二阶段圆桌研讨系统",
);
},
[buildCurrentStep2Snapshot, mergeActiveRun, step2, step3Report, triggerToast],
[buildCurrentStep2Snapshot, drafts, mergeActiveRun, step2, step3Report, triggerToast],
);
// ─── 会商自动化串联(模块 C) ──────────────────────────────────
@@ -1534,6 +1537,7 @@ export default function RoundtablePlanningPage() {
);
}}
onBackgroundSuspend={handleBackgroundSuspend}
isViewingHistory={drafts.loadedFromDraftRef.current}
onTriggerToast={triggerToast}
/>
)}