From 436519e0a6634bef542c06a4679b363e3873b79b Mon Sep 17 00:00:00 2001 From: Drsakura Date: Fri, 11 Sep 2026 12:53:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(embed):=20=E5=90=88=E5=90=8C=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E4=B8=8E=E5=B7=A5=E4=BD=9C=E5=8F=B0=E8=9E=8D=E6=88=90?= =?UTF-8?q?=E4=B8=80=E4=BD=93=EF=BC=88=E4=B8=BB=E9=A2=98/=E5=AD=97?= =?UTF-8?q?=E4=BD=93/=E9=85=8D=E8=89=B2/=E6=BB=9A=E5=8A=A8=E5=85=A8?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 深色外壳里嵌一张米白纸、外加两层滚动条,是「没浑然一体」的根源: - 宿主把当前主题下的设计令牌 postMessage 给嵌入页(跨源读不到对方的 CSS 变量),两边共用同一套配色/字体/圆角;切主题即时跟随,不用刷新 (src 里的 theme 只作首帧初值,避免换主题触发 iframe 重载白闪) - 收起嵌入页里重复的部件:自带侧栏、顶栏指标条(货号总数/供应商/待核对)、 右下角「问一句」悬浮球(宿主已有小皮) - 只留一层滚动:路由区固定为视口高度,滚动交给 iframe 内部 - 输入框沿用宿主的「无方框」规则:去掉描边与焦点光环,改底色区分 - 页面头去掉「新窗口打开」(打开的是裸 SKU 页),刷新改成图标按钮 - 令牌名对不上时会告警,免得嵌入页悄悄回落到旧配色 --- frontend/src/App.jsx | 2 +- frontend/src/pages/EmbeddedSku.jsx | 98 ++++++++++++++++++++++-------- frontend/src/styles/luxury.css | 7 +++ 3 files changed, 82 insertions(+), 25 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c726755..debe2d0 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -140,7 +140,7 @@ function App() { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/pages/EmbeddedSku.jsx b/frontend/src/pages/EmbeddedSku.jsx index 083ec5c..9c2fa1a 100644 --- a/frontend/src/pages/EmbeddedSku.jsx +++ b/frontend/src/pages/EmbeddedSku.jsx @@ -1,4 +1,5 @@ -import React, { useState, useRef } from 'react' +import React, { useCallback, useEffect, useRef, useState } from 'react' +import { RefreshIcon } from '../components/Icons' /** * SKU 功能嵌入页(整合过渡期方案)。 @@ -8,9 +9,40 @@ import React, { useState, useRef } from 'react' * * 现在嵌入的是独立的 sku-manager 服务(3300);等后端合并(P1/P2)完成后, * 把 SKU_ORIGIN 换成同源子路径(如 /sku)即可,页面组件本身不用动。 + * + * 外观一体:跨源 iframe 读不到宿主的 CSS 变量,所以由宿主把当前主题下 + * 这些变量的实际取值读出来 postMessage 过去,嵌入页照单贴到自己对应的 + * 变量上(嵌入页侧不含任何宿主变量名,见 sku-manager/public/embed.css)。 */ const SKU_ORIGIN = import.meta.env.VITE_SKU_ORIGIN || 'http://127.0.0.1:3300' +/** + * 设计令牌桥:左边是嵌入页的 CSS 变量名,右边是宿主这边的取值来源。 + * 只有这一处维护映射 —— 嵌入页负责结构,宿主负责配色与字体。 + */ +const TOKEN_BRIDGE = { + '--bg': '--bg-primary', // 卡片底色 = 外壳底色 → 嵌入页"消失" + '--panel': '--surface', // 卡片、输入框默认底 + '--panel-2': '--surface-deep', // 比卡片再深一档:输入框、拖入区 + '--raised': '--surface-hover', // 悬浮、拖拽经过 + '--line': '--border', + '--line-soft': '--border-light', + '--fg': '--text-primary', + '--fg-dim': '--text-secondary', + '--fg-mute': '--text-tertiary', + '--accent': '--primary', + '--accent-ink': '--primary', + '--accent-line': '--primary-border', + '--accent-glow': '--accent-glow', + '--accent-fill': '--primary-bg', // 输入框聚焦时的实底 + '--on-accent': '--bg-primary', // 强调色上的文字:取宿主底色,两种主题都自动是反色 + '--font-display': '--font-body', + '--font-body': '--font-body', + '--font-mono': '--font-mono', + '--r-sm': '--radius-sm', + '--r-md': '--radius-md' +} + const VIEW_META = { search: { title: '产品管理', @@ -26,14 +58,47 @@ const VIEW_META = { } } -function EmbeddedSku({ view = 'search' }) { +function EmbeddedSku({ view = 'search', theme = 'dark' }) { const [loaded, setLoaded] = useState(false) const frameRef = useRef(null) const meta = VIEW_META[view] || VIEW_META.search - const src = `${SKU_ORIGIN}/?embed=1&view=${encodeURIComponent(view)}` + + /* src 里的 theme 只当"首帧用哪个主题"的初始值:钉死成首次渲染时的值。 + 否则换主题时 React 会改 src,iframe 整页重载白闪一下。 */ + const initialTheme = useRef(theme).current + + const pushTheme = useCallback(() => { + const frame = frameRef.current + if (!frame?.contentWindow) return + const computed = window.getComputedStyle(document.documentElement) + const vars = {} + Object.entries(TOKEN_BRIDGE).forEach(([skuVar, hostVar]) => { + const value = computed.getPropertyValue(hostVar).trim() + /* 宿主改了变量名而这里没跟上时,嵌入页会悄悄回落到自己的旧配色 —— + 必须喊出来,否则要对着截图猜半天。 */ + if (!value) console.warn(`[embed] 宿主找不到变量 ${hostVar},嵌入页的 ${skuVar} 将回落默认值`) + vars[skuVar] = value + }) + frame.contentWindow.postMessage({ type: 'happy:embed', theme, vars }, '*') + }, [theme]) + + useEffect(() => { + if (!loaded) return undefined + /* 放到下一个宏任务:App 是在自己的 effect 里才把 data-theme 写到 上, + 而子组件的 effect 先于父组件执行,此刻读到的还是上一个主题的令牌。 */ + const id = window.setTimeout(pushTheme, 0) + return () => window.clearTimeout(id) + }, [loaded, pushTheme]) + + const reload = () => { + setLoaded(false) + frameRef.current?.contentWindow?.location.reload() + } + + const src = `${SKU_ORIGIN}/?embed=1&view=${encodeURIComponent(view)}&theme=${initialTheme}` return ( -
+

{meta.title}

@@ -41,31 +106,16 @@ function EmbeddedSku({ view = 'search' }) { {meta.sub}

-
- - - 新窗口打开 - -
+
-
+
{!loaded && (
- 正在加载 SKU 模块… + 正在加载{meta.title}…
)}