2026-08-11-mobile-oa-start-approval-phase2.md 25 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: 在小程序落地「发起审批」:原生模板列表 + H5 正式填写页(动态表单 + 审批流预览 + 草稿/提交)+「移动端菜单」注册与首页联调。

Architecture: 对齐设计 docs/superpowers/specs/2026-08-11-mobile-oa-start-approval-design.md §6。小程序只做入口与列表;填写页在 mobile-oa-h5 的 /oa-start(web-view 全屏),复用 Web /web/admin/oa-approval/* + JWT。阶段一水槽已证明 form-create/vant 与自研 type 可用,本阶段接真实 schema 与运行时 API。

Tech Stack: Vue3、Vite、Vant4、@form-create/vant、uni-app 微信小程序、web-view、FastAPI(菜单种子 + 可选 mini 菜单接口)

Spec: docs/superpowers/specs/2026-08-11-mobile-oa-start-approval-design.md

前置(阶段一已完成): mobile-oa-h5 工程、水槽、token 透传、P0~P2 自研控件注册、真机水槽验收。

Out of scope(本计划不做): 待办/已办/抄送办理、Web 设计器改造、小程序内直挂 form-create、草稿箱独立列表页(仅支持发起页内暂存)、套件与 Web 100% 业务细节(请假节假日等放 Task 8 可分期)。

实现状态(2026-08-11): Task 1~8 代码已落地并提交;Task 9 静态 type 核对已完成;真机全链路与菜单库授权仍待验收(见 spike 笔记验收表)。


File map

路径 职责
mobile-oa-h5/src/api/oa.js 扩展 templates / flow-preview / drafts / instances
mobile-oa-h5/src/oa/schema-prep.js 拉取后的 rule/option 预处理(labelAlign、upload ensure 等最小集)
mobile-oa-h5/src/oa/components/OaFlowPreview.vue 移动端审批流预览 + initiator_select
mobile-oa-h5/src/views/OaStart.vue 正式发起页:表单 + 流程 + 底栏
mobile-oa-h5/src/router.js /oa-start → OaStart.vue
mobile-h5/pages/oa/start/index.vue 已发布模板卡片列表
mobile-h5/pages/oa/start/webview.vue 整页 web-view 打开 /oa-start
mobile-h5/pages.json 注册上述路由
mobile-h5/utils/oa-h5.js 已有 buildOaStartUrl;按需微调
mobile-h5/pages/index/index.vue 接入真实/过渡菜单
mobile-h5/utils/menu.js 拉移动端菜单并映射 path
docs/sql/mobile_menu_oa_start.sql 「发起审批」菜单种子(挂在移动端根下)
backend/app/api/mini_menu.py(或扩 mini_auth) GET /mini/menus 当前用户移动端菜单
backend/app/api/__init__.py + main.py 注册路由

Web 对照(只读参考,勿改除非权限不够):

  • 列表:web-admin/src/views/oa-approval/start/index.vue
  • 弹窗:web-admin/src/components/oa-approval/OaStartFormDialog.vue
  • 流程:web-admin/src/components/oa-approval/OaFlowPreview.vue
  • API:web-admin/src/api/oaApproval.ts(BASE = /web/admin/oa-approval)

目标用户路径

首页「发起审批」
  → /pages/oa/start/index(模板卡片)
  → 点「发起」
  → /pages/oa/start/webview?appId=…
  → web-view: {OA_H5_ORIGIN}/oa-start?appId=&token=
  → 填表 / 看审批流 / 暂存或提交
  → navigateBack → 列表或首页

Task 1: 扩展 H5 OA 运行时 API

Files:

  • Modify: mobile-oa-h5/src/api/oa.js

  • [ ] Step 1: 在现有 oa.js 末尾追加运行时方法(保留已有 users/depts/upload 等)

// —— 发起审批运行时(对齐 web-admin oaApproval.ts)——

export function fetchOaGroupList() {
  return get('/web/admin/oa-approval/groups')
}

export function fetchOaTemplates() {
  return get('/web/admin/oa-approval/templates')
}

export function fetchOaTemplateDetail(appId) {
  return get(`/web/admin/oa-approval/templates/${encodeURIComponent(appId)}`)
}

export function fetchOaFlowPreview(appId, { formData, initiatorSelectByNode } = {}) {
  return post(`/web/admin/oa-approval/templates/${encodeURIComponent(appId)}/flow-preview`, {
    formData: formData || {},
    initiatorSelectByNode: initiatorSelectByNode || undefined
  })
}

export function fetchCreateOaInstanceDraft(data) {
  return post('/web/admin/oa-approval/drafts', data)
}

export function fetchUpdateOaInstanceDraft(id, data) {
  return request({
    url: `/web/admin/oa-approval/drafts/${encodeURIComponent(id)}`,
    method: 'PUT',
    data
  })
}

export function fetchOaInstanceDraftDetail(id) {
  return get(`/web/admin/oa-approval/drafts/${encodeURIComponent(id)}`)
}

export function fetchSubmitOaInstanceDraft(id, data) {
  return post(`/web/admin/oa-approval/drafts/${encodeURIComponent(id)}/submit`, data)
}

export function fetchStartOaInstance(data) {
  return post('/web/admin/oa-approval/instances', data)
}

说明:若 http.js 尚无通用 request 的 PUT,补上 export function put(url, data) { return request({ url, method: 'PUT', data }) } 并改用 put(...)。

  • Step 2: 浏览器带 token 冒烟(后端已启)
# 将 TOKEN 换成小程序登录 JWT
curl -s -H "Authorization: Bearer $TOKEN" \
  "http://127.0.0.1:8000/api/web/admin/oa-approval/templates" | head -c 400

Expected: JSON code: 200 且含 list(权限不足则记入 Task 7 权限核对,不阻塞前端骨架)。

  • Step 3: Commit
git add mobile-oa-h5/src/api/oa.js mobile-oa-h5/src/utils/http.js
git commit -m "feat(mobile-oa-h5): add OA start runtime API helpers"

Task 2: schema 最小预处理

Files:

  • Create: mobile-oa-h5/src/oa/schema-prep.js

  • [ ] Step 1: 写入最小预处理(移动端强制顶栏 label + 关闭内置提交钮;递归保证 upload 有 props)

import { oaFormOption } from './form-option.js'

function walkRules(rules, fn) {
  const list = Array.isArray(rules) ? rules : []
  for (const r of list) {
    if (!r || typeof r !== 'object') continue
    fn(r)
    if (Array.isArray(r.children)) walkRules(r.children, fn)
  }
}

/** 与 Web ensureOaUploadRuleProps 同思路的最小版 */
export function ensureOaUploadRuleProps(rules) {
  const next = JSON.parse(JSON.stringify(rules || []))
  walkRules(next, (r) => {
    if (r.type === 'upload' || r.type === 'fcUpload') {
      r.props = { ...(r.props || {}) }
      if (r.props.limit == null) r.props.limit = 9
    }
  })
  return next
}

