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 笔记验收表)。
| 路径 | 职责 |
|---|---|
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.vueweb-admin/src/components/oa-approval/OaStartFormDialog.vueweb-admin/src/components/oa-approval/OaFlowPreview.vueweb-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 → 列表或首页
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(...)。
# 将 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 权限核对,不阻塞前端骨架)。
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"
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 再对齐套件运行时。
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"
Files:
mobile-oa-h5/src/oa/components/OaFlowPreview.vue对齐 Web OaFlowPreview.vue 的数据契约:steps、resolved、pendingMessage、loading、initiatorSelectByNode(v-model)、userOptions。
关键行为:
resolved === false → 展示 pendingMessage(默认「必填信息填写后,流程将自动显示」)。resolved === true → 竖向步骤链:start / approval / cc / end;展示 name、会签/或签徽章、meta、people chips。assigneeTypes(或 assigneeType)含 initiator_select:展示已选人 +「添加」→ van-popup 内多选人员(数据来自 userOptions:{ id, nickName, userNo })。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"
/oa-start(表单 + 流程 + 草稿/提交)Files:
mobile-oa-h5/src/views/OaStart.vueModify: 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"。
import FormSink from './views/FormSink.vue'
import OaStart from './views/OaStart.vue'
// ...
{ path: '/oa-start', component: OaStart },
cd mobile-oa-h5 && npm run dev
# 打开:
# http://127.0.0.1:5178/oa-h5/oa-start?appId=<已发布模板ID>&token=<JWT>
Expected: 表单按模板渲染;改字段后审批流刷新;暂存/提交不报前端契约错误。
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"
Files:
mobile-h5/pages/oa/start/index.vuemobile-h5/pages.jsonCreate 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 归类,无分组归「未分组」。
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"
Files:
mobile-h5/pages/oa/start/webview.vuemobile-h5/pages.jsonModify: 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"
Files:
docs/sql/mobile_menu_oa_start.sqlbackend/app/api/mini_menu.pybackend/app/api/__init__.py、backend/app/main.pymobile-h5/utils/menu.jsmobile-h5/pages/index/index.vueModify: 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 等)。
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,直接复用。
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"
Files:
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 作为回归阻塞项,有请假/加班模板时必须完成再宣称「全量可发起」。
Files:
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 反代正确 |
published_latest_apps=27,CUSTOM 均已注册)cd backend && source venv/bin/activate
python ../scripts/oa_form_type_stats.py
| 风险 | 对策 |
|---|---|
小程序 JWT 调 /web/admin/* 403 |
核对角色菜单码 / 数据权限;必要时加 mini:oa:* 或放宽运行时只读接口 |
| web-view 域名 | 测试环境必须 HTTPS 业务域名;本地用局域网 IP + 开发者工具不校验 |
| postMessage 延迟 | 提交后主动 navigateBack;列表 onShow 可刷新 |
| 套件行为不一致 | Task 8;验收时优先覆盖含 kit 的模板 |
| 菜单 SQL 与库不一致 | 以 SysMenu 实列为准;先 SELECT 再 INSERT |
| 设计 §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 逐个交付并人工过一下列表→提交主路径。