feat(ui): 浅蓝主题、外观设置与圆桌看板主题适配,优化工作区布局

新增 lightblue 主题与外观设置联动,圆桌 Step3 看板/report 统一配色;工作区侧边栏、输入框与轻应用 iframe 主题参数同步更新。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
董宏凯andCursor committed 2026-06-14 12:43:16 +08:00
1 parent 30d4f9667b
commit c702119e66
39 files changed
+2562 -1441

No files matched your search

@@ -21,11 +21,14 @@ import {
} from "react";
import {
ArrowLeft,
Boxes,
CheckCircle,
Code2,
ExternalLink,
LayoutPanelTop,
Loader2,
Pause,
Pencil,
RefreshCw,
SendHorizontal,
Sparkles,
@@ -43,7 +46,7 @@ import { DASHBOARD_VIEW_STORAGE_PREFIX } from "@/roundtable-planning/pages/Dashb
import { useAutoScroll } from "@/roundtable-planning/hooks/useAutoScroll";
import { useDialogueSpeedMetrics } from "@/roundtable-planning/hooks/useDialogueSpeedMetrics";
import type { UseStep3DashboardReturn } from "@/roundtable-planning/hooks/useStep3Dashboard";
import { parseReportJson } from "@/roundtable-planning/lib/report-template";
import { parseReportJson, stripReportFence } from "@/roundtable-planning/lib/report-template";
interface Step3DashboardModeProps {
selectedModel: string;
@@ -65,6 +68,7 @@ function formatNow(): string {
}
type DashView = "chat" | "page";
type RenderMode = "app" | "html";
export function Step3DashboardMode({
selectedModel,
@@ -101,12 +105,13 @@ export function Step3DashboardMode({
// 视图:有产物默认看「大屏页面」,否则进「交互区」看抽取过程。
const [view, setView] = useState<DashView>(() => (hasResult ? "page" : "chat"));
// 完成即自动切到大屏页面(含问答修订后的再次完成)。
const prevPhaseRef = useRef(phase);
useEffect(() => {
if (phase === "done" && prevPhaseRef.current !== "done") setView("page");
prevPhaseRef.current = phase;
}, [phase]);
// 大屏页面的两种渲染:
// - "app" 应用渲染:用应用内 React 组件(DashboardApp)渲染,交互更丰富(Tab/动画/钻取)。
// - "html" 网页渲染:直接把自包含 HTML 大屏塞进 iframe,与「新窗口打开 / 分享 / 下载」一致。
const [renderMode, setRenderMode] = useState<RenderMode>("app");
// 绘制完成**不再**自动切到大屏页面:用户正在交互区看抽取过程/继续问答时,强行跳走很打断。
// 想看成品由用户手动点顶栏「查看大屏」。没有产物时初始即落交互区(见上 useState 初值)。
// 仅在交互区允许本流自动接管沙箱(并行流防抢);离开/卸载时让出。
useEffect(() => {
@@ -114,10 +119,22 @@ export function Step3DashboardMode({
return () => setPreviewEnabled(false);
}, [view, setPreviewEnabled]);
// 新窗口打开:把当前大屏数据写进 localStorage,在新标签页打开应用内 React 大屏
// (独立全屏路由 /roundtable-dashboard,无侧边栏)。无真实数据时 standalone 回落 DEMO。
// 新窗口打开:跟随当前渲染方式——
// - 网页渲染:把自包含 HTML 大屏(blob)直接在新标签页打开(纯网页,无应用壳);
// - 应用渲染:把当前大屏数据写进 localStorage,在新标签页打开应用内 React 大屏
// (独立全屏路由 /roundtable-dashboard,无侧边栏)。无真实数据时 standalone 回落 DEMO。
const handleOpenNewWindow = () => {
if (typeof window === "undefined") return;
if (renderMode === "html" && html) {
const blob = new Blob([html], { type: "text/html;charset=utf-8" });
const blobUrl = URL.createObjectURL(blob);
window.open(blobUrl, "_blank", "noopener,noreferrer");
// 延迟回收,给新标签页足够时间加载(过早 revoke 会打不开)。
window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000);
return;
}
const key = `${DASHBOARD_VIEW_STORAGE_PREFIX}${Date.now()}`;
try {
// 先清掉历史键,避免 localStorage 堆积;再写本次数据。
@@ -135,9 +152,9 @@ export function Step3DashboardMode({
window.open(url, "_blank", "noopener,noreferrer");
};
const handleRedraw = () => {
// 「编辑」:回到交互区接着上次的聊天记录继续问答修改(不丢弃上下文、不重画)。
const handleEdit = () => {
setView("chat");
redraw();
};
const isStreaming =
@@ -160,19 +177,57 @@ export function Step3DashboardMode({
<div className="flex items-center gap-2">
{view === "page" ? (
<>
{/* 渲染方式切换:应用渲染(React 组件)/ 网页渲染(自包含 HTML iframe) */}
<div className="inline-flex items-center gap-0.5 rounded-xl border border-slate-200 bg-slate-50 p-0.5">
<button
type="button"
onClick={() => setRenderMode("app")}
title="用应用内 React 组件渲染(Tab 切换、动画、席位钻取)"
className={`flex items-center gap-1 rounded-lg px-2.5 py-1 text-[10.5px] font-bold transition ${
renderMode === "app"
? "bg-white text-primary shadow-sm border border-slate-200"
: "text-slate-500 hover:text-slate-700"
}`}
>
<Boxes className="w-3.5 h-3.5" />
应用渲染
</button>
<button
type="button"
onClick={() => setRenderMode("html")}
disabled={!html}
title={
html
? "用自包含 HTML 大屏渲染(与新窗口打开 / 分享 / 下载一致)"
: "尚无已生成的 HTML 大屏,先完成一次绘制"
}
className={`flex items-center gap-1 rounded-lg px-2.5 py-1 text-[10.5px] font-bold transition disabled:opacity-40 disabled:cursor-not-allowed ${
renderMode === "html"
? "bg-white text-primary shadow-sm border border-slate-200"
: "text-slate-500 hover:text-slate-700"
}`}
>
<Code2 className="w-3.5 h-3.5" />
网页渲染
</button>
</div>
<button
type="button"
onClick={handleRedraw}
title="回到交互区,重新绘制(可在那里通过问答修改大屏)"
onClick={handleEdit}
title="回到交互区,加载上次聊天记录继续问答修改"
className="bg-white border border-slate-200 text-slate-700 hover:bg-slate-50 text-[10.5px] font-bold px-3 py-1.5 rounded-xl flex items-center gap-1 transition"
>
<RefreshCw className="w-3.5 h-3.5 text-slate-500" />
重新绘制
<Pencil className="w-3.5 h-3.5 text-slate-500" />
编辑
</button>
<button
type="button"
onClick={handleOpenNewWindow}
title="在新标签页打开 React 大屏页面(全屏,无侧边栏)"
title={
renderMode === "html"
? "在新标签页打开自包含 HTML 大屏(纯网页,可分享 / 下载)"
: "在新标签页打开应用内 React 大屏(全屏,无侧边栏)"
}
className="bg-white border border-slate-200 text-slate-700 hover:bg-slate-50 text-[10.5px] font-bold px-3 py-1.5 rounded-xl flex items-center gap-1 transition"
>
<ExternalLink className="w-3.5 h-3.5 text-slate-500" />
@@ -214,9 +269,18 @@ export function Step3DashboardMode({
{/* 主体:交互区(聊天 + 抽取沙箱) / 大屏页面(React) */}
{view === "page" ? (
<div className="flex-1 min-h-0 overflow-auto">
<DashboardApp data={dashboardData} />
</div>
renderMode === "html" && html ? (
<iframe
title="方案研讨大屏"
srcDoc={html}
className="flex-1 min-h-0 w-full border-0 bg-white"
sandbox="allow-scripts allow-popups allow-downloads allow-same-origin"
/>
) : (
<div className="flex-1 min-h-0 overflow-auto">
<DashboardApp data={dashboardData} />
</div>
)
) : (
<div className="flex-1 min-h-0 p-3">
<section className="h-full min-h-0 rounded-2xl border border-slate-200/60 bg-white overflow-hidden">
@@ -287,6 +351,19 @@ function DashboardChatPanel({
const dialogueMetrics = useDialogueSpeedMetrics(dialogues);
const [text, setText] = useState("");
// 消息列表里不展示 report-json 结构化数据本体(它已在右侧沙箱实时渲染),
// 把数据块替换成一句「看右侧沙箱」的提示,避免左右两边重复刷整段 JSON。
const displayDialogues = useMemo(
() =>
dialogues.map((d) => {
const content = d.content ?? "";
if (!content) return d;
const stripped = stripReportFence(content, d.streaming === true);
return stripped === content ? d : { ...d, content: stripped };
}),
[dialogues],
);
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const t = text.trim();
@@ -368,7 +445,7 @@ function DashboardChatPanel({
</div>
)}
{dialogues.map((msg) => (
{displayDialogues.map((msg) => (
<Step2Message
key={msg.id}
msg={msg}