export function prepareMobileFormSchema(formSchemaJson) {
  const schema = formSchemaJson || {}
  const rawRule = Array.isArray(schema.rule)
    ? schema.rule
    : Array.isArray(schema)
      ? schema
      : []
  const rule = ensureOaUploadRuleProps(rawRule)
  const option = {
    ...oaFormOption,
    ...(schema.option || {}),
    form: {
      ...(schema.option?.form || {}),
      labelAlign: 'top',
      inputAlign: 'left',
      colon: false
    },
    submitBtn: false,
    resetBtn: false
  }
  return { rule, option }
}

说明:Web 上 ensureLeaveKitRuleProps / owner 默认值等先不搬全量;表单能渲染、能提交即 MVP。Task 8 再对齐套件运行时。

  • Step 2: Commit
git add mobile-oa-h5/src/oa/schema-prep.js
git commit -m "feat(mobile-oa-h5): add minimal OA schema prep for mobile start"

Task 3: 移动端审批流预览组件

Files:

  • Create: mobile-oa-h5/src/oa/components/OaFlowPreview.vue

对齐 Web OaFlowPreview.vue 的数据契约:steps、resolved、pendingMessage、loading、initiatorSelectByNode(v-model)、userOptions。

  • Step 1: 实现组件骨架

关键行为:

  1. resolved === false → 展示 pendingMessage(默认「必填信息填写后,流程将自动显示」)。
  2. resolved === true → 竖向步骤链:start / approval / cc / end;展示 name、会签/或签徽章、meta、people chips。
  3. 若节点 assigneeTypes(或 assigneeType)含 initiator_select:展示已选人 +「添加」→ van-popup 内多选人员(数据来自 userOptions:{ id, nickName, userNo })。
  4. emit('update:initiatorSelectByNode', { ...map })。

模板结构示意(实现时补全样式,三墙绿):

