This commit is contained in:
zlwu committed 2026-08-12 15:48:15 +08:00
1 parent d310da1523
commit ec628e1b8d
79 files changed
+6749 -799

No files matched your search

+558
View File
@@ -0,0 +1,558 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>发起审批 · 底栏按钮重设计 Demo</title>
<style>
:root {
--ink: #0f172a;
--muted: #64748b;
--line: #e2e8f0;
--brand: #22c55e;
--brand-dark: #16a34a;
--brand-deep: #15803d;
--brand-soft: #ecfdf5;
}
* { box-sizing: border-box; }
html, body { margin: 0; min-height: 100%; }
body {
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", -apple-system, system-ui, sans-serif;
color: var(--ink);
background:
radial-gradient(900px 420px at 8% -8%, rgba(52, 211, 153, 0.16), transparent 55%),
radial-gradient(700px 380px at 92% 0%, rgba(148, 163, 184, 0.14), transparent 50%),
linear-gradient(180deg, #f8fffc 0%, #f4f7fb 50%, #f7f8fa 100%);
line-height: 1.45;
}
.wrap {
max-width: 1180px;
margin: 0 auto;
padding: 28px 18px 96px;
}
h1 {
margin: 0 0 8px;
font-size: 24px;
letter-spacing: -0.02em;
font-weight: 720;
}
.sub {
margin: 0;
max-width: 720px;
color: var(--muted);
font-size: 14px;
}
.note {
margin: 14px 0 22px;
padding: 10px 12px;
border-radius: 10px;
background: #fff;
border: 1px solid var(--line);
font-size: 13px;
color: var(--muted);
}
.note strong { color: var(--brand-deep); }
.layout {
display: grid;
grid-template-columns: 1fr minmax(300px, 360px);
gap: 22px;
align-items: start;
}
@media (max-width: 920px) {
.layout { grid-template-columns: 1fr; }
}
/* —— 方案列表 —— */
.schemes { display: grid; gap: 12px; }
.scheme {
appearance: none;
width: 100%;
text-align: left;
border: 1px solid var(--line);
background: #fff;
border-radius: 16px;
padding: 14px 16px;
cursor: pointer;
transition: border-color .15s, box-shadow .15s, background .15s;
font-family: inherit;
color: inherit;
}
.scheme:hover {
border-color: #cbd5e1;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05);
}
.scheme.is-on {
border-color: var(--brand);
background: linear-gradient(180deg, #f0fdf4, #fff);
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.18);
}
.scheme-top {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 6px;
}
.letter {
width: 28px; height: 28px;
border-radius: 8px;
display: grid; place-items: center;
font-size: 13px; font-weight: 750;
background: #f1f5f9;
color: #334155;
flex-shrink: 0;
}
.scheme.is-on .letter {
background: var(--brand-soft);
color: var(--brand-deep);
}
.scheme h2 {
margin: 0;
font-size: 15px;
font-weight: 680;
}
.scheme p {
margin: 0 0 10px;
font-size: 12.5px;
color: var(--muted);
padding-left: 38px;
}
.tag {
margin-left: auto;
font-size: 10px;
font-weight: 700;
padding: 2px 8px;
border-radius: 999px;
background: #f1f5f9;
color: #64748b;
}
.tag.rec {
background: var(--brand-soft);
color: var(--brand-deep);
}
/* —— 右侧手机 —— */
.stage {
position: sticky;
top: 16px;
}
.phone {
background: #111827;
border-radius: 36px;
padding: 10px;
box-shadow: 0 24px 50px rgba(15, 23, 42, 0.18);
}
.phone-screen {
border-radius: 28px;
overflow: hidden;
background: #f8fafc;
min-height: 640px;
display: flex;
flex-direction: column;
}
.phone-nav {
height: 52px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
border-bottom: 1px solid #f1f5f9;
font-size: 16px;
font-weight: 650;
color: #0f172a;
position: relative;
}
.phone-nav .back {
position: absolute;
left: 14px;
color: #64748b;
font-size: 14px;
font-weight: 500;
}
.form-mock {
flex: 1;
padding: 14px 14px 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.field {
background: #fff;
border-radius: 12px;
border: 1px solid #eef2f6;
padding: 12px 14px;
}
.field .lab {
font-size: 12px;
color: #94a3b8;
margin-bottom: 6px;
}
.field .val {
font-size: 14px;
color: #334155;
}
.field .val.ph { color: #cbd5e1; }
.stage-meta {
margin-top: 14px;
padding: 12px 14px;
background: rgba(255,255,255,.92);
border: 1px solid var(--line);
border-radius: 14px;
font-size: 13px;
color: var(--muted);
}
.stage-meta strong { color: var(--brand-deep); }
.stage-meta .why {
margin-top: 6px;
font-size: 12.5px;
line-height: 1.5;
}
/* ========== 按钮原子 ========== */
.ft {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px));
background: #fff;
border-top: 1px solid #eef2f6;
}
.btn {
margin: 0;
appearance: none;
font-family: inherit;
font-size: 15px;
line-height: 1;
border: none;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 44px;
padding: 0 16px;
border-radius: 12px;
font-weight: 600;
transition: transform .12s, opacity .12s, background .15s, box-shadow .15s;
-webkit-tap-highlight-color: transparent;
white-space: nowrap;
}
.btn:active { transform: scale(0.98); opacity: 0.92; }
.btn svg { width: 16px; height: 16px; flex-shrink: 0; }
/* 方案样式仅作用于右侧大屏,避免污染左侧迷你条 */
.phone-screen.s1 .ft { gap: 10px; padding: 12px 14px; }
.phone-screen.s1 .btn-cancel {
flex: 0 0 auto; height: 44px; padding: 0 10px;
background: transparent; color: #94a3b8;
font-weight: 500; font-size: 14px; border-radius: 0;
}
.phone-screen.s1 .btn-draft {
flex: 1; background: #f1f5f9; color: #334155; font-weight: 600;
}
.phone-screen.s1 .btn-submit {
flex: 1.35; background: var(--brand); color: #fff; font-weight: 700;
box-shadow: 0 8px 18px rgba(34, 197, 94, 0.28);
}
.phone-screen.s2 .ft { gap: 12px; padding: 12px 16px; }
.phone-screen.s2 .btn-cancel { display: none; }
.phone-screen.s2 .btn-draft {
flex: 0 0 auto; background: transparent; color: #64748b;
font-weight: 500; font-size: 15px; padding: 0 8px; height: 44px; border-radius: 0;
}
.phone-screen.s2 .btn-submit {
flex: 1; height: 46px; border-radius: 12px;
background: var(--brand-dark); color: #fff; font-weight: 680; letter-spacing: 0.02em;
}
.phone-screen.s3 .ft { gap: 12px; padding: 14px 16px; border-top-color: #f1f5f9; }
.phone-screen.s3 .btn-cancel { display: none; }
.phone-screen.s3 .btn-draft {
flex: 1; height: 46px; border-radius: 10px; background: #fff; color: #475569;
font-weight: 550; box-shadow: inset 0 0 0 1px #d0d5dd;
}
.phone-screen.s3 .btn-submit {
flex: 1; height: 46px; border-radius: 10px; background: #0f172a; color: #fff; font-weight: 650;
}
.phone-screen.s3 .btn-submit span.dot {
width: 6px; height: 6px; border-radius: 50%; background: var(--brand); margin-right: 2px;
}
.phone-screen.s4 { background: #f1f5f9; }
.phone-screen.s4 .ft {
margin: 0 12px 12px; padding: 8px; gap: 8px; border: none; border-radius: 18px;
background: rgba(255,255,255,0.92);
box-shadow: 0 1px 0 rgba(255,255,255,0.8) inset, 0 10px 28px rgba(15, 23, 42, 0.1);
backdrop-filter: blur(12px);
}
.phone-screen.s4 .btn-cancel {
flex: 0 0 44px; width: 44px; height: 44px; padding: 0; border-radius: 14px;
background: #f1f5f9; color: #64748b; font-size: 13px; font-weight: 600;
}
.phone-screen.s4 .btn-draft {
flex: 1; height: 44px; border-radius: 14px; background: #f8fafc; color: #334155;
font-weight: 600; box-shadow: inset 0 0 0 1px #e2e8f0;
}
.phone-screen.s4 .btn-submit {
flex: 1.2; height: 44px; border-radius: 14px;
background: linear-gradient(180deg, #4ade80, #16a34a); color: #fff; font-weight: 720;
box-shadow: 0 6px 14px rgba(22, 163, 74, 0.35);
}
.phone-screen.s5 .ft { gap: 0; padding: 0; border-top: 1px solid #e5e7eb; }
.phone-screen.s5 .btn { height: 52px; border-radius: 0; font-size: 15px; }
.phone-screen.s5 .btn-cancel {
flex: 0 0 76px; background: #fff; color: #64748b; font-weight: 500;
border-right: 1px solid #f1f5f9;
}
.phone-screen.s5 .btn-draft {
flex: 1; background: #fff; color: #1e293b; font-weight: 550;
border-right: 1px solid #f1f5f9;
}
.phone-screen.s5 .btn-submit {
flex: 1.25; background: var(--brand-dark); color: #fff; font-weight: 700;
}
.phone-screen.s6 .ft { gap: 10px; padding: 12px 14px; }
.phone-screen.s6 .btn-cancel {
flex: 0 0 auto; background: transparent; color: #94a3b8; font-weight: 500;
font-size: 14px; padding: 0 8px; height: 44px; border-radius: 0;
}
.phone-screen.s6 .btn-draft {
flex: 1; height: 44px; border-radius: 999px; background: #fff; color: var(--brand-deep);
font-weight: 620; box-shadow: inset 0 0 0 1.5px #86efac;
}
.phone-screen.s6 .btn-submit {
flex: 1.35; height: 44px; border-radius: 999px; background: var(--brand-deep);
color: #fff; font-weight: 700;
}
.phone-screen.s7 .ft { gap: 10px; padding: 12px 14px; }
.phone-screen.s7 .btn-cancel { display: none; }
.phone-screen.s7 .btn-draft {
flex: 0 0 auto; min-width: 108px; height: 46px; border-radius: 12px;
background: #fff; color: #475569; font-weight: 550; font-size: 14px; gap: 6px;
box-shadow: inset 0 0 0 1px #e2e8f0;
}
.phone-screen.s7 .btn-submit {
flex: 1; height: 46px; border-radius: 12px; background: var(--brand); color: #fff;
font-weight: 720; font-size: 16px; box-shadow: 0 10px 22px rgba(34, 197, 94, 0.28);
}
.phone-screen.s8 .ft {
gap: 10px; padding: 12px 14px; background: #fafbfc; border-top-color: #eef2f6;
}
.phone-screen.s8 .btn-cancel {
flex: 0 0 52px; height: 44px; padding: 0; border-radius: 12px;
background: #fff; color: #64748b; font-weight: 500; font-size: 14px;
box-shadow: inset 0 0 0 1px #e5e7eb;
}
.phone-screen.s8 .btn-draft {
flex: 1; height: 44px; border-radius: 12px; background: #eff6ff; color: #1d4ed8; font-weight: 620;
}
.phone-screen.s8 .btn-submit {
flex: 1.3; height: 44px; border-radius: 12px; background: var(--brand-dark); color: #fff; font-weight: 700;
}
/* 左侧迷你色条:只示意配色,不套完整底栏布局 */
.mini-ft {
display: flex; gap: 6px; align-items: center;
margin-left: 38px; pointer-events: none;
}
.mini-ft .ft { display: contents; }
.mini-ft .btn {
height: 28px; font-size: 11px; padding: 0 10px; border-radius: 8px;
box-shadow: none !important; min-width: 0;
}
.mini-ft .btn-cancel { display: none; }
.scheme.s1 .mini-ft .btn-draft { background: #f1f5f9; color: #334155; }
.scheme.s1 .mini-ft .btn-submit { background: #22c55e; color: #fff; }
.scheme.s2 .mini-ft .btn-draft { background: transparent; color: #64748b; padding: 0 4px; }
.scheme.s2 .mini-ft .btn-submit { flex: 1; background: #16a34a; color: #fff; }
.scheme.s3 .mini-ft .btn-draft { background: #fff; color: #475569; box-shadow: inset 0 0 0 1px #d0d5dd !important; }
.scheme.s3 .mini-ft .btn-submit { background: #0f172a; color: #fff; }
.scheme.s3 .mini-ft .dot { display: none; }
.scheme.s4 .mini-ft .btn-draft { background: #f8fafc; color: #334155; box-shadow: inset 0 0 0 1px #e2e8f0 !important; }
.scheme.s4 .mini-ft .btn-submit { background: linear-gradient(180deg, #4ade80, #16a34a); color: #fff; }
.scheme.s5 .mini-ft .btn { border-radius: 0; height: 30px; }
.scheme.s5 .mini-ft .btn-draft { background: #fff; color: #1e293b; box-shadow: inset 0 0 0 1px #e5e7eb !important; }
.scheme.s5 .mini-ft .btn-submit { background: #16a34a; color: #fff; }
.scheme.s6 .mini-ft .btn { border-radius: 999px; }
.scheme.s6 .mini-ft .btn-draft { background: #fff; color: #15803d; box-shadow: inset 0 0 0 1.5px #86efac !important; }
.scheme.s6 .mini-ft .btn-submit { background: #15803d; color: #fff; }
.scheme.s7 .mini-ft .btn-draft { background: #fff; color: #475569; box-shadow: inset 0 0 0 1px #e2e8f0 !important; }
.scheme.s7 .mini-ft .btn-submit { flex: 1; background: #22c55e; color: #fff; }
.scheme.s8 .mini-ft .btn-draft { background: #eff6ff; color: #1d4ed8; }
.scheme.s8 .mini-ft .btn-submit { background: #16a34a; color: #fff; }
</style>
</head>
<body>
<div class="wrap">
<h1>底栏按钮 · 重新设计</h1>
<p class="sub">
上一版问题:草稿与提交都抢绿色、圆角/描边千篇一律、缩略图看不出层级。
这一版只保留 <strong>8 个差异化方向</strong>,右侧大屏实时预览;核心原则是
<strong>草稿弱、提交强</strong>,避免双绿对撞。
</p>
<div class="note">
点左侧方案切换右侧手机。选好后回复编号,例如「用 <strong>S1</strong>」。
取消是否保留也可一并说明。
</div>
<div class="layout">
<div class="schemes" id="schemes"></div>
<aside class="stage">
<div class="phone">
<div class="phone-screen" id="phoneScreen">
<div class="phone-nav">
<span class="back">‹ 返回</span>
请假申请
</div>
<div class="form-mock">
<div class="field">
<div class="lab">请假类型</div>
<div class="val">年假</div>
</div>
<div class="field">
<div class="lab">开始时间</div>
<div class="val">2026-08-12 09:00</div>
</div>
<div class="field">
<div class="lab">结束时间</div>
<div class="val">2026-08-13 18:00</div>
</div>
<div class="field">
<div class="lab">请假事由</div>
<div class="val ph">请填写事由…</div>
</div>
</div>
<div class="ft" id="liveFt"></div>
</div>
</div>
<div class="stage-meta">
当前:<strong id="liveName">—</strong>
<div class="why" id="liveWhy"></div>
</div>
</aside>
</div>
</div>
<script>
const ICO_SAVE = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>`
const schemes = [
{
id: 'S1',
title: '石板次钮 + 薄荷主钮',
rec: true,
why: '草稿用中性灰底,不再抢绿;提交纯色+轻阴影,层级一眼可读。保留取消文字链。',
markup: `
<button class="btn btn-cancel" type="button">取消</button>
<button class="btn btn-draft" type="button">暂存草稿</button>
<button class="btn btn-submit" type="button">提交</button>`
},
{
id: 'S2',
title: '飞书式 · 文字草稿 + 宽提交',
why: '草稿降为次级文字操作,主视线落在提交。信息密度低、决策成本低。',
markup: `
<button class="btn btn-cancel" type="button">取消</button>
<button class="btn btn-draft" type="button">暂存草稿</button>
<button class="btn btn-submit" type="button">提交审批</button>`
},
{
id: 'S3',
title: '等宽双钮 · 白描边 + 墨黑提交',
why: '提交用近黑而非绿,品牌绿只做小圆点点缀,克制、偏工具产品。',
markup: `
<button class="btn btn-cancel" type="button">取消</button>
<button class="btn btn-draft" type="button">暂存草稿</button>
<button class="btn btn-submit" type="button"><span class="dot"></span>提交</button>`
},
{
id: 'S4',
title: '悬浮胶囊底栏',
why: '底栏浮起成卡片;取消为方块、草稿线框、提交亮绿渐变,现代感强。',
markup: `
<button class="btn btn-cancel" type="button">取消</button>
<button class="btn btn-draft" type="button">草稿</button>
<button class="btn btn-submit" type="button">提交</button>`
},
{
id: 'S5',
title: '通栏三段 · 右主色块',
why: '无圆角贴底通栏,右段深绿实心。偏企业后台/工具效率感。',
markup: `
<button class="btn btn-cancel" type="button">取消</button>
<button class="btn btn-draft" type="button">暂存草稿</button>
<button class="btn btn-submit" type="button">提交</button>`
},
{
id: 'S6',
title: '双胶囊 · 线框绿字 + 实心深绿',
why: '同圆角语言,但草稿只留线框墨绿字,提交实心深绿,绿不打架。',
markup: `
<button class="btn btn-cancel" type="button">取消</button>
<button class="btn btn-draft" type="button">暂存草稿</button>
<button class="btn btn-submit" type="button">提交</button>`
},
{
id: 'S7',
title: '紧凑草稿 + 超宽提交',
why: '草稿缩成带图标的次钮,提交占满剩余宽度,强调「主行动只有一个」。',
markup: `
<button class="btn btn-cancel" type="button">取消</button>
<button class="btn btn-draft" type="button">${ICO_SAVE}草稿</button>
<button class="btn btn-submit" type="button">提交</button>`
},
{
id: 'S8',
title: '冷暖分层 · 蓝草稿 / 绿提交',
why: '用色相区分动作:草稿偏冷蓝、提交偏暖绿,语义分离最强(略跳脱品牌单色)。',
markup: `
<button class="btn btn-cancel" type="button">取消</button>
<button class="btn btn-draft" type="button">暂存草稿</button>
<button class="btn btn-submit" type="button">提交</button>`
}
]
const list = document.getElementById('schemes')
const phone = document.getElementById('phoneScreen')
const liveFt = document.getElementById('liveFt')
const liveName = document.getElementById('liveName')
const liveWhy = document.getElementById('liveWhy')
function apply(id) {
const s = schemes.find((x) => x.id === id)
phone.className = 'phone-screen ' + id.toLowerCase()
liveFt.innerHTML = s.markup
liveName.textContent = id + ' · ' + s.title
liveWhy.textContent = s.why
list.querySelectorAll('.scheme').forEach((el) => {
el.classList.toggle('is-on', el.dataset.id === id)
})
}
schemes.forEach((s) => {
const btn = document.createElement('button')
btn.type = 'button'
btn.className = 'scheme ' + s.id.toLowerCase()
btn.dataset.id = s.id
btn.innerHTML = `
<div class="scheme-top">
<span class="letter">${s.id.replace('S', '')}</span>
<h2>${s.title}</h2>
${s.rec ? '<span class="tag rec">推荐</span>' : '<span class="tag">' + s.id + '</span>'}
</div>
<p>${s.why}</p>
<div class="mini-ft"><div class="ft">${s.markup}</div></div>`
btn.addEventListener('click', () => apply(s.id))
list.appendChild(btn)
})
apply('S1')
</script>
</body>
</html>
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,788 @@
# 移动端 OA 任务中心 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 详情办理(对齐 Web 业务,UI 对齐既有 mobile-h5 / mobile-oa-h5)。
**Architecture:** 列表在 `mobile-h5`;表单与办理在 `mobile-oa-h5` 新增 `/oa-detail`(`mode=view|approve`);草稿编辑继续 `/oa-start?draftId=`。API 复用 `/web/admin/oa-approval/*` + JWT。菜单种子挂「移动端菜单」根下。
**Tech Stack:** uni-app 微信小程序、`web-view`、Vue3、Vant4、`@form-create/vant`、FastAPI(菜单 SQL)
**Spec:** `docs/superpowers/specs/2026-08-12-mobile-oa-task-centers-design.md`
**原则(强制):** 功能语义对齐 `web-admin` OA 对应页与 `instances/detail.vue`;UI 对齐 `mobile-h5`(如 `pages/demo/list`、`pages/oa/start`)与 `mobile-oa-h5`(`OaStart.vue`、footer、picker),禁止照搬 Element 桌面布局。
**已完成(勿重复):** 五菜单 PNG → `mobile-h5/static/menu-icons/oa-*.png`;`van-icon` / `menu-icon-colors.js` / `backend/app/api/mini_menu.py` path→icon 映射。
**Out of scope:** 时间范围/分组/流程状态等已剔除筛选;Web 设计器改造;小程序内直挂 form-create。
---
## File map
| 路径 | 职责 |
|------|------|
| `docs/sql/mobile_menu_oa_task_centers.sql` | 五菜单种子(OA002–OA006) |
| `mobile-h5/utils/oa-api.js` | drafts / tasks / mark-read / counts |
| `mobile-h5/utils/oa-h5.js` | `buildOaDetailUrl`;扩展 start 预填若需要 |
| `mobile-h5/utils/oa-badge.js` | 拉草稿/待办/抄送角标(可新建) |
| `mobile-h5/pages.json` | 注册五列表 + 通用 webview(或扩 start/webview) |
| `mobile-h5/pages/oa/drafts/index.vue` | 草稿箱 |
| `mobile-h5/pages/oa/pending/index.vue` | 待办 |
| `mobile-h5/pages/oa/done/index.vue` | 已办 |
| `mobile-h5/pages/oa/mine/index.vue` | 我发起的 |
| `mobile-h5/pages/oa/cc/index.vue` | 抄送我的 |
| `mobile-h5/pages/oa/webview/index.vue` | 通用 web-view(start + detail)**或** 扩展 `start/webview.vue` |
| `mobile-h5/components/OaTaskCard.vue` | 任务/草稿卡片(移动端样式,非 BizListCard 业务 mock) |
| `mobile-oa-h5/src/api/oa.js` | instance detail + task actions + withdraw + cc read |
| `mobile-oa-h5/src/views/OaDetail.vue` | 详情只读 / 办理 |
| `mobile-oa-h5/src/oa/components/OaProgressTimeline.vue` | 审批进度(对齐 Web progress;可基于现有 Flow 组件) |
| `mobile-oa-h5/src/oa/components/OaActionSheet*.vue` | 意见 / 加签 / 转交弹层(Vant Dialog/Popup) |
| `mobile-oa-h5/src/router.js` | `/oa-detail` |
| `mobile-oa-h5/src/oa/print/`(可选) | PDF 打印移植 |
**Web 只读对照:**
- `web-admin/src/views/oa-approval/{drafts,pending,done,mine,cc}/index.vue`
- `web-admin/src/views/oa-approval/instances/detail.vue`
- `web-admin/src/api/oaApproval.ts`
- `web-admin/src/utils/oa-approval-print.ts`(打印)
---
## 目标用户路径
```
首页宫格(授权菜单)
→ 五列表之一(原生)
→ 草稿编辑 → webview → /oa-start?draftId=
→ 待办办理 → webview → /oa-detail?mode=approve&instanceId=&taskId=
→ 已办/抄送/我发起查看 → /oa-detail?mode=view&instanceId=
→ postMessage → navigateBack → 列表 onShow 刷新 + 角标
```
---
### Task 1: 菜单 SQL(S0)
**Files:**
- Create: `docs/sql/mobile_menu_oa_task_centers.sql`
- [ ] **Step 1: 按 `mobile_menu_oa_start.sql` 写五条幂等 INSERT**
完整内容:
```sql
-- 小程序 OA 五中心菜单种子(挂在「移动端菜单」根下)
-- 前置:docs/sql/mobile_menu_oa_start.sql(发起审批 OA001)可已执行
-- 角色授权请在 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
'MINI000000000000000000000000OA002',
'草稿箱',
@mini_root,
'C',
'/pages/oa/drafts/index',
NULL,
'mini:oa:drafts',
1,
'1',
'1',
'zkxw',
20,
'小程序草稿箱',
'admin',
'',
NOW(),
NULL,
NOW(),
0
FROM DUAL
WHERE @mini_root IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM sys_menu WHERE id = 'MINI000000000000000000000000OA002' AND del_flag = 0
);
-- 待办中心
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
'MINI000000000000000000000000OA003',
'待办中心',
@mini_root,
'C',
'/pages/oa/pending/index',
NULL,
'mini:oa:pending',
1,
'1',
'1',
'zkxw',
30,
'小程序待办中心',
'admin',
'',
NOW(),
NULL,
NOW(),
0
FROM DUAL
WHERE @mini_root IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM sys_menu WHERE id = 'MINI000000000000000000000000OA003' AND del_flag = 0
);
-- 已办中心
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
'MINI000000000000000000000000OA004',
'已办中心',
@mini_root,
'C',
'/pages/oa/done/index',
NULL,
'mini:oa:done',
1,
'1',
'1',
'zkxw',
40,
'小程序已办中心',
'admin',
'',
NOW(),
NULL,
NOW(),
0
FROM DUAL
WHERE @mini_root IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM sys_menu WHERE id = 'MINI000000000000000000000000OA004' AND del_flag = 0
);
-- 我发起的
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
'MINI000000000000000000000000OA005',
'我发起的',
@mini_root,
'C',
'/pages/oa/mine/index',
NULL,
'mini:oa:mine',
1,
'1',
'1',
'zkxw',
50,
'小程序我发起的',
'admin',
'',
NOW(),
NULL,
NOW(),
0
FROM DUAL
WHERE @mini_root IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM sys_menu WHERE id = 'MINI000000000000000000000000OA005' AND del_flag = 0
);
-- 抄送我的
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
'MINI000000000000000000000000OA006',
'抄送我的',
@mini_root,
'C',
'/pages/oa/cc/index',
NULL,
'mini:oa:cc',
1,
'1',
'1',
'zkxw',
60,
'小程序抄送我的',
'admin',
'',
NOW(),
NULL,
NOW(),
0
FROM DUAL
WHERE @mini_root IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM sys_menu WHERE id = 'MINI000000000000000000000000OA006' AND del_flag = 0
);
```
- [ ] **Step 2: 在测试库执行 SQL,Web 角色管理勾选后调 `GET /mini/menus` 应含 5 条 path**
Expected: list 中出现 `/pages/oa/drafts|pending|done|mine|cc/index`,icon 分别为 `oa-drafts|oa-pending|oa-done|oa-mine|oa-cc`。
- [ ] **Step 3: Commit**
```bash
git add docs/sql/mobile_menu_oa_task_centers.sql
git commit -m "$(cat <<'EOF'
feat(oa): add mobile menu seeds for five OA task centers
EOF
)"
```
---
### Task 2: 扩展双端 OA API(S0/S1 前置)
**Files:**
- Modify: `mobile-h5/utils/oa-api.js`
- Modify: `mobile-oa-h5/src/api/oa.js`
- [ ] **Step 1: 在 `mobile-h5/utils/oa-api.js` 追加(保留现有 templates 方法;按项目 `request.js` 支持的 get/post/del 写法实现)**
对齐 `web-admin/src/api/oaApproval.ts`:
```js
// drafts
export function fetchOaInstanceDraftList(params) { /* GET /web/admin/oa-approval/drafts */ }
export function fetchOaInstanceDraftCount() { /* GET .../drafts/count */ }
export function fetchDeleteOaInstanceDraft(id) { /* DELETE .../drafts/{id} */ }
export function fetchBatchDeleteOaInstanceDrafts(ids) { /* POST .../drafts/batch-delete { ids } */ }
export function fetchSubmitOaInstanceDraft(id, data) { /* POST .../drafts/{id}/submit */ }
// tasks
export function fetchOaTasks(kind, params) {
// kind: pending|done|mine|cc
// GET .../tasks/{kind} query: page,pageSize,appName,initiatorName,readStatus(cc)
}
export function fetchOaPendingTaskCount() { /* GET .../tasks/pending/count */ }
export function fetchOaCcUnreadCount() { /* GET .../tasks/cc/unread-count */ }
export function fetchMarkOaCcTaskRead(taskId) { /* POST .../tasks/{id}/read */ }
export function fetchBatchMarkOaCcTasksRead(ids) { /* POST .../tasks/cc/read-batch { ids } */ }
```
实现时复用文件内已有 `get` / `authHeader`;若 `request.js` 无 `del`/`post`,按同目录其它 utils 写法调用。每个函数必须带 `Authorization: Bearer`。
- [ ] **Step 2: 在 `mobile-oa-h5/src/api/oa.js` 追加**
```js
export function fetchOaInstanceDetail(id) {
return get(`/web/admin/oa-approval/instances/${encodeURIComponent(id)}`)
}
export function fetchApproveOaTask(taskId, comment) {
return post(`/web/admin/oa-approval/tasks/${encodeURIComponent(taskId)}/approve`, { comment })
}
export function fetchRejectOaTask(taskId, comment) {
return post(`/web/admin/oa-approval/tasks/${encodeURIComponent(taskId)}/reject`, { comment })
}
export function fetchReturnOaTask(taskId, comment) {
return post(`/web/admin/oa-approval/tasks/${encodeURIComponent(taskId)}/return`, { comment })
}
export function fetchAddSignOaTask(taskId, data) {
// data: { userIds: string[], comment?: string }
return post(`/web/admin/oa-approval/tasks/${encodeURIComponent(taskId)}/add-sign`, data)
}
export function fetchTransferOaTask(taskId, data) {
// data: { targetUserId: string, comment?: string }
return post(`/web/admin/oa-approval/tasks/${encodeURIComponent(taskId)}/transfer`, data)
}
export function fetchWithdrawOaInstance(instanceId) {
return post(`/web/admin/oa-approval/instances/${encodeURIComponent(instanceId)}/withdraw`, {})
}
export function fetchMarkOaCcTaskRead(taskId) {
return post(`/web/admin/oa-approval/tasks/${encodeURIComponent(taskId)}/read`, {})
}
```
- [ ] **Step 3: 本地用已登录 token 在浏览器/控制台或临时页面各打一条 drafts list / tasks pending 验证 200**
- [ ] **Step 4: Commit**
```bash
git add mobile-h5/utils/oa-api.js mobile-oa-h5/src/api/oa.js
git commit -m "$(cat <<'EOF'
feat(oa): extend mobile OA APIs for drafts, tasks, and detail actions
EOF
)"
```
---
### Task 3: 通用 web-view + `buildOaDetailUrl`(S0)
**Files:**
- Modify: `mobile-h5/utils/oa-h5.js`
- Create or Modify: `mobile-h5/pages/oa/webview/index.vue`(推荐新建通用页)**或** 扩展 `pages/oa/start/webview.vue`
- Modify: `mobile-h5/pages.json`
- [ ] **Step 1: 在 `oa-h5.js` 增加**
```js
/** 审批详情 / 办理页 */
export function buildOaDetailUrl({ instanceId, taskId, mode } = {}) {
const token = getToken()
const params = {}
if (instanceId) params.instanceId = instanceId
if (taskId) params.taskId = taskId
if (mode) params.mode = mode // view | approve
if (token) params.token = token
return withQuery('/oa-detail', params)
}
const DETAIL_TITLE_KEY = 'oa_detail_nav_title'
export function stashOaDetailTitle(name) { /* 同 stashOaStartTitle 模式 */ }
export function takeOaDetailTitle() { /* 同 takeOaStartTitle */ }
```
- [ ] **Step 2: 通用 webview 页逻辑**
`onLoad(query)`:
1. 无 token → `forceRelogin`
2. 若 `query.draftId` 或(`query.appId` 且无 `instanceId`)→ `buildOaStartUrl`
3. 若 `query.instanceId` → `buildOaDetailUrl({ instanceId, taskId, mode })`
4. 标题:`takeOaDetailTitle()` / `takeOaStartTitle()`
5. `CLOSE_ACTIONS` 扩展:`oa-detail-close`、`oa-detail-done`、`oa-detail-withdraw`、`oa-detail-restart` 等;H5 iframe `message` 与小程序 `@message` 行为对齐现有 start webview
- [ ] **Step 3: `pages.json` 注册五列表空壳路径 + webview(标题用中文业务名)**
示例条目:
```json
{
"path": "pages/oa/drafts/index",
"style": {
"navigationBarTitleText": "草稿箱",
"navigationBarBackgroundColor": "#ecfdf5",
"navigationBarTextStyle": "black"
}
}
```
同样注册 `pending`/`done`/`mine`/`cc`;webview 标题默认「审批详情」。
- [ ] **Step 4: 真机/开发者工具打开任意 webview URL 带 token,确认能加载 H5(detail 可先 404,本 Task 只要求 start 路径不回归)**
- [ ] **Step 5: Commit**
```bash
git add mobile-h5/utils/oa-h5.js mobile-h5/pages.json mobile-h5/pages/oa/webview/
git commit -m "$(cat <<'EOF'
feat(oa): add shared OA webview and detail URL builder
EOF
)"
```
---
### Task 4: 共用任务卡片 + 列表壳(UI)
**Files:**
- Create: `mobile-h5/components/OaTaskCard.vue`
- Create: `mobile-h5/styles/components/oa-task-list.scss`(若项目按组件拆 scss)
- Reference UI: `mobile-h5/pages/demo/list.vue`、`pages/oa/start/index.vue`、`styles/tokens.scss`
- [ ] **Step 1: 实现 `OaTaskCard`**
Props 建议:
```js
{
title: String, // 流程名
statusText: String,
statusTone: String, // warn|success|danger|info
metaLines: Array, // [{ label, value }]
unread: Boolean, // 抄送红点
actions: Array // [{ key, label, primary?, danger? }]
}
```
Emits: `action` with `{ key, ... }`;整卡点击可 emit `open`。
样式:三墙 Token;卡片圆角/间距对齐 start 列表与 demo list;**不要**复制 Web 表格列。
- [ ] **Step 2: 抽可复用 list mixin 或 `utils/oa-list.js`(可选)**
提供:`page`/`pageSize`/`loading`/`finished`、`loadMore`、`onPullDownRefresh` 重置。若不想抽 mixin,五页内复制最小分页逻辑亦可(YAGNI:先两页共用后再抽)。
- [ ] **Step 3: Commit**
```bash
git add mobile-h5/components/OaTaskCard.vue mobile-h5/styles/components/oa-task-list.scss
git commit -m "$(cat <<'EOF'
feat(oa): add mobile OA task card for center lists
EOF
)"
```
---
### Task 5: 草稿箱列表(S1)
**Files:**
- Create: `mobile-h5/pages/oa/drafts/index.vue`
- Modify: `mobile-h5/pages.json`(若 Task3 已注册可跳过)
**功能清单(规格 §2.1):** 列表、分页、流程名筛选、编辑、列表提交、单删、批量删、角标(角标可先调 count API,宫格角标 UI 放 Task 11)。
- [ ] **Step 1: 实现页面**
结构参考 `pages/demo/list.vue`:顶区搜索(仅关键词)+ 列表 + 多选模式批量删。
关键方法:
```js
async loadList(reset) {
// fetchOaInstanceDraftList({ keyword, page, pageSize })
}
openEdit(row) {
stashOaStartTitle(row.appName || row.title)
uni.navigateTo({
url: `/pages/oa/webview/index?draftId=${row.id}&appId=${row.appId || ''}`
// 若仍用 start/webview:`/pages/oa/start/webview?draftId=...`
})
}
async submitRow(row) {
// uni.showModal 确认 → fetchSubmitOaInstanceDraft(row.id) → 刷新
// 失败:可改为 openEdit 让用户改表单(对齐 Web)
}
async deleteRow(row) { /* confirm → fetchDeleteOaInstanceDraft */ }
async batchDelete() { /* selected ids → fetchBatchDeleteOaInstanceDrafts */ }
```
- [ ] **Step 2: onShow 刷新列表**
- [ ] **Step 3: 手工验收**
1. 发起页暂存 → 草稿箱可见
2. 编辑打开 H5 回填 → 再暂存/提交
3. 列表提交成功后草稿消失
4. 单删、批量删
- [ ] **Step 4: Commit**
```bash
git add mobile-h5/pages/oa/drafts/
git commit -m "$(cat <<'EOF'
feat(oa): add mobile draft box list with edit submit delete
EOF
)"
```
---
### Task 6: H5 `/oa-detail` 只读(S2)
**Files:**
- Create: `mobile-oa-h5/src/views/OaDetail.vue`
- Create: `mobile-oa-h5/src/oa/components/OaProgressTimeline.vue`(或复用/改造现有 flow 展示)
- Modify: `mobile-oa-h5/src/router.js`
- Reference: `OaStart.vue`(schema-prep、form-create disabled)、Web `detail.vue`(progress 字段)
- [ ] **Step 1: 注册路由**
```js
import OaDetail from './views/OaDetail.vue'
// ...
{ path: '/oa-detail', component: OaDetail }
```
- [ ] **Step 2: `OaDetail.vue` 最小只读**
1. `captureTokenFromUrl`(与 main/OaStart 一致)
2. 读 `instanceId`、`mode`(默认 view)、`taskId`
3. `fetchOaInstanceDetail` → `formSchemaJson` + `formData` → `schema-prep` → `<form-create-mobile disabled>`
4. 渲染 `progress` 时间线
5. 顶栏状态 Tag(文案映射对齐 Web `statusText`)
6. 底栏:`mode=view` 且非发起人特殊操作时仅「关闭」→ `postMessage` / `wx.miniProgram.navigateBack`,action=`oa-detail-close`
UI:padding、底栏按钮类名对齐 `OaStart` / `oa-form.css` / footer demo。
- [ ] **Step 3: 浏览器直接打开**
`http://127.0.0.1:5178/oa-h5/oa-detail?instanceId=真实ID&token=...&mode=view`
Expected: 表单只读可见、进度可见、关闭可返回。
- [ ] **Step 4: Commit**
```bash
git add mobile-oa-h5/src/views/OaDetail.vue mobile-oa-h5/src/oa/components/OaProgressTimeline.vue mobile-oa-h5/src/router.js
git commit -m "$(cat <<'EOF'
feat(oa): add mobile OA detail page in read-only mode
EOF
)"
```
---
### Task 7: 已办 / 我发起 / 抄送列表进 view(S2)
**Files:**
- Create: `mobile-h5/pages/oa/done/index.vue`
- Create: `mobile-h5/pages/oa/mine/index.vue`(本 Task 仅查看;打印/复制/撤销后续 Task)
- Create: `mobile-h5/pages/oa/cc/index.vue`(本 Task:列表+打开已读+进详情;阅读筛选/批量可本 Task 一并做或见 Task 10)
- [ ] **Step 1: 已办页**
筛选:`appName`、`initiatorName`(搜索框 + 可选第二条件,移动端可用一行双字段或展开筛选,**不要** Web 日期/状态)。
`fetchOaTasks('done', …)` → 卡片 → `stashOaDetailTitle` → navigate webview `instanceId&mode=view`。
- [ ] **Step 2: 我发起的页(查看)**
筛选仅 `appName`。`fetchOaTasks('mine', …)` → view。
- [ ] **Step 3: 抄送页(查看 + 打开已读)**
打开前或打开时:`fetchMarkOaCcTaskRead(taskId)`(task 行上的抄送任务 id,字段名对齐 Web `row.id`)。
再进 `mode=view`。H5 详情可再兜底 mark-read。
- [ ] **Step 4: 验收三页均可进只读详情并返回刷新**
- [ ] **Step 5: Commit**
```bash
git add mobile-h5/pages/oa/done/ mobile-h5/pages/oa/mine/ mobile-h5/pages/oa/cc/
git commit -m "$(cat <<'EOF'
feat(oa): add done mine cc lists opening read-only OA detail
EOF
)"
```
---
### Task 8: 待办列表 + 办理动作(S3)
**Files:**
- Create: `mobile-h5/pages/oa/pending/index.vue`
- Modify: `mobile-oa-h5/src/views/OaDetail.vue`
- Create: 意见弹层 / 加签选人 / 转交选人(复用 `OaPersonSelect` 或 Vant Popup + 已有选人)
**对照 Web `detail.vue`:** `myPendingTask`、`canApprove`、`allowReturn`、`allowAddSign`、转交排除规则。
- [ ] **Step 1: 待办列表**
筛选:流程名、申请人。进入:`mode=approve&instanceId=&taskId=`(taskId 用行上待办任务 id)。
- [ ] **Step 2: 详情底栏(`mode=approve` 且存在当前用户 pending task)**
| 按钮 | API |
|------|-----|
| 同意 | `fetchApproveOaTask(taskId, comment?)` |
| 拒绝 | `fetchRejectOaTask` |
| 打回 | 意见必填 → `fetchReturnOaTask` |
| 加签 | 选多人 → `fetchAddSignOaTask({ userIds, comment })` |
| 转交 | 选一人(排除本人与本节点已有待办)→ `fetchTransferOaTask({ targetUserId, comment })` |
成功后 toast + `postMessage({ source:'zkxw-oa-h5', action:'oa-detail-done' })` + navigateBack。
意见输入用 Vant Dialog/Popup,样式对齐 mobile-oa-h5,不要 Element MessageBox。
- [ ] **Step 3: 五条办理主路径各测至少一次(可用测试租户)**
- [ ] **Step 4: Commit**
```bash
git add mobile-h5/pages/oa/pending/ mobile-oa-h5/src/views/OaDetail.vue mobile-oa-h5/src/oa/components/
git commit -m "$(cat <<'EOF'
feat(oa): add pending list and approve reject return add-sign transfer
EOF
)"
```
---
### Task 9: 我发起的 — 撤销与复制重开(S4)
**Files:**
- Modify: `mobile-h5/pages/oa/mine/index.vue`
- Modify: `mobile-oa-h5/src/views/OaDetail.vue`
- Modify: `mobile-oa-h5/src/views/OaStart.vue`(接收预填)
- Modify: `mobile-h5/utils/oa-h5.js`(可选 storage 传 fromInstance 预填)
- [ ] **Step 1: 详情撤销**
条件:当前用户是发起人且实例 `running`(与 Web `canWithdraw` 一致)。
`fetchWithdrawOaInstance` → done 回列表。
- [ ] **Step 2: 复制并重新发起**
条件:发起人且 status ∈ `returned|cancelled|rejected`。
实现择一(优先与 Web 一致):
1. H5:`postMessage` action=`oa-detail-restart` + `appId` + 小程序再开 start webview,并用 `uni.setStorageSync('oa_restart_prefills', { formData, … })`
2. 或 H5 内 `router.replace({ path:'/oa-start', query:{ appId, fromInstanceId } })`,`OaStart` 拉实例详情回填
选定一种写清,并在 `OaStart` 实现回填;提交成功后勿留下旧 instanceId。
- [ ] **Step 3: 列表行也提供「复制并重新发起」入口(终态时)**
- [ ] **Step 4: 验收撤销与复制提交新单**
- [ ] **Step 5: Commit**
```bash
git add mobile-h5/pages/oa/mine/ mobile-oa-h5/src/views/OaDetail.vue mobile-oa-h5/src/views/OaStart.vue mobile-h5/utils/oa-h5.js
git commit -m "$(cat <<'EOF'
feat(oa): support withdraw and copy-restart on mobile mine flows
EOF
)"
```
---
### Task 10: 抄送增强(S5)
**Files:**
- Modify: `mobile-h5/pages/oa/cc/index.vue`
- [ ] **Step 1: 阅读状态筛选** — `readStatus`: `unread`|`read`|`all`(默认 unread,对齐 Web)
- [ ] **Step 2: 未读红点样式** — `OaTaskCard` `unread`
- [ ] **Step 3: 单条已读** — 不打开详情也可 mark
- [ ] **Step 4: 批量已读** — 多选 + `fetchBatchMarkOaCcTasksRead`
- [ ] **Step 5: 验收后 Commit**
```bash
git add mobile-h5/pages/oa/cc/
git commit -m "$(cat <<'EOF'
feat(oa): add cc read filters batch mark and unread styles
EOF
)"
```
---
### Task 11: 角标(S1/S3/S5/S7)
**Files:**
- Create: `mobile-h5/utils/oa-badge.js`
- Modify: `mobile-h5/components/MenuGrid.vue` 和/或 `pages/index/index.vue`
- [ ] **Step 1: `refreshOaMenuBadges`**
并行请求:
- `fetchOaInstanceDraftCount`
- `fetchOaPendingTaskCount`
- `fetchOaCcUnreadCount`
返回 map:`path → count`(`/pages/oa/drafts/index` 等)。展示:`>99` 显示 `99+`。
- [ ] **Step 2: 首页加载菜单后合并 badge;`onShow` 刷新**
MenuGrid 在 title 旁或图标右上角显示数字(样式轻量,对齐三墙,勿过重)。
- [ ] **Step 3: 从 webview 返回后各列表 `onShow` 也可触发首页角标(通过 storage 事件或全局简单 bus;最小做法:仅首页 onShow 刷新即可)
- [ ] **Step 4: Commit**
```bash
git add mobile-h5/utils/oa-badge.js mobile-h5/components/MenuGrid.vue mobile-h5/pages/index/index.vue
git commit -m "$(cat <<'EOF'
feat(oa): show draft pending cc badges on mobile home menu
EOF
)"
```
---
### Task 12: 打印(S6)
**Files:**
- 移植参考:`web-admin/src/utils/oa-approval-print.ts`
- Create: `mobile-oa-h5/src/oa/print/exportOaApprovalPdf.js`(或等价)
- Modify: `OaDetail.vue` + `mine`/`cc` 列表入口
- [ ] **Step 1: 在 H5 内实现 PDF 生成(优先 jspdf/html2canvas 等与 Web 同类方案;以 Web 工具函数逻辑为准)**
- [ ] **Step 2: 详情与列表「打印」按钮**:生成后尝试下载 / 新开预览;微信内若失败,`showToast` 明确文案(规格允许兜底,不阻塞其它功能)
- [ ] **Step 3: 真机验收;记录限制到 `docs/superpowers/specs/` 旁注或本 plan 底部「打印验收笔记」
- [ ] **Step 4: Commit**
```bash
git add mobile-oa-h5/src/oa/print/ mobile-oa-h5/src/views/OaDetail.vue mobile-h5/pages/oa/mine/ mobile-h5/pages/oa/cc/
git commit -m "$(cat <<'EOF'
feat(oa): add mobile approval PDF print for mine and cc
EOF
)"
```
---
### Task 13: 回归与图谱(收尾)
- [ ] **Step 1: 回归「发起审批」全链路未被破坏**(列表→填写→草稿→提交)
- [ ] **Step 2: 按规格 §8 测试要点过一遍勾选
- [ ] **Step 3: 若改了 backend `mini_menu.py`(本计划默认已改完),执行 `./scripts/update-graph.sh backend`
- [ ] **Step 4: 最终 commit 若有文档修补**
---
## Spec coverage 自检
| 规格项 | Task |
|--------|------|
| 菜单 SQL OA002–006 + 图标映射 | Task 1(图标已完成) |
| 功能对齐 Web / UI 对齐移动端 | 全文原则 + Task 4–8 |
| 草稿 1/2/3/6/7/8/9/10 | Task 5 + Task 11 |
| 待办办理五动作 + 角标 | Task 8 + 11 |
| 已办只读 | Task 6–7 |
| 我发起撤销/复制/打印 | Task 9 + 12 |
| 抄送阅读/批量/打印/角标 | Task 7/10/11/12 |
| `/oa-detail` | Task 6 + 8 + 9 |
| S0–S7 顺序 | Task 1→12 |
---
## 执行说明
实现时每完成一个 Task 再开下一个;打印(Task 12)可与角标并行,但不得阻塞 Task 5–10 合并。
@@ -0,0 +1,98 @@
# OA 人员弹窗方案 C 对齐 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:** 让发起审批表单【人员】选择弹窗列表与「选择审批人/抄送人」一致,采用 Demo 方案 C(分割线行表 + 姓名与副信息同行)。
**Architecture:** 只改 `OaPersonSelect.vue` 列表项模板与用户映射;在 `oa-form.css` 补充 `.oa-pick__name` / `.oa-pick__sub`。行级方案 C 样式已存在,无需重写。
**Tech Stack:** Vue 3、Vant、`mobile-oa-h5`
**Spec:** `docs/superpowers/specs/2026-08-12-oa-person-picker-scheme-c-design.md`
---
### Task 1: 列表项模板改为同行姓名 + 副信息
**Files:**
- Modify: `mobile-oa-h5/src/oa/components/OaPersonSelect.vue`
- [x] **Step 1: 替换多选 / 单选 `van-cell` 为 `#title` 插槽结构**
多选与单选均去掉 `:title` / `:label`,改为:
```vue
<van-cell
v-for="u in filtered"
:key="u.id"
clickable
@click="toggle(u.id)"
>
<template #icon>
<van-checkbox class="oa-pick__check" :name="u.id" @click.stop />
</template>
<template #title>
<span class="oa-pick__name">{{ u.label }}</span>
<span v-if="u.sub" class="oa-pick__sub">{{ u.sub }}</span>
</template>
</van-cell>
```
单选同理,`@click` 设 `pendingSingle`,icon 用 `van-radio`。
- [x] **Step 2: 更新 `loadUsers` 副信息与搜索**
```js
const phone = String(u.phoneNo || '').trim()
const userNo = String(u.userNo || '').trim()
const nick = String(u.nickName || '').trim()
const label = nick || userNo || id
const sub = phone || (userNo && userNo !== label ? userNo : '')
opts.push({ id, label, sub })
```
`filtered` 已用 `u.sub`,保持即可(手机或工号都会进 `sub`)。
可选:搜索 placeholder 改为「搜索姓名、工号或手机号」。
---
### Task 2: 补充同行文字样式
**Files:**
- Modify: `mobile-oa-h5/src/styles/oa-form.css`
- [x] **Step 1: 在 `.oa-pick__check` 规则前增加 name/sub 样式,并强化选中态**
```css
.oa-pick__name {
font-size: 14px;
color: var(--text-primary, #0f291e);
}
.oa-pick__sub {
margin-left: 8px;
font-size: 12px;
color: var(--text-muted, #5f7a6d);
}
.oa-pick__list .van-cell:has(.van-checkbox--checked) .oa-pick__name,
.oa-pick__list .van-cell:has(.van-radio--checked) .oa-pick__name {
color: #15803d;
}
```
(保留现有 `.van-cell__title` 选中规则作兜底。)
---
### Task 3: 验收与提交
- [ ] **Step 1: 手动核对** — FormSink / 发起页打开【人员】弹窗:分割线行、选中浅绿、姓名副信息同行;与流程选人弹窗观感一致。
- [ ] **Step 2: Commit**
```bash
git add mobile-oa-h5/src/oa/components/OaPersonSelect.vue mobile-oa-h5/src/styles/oa-form.css
git commit -m "fix(mobile-oa-h5): 人员选择弹窗列表对齐方案 C"
```
@@ -0,0 +1,273 @@
# 移动端 OA 任务中心(草稿 / 待办 / 已办 / 我发起 / 抄送)设计
> 日期:2026-08-12
> 工程:`mobile-h5`(微信小程序)+ `mobile-oa-h5`(OA 表单 H5)
> 对齐参考:`web-admin` 下 OA审批 六菜单(发起已落地移动端;本设计覆盖其余五页)
> 上游设计:`docs/superpowers/specs/2026-08-11-mobile-oa-start-approval-design.md`
> 菜单种子参考:`docs/sql/mobile_menu_oa_start.sql`
---
## 1. 背景与目标
### 1.1 现状
- Web:`发起审批` / `草稿箱` / `待办中心` / `已办中心` / `我发起的` / `抄送我的` 已齐;详情办理在 `instances/detail.vue`(同意/拒绝/打回/加签/转交/撤销/复制重开/打印)。
- 移动:`mobile-h5` 已实现「发起审批」列表;填写经 web-view 打开 `mobile-oa-h5` `/oa-start`(含 `draftId` 草稿再编辑)。
- `mobile-oa-h5` 尚无实例详情 / 审批办理路由;无五中心列表。
### 1.2 目标
1. 在小程序中一次性补齐五中心功能(可分步开发),能力以本文「已确认范围」为准,办理动作对齐 Web 详情。
2. 延续既有架构:**列表原生 `mobile-h5`;动态表单与办理在 `mobile-oa-h5`**。
3. 新增移动端菜单种子 SQL,挂在「移动端菜单」根下,便于角色授权。
### 1.3 非目标
- 改造 Web 设计器或 Web 六页交互。
- 在小程序原生内直挂 `@form-create/vant`。
- 已在核对中明确剔除的筛选(见 §2)。
- 审批流设计、应用管理、收款账户等非六中心能力。
### 1.4 架构选型(已确认)
**方案 A**:延续发起审批架构——五列表在 `mobile-h5`;详情/办理扩 `mobile-oa-h5` `/oa-detail`;草稿编辑继续走 `/oa-start?draftId=`。
不采用:列表全进 H5;或在原生重做表单运行时。
### 1.5 功能对齐 Web,UI 对齐移动端(已确认)
| 维度 | 权威来源 | 说明 |
|------|----------|------|
| **功能 / 业务语义** | `web-admin` OA 六中心 + `instances/detail` | API 字段、操作条件、状态文案、办理规则与 Web 一致;本文 §2 已裁剪的能力除外 |
| **UI / 交互样式** | `mobile-h5` + `mobile-oa-h5` 既有端到端模式 | **禁止**照搬 Web Element 桌面布局(多列表格筛选栏、桌面弹窗栅格等) |
移动端实现时优先对照:
- **列表 / 宫格 / 空态 / Token**:`mobile-h5`(如 `pages/oa/start`、`MenuGrid`、`styles/tokens.scss`、`BizListCard` 等已有列表卡片模式)
- **表单 / 底栏 / 选人 / 流预览**:`mobile-oa-h5`(如 `OaStart.vue`、`oa-form.css`、`tokens.css`、既有 picker / footer 按钮样式)
- **视觉规范**:`docs/miniprogram-ui-spec-sanqiang.md`(三墙色;勿混用 Web `#10b981`)
原则:**Web 决定「做什么」;移动端决定「长什么样、怎么点」。**
---
## 2. 已确认功能范围(逐页核对)
### 2.1 草稿箱
| 要 | 不要 |
|----|------|
| 列表;分页/下拉刷新;流程名筛选;编辑;列表提交审批;单删;批量删;菜单角标 | 保存时间范围;模板分组筛选 |
编辑 → `mobile-oa-h5` `/oa-start?draftId=`(已有能力)。
### 2.2 待办中心
| 要 | 不要 |
|----|------|
| 列表;分页;流程名+申请人筛选;进详情;角标 | 申请时间范围;流程状态筛选 |
| 详情:表单只读+流进度;同意/拒绝/打回/加签/转交 | — |
### 2.3 已办中心
| 要 | 不要 |
|----|------|
| 列表;分页;流程名+申请人;只读详情(表单+流) | 时间/状态筛选;办理操作;角标 |
### 2.4 我发起的
| 要 | 不要 |
|----|------|
| 列表;分页;流程名筛选;查看;打印;复制并重新发起;详情撤销;详情内复制重开 | 申请时间;流程状态筛选 |
### 2.5 抄送我的
| 要 | 不要 |
|----|------|
| 列表;分页;流程名+申请人;阅读状态筛选;未读样式;查看(打开标已读);单条已读;批量已读;打印;未读角标;只读详情 | 申请时间;流程状态筛选;办理操作 |
---
## 3. 架构
```
┌─ mobile-h5 ─────────────────────────────────────────────┐
│ 首页 MenuGrid ← fetchMyMobileMenus + 角色授权 │
│ /pages/oa/drafts|pending|done|mine|cc/index (列表) │
│ /pages/oa/webview(或复用 start/webview)整页 web-view │
│ 角标:drafts/count、pending/count、cc/unread-count │
└───────────────────────────┬─────────────────────────────┘
│ token + query
▼
┌─ mobile-oa-h5 ──────────────────────────────────────────┐
│ /oa-start 发起 + 草稿编辑(已有) │
│ /oa-detail 新增:mode=view|approve │
│ GET instances/{id};表单 disabled;流进度 │
│ approve:同意/拒绝/打回/加签/转交 │
│ 发起人:撤销 / 复制并重新发起 │
│ 打印:移植 Web PDF 能力 │
│ 结束:postMessage → 小程序 navigateBack + 刷新 │
└─────────────────────────────────────────────────────────┘
```
### 3.1 工程边界
| `mobile-h5` | `mobile-oa-h5` |
|-------------|----------------|
| 五列表、筛选、批量删/已读、角标、菜单 path | `formSchemaJson` 渲染、流进度、办理弹层、打印 |
| 草稿「编辑」→ 已有 start URL | `/oa-detail?instanceId=&taskId=&mode=&token=` |
### 3.2 API
继续复用 `/web/admin/oa-approval/*` + 小程序登录 JWT(与发起审批一致),不新开独立 mini OA 前缀(除非后续权限评审要求拆分)。
主要端点:
- 草稿:`/drafts*`、`/drafts/{id}/submit`、`/drafts/count`
- 任务列表:`/tasks/{pending|done|mine|cc}`
- 详情:`GET /instances/{id}`
- 办理:`POST /tasks/{id}/approve|reject|return|add-sign|transfer`
- 抄送已读:`POST /tasks/{id}/read`、`POST /tasks/cc/read-batch`
- 撤销:`POST /instances/{id}/withdraw`
- 角标:`/tasks/pending/count`、`/tasks/cc/unread-count`
### 3.3 UI Token 与样式权威
三墙品牌绿 `#0a9f6d` 等;**禁止**混用 Web Fresh Mint `#10b981`。
列表 / 详情视觉对齐 **已有移动端界面**,而非 Web 列表页截图:
- `mobile-h5`:发起审批列表、首页宫格、现有 list/card/empty 样式
- `mobile-oa-h5`:`/oa-start` 填写页结构、底栏按钮、Vant 弹层与自研 picker 交互
筛选在移动端宜用顶栏搜索 / 简易筛选条 / ActionSheet,避免 Web 多列 `filter-grid` 桌面形态。
---
## 4. 移动端菜单种子
参考 `docs/sql/mobile_menu_oa_start.sql`:解析「移动端菜单」根(`parent_id='0'` 且名称含「移动端」),幂等 `INSERT`。
新增脚本建议路径:`docs/sql/mobile_menu_oa_task_centers.sql`。
| menu_name | id | path | perms | order_num |
|-----------|-----|------|-------|-----------|
| 草稿箱 | `MINI000000000000000000000000OA002` | `/pages/oa/drafts/index` | `mini:oa:drafts` | 20 |
| 待办中心 | `MINI000000000000000000000000OA003` | `/pages/oa/pending/index` | `mini:oa:pending` | 30 |
| 已办中心 | `MINI000000000000000000000000OA004` | `/pages/oa/done/index` | `mini:oa:done` | 40 |
| 我发起的 | `MINI000000000000000000000000OA005` | `/pages/oa/mine/index` | `mini:oa:mine` | 50 |
| 抄送我的 | `MINI000000000000000000000000OA006` | `/pages/oa/cc/index` | `mini:oa:cc` | 60 |
约定:`menu_type='C'`、`is_frame=1`、`visible/status='1'`、`tenant_no='zkxw'`、`NOT EXISTS` 防重复;角色在 Web「角色管理」勾选后,首页 `MenuGrid` 经 `fetchMyMobileMenus()` 展示。
发起审批既有:`MINI000000000000000000000000OA001` / `mini:oa:start` / order 10,本脚本不改动。
### 4.1 宫格图标资源
PNG 放在 `mobile-h5/static/menu-icons/`(颜色已烘焙,宫格 `iconBg: transparent`)。后端 `mini_menu._mobile_menu_icon` 按 path 返回 icon 名,与 `van-icon` 的 `PNG_ICONS` 对齐:
| 菜单 | 文件 | icon 名 |
|------|------|---------|
| 草稿箱 | `oa-drafts.png` | `oa-drafts` |
| 待办中心 | `oa-pending.png` | `oa-pending` |
| 已办中心 | `oa-done.png` | `oa-done` |
| 我发起的 | `oa-mine.png` | `oa-mine` |
| 抄送我的 | `oa-cc.png` | `oa-cc` |
---
## 5. 小程序列表页
### 5.1 路由与 web-view
- 五页:`pages/oa/{drafts,pending,done,mine,cc}/index`
- 通用 web-view:扩展现有 `pages/oa/start/webview` 或抽 `pages/oa/webview/index`,支持 start 与 detail 两类 URL。
- `utils/oa-h5.js` 增补 `buildOaDetailUrl({ instanceId, taskId, mode })`;标题仍用 storage 暂存,避免中文进 URL。
### 5.2 列表行为摘要
| 页 | 筛选 | 行/工具栏 | 打开 H5 |
|----|------|-----------|---------|
| 草稿 | 流程名 | 编辑、提交、删;批量删 | `/oa-start?draftId=` |
| 待办 | 流程名、申请人 | 进入办理 | `/oa-detail?mode=approve&instanceId=&taskId=` |
| 已办 | 流程名、申请人 | 查看 | `mode=view` |
| 我发起 | 流程名 | 查看、打印、复制重开(终态) | view;复制 → start(预填策略见 §6) |
| 抄送 | 流程名、申请人、阅读状态 | 查看、已读、批量已读、打印 | view;打开时/前 mark-read |
共用:卡片列表、下拉刷新、分页;扩展 `mobile-h5/utils/oa-api.js`。
---
## 6. H5 `/oa-detail`
### 6.1 Query
| 参数 | 说明 |
|------|------|
| `instanceId` | 必填;`GET /instances/{id}` |
| `taskId` | 待办办理时定位任务 |
| `mode` | `view` \| `approve` |
| `token` | 与 `/oa-start` 相同,启动时捕获 |
### 6.2 结构
1. 顶:标题 + 状态 Tag
2. 中:form-create **disabled** + 审批流进度(移动端已有/可对齐的 Flow 组件)
3. 底:操作条(全屏 web-view,操作必须在 H5)
### 6.3 操作可见性(对齐 Web `instances/detail.vue`)
| 操作 | 条件 |
|------|------|
| 同意 / 拒绝 | `mode=approve` 且存在当前用户可办 task |
| 打回 | 同上且 `allowReturn !== false` |
| 加签 | 同上且 `allowAddSign !== false`;选人 |
| 转交 | 同上;选人排除本人与本节点已有待办;可选说明 |
| 撤销 | 当前用户为发起人且实例进行中 |
| 复制并重新发起 | 发起人且终态(returned / cancelled / rejected);跳转发起填写并预填 |
| 打印 | 我发起的、抄送场景需要;移植 Web `exportOaApprovalPdf` 思路 |
成功后:`postMessage({ source: 'zkxw-oa-h5', action: 'done'|'back' })` → 小程序返回并刷新列表/角标。
### 6.4 复制并重新发起
优先与 Web 一致:携带原实例表单数据进入新发起(`/oa-start` + `appId` + 预填来源)。具体 query/storage 约定在实现计划中与现有 `OaStart.vue` 对齐,避免破坏已有草稿链路。
### 6.5 打印风险
小程序 web-view 内 PDF 能力受限。S6 单独验收;若真机受限,允许「H5 生成文件 + 明确引导」兜底,**不阻塞** S0–S5。
---
## 7. 分步交付(S0–S7)
| 步骤 | 内容 | 验收 |
|------|------|------|
| S0 | 菜单 SQL;`pages.json`;通用 web-view / `buildOaDetailUrl` | 授权后菜单可见;未授权不可见 |
| S1 | 草稿箱全量已确认能力 + 草稿角标 | 编辑/提交/删/批量删可用 |
| S2 | `/oa-detail` 只读;已办 / 我发起 / 抄送进 view | 同实例只读正确;抄送打开已读 |
| S3 | 待办列表 + approve 五动作 + 待办角标 | 办理后待办消失;进度正确 |
| S4 | 我发起的撤销、复制重开(列表+详情) | 终态可复制;进行中可撤销 |
| S5 | 抄送阅读筛选、未读样式、单条/批量已读、未读角标 | 与 Web 语义一致 |
| S6 | 打印(我发起的 / 抄送) | 真机可打开或有明确失败提示 |
| S7 | 角标汇总与返回刷新 | 办理/已读后角标更新 |
---
## 8. 测试要点(摘要)
- 菜单:执行 SQL 后角色勾选/取消,首页宫格增减正确。
- 草稿:暂存→草稿箱可见→编辑提交→出现在「我发起的」、草稿消失。
- 待办:同意/拒绝/打回/加签/转交各至少一条主路径;转交人选人约束正确。
- 已办/抄送:仅只读,无办理按钮。
- 我发起:撤销后状态与 Web 一致;复制重开可提交新实例。
- 抄送:未读角标、批量已读、打开详情自动已读。
- 色值与 web-view 关闭桥接回归(发起审批不被破坏)。
---
## 9. 文档与实现衔接
- 本文为需求/架构权威;实现计划:`docs/superpowers/plans/2026-08-12-mobile-oa-task-centers.md`(Task 1–13,对应 S0–S7)。
- 变更若调整范围,先改本文再改计划与代码。