diff --git a/docs/superpowers/plans/2026-09-12-miniprogram-header-capsule-safe-area.md b/docs/superpowers/plans/2026-09-12-miniprogram-header-capsule-safe-area.md new file mode 100644 index 000000000..c47ee94f0 --- /dev/null +++ b/docs/superpowers/plans/2026-09-12-miniprogram-header-capsule-safe-area.md @@ -0,0 +1,494 @@ +# 小程序顶部栏胶囊安全区适配 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 让所有 `navigationStyle: custom` 页面的业务顶栏避开微信状态栏与右上角胶囊(方案 B:加高 + 右侧预留)。 + +**Architecture:** 复用 `utils/safeArea.js` 量纲;新增纯函数 `buildSafeAreaCssVarsStyle` 供顶栏根节点 `:style` 注入 CSS 变量;`.db-header` / `.bx-page .topbar` / `.mp-header` 统一消费 `--sq-*`。微信端不依赖 `document.documentElement`。 + +**Tech Stack:** uni-app Vue3、现有 SCSS 主题(`db-theme` / `bx-theme` / `mp-shell`)、`getMenuButtonBoundingClientRect` + +**Spec:** `docs/superpowers/specs/2026-09-12-miniprogram-header-capsule-safe-area-design.md` + +--- + +## 文件结构 + +| 路径 | 职责 | +|------|------| +| `miniprogram/utils/safeArea.js` | 量纲计算 + `buildSafeAreaCssVarsStyle` | +| `miniprogram/utils/__tests__/safeArea.style.test.mjs` | 纯函数 Node 单测(无 uni 运行时依赖的 style 映射) | +| `miniprogram/styles/db-theme.scss` | `.db-header` 方案 B 几何 | +| `miniprogram/styles/bx-theme.scss` | `.topbar` 对齐同一变量 | +| `miniprogram/styles/mp-shell.scss` | `.mp-header*` 用 safe-top + capsule-right | +| `miniprogram/styles/index.scss` | 确认 `.sq-hero-header` 变量路径一致(注释/兜底) | +| `miniprogram/components/app-safe-header.vue` | 改用 `buildSafeAreaCssVarsStyle`(供后续新页) | +| `miniprogram/pages/db/**/*.vue` | `:style="safeAreaStyle"` + `__nav` 结构 | +| `miniprogram/pages/bx/**/*.vue` + `components/bx-ui/bx-wf-config-page.vue` | topbar 绑定 style | +| `miniprogram/pages/{workbench,home,todo}/*.vue` 等 | mp-header 绑定 style | +| `docs/miniprogram-ui-spec-sanqiang.md` | §3.2 补充胶囊 SHALL | + +--- + +### Task 1: `buildSafeAreaCssVarsStyle` + 单测 + +**Files:** +- Modify: `miniprogram/utils/safeArea.js` +- Create: `miniprogram/utils/__tests__/safeArea.style.test.mjs` + +- [ ] **Step 1: 写失败单测(先测 style 映射)** + +创建 `miniprogram/utils/__tests__/safeArea.style.test.mjs`: + +```js +import assert from 'node:assert/strict' +import { buildSafeAreaCssVarsStyle } from '../safeArea.js' + +const style = buildSafeAreaCssVarsStyle({ + topSafeHeight: 44, + navContentHeight: 40, + headerHeight: 84, + capsuleRightSafeWidth: 96, +}) + +assert.equal(style['--sq-safe-top'], '44px') +assert.equal(style['--sq-nav-height'], '40px') +assert.equal(style['--sq-header-height'], '84px') +assert.equal(style['--sq-capsule-right-safe'], '96px') +console.log('ok') +``` + +- [ ] **Step 2: 运行确认失败** + +Run: + +```bash +cd miniprogram && node utils/__tests__/safeArea.style.test.mjs +``` + +Expected: `ERR_MODULE_NOT_FOUND` 或 `buildSafeAreaCssVarsStyle is not a function` + +- [ ] **Step 3: 实现函数** + +在 `miniprogram/utils/safeArea.js` 末尾追加: + +```js +export function buildSafeAreaCssVarsStyle(metrics) { + const m = metrics || getGlobalSafeArea() + return { + '--sq-safe-top': `${m.topSafeHeight}px`, + '--sq-nav-height': `${m.navContentHeight}px`, + '--sq-header-height': `${m.headerHeight}px`, + '--sq-capsule-right-safe': `${m.capsuleRightSafeWidth}px`, + } +} +``` + +确保文件可作为 ESM 被 Node 导入:若当前无 `"type":"module"`,测试改为动态 `createRequire` 不可行时,把测试改为内联复制映射断言并在注释中要求实现与之一致;**优先**给 `safeArea.js` 保持现有导出方式(uni 打包为普通 JS),测试文件用: + +```js +import { createRequire } from 'node:module' +const require = createRequire(import.meta.url) + +// 若 safeArea 依赖 uni,则只测纯映射:复制最小实现到测试或拆出 safeAreaCss.js +``` + +**推荐落地(避免 uni 依赖拖垮 Node 测试):** + +新建 `miniprogram/utils/safeAreaCss.js`(纯函数,无 `uni`): + +```js +export function buildSafeAreaCssVarsStyle(metrics) { + return { + '--sq-safe-top': `${metrics.topSafeHeight}px`, + '--sq-nav-height': `${metrics.navContentHeight}px`, + '--sq-header-height': `${metrics.headerHeight}px`, + '--sq-capsule-right-safe': `${metrics.capsuleRightSafeWidth}px`, + } +} +``` + +`safeArea.js` 改为: + +```js +import { buildSafeAreaCssVarsStyle } from './safeAreaCss.js' +export { buildSafeAreaCssVarsStyle } + +export function getSafeAreaStyle() { + return buildSafeAreaCssVarsStyle(getGlobalSafeArea()) +} +``` + +单测只 import `safeAreaCss.js`。 + +- [ ] **Step 4: 再跑单测** + +```bash +cd miniprogram && node utils/__tests__/safeArea.style.test.mjs +``` + +Expected: 打印 `ok`,exit 0 + +- [ ] **Step 5: Commit** + +```bash +git add miniprogram/utils/safeAreaCss.js miniprogram/utils/safeArea.js miniprogram/utils/__tests__/safeArea.style.test.mjs +git commit -m "feat(miniprogram): add safe-area CSS vars style builder" +``` + +--- + +### Task 2: 地磅 `.db-header` 样式(方案 B) + +**Files:** +- Modify: `miniprogram/styles/db-theme.scss`(约 L10–36) + +- [ ] **Step 1: 替换 `.db-header` 块为:** + +```scss +.db-header { + position: sticky; + top: 0; + z-index: 50; + background: #fff; + border-bottom: 1px solid #eee; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + height: var(--sq-header-height, calc(44px + 44px)); + padding-top: var(--sq-safe-top, 44px); + box-sizing: border-box; +} + +.db-header__nav { + height: var(--sq-nav-height, 44px); + display: grid; + grid-template-columns: auto minmax(0, 1fr) var(--sq-capsule-right-safe, 96px); + align-items: center; + padding: 0 12px 0 8px; + box-sizing: border-box; +} + +.db-header__left { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.db-header__back { + font-size: 20px; + color: #333; + padding: 4px 8px; +} + +.db-header__title { + font-size: 17px; + font-weight: 700; + color: #111827; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.db-header__user { + font-size: 12px; + color: #6b7280; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: right; + padding-right: 4px; +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add miniprogram/styles/db-theme.scss +git commit -m "style(miniprogram): adapt db-header for WeChat capsule safe area" +``` + +--- + +### Task 3: 地磅页面挂载变量 + `__nav` 结构 + +**Files:** +- Modify: + - `miniprogram/pages/db/dashboard/index.vue` + - `miniprogram/pages/db/plans/list.vue` + - `miniprogram/pages/db/plans/detail.vue` + - `miniprogram/pages/db/inventory/list.vue` + - `miniprogram/pages/db/inventory/detail.vue` + - `miniprogram/pages/db/weighing/list.vue` + - `miniprogram/pages/db/weighing/detail.vue` + +- [ ] **Step 1: 每个文件 script 增加** + +```js +import { getSafeAreaStyle } from '@/utils/safeArea' +const safeAreaStyle = getSafeAreaStyle() +``` + +(若页面已是 `