<template>
  <div class="oa-flow-preview">
    <div class="oa-flow-preview__title">审批流程</div>
    <van-loading v-if="loading" vertical>加载中…</van-loading>
    <div v-else-if="!resolved" class="oa-flow-preview__pending">
      {{ pendingMessage || '必填信息填写后,流程将自动显示' }}
    </div>
    <div v-else class="oa-flow-preview__chain">
      <div v-for="(s, idx) in steps" :key="s.id || idx" class="oa-flow-step">
        <!-- rail + card:名称、徽章、people / initiator_select -->
      </div>
    </div>
    <!-- van-popup 选人 -->
  </div>
</template>

辅助函数:

function isInitiatorSelect(s) {
  const types =
    Array.isArray(s.assigneeTypes) && s.assigneeTypes.length
      ? s.assigneeTypes
      : s.assigneeType
        ? [s.assigneeType]
        : []
  return types.includes('initiator_select')
}

Props:

defineProps({
  steps: { type: Array, default: () => [] },
  resolved: { type: Boolean, default: true },
  pendingMessage: { type: String, default: '' },
  loading: { type: Boolean, default: false },
  initiatorSelectByNode: { type: Object, default: () => ({}) },
  userOptions: { type: Array, default: () => [] }
})
  • [ ] Step 2: 在浏览器临时把静态 steps 塞进水槽或独立路由自测布局(可选;正式联调在 Task 4)。

  • [ ] Step 3: Commit

git add mobile-oa-h5/src/oa/components/OaFlowPreview.vue
git commit -m "feat(mobile-oa-h5): add mobile OaFlowPreview for start approval"

Task 4: 正式页 /oa-start(表单 + 流程 + 草稿/提交)

Files:

  • Create: mobile-oa-h5/src/views/OaStart.vue
  • Modify: mobile-oa-h5/src/router.js

  • [ ] Step 1: 实现 OaStart.vue 核心逻辑

入参(route.query):appId 必填;可选 draftId、token(token 已由 captureTokenFromUrl 处理)。

页面结构:

header: 模板名称(或「发起审批」)
body:
  form-create-mobile(上)
  OaFlowPreview(下)
footer 固定:
  取消 | 暂存草稿 | 提交

关键流程(对齐 OaStartFormDialog):

