Files
shanxia2.0/docs/superpowers/plans/2026-09-12-miniprogram-header-capsule-safe-area.md
T

495 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 小程序顶部栏胶囊安全区适配 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()
```
(若页面已是 `<script setup>`,直接按上;否则在 `data`/`setup` 暴露 `safeAreaStyle`。)
- [ ] **Step 2: 模板统一改结构**
仪表盘示例(有用户名):
```html
<view class="db-header" :style="safeAreaStyle">
<view class="db-header__nav">
<view class="db-header__left">
<text class="db-header__back" @click="goWorkbench">←</text>
<text class="db-header__title">仪表盘</text>
</view>
<text class="db-header__user">{{ username }}</text>
<view /><!-- 胶囊占位列 -->
</view>
</view>
```
列表/详情(无用户名):
```html
<view class="db-header" :style="safeAreaStyle">
<view class="db-header__nav">
<view class="db-header__left">
<text class="db-header__back" @click="goBack">←</text>
<text class="db-header__title">{{ title }}</text>
</view>
<view />
<view />
</view>
</view>
```
删除 `dashboard/index.vue` 中 scoped 重复的 `.db-header__user`(已迁入 theme),避免冲突。
- [ ] **Step 3: Commit**
```bash
git add miniprogram/pages/db/
git commit -m "fix(miniprogram): wire db pages to capsule safe-area header"
```
---
### Task 4: 报销 `.topbar` 样式 + 页面绑定
**Files:**
- Modify: `miniprogram/styles/bx-theme.scss`(`.bx-page .topbar` 约 L74–148)
- Modify 所有含 `<view class="topbar">` 的文件:
- `miniprogram/pages/bx/advance/{list,apply,detail}.vue`
- `miniprogram/pages/bx/reimburse/{list,apply,detail}.vue`
- `miniprogram/pages/bx/mine/list.vue`
- `miniprogram/pages/bx/all/list.vue`
- `miniprogram/pages/bx/invoice/{mine,admin}.vue`
- `miniprogram/pages/bx/admin/expense-item.vue`
- `miniprogram/components/bx-ui/bx-wf-config-page.vue`
- [ ] **Step 1: 替换 topbar 高度规则**
将:
```scss
height: calc(58px + env(safe-area-inset-top, 0px));
padding: env(safe-area-inset-top, 0px) 12px 0;
```
改为:
```scss
.bx-page .topbar {
position: sticky;
top: 0;
z-index: 50;
display: grid;
grid-template-columns: 1fr auto var(--sq-capsule-right-safe, 96px);
align-items: center;
height: var(--sq-header-height, calc(44px + 44px));
padding: var(--sq-safe-top, 44px) 12px 0;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 1px 0 rgba(217, 232, 226, 0.85), 0 8px 20px rgba(15, 23, 42, 0.06);
}
.bx-page .topbar::before {
height: var(--sq-header-height, calc(44px + 44px));
/* 其余渐变背景保持不变 */
}
```
标题仍 `justify-self: center`;`topbar-right` 作为中间/右侧业务槽时不得伸入最后一列——最后一列留给胶囊(grid 第三列为空即可;若现结构是 `1fr auto 1fr`,改为 `1fr auto var(--sq-capsule-right-safe)`,左侧放返回,中间标题,右侧列空)。
具体 grid:
```scss
grid-template-columns: minmax(0, 1fr) auto var(--sq-capsule-right-safe, 96px);
```
`topbar-title` 放第二列居中;`topbar-right` 若需对称,可并入第一列或改为不占用第三列。
- [ ] **Step 2: 每个 topbar 根节点**
```html
<view class="topbar" :style="safeAreaStyle">
```
script:
```js
import { getSafeAreaStyle } from '@/utils/safeArea'
const safeAreaStyle = getSafeAreaStyle()
```
- [ ] **Step 3: 修正 `expense-item.vue` 列表高度计算**
将:
```js
const topbar = 44 + (sys.statusBarHeight || 0)
```
改为:
```js
import { getGlobalSafeArea } from '@/utils/safeArea'
const topbar = getGlobalSafeArea().headerHeight
```
- [ ] **Step 4: Commit**
```bash
git add miniprogram/styles/bx-theme.scss miniprogram/pages/bx/ miniprogram/components/bx-ui/bx-wf-config-page.vue
git commit -m "fix(miniprogram): adapt bx topbar for WeChat capsule safe area"
```
---
### Task 5: 壳层 `.mp-header`(工作台/首页/待办/占位)
**Files:**
- Modify: `miniprogram/styles/mp-shell.scss`(`.mp-header` 及 `--home` / `--workbench`)
- Modify:
- `miniprogram/pages/workbench/workbench.vue`
- `miniprogram/pages/home/home.vue`
- `miniprogram/pages/todo/todo.vue`
- `miniprogram/pages/common/biz-placeholder.vue`
- `miniprogram/components/bx-ui/bx-placeholder-page.vue`
- Modify: `miniprogram/components/app-safe-header.vue`(改用 `buildSafeAreaCssVarsStyle`)
- [ ] **Step 1: 更新 mp-shell 头部 padding**
```scss
.mp-header {
background: var(--mp-header-gradient);
color: #fff;
padding:
calc(28rpx + var(--sq-safe-top, env(safe-area-inset-top, 0px)))
calc(32rpx + var(--sq-capsule-right-safe, 96px))
32rpx
32rpx;
border-bottom-left-radius: var(--mp-radius-xl);
border-bottom-right-radius: var(--mp-radius-xl);
box-shadow: 0 16rpx 40rpx rgba(10, 159, 109, 0.12);
box-sizing: border-box;
}
.mp-header--home {
padding-top: calc(40rpx + var(--sq-safe-top, env(safe-area-inset-top, 0px)));
padding-bottom: 48rpx;
}
.mp-header--workbench {
/* 去掉魔法 40px 双叠加;仅用 safe-top + 内容间距 */
padding-top: calc(48rpx + var(--sq-safe-top, env(safe-area-inset-top, 0px)));
padding-bottom: 56rpx;
min-height: calc(200rpx + var(--sq-safe-top, 0px));
box-sizing: border-box;
}
```
工作台 `mp-header__row` 已有左右分栏;右侧用户栈靠 `padding-right` 的 capsule 预留避开胶囊,**不要**再把退出按钮画进胶囊区。
- [ ] **Step 2: 各页 `:style="safeAreaStyle"` + `getSafeAreaStyle()`**
```html
<view class="mp-header mp-header--workbench" :style="safeAreaStyle">
```
- [ ] **Step 3: `app-safe-header.vue` 改用**
```js
import { getGlobalSafeArea, buildSafeAreaCssVarsStyle } from '@/utils/safeArea'
const headerStyle = computed(() => ({
...buildSafeAreaCssVarsStyle(getGlobalSafeArea()),
'--sq-header-background': props.background,
}))
```
- [ ] **Step 4: Commit**
```bash
git add miniprogram/styles/mp-shell.scss miniprogram/pages/workbench/workbench.vue miniprogram/pages/home/home.vue miniprogram/pages/todo/todo.vue miniprogram/pages/common/biz-placeholder.vue miniprogram/components/bx-ui/bx-placeholder-page.vue miniprogram/components/app-safe-header.vue
git commit -m "fix(miniprogram): adapt mp-header for WeChat capsule safe area"
```
---
### Task 6: 规范文档 + 手工验收清单
**Files:**
- Modify: `docs/miniprogram-ui-spec-sanqiang.md` §3.2
- [ ] **Step 1: 在 §3.2 追加**
```markdown
#### 自定义导航与微信胶囊(SHALL)
- 使用 `navigationStyle: custom` 的页面,业务顶栏 SHALL 避开状态栏与右上角胶囊。
- 量纲 SHALL 来自 `statusBarHeight` 与 `getMenuButtonBoundingClientRect`(见 `utils/safeArea.js`)。
- CSS 变量:`--sq-safe-top`、`--sq-nav-height`、`--sq-header-height`、`--sq-capsule-right-safe`;微信端 SHALL 通过顶栏根节点内联 style 注入,不得仅依赖 H5 `document`。
- 白底顶栏:总高 `--sq-header-height`,内容行高 `--sq-nav-height`,右侧列预留 `--sq-capsule-right-safe`,该列 SHALL NOT 放置可点击业务按钮。
- 渐变大头:`padding-top` 含 `--sq-safe-top`,`padding-right` 含 `--sq-capsule-right-safe`。
```
- [ ] **Step 2: 手工验收(微信开发者工具 + 真机体验版)**
| 页面 | 检查 |
|------|------|
| 地磅仪表盘 | 标题/返回/用户名不被胶囊挡 |
| 地磅任一列表+详情 | 同上 |
| 报销任一列表 | 同上 |
| 工作台 | 标题与退出不与胶囊重叠 |
| H5 预览(若有) | 无胶囊时右侧无异常大空白 |
- [ ] **Step 3: Commit**
```bash
git add docs/miniprogram-ui-spec-sanqiang.md
git commit -m "docs: require miniprogram custom header capsule safe area"
```
---
## Spec 覆盖自检
| Spec 要求 | Task |
|-----------|------|
| 方案 B 几何 | 2, 4, 5 |
| 微信端变量注入 | 1, 3, 4, 5 |
| 地磅全链路 | 2, 3 |
| 报销 topbar | 4 |
| mp-header / 工作台 | 5 |
| 规范 SHALL | 6 |
| 无胶囊兜底 | Task1 metrics + SCSS fallback;H5 右侧 0 |
| 不改默认导航页 | 未列入文件 |
## 执行交接
Plan complete and saved to `docs/superpowers/plans/2026-09-12-miniprogram-header-capsule-safe-area.md`. Two execution options:
**1. Subagent-Driven (recommended)** — 每个 Task 派生子代理,Task 间复查
**2. Inline Execution** — 本会话按 executing-plans 连续执行并设检查点
Which approach?