Ver Fonte

docs: add mobile OA detail footer fold and default comment design

Capture scheme A (⋯ + return + approve), ActionSheet more actions, and default remark copy for pending approval detail.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu há 2 meses atrás
pai
commit
1c844398a9

+ 113 - 0
docs/superpowers/specs/2026-08-12-mobile-oa-detail-actions-fold-design.md

@@ -0,0 +1,113 @@
+# 移动端 OA 待办详情 · 底栏操作折叠与默认备注
+
+| 项 | 内容 |
+|----|------|
+| 日期 | 2026-08-12 |
+| 范围 | `mobile-oa-h5` 审批详情底栏(待办中心 webview);意见 / 加签 / 转交默认备注 |
+| 视觉 Demo | `mobile-ui/zkxw-mobile-oa-detail-actions-demo.html`(方案 A) |
+| 关联 | `2026-08-12-mobile-oa-task-centers-design.md`(详情办理能力) |
+
+## 1. 背景
+
+待办中心打开审批详情(`mode=approve`)时,底栏一次性展示关闭、打印、拒绝、打回、加签、转交、同意,按钮过多、字号被迫缩小,主路径不清晰。
+
+产品要求:
+
+1. 默认只展示 **同意**、**打回**;其余收入 **⋯ 更多**。
+2. 点击同意等需要备注的操作时弹出意见输入,并 **预填默认备注**(可改)。
+
+## 2. 目标与非目标
+
+### 目标
+
+- 审批态底栏:`⋯` + 打回(若允许)+ 同意。
+- 更多:底部 ActionSheet,含拒绝 / 加签(若允许) / 转交 / 打印 / 关闭。
+- 同意、打回、拒绝、加签说明、转交说明均预填默认备注。
+
+### 非目标
+
+- 不改 `mobile-h5` 列表页。
+- 不改后端 API / 意见必填引擎规则(打回仍前端必填校验)。
+- 不改非审批态底栏(只读关闭/打印、撤销、复制并重新发起)。
+- 不做 Web 管理端详情底栏同步(除非另开变更)。
+
+## 3. 方案选择
+
+采用 Demo **方案 A**:
+
+| 方案 | 说明 | 结论 |
+|------|------|------|
+| A | `⋯` + 打回 + 同意;更多 = ActionSheet | **采用** |
+| B | 关闭常显 + `⋯` + 打回 + 同意 | 未采用 |
+| C | 文字「更多」+ 气泡菜单 | 未采用 |
+
+## 4. UI 与交互
+
+### 4.1 审批态底栏(`canApprove === true`)
+
+从左到右:
+
+1. **⋯**(更多):打开 ActionSheet。
+2. **打回**:仅当 `detail.allowReturn !== false`;否则不渲染,底栏为 `⋯` + 同意。
+3. **同意**:主色实心按钮;打开意见弹窗。
+
+样式对齐现有 `oa-detail__btn*`:`⋯` 为固定宽度描边按钮(约 44px),打回 ghost,同意 primary。
+
+### 4.2 更多 ActionSheet
+
+顺序固定:
+
+1. 拒绝  
+2. 加签(仅 `detail.allowAddSign !== false`)  
+3. 转交  
+4. 打印  
+5. 关闭  
+
+- 使用 Vant `van-action-sheet`(或等价底部 Popup 列表)。
+- 点击某项后关闭 Sheet,再执行对应逻辑(意见弹窗 / 加签选人 / 转交选人 / `onPrint` / `onClose`)。
+- `acting` 或 `printing` 时底栏与更多项禁用。
+
+### 4.3 非审批态
+
+保持现状:关闭、打印、(条件)撤销、(条件)复制并重新发起。**不**引入「更多」。
+
+## 5. 默认备注
+
+打开对应输入时预填;用户可修改或清空;提交前 `trim`。
+
+| 操作 | 默认备注 | 必填 |
+|------|----------|------|
+| 同意 | `同意` | 否 |
+| 打回 | `请修改后重新提交` | 是(清空则 Toast「打回意见必填」,不关闭弹窗) |
+| 拒绝 | `拒绝` | 否 |
+| 加签说明 | `加签` | 否 |
+| 转交说明 | `转交` | 否 |
+
+实现要点:
+
+- `openComment(kind)`:按 kind 写入 `commentText` 默认值(不再置 `''`)。
+- `openAddSign` / `openTransfer`:说明字段预填「加签」「转交」(不再置 `''`)。
+- 提交仍走现有 `fetchApproveOaTask` / `fetchRejectOaTask` / `fetchReturnOaTask` / `fetchAddSignOaTask` / `fetchTransferOaTask`;成功后 `closeToMini` 行为不变。
+
+## 6. 实现落点
+
+| 文件 | 变更 |
+|------|------|
+| `mobile-oa-h5/src/views/OaDetail.vue` | 审批态底栏折叠;ActionSheet;默认备注 |
+| `mobile-ui/zkxw-mobile-oa-detail-actions-demo.html` | 已存在,作视觉权威 |
+
+无需新 API、无需改 `mobile-h5`。
+
+## 7. 验收
+
+1. 待办详情审批态默认只见 `⋯`、打回、同意(无打回权限时仅 `⋯`、同意)。  
+2. 更多含拒绝、加签(若允许)、转交、打印、关闭,顺序正确。  
+3. 同意 / 打回 / 拒绝弹窗预填默认备注;打回清空后无法提交。  
+4. 加签 / 转交面板说明预填「加签」「转交」。  
+5. 操作成功后仍通知宿主关闭并刷新列表(与现网一致)。
+
+## 8. 决策记录
+
+| 日期 | 决策 |
+|------|------|
+| 2026-08-12 | 采用方案 A;默认备注文案按 §5;关闭/打印进更多。 |