import { computed, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { showToast, showConfirmDialog } from 'vant'
import {
  fetchOaTemplateDetail,
  fetchOaFlowPreview,
  fetchCreateOaInstanceDraft,
  fetchUpdateOaInstanceDraft,
  fetchOaInstanceDraftDetail,
  fetchSubmitOaInstanceDraft,
  fetchStartOaInstance,
  fetchUserList
} from '../api/oa.js'
import { prepareMobileFormSchema } from '../oa/schema-prep.js'
import OaFlowPreview from '../oa/components/OaFlowPreview.vue'

// formData, formRule, formOption, fapi
// editingDraftId, appMeta { id, appName }
// flowSteps, flowResolved, flowPendingMessage, previewLoading
// initiatorSelectByNode, userOptions

async function loadByAppId(appId) {
  const detail = await fetchOaTemplateDetail(appId)
  appMeta.value = { id: appId, appName: detail?.appName || detail?.name || '发起审批' }
  const prepared = prepareMobileFormSchema(detail?.formSchemaJson)
  formRule.value = prepared.rule
  formOption.value = prepared.option
  formData.value = {} // 后续 Task 8 再补 owner/kit defaults
  // 若 flow 含 initiator_select,分页拉 users(pageSize<=100)
  await loadFlowPreview()
}

async function loadFlowPreview() {
  if (!appMeta.value?.id) return
  previewLoading.value = true
  try {
    const preview = await fetchOaFlowPreview(appMeta.value.id, {
      formData: { ...formData.value },
      initiatorSelectByNode: { ...initiatorSelectByNode.value }
    })
    flowResolved.value = preview?.resolved !== false
    flowPendingMessage.value = preview?.message || '必填信息填写后,流程将自动显示'
    flowSteps.value = flowResolved.value ? preview?.steps || [] : []
  } catch (e) {
    showToast(e?.message || '审批流预览失败')
    flowSteps.value = []
    flowResolved.value = true
  } finally {
    previewLoading.value = false
  }
}

// watch formData + initiatorSelectByNode deep,300ms debounce → loadFlowPreview

async function saveDraft() { /* create or update;成功 toast + closeToMini */ }
async function submit() {
  // 1) initiator_select 节点必须有人选
  // 2) fapi.validate()
  // 3) draft ? submitDraft : startInstance
  // 4) closeToMini({ ok: true, instanceId })
}

function closeToMini(payload) {
  try {
    // 微信小程序 web-view
    // eslint-disable-next-line no-undef
    if (typeof wx !== 'undefined' && wx.miniProgram?.navigateBack) {
      wx.miniProgram.postMessage({ data: payload || { action: 'oa-start-close' } })
      wx.miniProgram.navigateBack()
      return
    }
  } catch (_) {}
  showToast('已完成(非小程序环境)')
}

onMounted:

const route = useRoute()
const appId = String(route.query.appId || '').trim()
const draftId = String(route.query.draftId || '').trim()
if (draftId) {
  const d = await fetchOaInstanceDraftDetail(draftId)
  editingDraftId.value = draftId
  await loadByAppId(d.appId)
  formData.value = d.formData || {}
  initiatorSelectByNode.value = d.initiatorSelectByNode || {}
  await loadFlowPreview()
} else if (appId) {
  await loadByAppId(appId)
} else {
  showToast('缺少 appId')
}

底栏按钮须带图标习惯可简化为纯文字(H5 Vant);主按钮「提交」type="primary"。

  • Step 2: 改路由
import FormSink from './views/FormSink.vue'
import OaStart from './views/OaStart.vue'

// ...
{ path: '/oa-start', component: OaStart },
  • Step 3: 本机验证
cd mobile-oa-h5 && npm run dev
# 打开:
# http://127.0.0.1:5178/oa-h5/oa-start?appId=<已发布模板ID>&token=<JWT>

Expected: 表单按模板渲染;改字段后审批流刷新;暂存/提交不报前端契约错误。

  • Step 4: Commit
git add mobile-oa-h5/src/views/OaStart.vue mobile-oa-h5/src/router.js
git commit -m "feat(mobile-oa-h5): implement oa-start page with form, flow, draft and submit"

Task 5: 小程序模板列表页

Files:

  • Create: mobile-h5/pages/oa/start/index.vue
  • Modify: mobile-h5/pages.json
  • Create or Modify: mobile-h5/utils/oa-api.js(列表用小程序 request,不要走 H5)

  • [ ] Step 1: 封装小程序侧 OA API

// mobile-h5/utils/oa-api.js
import { get } from './request.js'

export function fetchOaGroupList() {
  return get('/web/admin/oa-approval/groups')
}

export function fetchOaTemplates() {
  return get('/web/admin/oa-approval/templates')
}

(request.js 的 base 已含 /api,path 勿再写 /api。)

  • [ ] Step 2: 实现列表页(对齐 Web 卡片思路,三墙样式)

  • 顶栏搜索(审批单名称)

  • 分组 Tab:全部 + groups

  • 分组 section + 卡片:iconUrl(可先占位色块)、appName、description、按钮「发起」

  • 点击「发起」→ uni.navigateTo({ url: '/pages/oa/start/webview?appId=' + id })

分组展示逻辑可抄 Web start/index.vue 的 displayGroups 思路:按 groupId 归类,无分组归「未分组」。

  • Step 3: pages.json 注册
{
  "path": "pages/oa/start/index",
  "style": {
    "navigationBarTitleText": "发起审批",
    "navigationBarBackgroundColor": "#0a9f6d",
    "navigationBarTextStyle": "white"
  }
}
  • [ ] Step 4: 登录后进列表冒烟 Expected: 看到已发布模板;点发起进入下一页。

  • [ ] Step 5: Commit

git add mobile-h5/utils/oa-api.js mobile-h5/pages/oa/start/index.vue mobile-h5/pages.json
git commit -m "feat(mobile-h5): add OA start template list page"

Task 6: 小程序 web-view 发起页

Files:

  • Create: mobile-h5/pages/oa/start/webview.vue
  • Modify: mobile-h5/pages.json
  • Modify: mobile-h5/utils/oa-h5.js(确认 buildOaStartUrl 已带 token)

  • [ ] Step 1: webview 页

<template>
  <web-view v-if="src" :src="src" @message="onMessage" />
</template>

<script>
import { buildOaStartUrl } from '../../../utils/oa-h5.js'
import { getToken } from '../../../utils/auth.js'

export default {
  data() {
    return { src: '' }
  },
  onLoad(query) {
    if (!getToken()) {
      uni.showToast({ title: '请先登录', icon: 'none' })
      setTimeout(() => uni.navigateBack(), 500)
      return
    }
    this.src = buildOaStartUrl({
      appId: query.appId,
      draftId: query.draftId
    })
  },
  methods: {
    onMessage(e) {
      // 微信多在后退时触发;可用于刷新列表
      console.log('oa-start message', e)
    }
  }
}
</script>

确认 buildOaStartUrl:

export function buildOaStartUrl({ appId, draftId }) {
  const base = trimSlash(OA_H5_ORIGIN)
  const q = new URLSearchParams()
  if (appId) q.set('appId', appId)
  if (draftId) q.set('draftId', draftId)
  const token = getToken()
  if (token) q.set('token', token)
  return `${base}/oa-start?${q.toString()}`
}
  • [ ] Step 2: pages.json 注册 pages/oa/start/webview(navigationStyle 可默认;标题「填写审批」)。

  • [ ] Step 3: 真机/开发者工具:列表 → 发起 → H5 能打开且带 token。

  • [ ] Step 4: Commit

git add mobile-h5/pages/oa/start/webview.vue mobile-h5/pages.json mobile-h5/utils/oa-h5.js
git commit -m "feat(mobile-h5): add OA start web-view page"

Task 7: 移动端菜单种子 + mini 菜单 API + 首页联调

Files:

  • Create: docs/sql/mobile_menu_oa_start.sql
  • Create: backend/app/api/mini_menu.py
  • Modify: backend/app/api/__init__.py、backend/app/main.py
  • Create: mobile-h5/utils/menu.js
  • Modify: mobile-h5/pages/index/index.vue
  • Modify: mobile-h5/utils/request.js 或 auth 按现有 get 封装

  • [ ] Step 1: SQL 种子(在测试库执行;先查移动端根 id)

-- 查移动端根(parent_id='0' 且名称含「移动端」)
SELECT id, menu_name FROM sys_menu
WHERE del_flag='0' AND parent_id='0' AND menu_name LIKE '%移动端%';

-- 假设根 id 为 @mini_root(请替换为真实 UUID)
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,
  order_num, visible, status, tenant_no, create_by, create_time, del_flag, remark
) VALUES (
  REPLACE(UUID(),'-',''),  -- 或固定 UUID 便于幂等
  '发起审批',
  @mini_root,
  'C',
  '/pages/oa/start/index',
  NULL,
  'mini:oa:start',
  10,
  '1',
  '1',
  'zkxw',  -- 按租户实际调整
  'admin',
  NOW(),
  '0',
  '小程序发起审批'
);

