From 81f2a48c6dbcc449d0e3f0285c18c080e3482480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Fri, 12 Jun 2026 15:12:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=AC=E5=BC=80=E5=B5=8C=E5=85=A5?= =?UTF-8?q?=E9=A1=B5=E4=B8=BB=E9=A2=98=E6=94=AF=E6=8C=81=E5=A4=96=E9=83=A8?= =?UTF-8?q?=E6=8C=87=E5=AE=9A=20+=20=E6=8E=92=E8=A1=8C=E6=A6=9C=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=E7=A7=BB=E8=87=B3=E5=B7=A6=E4=B8=8A=E8=A7=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 公开页(/share、/public、/roundtable-share)新增 ?theme= 与 deerflow:set-theme postMessage 两种外部主题指定方式,经 usePublicEmbedTheme 接成 next-themes forcedTheme,不写 localStorage - 排行榜入口从欢迎语旁移到 workspace-header 左上角应用名旁 - 新增 docs/embed-public-pages-theme.md 嵌入主题使用文档 Co-Authored-By: Claude Fable 5 --- frontend-web/docs/embed-public-pages-theme.md | 154 ++++++++++++++++++ frontend-web/src/App.tsx | 10 +- .../src/components/workspace/welcome.tsx | 16 -- .../components/workspace/workspace-header.tsx | 16 +- frontend-web/src/core/embed/background.ts | 73 ++++++++- 5 files changed, 245 insertions(+), 24 deletions(-) create mode 100644 frontend-web/docs/embed-public-pages-theme.md diff --git a/frontend-web/docs/embed-public-pages-theme.md b/frontend-web/docs/embed-public-pages-theme.md new file mode 100644 index 00000000..6a4c8c69 --- /dev/null +++ b/frontend-web/docs/embed-public-pages-theme.md @@ -0,0 +1,154 @@ +# 公开页 · iframe 嵌入外观对接说明(主题 / 背景色) + +本文档面向**嵌入方(宿主系统)**,说明如何在用 ` + + + + + + +``` + +--- + +## 3. postMessage(嵌入后动态切换) + +适用于宿主站点支持用户切主题、需要 iframe 实时跟随的场景。向 iframe 的 `contentWindow` 发消息即可,**无需等待子页面回执**(监听器在应用挂载时注册)。 + +### 3.1 消息格式 + +```js +const frame = document.getElementById("deerflow-frame"); + +// 切整体明暗主题(theme 取值同 2.2) +frame.contentWindow.postMessage({ type: "deerflow:set-theme", theme: "dark" }, "*"); + +// 改背景色 / 文字色(字段名与 URL 参数一致,bg/background、text/fg 均可) +frame.contentWindow.postMessage({ type: "deerflow:set-bg", bg: "#001529", text: "#fff" }, "*"); +// 兼容别名:type 也可写 "deerflow:set-background" +``` + +说明: + +- 两类消息相互独立,可只发其一;`set-bg` 里 `bg`、`text` 也可只传其一,未传的字段保持当前值。 +- 非法颜色值 / 不认识的 `theme` 取值会被忽略,不会清空已有设置。 +- targetOrigin 建议按需收紧(写 DeerFlow 站点的 origin 而非 `"*"`)。 + +### 3.2 宿主主题联动示例 + +```js +function syncDeerflowTheme(isDark) { + const win = document.getElementById("deerflow-frame").contentWindow; + win.postMessage({ type: "deerflow:set-theme", theme: isDark ? "dark" : "light" }, "*"); + win.postMessage( + { type: "deerflow:set-bg", bg: isDark ? "#001529" : "#ffffff" }, + "*", + ); +} + +// iframe 首次加载用 URL 参数兜底,之后切换走 postMessage +myThemeStore.subscribe((theme) => syncDeerflowTheme(theme === "dark")); +``` + +--- + +## 4. 优先级与默认行为 + +同一项配置多处传入时,按以下优先级取值(高 → 低): + +1. **postMessage** 动态设置的值 +2. hash 路由内的 URL 参数(`#/路由?theme=...`) +3. `#` 之前的 URL 参数(`?theme=...#/路由`) +4. 都没传 → DeerFlow 默认配色(浅色主题 + 页面自带 Tailwind 配色) + +其他保证: + +- 外部主题只影响**当前 iframe 的渲染**,不写 localStorage——不会污染用户直接访问 DeerFlow 主站时自己选择的主题。 +- `bg`/`text` 是内联样式,作用在页面根容器上,优先级高于主题变量;与 `theme` 叠加时以 `bg`/`text` 为准。 + +--- + +## 5. 常见问题排查 + +| 现象 | 排查 | +|------|------| +| `theme=dark` 不生效 | 确认路由属于第 1 节列出的公开页;确认参数拼在 hash 路由后(`/#/share/xx?theme=dark`),而不是写成了 `/#/share/xx#theme=dark` | +| `bg=#fff` 不生效 | URL 里 `#` 没编码会被浏览器截断,必须写 `%23fff`;或直接省略 `#` 写 `bg=fff` | +| postMessage 无反应 | 确认消息体是对象且 `type` 拼写为 `deerflow:set-theme` / `deerflow:set-bg`;确认在 iframe `load` 之后发送 | +| 颜色被忽略 | 检查是否符合 2.3 白名单格式(如 `linear-gradient(...)` 等复杂值不支持) | +| 主题切了但个别区域颜色没变 | 该区域可能使用了固定配色;把页面路由与截图反馈给 DeerFlow 维护方补充 `dark:` 样式 | diff --git a/frontend-web/src/App.tsx b/frontend-web/src/App.tsx index 23999e96..7fa0e118 100644 --- a/frontend-web/src/App.tsx +++ b/frontend-web/src/App.tsx @@ -13,6 +13,7 @@ import { resolveEmbedRoundtableTheme, resolveEmbedTheme, } from "@/core/embed/params"; +import { usePublicEmbedTheme } from "@/core/embed/background"; import { readWujieProps } from "@/core/embed/wujie"; import { HashRouter, useLocation } from "react-router-dom"; @@ -31,9 +32,13 @@ import EmbedRoundtablePage from "./pages/EmbedRoundtablePage"; import { ChatRuntime } from "./pages/WorkspaceRoutes"; import { NotFoundPage } from "./pages/NotFoundPage"; +// 公开页路由前缀:这些页面会被第三方 iframe 嵌入,主题可由外部指定。 +const PUBLIC_EMBED_PREFIXES = ["/share/", "/public/", "/roundtable-share/"]; + function AppProviders({ children }: { children: React.ReactNode }) { const locale = useMemo(() => detectLocale(), []); const location = useLocation(); + const publicEmbedTheme = usePublicEmbedTheme(); // 嵌入路由的主题用 forcedTheme 强制:只影响当前渲染,不写 localStorage。 // 千万不要在嵌入页里调 ThemeContext.setTheme —— 它会持久化主题并打上 // 「用户已显式选择」标记。无界子应用与主站同域共享 localStorage,宿主 @@ -52,8 +57,11 @@ function AppProviders({ children }: { children: React.ReactNode }) { const raw = new URLSearchParams(location.search).get("theme")?.trim(); return raw ? resolveEmbedTheme(raw) : undefined; } + if (PUBLIC_EMBED_PREFIXES.some((p) => location.pathname.startsWith(p))) { + return publicEmbedTheme; + } return undefined; - }, [location.pathname, location.search]); + }, [location.pathname, location.search, publicEmbedTheme]); return ( {t.welcome.greeting} - {/* 用户排行榜入口:紧挨欢迎语的"CM助手",新标签打开免登录的公开排行榜页。 */} - )} diff --git a/frontend-web/src/components/workspace/workspace-header.tsx b/frontend-web/src/components/workspace/workspace-header.tsx index ba1767d8..c66c26db 100644 --- a/frontend-web/src/components/workspace/workspace-header.tsx +++ b/frontend-web/src/components/workspace/workspace-header.tsx @@ -1,6 +1,6 @@ "use client"; -import { ChevronLeft, ChevronRight } from "lucide-react"; +import { ChevronLeft, ChevronRight, Trophy } from "lucide-react"; import Link from "next/link"; import { Button } from "@/components/ui/button"; @@ -86,6 +86,20 @@ export function WorkspaceHeader({ className }: { className?: string }) { ) : ( titleBlock )} + {/* 用户排行榜入口:紧挨左上角的应用名,新标签打开免登录的公开排行榜页。 */} + )} diff --git a/frontend-web/src/core/embed/background.ts b/frontend-web/src/core/embed/background.ts index 5b88656a..d4c5e9b8 100644 --- a/frontend-web/src/core/embed/background.ts +++ b/frontend-web/src/core/embed/background.ts @@ -2,19 +2,23 @@ import { useEffect, useMemo, useState } from "react"; import { useLocation } from "react-router-dom"; /** - * 公开页(被第三方 iframe 嵌套)的可配置背景色。 + * 公开页(被第三方 iframe 嵌套)的可配置外观:背景色 + 整体明暗主题。 * * 这些 `/public/*`、`/share/*`、`/roundtable-share/*` 页面会被别的系统用 - * `