+ 964 - 0
mobile-ui/zkxw-mobile-oa-detail-actions-demo.html

@@ -0,0 +1,964 @@
+<!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;
+      --danger: #ef4444;
+      --warn: #d97706;
+    }
+    * { 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: 760px;
+      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 {
+      display: inline-block;
+      margin: 0 0 0 38px;
+      padding: 2px 8px;
+      border-radius: 999px;
+      font-size: 11px;
+      font-weight: 650;
+      background: #f1f5f9;
+      color: #475569;
+    }
+    .scheme.is-on .tag {
+      background: var(--brand-soft);
+      color: var(--brand-deep);
+    }
+    .tag.rec {
+      background: #ecfdf5;
+      color: #047857;
+    }
+
+    .defaults {
+      margin-top: 18px;
+      padding: 14px 16px;
+      border-radius: 16px;
+      background: #fff;
+      border: 1px solid var(--line);
+    }
+    .defaults h3 {
+      margin: 0 0 10px;
+      font-size: 14px;
+      font-weight: 680;
+    }
+    .defaults table {
+      width: 100%;
+      border-collapse: collapse;
+      font-size: 12.5px;
+    }
+    .defaults th,
+    .defaults td {
+      text-align: left;
+      padding: 8px 6px;
+      border-bottom: 1px solid #f1f5f9;
+      vertical-align: top;
+    }
+    .defaults th {
+      color: var(--muted);
+      font-weight: 600;
+      font-size: 11px;
+    }
+    .defaults code {
+      display: inline-block;
+      padding: 2px 6px;
+      border-radius: 6px;
+      background: #f8fafc;
+      border: 1px solid #e2e8f0;
+      font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+      font-size: 12px;
+      color: #0f172a;
+    }
+    .defaults .hint {
+      margin: 10px 0 0;
+      font-size: 12px;
+      color: var(--muted);
+    }
+
+    /* —— Phone preview —— */
+    .preview-col { position: sticky; top: 16px; }
+    .phone {
+      width: 100%;
+      max-width: 360px;
+      margin: 0 auto;
+      border-radius: 28px;
+      border: 1px solid #cbd5e1;
+      background: #0f172a;
+      padding: 10px;
+      box-shadow: 0 24px 60px rgba(15, 23, 42, 0.18);
+    }
+    .phone-inner {
+      position: relative;
+      height: 640px;
+      border-radius: 20px;
+      overflow: hidden;
+      background: #fff;
+      display: flex;
+      flex-direction: column;
+    }
+    .status-bar {
+      display: flex;
+      justify-content: space-between;
+      padding: 8px 16px 4px;
+      font-size: 11px;
+      font-weight: 650;
+      color: #334155;
+      background: #fff;
+    }
+    .nav {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      padding: 8px 12px 10px;
+      border-bottom: 1px solid #f1f5f9;
+      background: #fff;
+    }
+    .nav .back {
+      width: 28px; height: 28px;
+      border-radius: 8px;
+      display: grid; place-items: center;
+      background: #f1f5f9;
+      color: #475569;
+      font-size: 14px;
+    }
+    .nav h4 {
+      margin: 0;
+      font-size: 15px;
+      font-weight: 700;
+    }
+    .scroll {
+      flex: 1;
+      overflow: auto;
+      padding: 12px 12px 108px;
+      background: #f8fafc;
+      -webkit-overflow-scrolling: touch;
+    }
+    .card {
+      background: #fff;
+      border-radius: 14px;
+      border: 1px solid #eef2f6;
+      padding: 12px 14px;
+      margin-bottom: 10px;
+    }
+    .card .title-row {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      margin-bottom: 10px;
+    }
+    .card h5 {
+      margin: 0;
+      font-size: 15px;
+      font-weight: 700;
+    }
+    .pill {
+      padding: 2px 8px;
+      border-radius: 6px;
+      font-size: 11px;
+      font-weight: 650;
+      color: var(--warn);
+      background: #fffbeb;
+    }
+    .field {
+      display: flex;
+      justify-content: space-between;
+      gap: 12px;
+      padding: 8px 0;
+      border-top: 1px solid #f1f5f9;
+      font-size: 13px;
+    }
+    .field:first-of-type { border-top: none; }
+    .field .k { color: var(--muted); }
+    .field .v { color: #0f172a; font-weight: 560; text-align: right; }
+    .timeline {
+      font-size: 12.5px;
+      color: #334155;
+    }
+    .timeline .item {
+      display: grid;
+      grid-template-columns: 14px 1fr;
+      gap: 8px;
+      margin-bottom: 10px;
+    }
+    .dot {
+      width: 10px; height: 10px;
+      margin-top: 4px;
+      border-radius: 50%;
+      background: var(--brand);
+      box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.18);
+    }
+    .dot.mute { background: #94a3b8; box-shadow: none; }
+    .timeline .t { font-weight: 650; }
+    .timeline .m { color: var(--muted); margin-top: 2px; }
+
+    .ft {
+      position: absolute;
+      left: 0; right: 0; bottom: 0;
+      z-index: 5;
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px));
+      background: #fff;
+      border-top: 1px solid #eef2f6;
+      box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.04);
+    }
+    .btn {
+      appearance: none;
+      border: none;
+      height: 40px;
+      border-radius: 12px;
+      font: inherit;
+      font-size: 14px;
+      font-weight: 650;
+      cursor: pointer;
+      padding: 0 12px;
+      transition: opacity .12s, transform .12s;
+    }
+    .btn:active { opacity: 0.88; transform: scale(0.98); }
+    .btn-close {
+      flex: 0 0 auto;
+      min-width: 56px;
+      background: #f1f5f9;
+      color: #334155;
+    }
+    .btn-ghost {
+      flex: 1 1 0;
+      min-width: 0;
+      border: 1px solid #e2e8f0;
+      background: #fff;
+      color: #334155;
+    }
+    .btn-primary {
+      flex: 1.2 1 0;
+      min-width: 0;
+      background: var(--brand);
+      color: #fff;
+      font-weight: 750;
+    }
+    .btn-more {
+      flex: 0 0 auto;
+      width: 44px;
+      padding: 0;
+      border: 1px solid #e2e8f0;
+      background: #fff;
+      color: #334155;
+      font-size: 18px;
+      letter-spacing: 1px;
+      line-height: 1;
+    }
+    .btn-more.wide {
+      width: auto;
+      min-width: 72px;
+      font-size: 14px;
+      letter-spacing: 0;
+    }
+
+    /* before (crowded) */
+    .ft.before .btn {
+      flex: 1 1 auto;
+      min-width: 0;
+      padding: 0 6px;
+      font-size: 12px;
+    }
+    .ft.before .btn-primary { flex: 1.1 1 auto; }
+
+    /* overlay */
+    .mask {
+      position: absolute;
+      inset: 0;
+      background: rgba(15, 23, 42, 0.42);
+      z-index: 10;
+      opacity: 0;
+      pointer-events: none;
+      transition: opacity .18s;
+    }
+    .mask.is-on {
+      opacity: 1;
+      pointer-events: auto;
+    }
+
+    .sheet {
+      position: absolute;
+      left: 0; right: 0; bottom: 0;
+      z-index: 12;
+      background: #fff;
+      border-radius: 18px 18px 0 0;
+      transform: translateY(110%);
+      transition: transform .22s ease;
+      padding: 8px 0 calc(12px + env(safe-area-inset-bottom, 0px));
+      box-shadow: 0 -8px 28px rgba(15, 23, 42, 0.12);
+    }
+    .sheet.is-on { transform: translateY(0); }
+    .sheet .handle {
+      width: 36px; height: 4px;
+      border-radius: 999px;
+      background: #e2e8f0;
+      margin: 4px auto 10px;
+    }
+    .sheet .row {
+      display: flex;
+      align-items: center;
+      gap: 12px;
+      width: 100%;
+      padding: 14px 18px;
+      border: none;
+      background: transparent;
+      font: inherit;
+      font-size: 15px;
+      font-weight: 600;
+      color: #0f172a;
+      cursor: pointer;
+      text-align: left;
+    }
+    .sheet .row:active { background: #f8fafc; }
+    .sheet .row .ico {
+      width: 32px; height: 32px;
+      border-radius: 10px;
+      display: grid; place-items: center;
+      background: #f1f5f9;
+      color: #475569;
+      font-size: 14px;
+    }
+    .sheet .row.danger { color: var(--danger); }
+    .sheet .row.danger .ico { background: #fef2f2; color: var(--danger); }
+    .sheet .cancel {
+      margin: 8px 12px 0;
+      height: 44px;
+      border-radius: 12px;
+      border: none;
+      background: #f1f5f9;
+      font: inherit;
+      font-size: 15px;
+      font-weight: 650;
+      color: #334155;
+      cursor: pointer;
+      width: calc(100% - 24px);
+    }
+
+    .popover {
+      position: absolute;
+      right: 12px;
+      bottom: 62px;
+      z-index: 12;
+      min-width: 148px;
+      background: #fff;
+      border: 1px solid #e2e8f0;
+      border-radius: 12px;
+      box-shadow: 0 12px 32px rgba(15, 23, 42, 0.14);
+      padding: 6px;
+      opacity: 0;
+      pointer-events: none;
+      transform: translateY(6px);
+      transition: opacity .15s, transform .15s;
+    }
+    .popover.is-on {
+      opacity: 1;
+      pointer-events: auto;
+      transform: translateY(0);
+    }
+    .popover button {
+      display: block;
+      width: 100%;
+      text-align: left;
+      border: none;
+      background: transparent;
+      padding: 10px 12px;
+      border-radius: 8px;
+      font: inherit;
+      font-size: 13.5px;
+      font-weight: 600;
+      color: #0f172a;
+      cursor: pointer;
+    }
+    .popover button:active { background: #f8fafc; }
+    .popover button.danger { color: var(--danger); }
+
+    .dialog {
+      position: absolute;
+      left: 50%;
+      top: 42%;
+      z-index: 14;
+      width: calc(100% - 48px);
+      max-width: 300px;
+      transform: translate(-50%, -50%) scale(0.96);
+      background: #fff;
+      border-radius: 16px;
+      padding: 16px 16px 12px;
+      box-shadow: 0 20px 48px rgba(15, 23, 42, 0.2);
+      opacity: 0;
+      pointer-events: none;
+      transition: opacity .15s, transform .15s;
+    }
+    .dialog.is-on {
+      opacity: 1;
+      pointer-events: auto;
+      transform: translate(-50%, -50%) scale(1);
+    }
+    .dialog h3 {
+      margin: 0 0 10px;
+      text-align: center;
+      font-size: 16px;
+      font-weight: 720;
+    }
+    .dialog textarea {
+      width: 100%;
+      min-height: 84px;
+      resize: none;
+      border: 1px solid #e2e8f0;
+      border-radius: 10px;
+      padding: 10px 12px;
+      font: inherit;
+      font-size: 14px;
+      color: #0f172a;
+      background: #f8fafc;
+      outline: none;
+    }
+    .dialog textarea:focus {
+      border-color: #86efac;
+      box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.15);
+      background: #fff;
+    }
+    .dialog .tip {
+      margin: 8px 0 0;
+      font-size: 11.5px;
+      color: var(--muted);
+    }
+    .dialog .acts {
+      display: grid;
+      grid-template-columns: 1fr 1fr;
+      margin-top: 12px;
+      border-top: 1px solid #f1f5f9;
+    }
+    .dialog .acts button {
+      border: none;
+      background: transparent;
+      height: 42px;
+      font: inherit;
+      font-size: 15px;
+      font-weight: 650;
+      cursor: pointer;
+    }
+    .dialog .acts .cancel { color: #64748b; border-right: 1px solid #f1f5f9; }
+    .dialog .acts .ok { color: var(--brand-deep); font-weight: 750; }
+
+    .toast {
+      position: absolute;
+      left: 50%;
+      bottom: 88px;
+      z-index: 20;
+      transform: translateX(-50%) translateY(8px);
+      background: rgba(15, 23, 42, 0.88);
+      color: #fff;
+      padding: 8px 14px;
+      border-radius: 999px;
+      font-size: 12.5px;
+      font-weight: 600;
+      opacity: 0;
+      pointer-events: none;
+      transition: opacity .18s, transform .18s;
+      white-space: nowrap;
+      max-width: 90%;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+    .toast.is-on {
+      opacity: 1;
+      transform: translateX(-50%) translateY(0);
+    }
+
+    .actions {
+      margin-top: 14px;
+      display: flex;
+      flex-wrap: wrap;
+      gap: 8px;
+      justify-content: center;
+    }
+    .chip {
+      appearance: none;
+      border: 1px solid var(--line);
+      background: #fff;
+      border-radius: 999px;
+      padding: 8px 12px;
+      font: inherit;
+      font-size: 12px;
+      font-weight: 650;
+      color: #475569;
+      cursor: pointer;
+    }
+    .chip:hover { border-color: #94a3b8; }
+  </style>
+</head>
+<body>
+  <div class="wrap">
+    <h1>待办详情 · 底栏操作折叠 & 默认备注</h1>
+    <p class="sub">
+      对齐现网 <code>mobile-oa-h5</code> 审批详情底栏:默认只露出「同意 / 打回」,其余收入「更多」;
+      需要意见的操作弹窗预填默认备注,用户可改。
+    </p>
+    <div class="note">
+      <strong>推荐方案 A</strong>:底栏「⋯ / 打回 / 同意」;更多用底部 ActionSheet。
+      右侧手机可点击交互。默认备注见下方表格,可在弹窗内修改。
+    </div>
+
+    <div class="layout">
+      <div>
+        <div class="schemes" id="schemes">
+          <button type="button" class="scheme is-on" data-scheme="A">
+            <div class="scheme-top">
+              <span class="letter">A</span>
+              <h2>⋯ + 打回 + 同意(推荐)</h2>
+            </div>
+            <p>主路径最清晰:主操作同意、次操作打回常显;拒绝 / 加签 / 转交 / 打印 / 关闭进更多(底部 ActionSheet)。</p>
+            <span class="tag rec">推荐 · 对齐「默认只展示同意、打回」</span>
+          </button>
+          <button type="button" class="scheme" data-scheme="B">
+            <div class="scheme-top">
+              <span class="letter">B</span>
+              <h2>关闭常显 + ⋯ + 打回 + 同意</h2>
+            </div>
+            <p>保留「关闭」快捷退出;更多里放拒绝 / 加签 / 转交 / 打印。比 A 多一颗常驻按钮。</p>
+            <span class="tag">可选 · 退出路径更短</span>
+          </button>
+          <button type="button" class="scheme" data-scheme="C">
+            <div class="scheme-top">
+              <span class="letter">C</span>
+              <h2>⋯文字「更多」+ 气泡菜单</h2>
+            </div>
+            <p>底栏文案「更多」替代三点;点击后右下角 Popover 列出次要操作,不遮挡整屏。</p>
+            <span class="tag">可选 · 轻量气泡</span>
+          </button>
+          <button type="button" class="scheme" data-scheme="before">
+            <div class="scheme-top">
+              <span class="letter">旧</span>
+              <h2>现状:全部平铺</h2>
+            </div>
+            <p>关闭 / 打印 / 拒绝 / 打回 / 加签 / 转交 / 同意 一排挤开,字号被迫缩小。</p>
+            <span class="tag">对比</span>
+          </button>
+        </div>
+
+        <div class="defaults">
+          <h3>默认备注文案(弹窗预填,可改)</h3>
+          <table>
+            <thead>
+              <tr><th>操作</th><th>默认备注</th><th>说明</th></tr>
+            </thead>
+            <tbody>
+              <tr><td>同意</td><td><code>同意</code></td><td>意见可选,预填后可直接确定</td></tr>
+              <tr><td>打回</td><td><code>请修改后重新提交</code></td><td>意见必填;预填满足必填,可改</td></tr>
+              <tr><td>拒绝</td><td><code>拒绝</code></td><td>意见可选</td></tr>
+              <tr><td>加签</td><td><code>加签</code></td><td>说明可选;选人后仍可改备注</td></tr>
+              <tr><td>转交</td><td><code>转交</code></td><td>说明可选;选人后仍可改备注</td></tr>
+            </tbody>
+          </table>
+          <p class="hint">点右侧「同意 / 打回 / 拒绝」可看预填弹窗;加签 / 转交在本 Demo 仅 Toast 示意(真实页仍为选人面板 + 预填说明)。</p>
+        </div>
+      </div>
+
+      <div class="preview-col">
+        <div class="phone">
+          <div class="phone-inner" id="phone">
+            <div class="status-bar"><span>9:41</span><span>●●●</span></div>
+            <div class="nav">
+              <span class="back">‹</span>
+              <h4>审批详情</h4>
+            </div>
+            <div class="scroll">
+              <div class="card">
+                <div class="title-row">
+                  <h5>请假申请 · 张三</h5>
+                  <span class="pill">处理中</span>
+                </div>
+                <div class="field"><span class="k">请假类型</span><span class="v">年假</span></div>
+                <div class="field"><span class="k">开始时间</span><span class="v">2026-08-18 09:00</span></div>
+                <div class="field"><span class="k">结束时间</span><span class="v">2026-08-19 18:00</span></div>
+                <div class="field"><span class="k">时长</span><span class="v">2 天</span></div>
+              </div>
+              <div class="card timeline">
+                <div class="item"><span class="dot"></span><div><div class="t">发起人 · 张三</div><div class="m">已提交 · 08-12 10:02</div></div></div>
+                <div class="item"><span class="dot mute"></span><div><div class="t">审批人 · 我</div><div class="m">待处理</div></div></div>
+              </div>
+            </div>
+
+            <footer class="ft" id="ft"></footer>
+
+            <div class="mask" id="mask"></div>
+
+            <div class="sheet" id="sheet">
+              <div class="handle"></div>
+              <div id="sheetRows"></div>
+              <button type="button" class="cancel" id="sheetCancel">取消</button>
+            </div>
+
+            <div class="popover" id="popover"></div>
+
+            <div class="dialog" id="dialog">
+              <h3 id="dialogTitle">同意</h3>
+              <textarea id="dialogText" maxlength="500"></textarea>
+              <p class="tip" id="dialogTip">可修改默认备注后确定</p>
+              <div class="acts">
+                <button type="button" class="cancel" id="dialogCancel">取消</button>
+                <button type="button" class="ok" id="dialogOk">确定</button>
+              </div>
+            </div>
+
+            <div class="toast" id="toast"></div>
+          </div>
+        </div>
+
+        <div class="actions">
+          <button type="button" class="chip" data-demo="approve">演示 · 同意弹窗</button>
+          <button type="button" class="chip" data-demo="return">演示 · 打回弹窗</button>
+          <button type="button" class="chip" data-demo="reject">演示 · 拒绝弹窗</button>
+          <button type="button" class="chip" data-demo="more">演示 · 打开更多</button>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <script>
+    const DEFAULTS = {
+      approve: { title: '同意', text: '同意', tip: '意见可选,已预填默认备注', required: false },
+      return: { title: '打回', text: '请修改后重新提交', tip: '意见必填;已预填,可直接确定或修改', required: true },
+      reject: { title: '拒绝', text: '拒绝', tip: '意见可选,已预填默认备注', required: false },
+      addSign: { title: '加签', text: '加签', tip: '说明可选', required: false },
+      transfer: { title: '转交', text: '转交', tip: '说明可选', required: false }
+    }
+
+    const MORE_ITEMS = [
+      { key: 'reject', label: '拒绝', danger: true, ico: '✕' },
+      { key: 'addSign', label: '加签', ico: '+' },
+      { key: 'transfer', label: '转交', ico: '⇄' },
+      { key: 'print', label: '打印', ico: '🖨' },
+      { key: 'close', label: '关闭', ico: '◁' }
+    ]
+
+    let scheme = 'A'
+    let commentKind = 'approve'
+    let toastTimer = null
+
+    const ft = document.getElementById('ft')
+    const mask = document.getElementById('mask')
+    const sheet = document.getElementById('sheet')
+    const sheetRows = document.getElementById('sheetRows')
+    const popover = document.getElementById('popover')
+    const dialog = document.getElementById('dialog')
+    const dialogTitle = document.getElementById('dialogTitle')
+    const dialogText = document.getElementById('dialogText')
+    const dialogTip = document.getElementById('dialogTip')
+    const toastEl = document.getElementById('toast')
+
+    function toast(msg) {
+      toastEl.textContent = msg
+      toastEl.classList.add('is-on')
+      clearTimeout(toastTimer)
+      toastTimer = setTimeout(() => toastEl.classList.remove('is-on'), 1600)
+    }
+
+    function closeOverlays() {
+      mask.classList.remove('is-on')
+      sheet.classList.remove('is-on')
+      popover.classList.remove('is-on')
+      dialog.classList.remove('is-on')
+    }
+
+    function openComment(kind) {
+      const cfg = DEFAULTS[kind]
+      if (!cfg) return
+      commentKind = kind
+      closeOverlays()
+      dialogTitle.textContent = cfg.title
+      dialogText.value = cfg.text
+      dialogTip.textContent = cfg.tip
+      mask.classList.add('is-on')
+      dialog.classList.add('is-on')
+      setTimeout(() => {
+        dialogText.focus()
+        dialogText.select()
+      }, 50)
+    }
+
+    function onMoreAction(key) {
+      closeOverlays()
+      if (key === 'reject') {
+        openComment('reject')
+        return
+      }
+      if (key === 'addSign') {
+        toast('加签:选人面板 + 默认备注「加签」')
+        return
+      }
+      if (key === 'transfer') {
+        toast('转交:选人面板 + 默认备注「转交」')
+        return
+      }
+      if (key === 'print') {
+        toast('打印')
+        return
+      }
+      if (key === 'close') {
+        toast('关闭详情')
+      }
+    }
+
+    function renderMoreContent(includeClose) {
+      const items = MORE_ITEMS.filter((x) => includeClose || x.key !== 'close')
+      sheetRows.innerHTML = items.map((it) => `
+        <button type="button" class="row ${it.danger ? 'danger' : ''}" data-more="${it.key}">
+          <span class="ico">${it.ico}</span>
+          <span>${it.label}</span>
+        </button>
+      `).join('')
+      popover.innerHTML = items.map((it) => `
+        <button type="button" class="${it.danger ? 'danger' : ''}" data-more="${it.key}">${it.label}</button>
+      `).join('')
+    }
+
+    function openMore() {
+      dialog.classList.remove('is-on')
+      if (scheme === 'C') {
+        mask.classList.add('is-on')
+        popover.classList.add('is-on')
+        sheet.classList.remove('is-on')
+        return
+      }
+      mask.classList.add('is-on')
+      sheet.classList.add('is-on')
+      popover.classList.remove('is-on')
+    }
+
+    function renderFooter() {
+      closeOverlays()
+      ft.className = 'ft'
+      if (scheme === 'before') {
+        ft.classList.add('before')
+        ft.innerHTML = `
+          <button type="button" class="btn btn-close" data-act="close">关闭</button>
+          <button type="button" class="btn btn-ghost" data-act="print">打印</button>
+          <button type="button" class="btn btn-ghost" data-act="reject">拒绝</button>
+          <button type="button" class="btn btn-ghost" data-act="return">打回</button>
+          <button type="button" class="btn btn-ghost" data-act="addSign">加签</button>
+          <button type="button" class="btn btn-ghost" data-act="transfer">转交</button>
+          <button type="button" class="btn btn-primary" data-act="approve">同意</button>
+        `
+        renderMoreContent(true)
+        return
+      }
+      if (scheme === 'A') {
+        ft.innerHTML = `
+          <button type="button" class="btn btn-more" data-act="more" aria-label="更多">⋯</button>
+          <button type="button" class="btn btn-ghost" data-act="return">打回</button>
+          <button type="button" class="btn btn-primary" data-act="approve">同意</button>
+        `
+        renderMoreContent(true)
+        return
+      }
+      if (scheme === 'B') {
+        ft.innerHTML = `
+          <button type="button" class="btn btn-close" data-act="close">关闭</button>
+          <button type="button" class="btn btn-more" data-act="more" aria-label="更多">⋯</button>
+          <button type="button" class="btn btn-ghost" data-act="return">打回</button>
+          <button type="button" class="btn btn-primary" data-act="approve">同意</button>
+        `
+        renderMoreContent(false)
+        return
+      }
+      if (scheme === 'C') {
+        ft.innerHTML = `
+          <button type="button" class="btn btn-more wide" data-act="more">更多</button>
+          <button type="button" class="btn btn-ghost" data-act="return">打回</button>
+          <button type="button" class="btn btn-primary" data-act="approve">同意</button>
+        `
+        renderMoreContent(true)
+      }
+    }
+
+    document.getElementById('schemes').addEventListener('click', (e) => {
+      const btn = e.target.closest('.scheme')
+      if (!btn) return
+      document.querySelectorAll('.scheme').forEach((el) => el.classList.toggle('is-on', el === btn))
+      scheme = btn.dataset.scheme
+      renderFooter()
+    })
+
+    ft.addEventListener('click', (e) => {
+      const btn = e.target.closest('[data-act]')
+      if (!btn) return
+      const act = btn.dataset.act
+      if (act === 'more') {
+        openMore()
+        return
+      }
+      if (act === 'approve' || act === 'return' || act === 'reject') {
+        openComment(act)
+        return
+      }
+      if (act === 'addSign') {
+        toast('加签:选人面板 + 默认备注「加签」')
+        return
+      }
+      if (act === 'transfer') {
+        toast('转交:选人面板 + 默认备注「转交」')
+        return
+      }
+      if (act === 'print') {
+        toast('打印')
+        return
+      }
+      if (act === 'close') {
+        toast('关闭详情')
+      }
+    })
+
+    sheetRows.addEventListener('click', (e) => {
+      const btn = e.target.closest('[data-more]')
+      if (!btn) return
+      onMoreAction(btn.dataset.more)
+    })
+    popover.addEventListener('click', (e) => {
+      const btn = e.target.closest('[data-more]')
+      if (!btn) return
+      onMoreAction(btn.dataset.more)
+    })
+
+    mask.addEventListener('click', closeOverlays)
+    document.getElementById('sheetCancel').addEventListener('click', closeOverlays)
+    document.getElementById('dialogCancel').addEventListener('click', closeOverlays)
+    document.getElementById('dialogOk').addEventListener('click', () => {
+      const cfg = DEFAULTS[commentKind]
+      const text = String(dialogText.value || '').trim()
+      if (cfg?.required && !text) {
+        toast('请填写意见')
+        return
+      }
+      closeOverlays()
+      toast(`${cfg.title} · ${text || '(无备注)'}`)
+    })
+
+    document.querySelector('.actions').addEventListener('click', (e) => {
+      const chip = e.target.closest('[data-demo]')
+      if (!chip) return
+      const d = chip.dataset.demo
+      if (d === 'more') {
+        if (scheme === 'before') {
+          toast('请先切到方案 A/B/C')
+          return
+        }
+        openMore()
+        return
+      }
+      openComment(d)
+    })
+
+    renderFooter()
+  </script>
+</body>
+</html>