然后在 Web「角色管理」给目标角色勾选该菜单。字段名以 docs/sql/zkxw.sql / SysMenu 模型为准,实施时对照列名微调(menu_name/path/perms/order_num 等)。

  • Step 2: 后端 GET /api/mini/menus
# backend/app/api/mini_menu.py
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.auth import get_current_user  # 以项目实际依赖名为准
from app.core.response import success_response
from app.models.models import SysMenu
from app.core.menu_guard import get_user_menu_codes

router = APIRouter(prefix="/mini", tags=["小程序菜单"])

@router.get("/menus")
async def list_my_mobile_menus(
    db: Session = Depends(get_db),
    user: dict = Depends(get_current_user),
):
    """返回当前用户在「移动端菜单」树下有权限的叶子/菜单项(扁平列表)。"""
    # 1) 找到 parent_id=0 且名称含「移动端」的根
    # 2) 取该根下所有 del_flag=0 的菜单
    # 3) 用 get_user_menu_codes / role_menu 过滤(super_admin 可返回全部)
    # 4) 返回 [{ id, title: menu_name, path, icon: icon, perms }]
    ...
    return success_response({"list": items})

注册:app/api/__init__.py + app/main.py include_router(..., prefix=settings.API_V1_PREFIX)。

鉴权:与 mini_auth 相同 JWT;若现有 get_current_user 已覆盖小程序 token,直接复用。

  • Step 3: 小程序 menu.js + 首页
// mobile-h5/utils/menu.js
import { get } from './request.js'

export async function fetchMyMobileMenus() {
  const data = await get('/mini/menus')
  const list = data?.list || data || []
  return list.map((m) => ({
    id: m.id,
    title: m.title || m.menuName || m.menu_name,
    icon: m.icon || 'orders-o',
    path: m.path
  })).filter((m) => m.path)
}

首页 onShow / onLoad:

import { fetchMyMobileMenus } from '../../utils/menu.js'

async loadMenus() {
  try {
    this.menus = await fetchMyMobileMenus()
  } catch (e) {
    // 过渡:失败时仍可保留水槽测试项,避免开发卡死
    this.menus = [
      { id: 'oa-start', title: '发起审批', icon: 'edit', path: '/pages/oa/start/index' },
      { id: 'oa-form-sink', title: '表单水槽测试', icon: 'orders-o', path: '/pages/oa/form-sink/index' }
    ]
  }
}

