feat(ui): theme & layout improvements

- Dark-blue theme background assets and styling integration
- Appearance settings with layoutMode (full/compact) preference
- Settings dialog and auth context updates
- AI writing form and panel refinements
- Open Canvas context updates
- Roundtable planning styling and dashboard enhancements
- Strategy components and header updates
- Light app management and routing improvements
- Global style enhancements for dark theme
- Various page layout and workspace routing optimizations
This commit is contained in:
董宏凯 committed 2026-06-14 19:30:54 +08:00
1 parent da2864f422
commit 56449ed129
33 files changed
+1133 -491

No files matched your search

@@ -46,7 +46,13 @@ 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, stripReportFence } from "@/roundtable-planning/lib/report-template";
import {
parseReportJson,
renderDashboardHtml,
stripReportFence,
type DashboardTheme,
} from "@/roundtable-planning/lib/report-template";
import { useTheme } from "@/contexts/ThemeContext";
interface Step3DashboardModeProps {
selectedModel: string;
@@ -94,13 +100,32 @@ export function Step3DashboardMode({
openDashboardPreview,
} = dashboard;
// 真实数据 → 大屏数据:优先本轮实时生成,其次草稿快照;都没有则 undefined(用 DEMO)。
const dashboardData = useMemo(() => {
const report = dashboard.data ?? parseReportJson(savedReport?.dashboardJson ?? null);
return report ? adaptReportData(report, formatNow()) : undefined;
}, [dashboard.data, savedReport?.dashboardJson]);
// 真实数据(report-json):优先本轮实时生成,其次草稿快照。应用渲染/网页渲染共用。
const report = useMemo(
() => dashboard.data ?? parseReportJson(savedReport?.dashboardJson ?? null),
[dashboard.data, savedReport?.dashboardJson],
);
const hasResult = !!dashboardData || !!html;
// 应用渲染用的大屏数据(不传则 DashboardApp 回落 DEMO)。
const dashboardData = useMemo(
() => (report ? adaptReportData(report, formatNow()) : undefined),
[report],
);
// 网页渲染主题跟随应用主题:renderDashboardHtml 是确定性的,主题切换时按 report
// 当场重渲(而非用 hook 在生成时烤死主题的 `html`),从而 iframe 大屏色随父元素变化。
// 仅 light/dark 两套(lightblue 归 light,与页面 themeMode 映射一致)。
const { theme } = useTheme();
const dashThemeMode: DashboardTheme = theme === "dark" ? "dark" : "light";
const dashboardHtml = useMemo(
() =>
report
? renderDashboardHtml(report, { generatedAt: formatNow(), theme: dashThemeMode })
: html,
[report, dashThemeMode, html],
);
const hasResult = !!dashboardData || !!dashboardHtml;
// 视图:有产物默认看「大屏页面」,否则进「交互区」看抽取过程。
const [view, setView] = useState<DashView>(() => (hasResult ? "page" : "chat"));
@@ -110,6 +135,27 @@ export function Step3DashboardMode({
// - "html" 网页渲染:直接把自包含 HTML 大屏塞进 iframe,与「新窗口打开 / 分享 / 下载」一致。
const [renderMode, setRenderMode] = useState<RenderMode>("app");
// 网页渲染:用 blob URL 作 iframe `src`(而非 `srcDoc`)。
// 原因:`srcdoc` 文档的 base URL 继承自父页面,大屏内的纯 CSS `:target` 导航锚点
// (#overview / #seat-x)会被解析成「父页面URL#seat-x」,导致点击导航时整个 iframe
// 跳去加载主应用并命中 HashRouter 的 404(Page not found)。blob: 文档有自己的 URL,
// 片段锚点在 iframe 内部解析,:target 切页正常 —— 与「新窗口打开」一致。
//
// 必须在 **effect 内部**创建/回收 blob:StrictMode(dev) 会 setup→cleanup→setup 双触发
// effect,若用 useMemo 跨两次 render 共享同一个 URL,cleanup 会把它 revoke 掉而 iframe
// 仍指向它 → 报「该文件可能已被移至别处」。effect 内每次 run 各自建 URL、cleanup 只回收
// 本 run 的,最后提交的那个存活,故 StrictMode-safe。dashboardHtml 随主题变 → blob 重建。
const [htmlBlobUrl, setHtmlBlobUrl] = useState<string | null>(null);
useEffect(() => {
if (typeof window === "undefined" || !dashboardHtml) {
setHtmlBlobUrl(null);
return;
}
const url = URL.createObjectURL(new Blob([dashboardHtml], { type: "text/html;charset=utf-8" }));
setHtmlBlobUrl(url);
return () => URL.revokeObjectURL(url);
}, [dashboardHtml]);
// 绘制完成**不再**自动切到大屏页面:用户正在交互区看抽取过程/继续问答时,强行跳走很打断。
// 想看成品由用户手动点顶栏「查看大屏」。没有产物时初始即落交互区(见上 useState 初值)。
@@ -126,8 +172,8 @@ export function Step3DashboardMode({
const handleOpenNewWindow = () => {
if (typeof window === "undefined") return;
if (renderMode === "html" && html) {
const blob = new Blob([html], { type: "text/html;charset=utf-8" });
if (renderMode === "html" && dashboardHtml) {
const blob = new Blob([dashboardHtml], { type: "text/html;charset=utf-8" });
const blobUrl = URL.createObjectURL(blob);
window.open(blobUrl, "_blank", "noopener,noreferrer");
// 延迟回收,给新标签页足够时间加载(过早 revoke 会打不开)。
@@ -195,9 +241,9 @@ export function Step3DashboardMode({
<button
type="button"
onClick={() => setRenderMode("html")}
disabled={!html}
disabled={!dashboardHtml}
title={
html
dashboardHtml
? "用自包含 HTML 大屏渲染(与新窗口打开 / 分享 / 下载一致)"
: "尚无已生成的 HTML 大屏,先完成一次绘制"
}
@@ -269,10 +315,10 @@ export function Step3DashboardMode({
{/* 主体:交互区(聊天 + 抽取沙箱) / 大屏页面(React) */}
{view === "page" ? (
renderMode === "html" && html ? (
renderMode === "html" && htmlBlobUrl ? (
<iframe
title="方案研讨大屏"
srcDoc={html}
src={htmlBlobUrl}
className="flex-1 min-h-0 w-full border-0 bg-white"
sandbox="allow-scripts allow-popups allow-downloads allow-same-origin"
/>