feat(embed): 合同导入与工作台融成一体(主题/字体/配色/滚动全对齐)

深色外壳里嵌一张米白纸、外加两层滚动条,是「没浑然一体」的根源:

- 宿主把当前主题下的设计令牌 postMessage 给嵌入页(跨源读不到对方的
  CSS 变量),两边共用同一套配色/字体/圆角;切主题即时跟随,不用刷新
  (src 里的 theme 只作首帧初值,避免换主题触发 iframe 重载白闪)
- 收起嵌入页里重复的部件:自带侧栏、顶栏指标条(货号总数/供应商/待核对)、
  右下角「问一句」悬浮球(宿主已有小皮)
- 只留一层滚动:路由区固定为视口高度,滚动交给 iframe 内部
- 输入框沿用宿主的「无方框」规则:去掉描边与焦点光环,改底色区分
- 页面头去掉「新窗口打开」(打开的是裸 SKU 页),刷新改成图标按钮
- 令牌名对不上时会告警,免得嵌入页悄悄回落到旧配色
This commit is contained in:
Drsakura
2026-09-11 12:53:22 +08:00
parent 453f3b1583
commit 436519e0a6
3 changed files with 82 additions and 25 deletions
+74 -24
View File
@@ -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 写到 <html> 上,
而子组件的 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 (
<div className="page-container">
<div className="page-container embed-shell">
<div className="page-header">
<div>
<h1 className="page-title">{meta.title}</h1>
@@ -41,31 +106,16 @@ function EmbeddedSku({ view = 'search' }) {
{meta.sub}
</p>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button className="btn btn-sm" onClick={() => frameRef.current?.contentWindow?.location.reload()}>
刷新
</button>
<a className="btn btn-sm" href={src} target="_blank" rel="noreferrer" style={{ textDecoration: 'none' }}>
新窗口打开
</a>
</div>
<button type="button" className="embed-refresh" onClick={reload} title="重新加载" aria-label="重新加载">
<RefreshIcon size={16} />
</button>
</div>
<div
style={{
position: 'relative',
height: 'calc(100vh - 210px)',
minHeight: '560px',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-lg)',
overflow: 'hidden',
background: 'var(--surface)'
}}
>
<div className="embed-frame">
{!loaded && (
<div className="loading" style={{ position: 'absolute', inset: 0, zIndex: 1 }}>
<div className="spinner"></div>
<span style={{ marginLeft: '12px' }}>正在加载 SKU 模块…</span>
<span style={{ marginLeft: '12px' }}>正在加载{meta.title}…</span>
</div>
)}
<iframe