正式环境以 API 为准;水槽项可不进生产角色权限。

  • [ ] Step 4: 验证 角色授权后首页出现「发起审批」→ 列表 → 填写 → 提交。

  • [ ] Step 5: Commit

git add docs/sql/mobile_menu_oa_start.sql \
  backend/app/api/mini_menu.py backend/app/api/__init__.py backend/app/main.py \
  mobile-h5/utils/menu.js mobile-h5/pages/index/index.vue
git commit -m "feat: register mobile OA start menu and wire mini menus API to home"

Task 8: 套件运行时对齐(可分期,建议第二迭代)

Files:

  • Create: mobile-oa-h5/src/oa/kit-runtime.js(从 Web oa-form-create-*-kit.ts 移植纯函数部分)
  • Modify: mobile-oa-h5/src/views/OaStart.vue(watch formData 时调用 sync)

  • [x] Step 1: 按 zkxw-sc 已发布模板优先级移植(commit e83d9259)

顺序:leaveKit 时长 → overtimeKit(含节假日 API 若 Web 已有)→ tripKit 天数 → confirmationKit / resignationKit / owner 默认值。

原则:只搬纯函数(输入 schema+formData → 新 formData),不搬 Element UI。

  • [ ] Step 2: 双端对照清单(同一模板 Web vs 小程序:默认值、改开始时间后时长、提交 payload)。待真机

  • [x] Step 3: Commit

git add mobile-oa-h5/src/oa/kit-runtime.js mobile-oa-h5/src/views/OaStart.vue
git commit -m "feat(mobile-oa-h5): align OA kit runtime helpers with web-admin"

若排期紧:Task 1~7 合并验收后即可开菜单;Task 8 作为回归阻塞项,有请假/加班模板时必须完成再宣称「全量可发起」。


Task 9: 端到端验收与文档勾选

Files:

  • Modify: docs/superpowers/specs/2026-08-11-mobile-oa-form-sink-spike-notes.md(可追加阶段二验收记录)
  • 可选:更新设计文档 §9 实施顺序勾选状态

  • [ ] Step 1: 验收清单(全部勾选才算阶段二完成) — 代码侧就绪;真机 1~8 项见 spike 笔记「阶段二验收清单(Task 9)」待勾

# 项 通过标准
1 首页菜单 「发起审批」来自移动端菜单授权(或明确过渡硬编码已替换)
2 模板列表 已发布模板卡片可见,分组/搜索可用
3 打开填写 web-view 加载 /oa-start,token 有效,接口 200
4 动态表单 至少 1 个含 upload/money/person 的真实模板可填
5 审批流 条件未齐 pending;填齐后 steps 出现;initiator_select 可选人
6 暂存 草稿创建成功,再开 draftId 可恢复
7 提交 实例创建成功,返回小程序
8 部署 测试环境 OA_H5_ORIGIN + 业务域名 + /api 反代正确
  • Step 2: 跑一遍 type 统计,确认无未注册 type(2026-08-11:published_latest_apps=27,CUSTOM 均已注册)
cd backend && source venv/bin/activate
python ../scripts/oa_form_type_stats.py
  • Step 3: Commit 文档

风险与对策(实施时)

风险 对策
小程序 JWT 调 /web/admin/* 403 核对角色菜单码 / 数据权限;必要时加 mini:oa:* 或放宽运行时只读接口
web-view 域名 测试环境必须 HTTPS 业务域名;本地用局域网 IP + 开发者工具不校验
postMessage 延迟 提交后主动 navigateBack;列表 onShow 可刷新
套件行为不一致 Task 8;验收时优先覆盖含 kit 的模板
菜单 SQL 与库不一致 以 SysMenu 实列为准;先 SELECT 再 INSERT

Spec coverage(自检)

设计 §6 要求 本计划任务
小程序模板列表 Task 5
H5 正式填写 + draft/submit Task 4
flow-preview + initiator_select Task 3 + 4
移动端菜单注册 + 首页 Task 7
复用 OA API Task 1
已发布可完整发起 Task 4 + 8 + 9
不按流程单独做页 全程单一 OaStart.vue

建议执行方式

完成 Task 1~7 即可对内试用「发起审批」;Task 8 按真实模板缺口补齐。实现时推荐 subagent-driven-development 按 Task 逐个交付并人工过一下列表→提交主路径。