docs: narrow OA mobile form spike by zkxw-sc usage and add phase-1 plan.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-11 11:54:14 +08:00
1 parent 3c5fb79c30
commit d92472d2cd
2 files changed
+871 -24

No files matched your search

@@ -0,0 +1,837 @@
# 移动端发起审批 · 阶段一(水槽 Spike)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:** 搭好独立 OA 表单 H5(`@form-create/vant`)与小程序整页 web-view 入口,用「按 zkxw-sc 真实用量」的厨房水槽 rule 在微信真机验证渲染与交互。
**Architecture:** 小程序首页硬编码测试菜单 → `/pages/oa/form-sink/index` 整页 `web-view` → 独立工程 `mobile-oa-h5` 的 `/oa-form-sink`。水槽 type 以测试库已发布 `form_schema_json` 统计为准(P0 先通),不为未使用 type 空转。正式模板列表 / 草稿提交 / 菜单注册见后续阶段二计划。
**Tech Stack:** Vue3、Vite、Vant4、`@form-create/vant`、uni-app(HBuilderX)微信小程序、`web-view`
**Spec:** `docs/superpowers/specs/2026-08-11-mobile-oa-start-approval-design.md`
**Out of scope (本计划):** 正式发起 API、flow-preview 真联调、移动端菜单后台注册、全量套件精修(P2 可先 stub 占位)。
---
## File map
| 路径 | 职责 |
|------|------|
| `scripts/oa_form_type_stats.py` | 统计 `zkxw-sc` 已发布模板中的 `type` 频率 |
| `mobile-oa-h5/package.json` | H5 依赖与脚本 |
| `mobile-oa-h5/vite.config.js` | Vite + `@` 别名 + 开发 server |
| `mobile-oa-h5/index.html` | H5 入口 HTML |
| `mobile-oa-h5/src/main.js` | 挂载 Vue、Vant、form-create/vant、自研 type 注册 |
| `mobile-oa-h5/src/App.vue` | 路由出口 |
| `mobile-oa-h5/src/router.js` | `/oa-form-sink`、`/oa-start`(后者可占位) |
| `mobile-oa-h5/src/styles/tokens.css` | 三墙 Token(`#0a9f6d` 等) |
| `mobile-oa-h5/src/oa/register-components.js` | 注册自研 type(与 Web 同名) |
| `mobile-oa-h5/src/oa/kitchen-sink-rule.js` | 水槽 rule(按库内实际 type) |
| `mobile-oa-h5/src/oa/components/*.vue` | 自研控件 stub / 初版(P0 可用) |
| `mobile-oa-h5/src/views/FormSink.vue` | 水槽页:form-create + 底栏校验/导出 |
| `mobile-oa-h5/.env.development` | `VITE_API_BASE` 等 |
| `mobile-h5/utils/oa-h5.js` | H5 水槽 / 发起页 URL 拼装 |
| `mobile-h5/pages/oa/form-sink/index.vue` | 整页 web-view |
| `mobile-h5/pages/index/index.vue` | 注入测试菜单 |
| `mobile-h5/pages.json` | 注册 form-sink 路由 |
---
### Task 1: 固化 type 统计脚本
**Files:**
- Create: `scripts/oa_form_type_stats.py`
- [ ] **Step 1: 写入统计脚本**
```python
#!/usr/bin/env python3
"""统计 zkxw-sc(或当前 DATABASE_URL)已发布 OA 模板 form_schema 中的 type。"""
from __future__ import annotations
import json
import sys
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "backend"))
from sqlalchemy import create_engine, text # noqa: E402
from app.core.config import settings # noqa: E402
CUSTOM = {
"money",
"person",
"ownerPerson",
"dept",
"ownerDept",
"post",
"receiveAccount",
"relatedApproval",
"upload",
"fcUpload",
"leaveKit",
"overtimeKit",
"confirmationKit",
"resignationKit",
"tripKit",
"tripItinerary",
}
def walk_types(node, out: list) -> None:
if node is None:
return
if isinstance(node, dict):
t = node.get("type")
if isinstance(t, str) and t.strip():
out.append(t.strip())
for v in node.values():
if isinstance(v, (dict, list)):
walk_types(v, out)
elif isinstance(node, list):
for item in node:
walk_types(item, out)
def main() -> None:
engine = create_engine(settings.DATABASE_URL)
sql = text(
"""
SELECT a.app_name, v.form_schema_json
FROM oa_app a
JOIN oa_app_version v ON v.app_id = a.id
WHERE a.del_flag = 0 AND v.del_flag = 0
AND v.status = 'published'
AND (v.is_latest_published = 1 OR v.is_latest_published = '1')
ORDER BY a.app_name
"""
)
counter: Counter[str] = Counter()
with engine.connect() as conn:
rows = conn.execute(sql).mappings().all()
print(f"published_latest_apps={len(rows)}")
for row in rows:
raw = row["form_schema_json"] or ""
try:
schema = json.loads(raw) if raw else {}
except json.JSONDecodeError:
print("BAD_JSON", row["app_name"])
continue
types: list[str] = []
if isinstance(schema, list):
walk_types(schema, types)
else:
walk_types(schema.get("rule", schema), types)
for t in types:
counter[t] += 1
print("=== frequency ===")
for t, c in counter.most_common():
kind = "CUSTOM" if t in CUSTOM else "base"
print(f"{c:4d} {kind:6s} {t}")
print("=== custom used ===")
print(sorted(set(counter) & CUSTOM))
if __name__ == "__main__":
main()
```
- [ ] **Step 2: 运行脚本确认与设计文档一致**
Run:
```bash
cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python ../scripts/oa_form_type_stats.py
```
Expected: `published_latest_apps=27`(或接近),`upload`/`money`/`relatedApproval`/`receiveAccount` 排在自研前列。
- [ ] **Step 3: Commit**
```bash
git add scripts/oa_form_type_stats.py
git commit -m "chore: add OA published form type stats script against zkxw-sc"
```
---
### Task 2: 初始化 `mobile-oa-h5` 工程
**Files:**
- Create: `mobile-oa-h5/package.json`
- Create: `mobile-oa-h5/vite.config.js`
- Create: `mobile-oa-h5/index.html`
- Create: `mobile-oa-h5/src/main.js`
- Create: `mobile-oa-h5/src/App.vue`
- Create: `mobile-oa-h5/src/router.js`
- Create: `mobile-oa-h5/src/styles/tokens.css`
- Create: `mobile-oa-h5/.gitignore`
- Create: `mobile-oa-h5/.env.development`
- [ ] **Step 1: 写入 package.json**
```json
{
"name": "mobile-oa-h5",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@form-create/vant": "^3.2.14",
"vant": "^4.9.21",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.0.7"
}
}
```
(若 `@form-create/vant` 版本解析失败,以 `npm view @form-create/vant version` 的 next/最新 Vue3 线为准。)
- [ ] **Step 2: 写入 vite.config.js / index.html / 入口**
`mobile-oa-h5/vite.config.js`:
```js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
plugins: [vue()],
base: '/oa-h5/',
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
host: '0.0.0.0',
port: 5178,
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true
}
}
}
})
```
`mobile-oa-h5/index.html`:
```html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
/>
<title>中科兴皖 · OA 表单</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
```
`mobile-oa-h5/src/styles/tokens.css`:
```css
:root {
--brand-green: #0a9f6d;
--brand-green-dark: #087f57;
--brand-green-soft: #e9f9f2;
--bg-page: #f5f8f7;
--text-primary: #1f2a26;
--text-muted: #6b7b74;
--border: #dcebe4;
}
body {
margin: 0;
background: var(--bg-page);
color: var(--text-primary);
font-family: system-ui, -apple-system, 'PingFang SC', sans-serif;
}
```
`mobile-oa-h5/src/App.vue`:
```vue
<template>
<router-view />
</template>
```
`mobile-oa-h5/src/router.js`:
```js
import { createRouter, createWebHistory } from 'vue-router'
import FormSink from './views/FormSink.vue'
export default createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{ path: '/', redirect: '/oa-form-sink' },
{ path: '/oa-form-sink', component: FormSink },
{
path: '/oa-start',
component: {
template: '<div style="padding:16px">oa-start 占位(阶段二)</div>'
}
}
]
})
```
`mobile-oa-h5/src/main.js`:
```js
import { createApp } from 'vue'
import Vant from 'vant'
import 'vant/lib/index.css'
import formCreateMobile from '@form-create/vant'
import App from './App.vue'
import router from './router'
import './styles/tokens.css'
import { registerOaComponents } from './oa/register-components'
const app = createApp(App)
app.use(router)
app.use(Vant)
app.use(formCreateMobile)
registerOaComponents(formCreateMobile)
app.mount('#app')
```
`mobile-oa-h5/.gitignore`:
```
node_modules/
dist/
.DS_Store
.env.local
```
`mobile-oa-h5/.env.development`:
```
VITE_API_BASE=/api
```
- [ ] **Step 3: 安装依赖**
```bash
cd /Users/wuzilong/work/00code/zkxw/mobile-oa-h5 && npm install
```
Expected: 无报错,存在 `node_modules/@form-create/vant` 与 `node_modules/vant`。
- [ ] **Step 4: Commit**
```bash
git add mobile-oa-h5
git commit -m "chore: scaffold mobile-oa-h5 with Vite, Vant and form-create"
```
---
### Task 3: 水槽 rule + 自研 type 注册(P0 可用,P2 stub)
**Files:**
- Create: `mobile-oa-h5/src/oa/kitchen-sink-rule.js`
- Create: `mobile-oa-h5/src/oa/register-components.js`
- Create: `mobile-oa-h5/src/oa/components/OaStubField.vue`
- Create: `mobile-oa-h5/src/views/FormSink.vue`
- [ ] **Step 1: Stub 通用组件**
`mobile-oa-h5/src/oa/components/OaStubField.vue`:
```vue
<template>
<div class="oa-stub">
<div class="oa-stub__title">{{ title || typeName }}</div>
<van-field
v-model="inner"
:placeholder="'[' + typeName + '] 占位,后续替换真实交互'"
@update:model-value="onInput"
/>
</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
const props = defineProps({
modelValue: { type: [String, Number, Array, Object], default: '' },
formCreateInject: { type: Object, default: null }
})
const emit = defineEmits(['update:modelValue'])
const typeName = computed(
() => props.formCreateInject?.rule?.type || 'unknown'
)
const title = computed(() => props.formCreateInject?.rule?.title || '')
const inner = ref(
typeof props.modelValue === 'string' || typeof props.modelValue === 'number'
? String(props.modelValue ?? '')
: JSON.stringify(props.modelValue ?? '')
)
watch(
() => props.modelValue,
(v) => {
inner.value =
typeof v === 'string' || typeof v === 'number'
? String(v ?? '')
: JSON.stringify(v ?? '')
}
)
function onInput(v) {
emit('update:modelValue', v)
}
</script>
<style scoped>
.oa-stub {
margin-bottom: 12px;
}
.oa-stub__title {
margin: 0 0 6px;
font-size: 14px;
font-weight: 600;
}
</style>
```
- [ ] **Step 2: 注册表(type 名与 Web 一致)**
`mobile-oa-h5/src/oa/register-components.js`:
```js
import OaStubField from './components/OaStubField.vue'
/** 与 web-admin formCreate.component 名称对齐;阶段一可用 stub */
const CUSTOM_TYPES = [
'upload',
'money',
'relatedApproval',
'receiveAccount',
'dept',
'person',
'ownerPerson',
'ownerDept',
'post',
'leaveKit',
'overtimeKit',
'confirmationKit',
'resignationKit',
'tripKit',
'tripItinerary'
]
export function registerOaComponents(formCreate) {
if (!formCreate || typeof formCreate.component !== 'function') return
for (const name of CUSTOM_TYPES) {
formCreate.component(name, OaStubField)
}
}
```
- [ ] **Step 3: kitchen-sink-rule(覆盖库内出现过的 type)**
`mobile-oa-h5/src/oa/kitchen-sink-rule.js`:
```js
/** 按 zkxw-sc 已发布模板实际出现的 type 构造;非设计器理论全集 */
export const kitchenSinkRule = [
{ type: 'input', field: 'title', title: '单行文本', value: '', $required: true },
{ type: 'textarea', field: 'remark', title: '多行文本', value: '' },
{
type: 'select',
field: 'level',
title: '下拉',
value: '',
options: [
{ label: '普通', value: 'normal' },
{ label: '紧急', value: 'urgent' }
]
},
{ type: 'inputNumber', field: 'qty', title: '数字', value: 1 },
{
type: 'datePicker',
field: 'bizDate',
title: '日期',
value: '',
props: { type: 'date' }
},
{ type: 'upload', field: 'files', title: '附件 upload', value: [] },
{ type: 'money', field: 'amount', title: '金额 money', value: '' },
{
type: 'relatedApproval',
field: 'related',
title: '关联审批 relatedApproval',
value: []
},
{
type: 'receiveAccount',
field: 'payAccount',
title: '收款账户 receiveAccount',
value: []
},
{ type: 'dept', field: 'deptId', title: '部门 dept', value: '' },
{ type: 'person', field: 'personIds', title: '人员 person', value: [] },
{
type: 'ownerPerson',
field: 'ownerPerson',
title: '所属人 ownerPerson',
value: ''
},
{
type: 'ownerDept',
field: 'ownerDept',
title: '所属部门 ownerDept',
value: ''
},
{ type: 'post', field: 'postId', title: '岗位 post', value: '' },
{ type: 'leaveKit', field: 'leaveKit', title: '请假套件 leaveKit', value: {} },
{
type: 'overtimeKit',
field: 'overtimeKit',
title: '加班套件 overtimeKit',
value: {}
},
{
type: 'confirmationKit',
field: 'confirmationKit',
title: '转正套件 confirmationKit',
value: {}
},
{
type: 'resignationKit',
field: 'resignationKit',
title: '离职套件 resignationKit',
value: {}
},
{ type: 'tripKit', field: 'tripKit', title: '差旅套件 tripKit', value: {} },
{
type: 'tripItinerary',
field: 'tripItinerary',
title: '行程 tripItinerary',
value: []
}
]
export const kitchenSinkOption = {
submitBtn: false,
resetBtn: false,
form: { labelPosition: 'top' }
}
```
- [ ] **Step 4: FormSink 页面**
`mobile-oa-h5/src/views/FormSink.vue`:
```vue
<template>
<div class="sink">
<header class="sink__hd">表单水槽测试</header>
<div class="sink__body">
<form-create-mobile
v-model="formData"
v-model:api="fapi"
:rule="rule"
:option="option"
/>
<pre class="sink__json">{{ preview }}</pre>
</div>
<footer class="sink__ft">
<van-button block type="primary" @click="onValidate">校验并导出</van-button>
</footer>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import { showToast } from 'vant'
import { kitchenSinkOption, kitchenSinkRule } from '../oa/kitchen-sink-rule'
const formData = ref({})
const fapi = ref(null)
const rule = ref(kitchenSinkRule)
const option = ref(kitchenSinkOption)
const preview = computed(() => JSON.stringify(formData.value, null, 2))
async function onValidate() {
try {
await fapi.value?.validate?.()
showToast('校验通过')
} catch (e) {
showToast(e?.message || '校验失败')
}
}
</script>
<style scoped>
.sink {
min-height: 100vh;
display: flex;
flex-direction: column;
background: var(--bg-page);
}
.sink__hd {
padding: 14px 16px;
font-weight: 700;
color: #fff;
background: linear-gradient(135deg, var(--brand-green), var(--brand-green-dark));
}
.sink__body {
flex: 1;
padding: 12px 16px 88px;
}
.sink__json {
margin-top: 16px;
padding: 10px;
overflow: auto;
font-size: 12px;
background: #fff;
border: 1px solid var(--border);
border-radius: 8px;
}
.sink__ft {
position: fixed;
right: 0;
bottom: 0;
left: 0;
padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
background: #fff;
border-top: 1px solid var(--border);
}
</style>
```
- [ ] **Step 5: 本地浏览器验证**
```bash
cd /Users/wuzilong/work/00code/zkxw/mobile-oa-h5 && npm run dev
```
Expected: 打开 `http://127.0.0.1:5178/oa-h5/oa-form-sink`,基础控件与自研 stub 均可见;点「校验并导出」可更新 JSON。
- [ ] **Step 6: Commit**
```bash
git add mobile-oa-h5/src
git commit -m "feat(mobile-oa-h5): add kitchen-sink form page with OA type stubs"
```
---
### Task 4: 小程序测试菜单 + 整页 web-view
**Files:**
- Create: `mobile-h5/utils/oa-h5.js`
- Create: `mobile-h5/pages/oa/form-sink/index.vue`
- Modify: `mobile-h5/pages.json`
- Modify: `mobile-h5/pages/index/index.vue`
- Modify: `mobile-h5/utils/config.js`(增加 `OA_H5_ORIGIN`)
- [ ] **Step 1: 配置 H5 源**
在 `mobile-h5/utils/config.js` 追加:
```js
/**
* OA 表单 H5 源(web-view)。
* 本地:电脑局域网 IP + Vite 端口,须与手机同网;真机/体验版改为已配置业务域名。
* 例:http://192.168.1.8:5178/oa-h5/
*/
export const OA_H5_ORIGIN = 'http://127.0.0.1:5178/oa-h5'
```
`mobile-h5/utils/oa-h5.js`:
```js
import { OA_H5_ORIGIN } from './config.js'
import { getToken } from './auth.js'
function trimSlash(s) {
return String(s || '').replace(/\/$/, '')
}
/** 水槽页 URL */
export function buildFormSinkUrl() {
const base = trimSlash(OA_H5_ORIGIN)
const token = encodeURIComponent(getToken() || '')
return `${base}/oa-form-sink?token=${token}`
}
/** 正式发起页 URL(阶段二) */
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: web-view 页**
`mobile-h5/pages/oa/form-sink/index.vue`:
```vue
<template>
<web-view v-if="src" :src="src" @message="onMessage" />
</template>
<script>
import { buildFormSinkUrl } from '../../../utils/oa-h5.js'
export default {
data() {
return { src: '' }
},
onLoad() {
this.src = buildFormSinkUrl()
},
methods: {
onMessage(e) {
console.log('oa-h5 message', e && e.detail)
}
}
}
</script>
```
- [ ] **Step 3: 注册路由**
在 `mobile-h5/pages.json` 的 `pages` 数组中增加:
```json
{
"path": "pages/oa/form-sink/index",
"style": {
"navigationBarTitleText": "表单水槽测试",
"navigationBarBackgroundColor": "#0a9f6d",
"navigationBarTextStyle": "white"
}
}
```
- [ ] **Step 4: 首页注入测试菜单**
修改 `mobile-h5/pages/index/index.vue` 的 `data()`,将 `menus: []` 改为至少包含:
```js
menus: [
{
id: 'oa-form-sink',
title: '表单水槽测试',
icon: 'orders-o',
path: '/pages/oa/form-sink/index',
tone: ''
}
]
```
(`onShow` 暂不覆盖该数组;阶段二接真实菜单时再合并/替换。)
- [ ] **Step 5: 真机 / 开发者工具验收清单**
1. `mobile-oa-h5`:`npm run dev`,`OA_H5_ORIGIN` 改为手机可达地址(非 `127.0.0.1`)。
2. 微信公众平台配置业务域名(体验版);开发工具可勾选不校验合法域名。
3. 打开小程序首页 → 点「表单水槽测试」→ web-view 加载水槽。
4. 确认:无白屏;基础字段可填;自研 stub 可见;校验按钮可用。
- [ ] **Step 6: Commit**
```bash
git add mobile-h5/utils/config.js mobile-h5/utils/oa-h5.js \
mobile-h5/pages/oa/form-sink/index.vue mobile-h5/pages.json \
mobile-h5/pages/index/index.vue
git commit -m "feat(mobile-h5): add OA form-sink web-view entry and test menu"
```
---
### Task 5: 记录 Spike 结论(为阶段二铺路)
**Files:**
- Create: `docs/superpowers/specs/2026-08-11-mobile-oa-form-sink-spike-notes.md`(实施当天日期可改)
- [ ] **Step 1: 按模板写结论**
```markdown
# OA 表单水槽 Spike 记录
- 日期:
- 环境:微信开发者工具 / 真机型号 /
- H5 地址:
- 结果:通过 / 阻塞
- 问题列表:
1.
- 下一步:开始 P0 真实控件(upload/money/relatedApproval/receiveAccount)移植计划
```
- [ ] **Step 2: Commit notes**
```bash
git add docs/superpowers/specs/2026-08-11-mobile-oa-form-sink-spike-notes.md
git commit -m "docs: record mobile OA form-sink spike results"
```
---
## 阶段二预告(另开计划,本计划不实施)
1. P0 控件真实实现(对照 Web:`upload` / `money` / `relatedApproval` / `receiveAccount`)。
2. H5 `/oa-start` + templates / flow-preview / drafts / instances。
3. 小程序原生模板列表页。
4. 移动端菜单注册 + 首页菜单 API。
5. P1/P2 控件与套件 + 按已发布模板回归。
**zkxw-sc 用量优先级(2026-08-11):**
P0 `upload` > `money` > `relatedApproval` > `receiveAccount`;P1 `dept`/`person`;P2 各套件与 `owner*`/`post`。
---
## Spec coverage(自检)
| 设计要求 | 本计划任务 |
|----------|------------|
| 独立 H5 + `@form-create/vant` | Task 2–3 |
| 水槽按 zkxw-sc 真实 type | Task 1、Task 3 rule |
| 小程序测试菜单 + 整页 web-view | Task 4 |
| 一套 JSON 渲染、不按流程单独做页 | 架构 + 阶段二 `/oa-start` 占位 |
| 正式列表/提交/菜单 | 明确 out of scope → 阶段二 |
---
## 执行方式
Plan complete. Two execution options:
1. **Subagent-Driven(推荐)** — 每任务新开子代理,任务间评审
2. **Inline Execution** — 本会话按 executing-plans 连续做并设检查点
Which approach?
@@ -18,7 +18,7 @@
### 1.2 目标
1. 在微信小程序中提供「发起审批」:模板列表(原生)+ 填写页(H5,经 web-view)。
2. **凡 Web 可发起的已发布模板,移动端均可完整填写**(基础控件、自研组件、自研套件全覆盖)。
2. **已在 Web 发布的模板,移动端须能完整填写**(覆盖该模板 schema 中出现的基础控件、自研组件、套件);移植与 Spike 优先按测试库真实用量,而非设计器理论全集。
3. **不按流程单独开发界面**:一个 H5 填写页根据 `appId` 拉取 JSON 动态渲染。
4. 阶段一先做「厨房水槽」真机验证,暴露 web-view + form-create/vant + 全 type 注册问题。
@@ -37,7 +37,7 @@
| 发布目标 | 微信小程序(`mobile-h5`) |
| 表单承载 | 独立部署 H5 + 小程序整页 `web-view` |
| 列表 vs 填写 | **A1**:模板列表用小程序原生;仅填写页进 H5 |
| 表单能力 | **全量对齐 Web**(基础 + 自研组件 + 套件) |
| 表单能力 | **已发布模板可完整发起**;Spike/移植范围以 `zkxw-sc` 真实 schema 用量为准(非设计器理论全集) |
| 多流程 UI | **一套 H5 页 + JSON 渲染**,不为每个流程单独做页 |
| 阶段一验证 | 首页硬编码测试菜单 → web-view → H5 水槽页(手工 kitchen-sink rule) |
| 品牌色 | 三墙 `#0a9f6d` 等;禁止混用 Web `#10b981` |
@@ -114,9 +114,30 @@ H5 建议独立目录(如 `mobile-oa-h5/`),Vite 构建并部署到已配
- 首页「功能菜单」本地注入测试项:`表单水槽测试` → `/pages/oa/form-sink/index`。
- 不依赖后台菜单接口(首页当前 `menus=[]`)。
### 5.3 H5 水槽页
### 5.3 H5 水槽页(按测试库真实用量收窄)
- 手工 `kitchen-sink-rule`:覆盖全部基础控件 + 自研组件 + 自研套件。
不必按「设计器里理论存在的全部 type」做全量水槽。以测试库 **`zkxw-sc`** 中**已发布且最新**版本的 `oa_app_version.form_schema_json` 为准,统计客户实际用到的控件/套件,水槽 rule **只覆盖这些 type**(另加少量基础控件兜底)。
**2026-08-11 抽样结果(27 个已发布最新模板):**
| 优先级 | type | 出现次数(约) | 说明 |
|--------|------|----------------|------|
| P0 | `input` / `textarea` / `datePicker`(+`date`/`datetime`) / `select` / `inputNumber`/`number` | 高 | 基础控件 |
| P0 | `upload` | 31 | 最常用自研 |
| P0 | `money` | 13 | |
| P0 | `relatedApproval` | 11 | |
| P0 | `receiveAccount` | 7 | |
| P1 | `dept` | 5 | |
| P1 | `person` | 3 | |
| P2 | `overtimeKit` | 2 | 套件 |
| P2 | `leaveKit` / `confirmationKit` / `resignationKit` / `tripKit`(+`tripItinerary`) | 各 1 | 套件 |
| P2 | `ownerPerson` / `ownerDept` / `post` | 各 1 | |
当前已发布模板中**未出现**、可暂缓移动端移植的 type:以库内统计为准;若后续设计器新增并发布,再补注册与水槽项。
复测命令(本机已配 `backend/.env` → `zkxw-sc`):解析 `form_schema_json` 递归收集 `type` 字段即可;实施计划中附脚本。
- 手工 `kitchen-sink-rule`:覆盖上表实际用到的 type(可按 P0→P2 分批先通)。
- 底栏:校验、打印/展示 `formData`(本阶段可不接真实发起 API)。
- 审批流区:可静态占位,避免干扰表单验证。
@@ -165,31 +186,20 @@ H5 建议独立目录(如 `mobile-oa-h5/`),Vite 构建并部署到已配
---
## 7. 自研组件与套件(全量)
## 7. 自研组件与套件(按真实用量,非理论全量)
产品目标仍是:**已发布模板在移动端可完整发起**。实现顺序与 Spike 验证范围以 **`zkxw-sc` 已发布 schema 统计** 为准,避免为未使用的控件空转。
`type` 名称与 Web 注册保持一致。H5 侧用 Vant/移动交互重实现,不能直接复用 Element Plus 组件。
**组件类(含但不限于):**
### 7.1 移植顺序(按 2026-08-11 库内频率)
- 上传、金额、人员、所属人、部门、所属部门、岗位、收款账户、关联审批等。
1. **P0 高频**:`upload`、`money`、`relatedApproval`、`receiveAccount` + 基础控件
2. **P1**:`dept`、`person`
3. **P2 套件与低频**:`overtimeKit`、`leaveKit`、`confirmationKit`、`resignationKit`、`tripKit`/`tripItinerary`、`ownerPerson`、`ownerDept`、`post`
4. 运行时细节对齐 + 按模板清单回归(付款单、日常报销、请假、出差等)
**套件类(含但不限于):**
- 请假、加班、转正、离职、差旅/行程等。
**运行时:**
- 默认值、时长/日期自动计算、手改标记与恢复、节假日等与 Web 一致的联动逻辑。
### 7.1 移植顺序(建议)
1. 上传、金额、人员/部门/岗位、所属人/所属部门
2. 收款账户、关联审批
3. 请假 / 加班 / 转正 / 离职套件
4. 差旅 / 行程套件
5. 运行时细节对齐与真实模板回归
阶段一水槽 rule 应预先包含上述全部 type,用于尽早暴露缺口。
发布前用同一统计脚本再跑一遍;若出现新 type,补移植后再开菜单。
---