2026-08-14-wechat-notify-menu-entry.md 14 KB

开启微信通知菜单聚合入口 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(风格对齐 oa-corner-index.test.js)

// mobile-h5/utils/mp-follow-confirm.test.js
import assert from 'node:assert/strict'
import {
  followStorageKey,
  isFollowConfirmed,
  setFollowConfirmed,
  resolveStepDone
} from './mp-follow-confirm.js'

assert.equal(followStorageKey('u1'), 'mp_follow_confirmed:u1')
assert.equal(followStorageKey(''), 'mp_follow_confirmed:anonymous')

const store = new Map()
const api = {
  getStorageSync: (k) => store.get(k),
  setStorageSync: (k, v) => {
    store.set(k, v)
  }
}
assert.equal(isFollowConfirmed('u1', api), false)
setFollowConfirmed('u1', true, api)
assert.equal(isFollowConfirmed('u1', api), true)
assert.equal(isFollowConfirmed('u2', api), false)

assert.deepEqual(resolveStepDone({ bound: true, followConfirmed: false }), {
  step1Done: true,
  step2Done: true
})
assert.deepEqual(resolveStepDone({ bound: false, followConfirmed: true }), {
  step1Done: true,
  step2Done: false
})
assert.deepEqual(resolveStepDone({ bound: false, followConfirmed: false }), {
  step1Done: false,
  step2Done: false
})

console.log('mp-follow-confirm: ok')
  • Step 2: Run test — expect FAIL
cd mobile-h5 && node utils/mp-follow-confirm.test.js

Expected: FAIL(模块不存在)

  • Step 3: Implement utility
// 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
  }
}
  • Step 4: Run test — expect PASS
cd mobile-h5 && node utils/mp-follow-confirm.test.js

Expected: 打印 mp-follow-confirm: ok,exit 0

  • Step 5: Commit
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 增加:

'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 判断后增加:

if "oa/wechat-notify" in p:
    return "oa-wechat-notify"

_mobile_menu_icon_colors 的 any(...) 元组增加 "oa/wechat-notify"。

  • Step 4: Commit
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 条目后)

{
  "path": "pages/oa/wechat-notify/index",
  "style": {
    "navigationBarTitleText": "开启微信通知",
    "navigationBarBackgroundColor": "#ecfdf5",
    "navigationBarTextStyle": "black"
  }
}
  • Step 2: Implement page

关键页面:无顶部「已开启」横幅;两张步骤卡。逻辑要点:

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

-- 小程序「开启微信通知」菜单(抄送我的之后)
-- 前置: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: 在测试库执行(或提示运维执行)
# 按本地习惯执行;示例:
# mysql ... < docs/sql/mobile_menu_oa_wechat_notify.sql

Web 角色管理给目标角色勾选「开启微信通知」。

  • Step 3: Commit
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

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

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
./scripts/update-graph.sh backend
  • Step 3: Commit
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: 跑工具单测
cd mobile-h5 && node utils/mp-follow-confirm.test.js

Expected: 打印 mp-follow-confirm: ok


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/占位步骤。