提交
This commit is contained in:
1 parent
d310da1523
commit
ec628e1b8d
79 files changed
+6749
-799
No files matched your search
@@ -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)。
|
||||
- 变更若调整范围,先改本文再改计划与代码。
|
||||
Reference in new issue
Block a user