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}…
)}