docs: add implementation plan for WeChat notify menu entry
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
26fe6182df
commit
514569e667
1 file changed
+559
@@ -0,0 +1,559 @@
|
||||
# 开启微信通知菜单聚合入口 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:** 在「抄送我的」后新增「开启微信通知」菜单,单页完成关注(步骤 1)与绑定(步骤 2),并移除登录页 / 首页顶栏旧入口。
|
||||
|
||||
**Architecture:** 新增原生页 `pages/oa/wechat-notify/index`:步骤 1 用 `openOfficialAccountProfile` / 二维码 + 本地「我已关注」;步骤 2 `navigateTo` 既有 `mp-bind` web-view。菜单 icon 由后端 `mini_menu._mobile_menu_icon` 按 path 映射到 `oa-wechat-notify` PNG。不改 bind API 与推送逻辑。
|
||||
|
||||
**Tech Stack:** uni-app Vue2/3(mobile-h5)、既有 `/mini/auth/mp-bind-status`、MySQL `sys_menu` 种子 SQL
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-08-14-wechat-notify-menu-entry-design.md`
|
||||
**Demo:** `mobile-ui/zkxw-mobile-wechat-notify-menu-demo.html`
|
||||
|
||||
---
|
||||
|
||||
## File map
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `mobile-h5/utils/mp-follow-confirm.js` | 按 userId 读写「我已关注」本地标记 |
|
||||
| `mobile-h5/utils/mp-follow-confirm.test.js` | 上述工具单测(Node 可跑则用;否则用纯函数导出测) |
|
||||
| `mobile-h5/pages/oa/wechat-notify/index.vue` | 步骤 1 / 2 设置页 |
|
||||
| `mobile-h5/pages.json` | 注册路由 |
|
||||
| `mobile-h5/components/van-icon/van-icon.vue` | PNG 映射 `oa-wechat-notify` |
|
||||
| `mobile-h5/utils/menu-icon-colors.js` | OA PNG 透明底集合 |
|
||||
| `backend/app/api/mini_menu.py` | path → icon / transparent 色 |
|
||||
| `docs/sql/mobile_menu_oa_wechat_notify.sql` | 菜单种子 OA007 |
|
||||
| `mobile-h5/pages/login/index.vue` + `login.scss` | 删除关注入口 |
|
||||
| `mobile-h5/pages/index/index.vue` | 删除顶栏通知入口 |
|
||||
| `docs/superpowers/specs/2026-08-13-oa-wechat-service-notify-design.md` | §6.1/6.2 加迁移注记 |
|
||||
|
||||
图标资源已存在:`mobile-h5/static/menu-icons/oa-wechat-notify.png`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 本地「我已关注」工具 + 单测
|
||||
|
||||
**Files:**
|
||||
- Create: `mobile-h5/utils/mp-follow-confirm.js`
|
||||
- Create: `mobile-h5/utils/mp-follow-confirm.test.js`
|
||||
|
||||
- [ ] **Step 1: Write failing test**
|
||||
|
||||
```javascript
|
||||
// mobile-h5/utils/mp-follow-confirm.test.js
|
||||
const {
|
||||
followStorageKey,
|
||||
isFollowConfirmed,
|
||||
setFollowConfirmed,
|
||||
resolveStepDone
|
||||
} = require('./mp-follow-confirm.js')
|
||||
|
||||
describe('mp-follow-confirm', () => {
|
||||
const store = new Map()
|
||||
const api = {
|
||||
getStorageSync: (k) => store.get(k),
|
||||
setStorageSync: (k, v) => store.set(k, v)
|
||||
}
|
||||
|
||||
beforeEach(() => store.clear())
|
||||
|
||||
test('followStorageKey isolates by userId', () => {
|
||||
expect(followStorageKey('u1')).toBe('mp_follow_confirmed:u1')
|
||||
expect(followStorageKey('')).toBe('mp_follow_confirmed:anonymous')
|
||||
})
|
||||
|
||||
test('set/get follow confirmed', () => {
|
||||
expect(isFollowConfirmed('u1', api)).toBe(false)
|
||||
setFollowConfirmed('u1', true, api)
|
||||
expect(isFollowConfirmed('u1', api)).toBe(true)
|
||||
expect(isFollowConfirmed('u2', api)).toBe(false)
|
||||
})
|
||||
|
||||
test('resolveStepDone: bound forces both done', () => {
|
||||
expect(resolveStepDone({ bound: true, followConfirmed: false })).toEqual({
|
||||
step1Done: true,
|
||||
step2Done: true
|
||||
})
|
||||
expect(resolveStepDone({ bound: false, followConfirmed: true })).toEqual({
|
||||
step1Done: true,
|
||||
step2Done: false
|
||||
})
|
||||
expect(resolveStepDone({ bound: false, followConfirmed: false })).toEqual({
|
||||
step1Done: false,
|
||||
step2Done: false
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
若工程无 Jest/`describe`,改为与 `oa-corner-index.test.js` 相同的断言风格(见该文件:自跑 `assert`)。先打开 `mobile-h5/utils/oa-corner-index.test.js` 对齐 runner,再写等价断言。
|
||||
|
||||
- [ ] **Step 2: Run test — expect FAIL**
|
||||
|
||||
```bash
|
||||
cd mobile-h5 && node --test utils/mp-follow-confirm.test.js
|
||||
# 或: node utils/mp-follow-confirm.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL(模块不存在)
|
||||
|
||||
- [ ] **Step 3: Implement utility**
|
||||
|
||||
```javascript
|
||||
// mobile-h5/utils/mp-follow-confirm.js
|
||||
export function followStorageKey(userId) {
|
||||
const id = String(userId || '').trim() || 'anonymous'
|
||||
return 'mp_follow_confirmed:' + id
|
||||
}
|
||||
|
||||
export function isFollowConfirmed(userId, storage) {
|
||||
const s = storage || (typeof uni !== 'undefined' ? uni : null)
|
||||
if (!s || typeof s.getStorageSync !== 'function') return false
|
||||
try {
|
||||
return String(s.getStorageSync(followStorageKey(userId)) || '') === '1'
|
||||
} catch (_) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function setFollowConfirmed(userId, confirmed, storage) {
|
||||
const s = storage || (typeof uni !== 'undefined' ? uni : null)
|
||||
if (!s || typeof s.setStorageSync !== 'function') return
|
||||
try {
|
||||
s.setStorageSync(followStorageKey(userId), confirmed ? '1' : '0')
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns {{ step1Done: boolean, step2Done: boolean }} */
|
||||
export function resolveStepDone({ bound, followConfirmed }) {
|
||||
if (bound) return { step1Done: true, step2Done: true }
|
||||
return {
|
||||
step1Done: !!followConfirmed,
|
||||
step2Done: false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
若测试用 `require`,在文件末尾加:
|
||||
|
||||
```javascript
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
followStorageKey,
|
||||
isFollowConfirmed,
|
||||
setFollowConfirmed,
|
||||
resolveStepDone
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
(或改测试为与现有 ESM 测试一致。)
|
||||
|
||||
- [ ] **Step 4: Run test — expect PASS**
|
||||
|
||||
```bash
|
||||
cd mobile-h5 && node --test utils/mp-follow-confirm.test.js
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add mobile-h5/utils/mp-follow-confirm.js mobile-h5/utils/mp-follow-confirm.test.js
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(mobile-h5): add local follow-confirm helpers for WeChat notify steps
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 图标映射(van-icon + 色 + 后端 path)
|
||||
|
||||
**Files:**
|
||||
- Modify: `mobile-h5/components/van-icon/van-icon.vue`
|
||||
- Modify: `mobile-h5/utils/menu-icon-colors.js`
|
||||
- Modify: `backend/app/api/mini_menu.py`
|
||||
|
||||
- [ ] **Step 1: van-icon PNG map**
|
||||
|
||||
在 `PNG_ICONS` 增加:
|
||||
|
||||
```javascript
|
||||
'oa-wechat-notify': '/static/menu-icons/oa-wechat-notify.png'
|
||||
```
|
||||
|
||||
- [ ] **Step 2: menu-icon-colors**
|
||||
|
||||
`OA_PNG_ICONS` 与 `MENU_ICON_COLOR_DEFAULTS` 增加 `'oa-wechat-notify'`(transparent)。
|
||||
|
||||
- [ ] **Step 3: backend mini_menu.py**
|
||||
|
||||
`_mobile_menu_icon`:在 `oa/cc` 判断后增加:
|
||||
|
||||
```python
|
||||
if "oa/wechat-notify" in p:
|
||||
return "oa-wechat-notify"
|
||||
```
|
||||
|
||||
`_mobile_menu_icon_colors` 的 `any(...)` 元组增加 `"oa/wechat-notify"`。
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add mobile-h5/components/van-icon/van-icon.vue \
|
||||
mobile-h5/utils/menu-icon-colors.js backend/app/api/mini_menu.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat: map oa-wechat-notify icon for mobile menu grid
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 新建「开启微信通知」步骤页 + pages.json
|
||||
|
||||
**Files:**
|
||||
- Create: `mobile-h5/pages/oa/wechat-notify/index.vue`
|
||||
- Modify: `mobile-h5/pages.json`
|
||||
|
||||
- [ ] **Step 1: Register in pages.json**(紧挨 `mp-bind` 条目后)
|
||||
|
||||
```json
|
||||
{
|
||||
"path": "pages/oa/wechat-notify/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "开启微信通知",
|
||||
"navigationBarBackgroundColor": "#ecfdf5",
|
||||
"navigationBarTextStyle": "black"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Implement page**
|
||||
|
||||
关键页面:无顶部「已开启」横幅;两张步骤卡。逻辑要点:
|
||||
|
||||
```javascript
|
||||
import {
|
||||
fetchMpBindStatus,
|
||||
getUser,
|
||||
forceRelogin,
|
||||
getToken
|
||||
} from '../../../utils/auth.js'
|
||||
import {
|
||||
isFollowConfirmed,
|
||||
setFollowConfirmed,
|
||||
resolveStepDone
|
||||
} from '../../../utils/mp-follow-confirm.js'
|
||||
import { MP_GH_USERNAME, MP_QRCODE_URL } from '../../../utils/config.js'
|
||||
|
||||
function currentUserId() {
|
||||
const u = getUser() || {}
|
||||
return u.id || u.userId || u.username || ''
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
bound: false,
|
||||
followConfirmed: false,
|
||||
showQr: false,
|
||||
mpQrcodeUrl: MP_QRCODE_URL,
|
||||
step1Done: false,
|
||||
step2Done: false
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
if (!getToken()) {
|
||||
forceRelogin('请先登录')
|
||||
return
|
||||
}
|
||||
this.followConfirmed = isFollowConfirmed(currentUserId())
|
||||
this.refreshBind()
|
||||
},
|
||||
methods: {
|
||||
applySteps() {
|
||||
const r = resolveStepDone({
|
||||
bound: this.bound,
|
||||
followConfirmed: this.followConfirmed
|
||||
})
|
||||
this.step1Done = r.step1Done
|
||||
this.step2Done = r.step2Done
|
||||
},
|
||||
async refreshBind() {
|
||||
try {
|
||||
const res = await fetchMpBindStatus()
|
||||
this.bound = !!(res && res.bound)
|
||||
} catch (_) {
|
||||
this.bound = false
|
||||
}
|
||||
this.applySteps()
|
||||
},
|
||||
onFollow() {
|
||||
const username = (MP_GH_USERNAME || '').trim()
|
||||
const openProfile =
|
||||
typeof wx !== 'undefined' && typeof wx.openOfficialAccountProfile === 'function'
|
||||
if (openProfile && username) {
|
||||
wx.openOfficialAccountProfile({
|
||||
username,
|
||||
fail: () => {
|
||||
this.showQr = true
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
this.showQr = true
|
||||
},
|
||||
onConfirmFollowed() {
|
||||
setFollowConfirmed(currentUserId(), true)
|
||||
this.followConfirmed = true
|
||||
this.showQr = false
|
||||
this.applySteps()
|
||||
uni.showToast({ title: '已标记关注', icon: 'none' })
|
||||
},
|
||||
onBind() {
|
||||
if (!this.step1Done || this.step2Done) return
|
||||
uni.navigateTo({ url: '/pages/oa/mp-bind/index' })
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
模板结构对齐 Demo(步骤角标「未完成/已完成」;步骤 2 按钮在 `!step1Done` 时 disabled,文案「请先完成步骤 1」;`step2Done` 时「已绑定」disabled)。二维码用 `MP_QRCODE_URL`,`show-menu-by-longpress`。样式用三墙 token(`#0a9f6d` 等),scoped SCSS,勿用 web-admin `#10b981`。
|
||||
|
||||
- [ ] **Step 3: Smoke in HBuilder / 微信开发者工具**
|
||||
|
||||
打开 `/pages/oa/wechat-notify/index`:两步可见;未确认时步骤 2 不可点。
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add mobile-h5/pages/oa/wechat-notify/index.vue mobile-h5/pages.json
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(mobile-h5): add WeChat notify setup page with follow and bind steps
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 菜单 SQL 种子
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/sql/mobile_menu_oa_wechat_notify.sql`
|
||||
|
||||
- [ ] **Step 1: Write SQL**(风格对齐 `mobile_menu_oa_task_centers.sql`)
|
||||
|
||||
```sql
|
||||
-- 小程序「开启微信通知」菜单(抄送我的之后)
|
||||
-- 前置:docs/sql/mobile_menu_oa_task_centers.sql(OA006)可已执行
|
||||
-- 角色授权请在 Web「角色管理」勾选本菜单。
|
||||
|
||||
SET @mini_root = (
|
||||
SELECT id FROM sys_menu
|
||||
WHERE del_flag = 0 AND parent_id = '0' AND menu_name LIKE '%移动端%'
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_menu (
|
||||
id, menu_name, parent_id, menu_type, path, component, perms,
|
||||
is_frame, visible, status, tenant_no, order_num, remark,
|
||||
create_by, create_dept_id, create_time, update_by, update_time, del_flag
|
||||
)
|
||||
SELECT
|
||||
'MINI000000000000000000000000OA007',
|
||||
'开启微信通知',
|
||||
@mini_root,
|
||||
'C',
|
||||
'/pages/oa/wechat-notify/index',
|
||||
NULL,
|
||||
'mini:oa:wechat-notify',
|
||||
1,
|
||||
'1',
|
||||
'1',
|
||||
'zkxw',
|
||||
70,
|
||||
'小程序开启微信通知(关注+绑定)',
|
||||
'admin',
|
||||
'',
|
||||
NOW(),
|
||||
NULL,
|
||||
NOW(),
|
||||
0
|
||||
FROM DUAL
|
||||
WHERE @mini_root IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_menu WHERE id = 'MINI000000000000000000000000OA007' AND del_flag = 0
|
||||
);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 在测试库执行**(或提示运维执行)
|
||||
|
||||
```bash
|
||||
# 按本地习惯执行;示例:
|
||||
# mysql ... < docs/sql/mobile_menu_oa_wechat_notify.sql
|
||||
```
|
||||
|
||||
Web 角色管理给目标角色勾选「开启微信通知」。
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/sql/mobile_menu_oa_wechat_notify.sql
|
||||
git commit -m "$(cat <<'EOF'
|
||||
docs(sql): seed mobile menu for WeChat notify setup
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 移除登录页关注入口
|
||||
|
||||
**Files:**
|
||||
- Modify: `mobile-h5/pages/login/index.vue`
|
||||
- Modify: `mobile-h5/styles/components/login.scss`
|
||||
|
||||
- [ ] **Step 1: Remove template blocks**
|
||||
|
||||
删除:
|
||||
|
||||
- `<view class="login-follow-mp" ...>关注服务号...</view>`
|
||||
- 整块 `login-mp-qrcode` 弹层
|
||||
|
||||
- [ ] **Step 2: Remove script**
|
||||
|
||||
删除:`MP_GH_USERNAME` / `MP_QRCODE_URL` import(若不再使用)、`showMpQrcode`、`mpQrcodeUrl`、`onFollowMp`。
|
||||
|
||||
- [ ] **Step 3: Remove SCSS**
|
||||
|
||||
删除 `.login-follow-mp`、`.login-mp-qrcode*` 相关规则。
|
||||
|
||||
- [ ] **Step 4: 目视确认登录页无关注链接**
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add mobile-h5/pages/login/index.vue mobile-h5/styles/components/login.scss
|
||||
git commit -m "$(cat <<'EOF'
|
||||
fix(mobile-h5): remove follow-MP entry from login page
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 移除首页顶栏通知入口
|
||||
|
||||
**Files:**
|
||||
- Modify: `mobile-h5/pages/index/index.vue`
|
||||
|
||||
- [ ] **Step 1: Remove template**
|
||||
|
||||
删除 `home-mp-notify` 的 `<text>`;`meta__right` 仅保留「退出登录」。
|
||||
|
||||
- [ ] **Step 2: Remove script**
|
||||
|
||||
删除:`fetchMpBindStatus` import(若仅用于顶栏)、`mpNotifyBound`、`refreshMpBindStatus`、`onTapMpNotify`,以及 `onShow` 里对 `refreshMpBindStatus` 的调用。
|
||||
|
||||
- [ ] **Step 3: Remove scoped styles**
|
||||
|
||||
删除 `.home-mp-notify` / `--bound`;若 `.meta__right` 仅因通知入口设为 column,可简化为与退出对齐的合理布局(保持「退出登录」可见即可)。
|
||||
|
||||
- [ ] **Step 4: 确认首页菜单仍可点进新页;顶栏无旧文案**
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add mobile-h5/pages/index/index.vue
|
||||
git commit -m "$(cat <<'EOF'
|
||||
fix(mobile-h5): remove WeChat notify shortcut from home header
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 旧设计文档迁移注记 + 图谱(若改了 backend)
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/superpowers/specs/2026-08-13-oa-wechat-service-notify-design.md`
|
||||
- Run: `./scripts/update-graph.sh backend`(因改了 `mini_menu.py`)
|
||||
|
||||
- [ ] **Step 1: Annotate §6.1 / §6.2**
|
||||
|
||||
在 §6.1、§6.2 段首加:
|
||||
|
||||
> **入口已迁移(2026-08-14):** 登录页关注与工作台顶栏绑定入口已废弃;统一见 `docs/superpowers/specs/2026-08-14-wechat-notify-menu-entry-design.md` 与菜单「开启微信通知」。
|
||||
|
||||
- [ ] **Step 2: Update graph**
|
||||
|
||||
```bash
|
||||
./scripts/update-graph.sh backend
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/superpowers/specs/2026-08-13-oa-wechat-service-notify-design.md \
|
||||
backend/graphify-out/
|
||||
git commit -m "$(cat <<'EOF'
|
||||
docs: note WeChat notify entry migration to dedicated menu
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 端到端验收
|
||||
|
||||
- [ ] **Step 1: Checklist**
|
||||
|
||||
1. 登录页无「关注服务号,接收审批通知」。
|
||||
2. 首页顶栏无「开启微信审批通知 / 微信通知已开启」。
|
||||
3. 角色授权后,宫格在「抄送我的」后出现「开启微信通知」+ 铃铛 PNG。
|
||||
4. 步骤 1:去关注 → 我已关注 → 角标已完成;步骤 2 可点。
|
||||
5. 步骤 1 未确认时步骤 2 禁用。
|
||||
6. 去绑定成功返回后步骤 2 已完成。
|
||||
7. 已绑定账号进入:两步均为已完成(无需点「我已关注」)。
|
||||
8. 换账号:本地关注标记不串号。
|
||||
9. 页面无顶部「已开启」总状态条。
|
||||
|
||||
- [ ] **Step 2: 跑工具单测**
|
||||
|
||||
```bash
|
||||
cd mobile-h5 && node --test utils/mp-follow-confirm.test.js
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
---
|
||||
|
||||
## Spec coverage (self-review)
|
||||
|
||||
| Spec 要求 | Task |
|
||||
|-----------|------|
|
||||
| 新菜单「开启微信通知」在抄送后 | Task 4 SQL + Task 2/3 路由图标 |
|
||||
| 步骤 1/2 同页、无顶部已开启条 | Task 3 |
|
||||
| 我已关注本地确认 | Task 1 + 3 |
|
||||
| 步骤 2 走 mp-bind | Task 3 |
|
||||
| bound 则两步完成 | Task 1 `resolveStepDone` + Task 3 |
|
||||
| 去掉登录关注 | Task 5 |
|
||||
| 去掉首页顶栏 | Task 6 |
|
||||
| icon 英文化 PNG | 已有资源 + Task 2 |
|
||||
| 旧设计迁移说明 | Task 7 |
|
||||
|
||||
无 TBD/占位步骤。
|
||||
Reference in new issue
Block a user