feat(圆桌): 后台挂起按钮移至顶部暂停按钮右侧 + 实时研讨复位历史闸门
- Step2 顶部「暂停/恢复会商」右侧新增「后台挂起」按钮(查看历史时隐藏),移除底部干预栏里的旧入口 - handleConfirmAgents 从第一步开启实时研讨时复位 loadedFromDraftRef,修复看过历史后再开新讨论按钮不出现的问题 - 大屏 dashboard 多组件样式与业务链条编辑页细节调整 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
634cb1413b
commit
61754acb87
18 files changed
+672
-368
No files matched your search
@@ -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
-22
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user