Parcourir la source

docs: add mp CRM product confirm dialog design (delete B, other A).

Co-authored-by: Cursor <cursoragent@cursor.com>
wuzilong il y a 3 semaines
Parent
commit
3138666ae2

+ 443 - 0
bak/mobile-ui/shanxia-mp-crm-product-delete-confirm-demo.html

@@ -0,0 +1,443 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+  <title>山峡化工 · 产品管理 · 删除确认弹窗 Demo</title>
+  <style>
+    :root {
+      --brand: #0a9f6d;
+      --brand-dark: #087f57;
+      --brand-soft: #e9f9f2;
+      --bg: #f5f8f7;
+      --surface: #fff;
+      --text: #1f2a26;
+      --muted: #6b7b74;
+      --border: #dcebe4;
+      --danger: #dc2626;
+      --danger-soft: #fef2f2;
+      --header: linear-gradient(180deg, #087f57 0%, #087f57 28%, #0a9f6d 78%, #0a9f6d 100%);
+    }
+    * { box-sizing: border-box; margin: 0; padding: 0; }
+    button { font: inherit; cursor: pointer; }
+    body {
+      background: #cfd9d4;
+      color: var(--text);
+      font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif;
+      font-size: 14px;
+      line-height: 1.45;
+      min-height: 100vh;
+    }
+    .stage { max-width: 1100px; margin: 0 auto; padding: 16px 12px 48px; }
+    .banner {
+      background: #1f2a26; color: #fff; border-radius: 12px; padding: 12px 14px;
+      margin-bottom: 12px; font-size: 12px; line-height: 1.55;
+    }
+    .banner strong { color: #9fe3c4; }
+    .banner li { margin-left: 1.1em; margin-top: 4px; }
+    .mode-bar {
+      display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px;
+    }
+    .mode-bar button {
+      border: 1px solid #b7c9c0; background: #fff; border-radius: 10px;
+      padding: 10px 14px; font-size: 13px; font-weight: 600; color: var(--text);
+    }
+    .mode-bar button.active {
+      background: var(--brand-soft); border-color: #b7ebce; color: var(--brand-dark);
+    }
+    .phones {
+      display: grid;
+      grid-template-columns: 1fr;
+      gap: 16px;
+    }
+    @media (min-width: 900px) {
+      .phones.side-by-side { grid-template-columns: 1fr 1fr; }
+      .phones.side-by-side-3 { grid-template-columns: 1fr 1fr 1fr; }
+    }
+    .phone-wrap { display: none; }
+    .phone-wrap.show { display: block; }
+    .phones.side-by-side .phone-wrap,
+    .phones.side-by-side-3 .phone-wrap { display: block; }
+    .phone-label {
+      font-size: 12px; font-weight: 700; color: #24312c; margin-bottom: 8px;
+      display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
+    }
+    .phone-label .pill {
+      font-size: 10px; font-weight: 600; padding: 2px 8px; border-radius: 999px;
+    }
+    .pill--old { background: #fee2e2; color: #b91c1c; }
+    .pill--rec { background: var(--brand-soft); color: var(--brand-dark); }
+    .pill--alt { background: #fff7e8; color: #a16207; }
+    .phone {
+      position: relative;
+      background: var(--bg);
+      border-radius: 28px;
+      overflow: hidden;
+      border: 8px solid #24312c;
+      height: 640px;
+      box-shadow: 0 18px 40px rgba(0,0,0,.16);
+      display: flex;
+      flex-direction: column;
+    }
+    .status-bar {
+      flex: 0 0 auto;
+      height: 28px; background: var(--brand-dark); color: rgba(255,255,255,.85);
+      display: flex; align-items: center; justify-content: space-between;
+      padding: 0 14px; font-size: 11px; font-weight: 600;
+    }
+    .header {
+      flex: 0 0 auto;
+      background: var(--header);
+      color: #fff;
+      padding: 10px 12px 14px;
+      border-bottom-left-radius: 22px;
+      border-bottom-right-radius: 22px;
+    }
+    .header-row {
+      display: flex; align-items: center; gap: 8px; min-height: 44px;
+    }
+    .header-row h1 { font-size: 17px; font-weight: 700; }
+    .header-back { font-size: 20px; line-height: 1; opacity: .95; }
+    .body {
+      flex: 1; overflow: hidden; padding: 12px 10px; position: relative;
+    }
+    .card {
+      background: #fff; border: 1px solid var(--border); border-radius: 14px;
+      padding: 12px; box-shadow: 0 2px 8px rgba(34,64,52,.04);
+    }
+    .card-title { font-size: 15px; font-weight: 700; margin-bottom: 6px; }
+    .card-meta { font-size: 12px; color: var(--muted); margin-bottom: 10px; }
+    .card-actions { display: flex; gap: 8px; }
+    .mini-btn {
+      flex: 1; height: 30px; border-radius: 8px; font-size: 13px; font-weight: 500;
+      border: 1px solid var(--border); background: #fff; color: var(--text);
+    }
+    .mini-btn.danger {
+      color: var(--danger); border-color: #fecaca;
+    }
+    .mask {
+      position: absolute; inset: 0;
+      background: rgba(15, 23, 20, .45);
+      display: flex; align-items: center; justify-content: center;
+      padding: 20px 18px;
+    }
+
+    /* ── 现状:丑(纵向堆叠、无层级)── */
+    .dlg-old {
+      width: 100%; max-width: 280px;
+      background: #fff;
+      border: 1px solid #e5e7eb;
+      border-radius: 4px;
+      overflow: hidden;
+    }
+    .dlg-old__title {
+      padding: 14px 14px 0;
+      font-size: 15px; font-weight: 600; text-align: left;
+    }
+    .dlg-old__msg {
+      padding: 10px 14px 16px;
+      font-size: 14px; color: #111; text-align: left; line-height: 1.5;
+    }
+    .dlg-old__btn {
+      display: block; width: 100%;
+      height: 44px; background: #fff; border: 0;
+      border-top: 1px solid #e5e7eb;
+      font-size: 15px; color: #111;
+    }
+
+    /* ── 方案 A:横排 + 三墙绿确认(推荐,对齐客户管理)── */
+    .dlg-a {
+      width: 100%; max-width: 300px;
+      background: #fff;
+      border-radius: 16px;
+      box-shadow: 0 16px 40px rgba(15, 40, 30, .22);
+      overflow: hidden;
+    }
+    .dlg-a__head {
+      padding: 20px 20px 4px;
+      text-align: center;
+    }
+    .dlg-a__title {
+      font-size: 16px; font-weight: 700; color: var(--text);
+    }
+    .dlg-a__msg {
+      padding: 10px 20px 20px;
+      font-size: 14px; color: var(--muted); text-align: center; line-height: 1.55;
+    }
+    .dlg-a__foot {
+      display: flex; gap: 10px;
+      padding: 0 16px 16px;
+    }
+    .dlg-a__btn {
+      flex: 1; height: 40px; border-radius: 10px;
+      font-size: 14px; font-weight: 600;
+      border: 1px solid var(--border); background: #fff; color: var(--text);
+    }
+    .dlg-a__btn.ok {
+      background: var(--brand); border-color: var(--brand); color: #fff;
+    }
+
+    /* ── 方案 B:横排 + 危险红确认(强调不可恢复)── */
+    .dlg-b {
+      width: 100%; max-width: 300px;
+      background: #fff;
+      border-radius: 16px;
+      box-shadow: 0 16px 40px rgba(15, 40, 30, .22);
+      overflow: hidden;
+    }
+    .dlg-b__icon {
+      width: 44px; height: 44px; margin: 20px auto 0;
+      border-radius: 50%; background: var(--danger-soft);
+      display: flex; align-items: center; justify-content: center;
+      color: var(--danger); font-size: 22px; font-weight: 700;
+    }
+    .dlg-b__title {
+      padding: 12px 20px 0;
+      font-size: 16px; font-weight: 700; color: var(--text); text-align: center;
+    }
+    .dlg-b__msg {
+      padding: 8px 20px 20px;
+      font-size: 14px; color: var(--muted); text-align: center; line-height: 1.55;
+    }
+    .dlg-b__foot {
+      display: flex; gap: 10px;
+      padding: 0 16px 16px;
+    }
+    .dlg-b__btn {
+      flex: 1; height: 40px; border-radius: 10px;
+      font-size: 14px; font-weight: 600;
+      border: 1px solid var(--border); background: #fff; color: var(--text);
+    }
+    .dlg-b__btn.ok {
+      background: var(--danger); border-color: var(--danger); color: #fff;
+    }
+
+    /* ── 方案 C:底栏毛发线横排(贴近原生微信,确认字用品牌绿)── */
+    .dlg-c {
+      width: 100%; max-width: 300px;
+      background: #fff;
+      border-radius: 14px;
+      overflow: hidden;
+      box-shadow: 0 12px 32px rgba(15, 40, 30, .18);
+    }
+    .dlg-c__title {
+      padding: 22px 20px 6px;
+      font-size: 17px; font-weight: 700; text-align: center; color: var(--text);
+    }
+    .dlg-c__msg {
+      padding: 0 22px 22px;
+      font-size: 14px; color: #4b5c55; text-align: center; line-height: 1.55;
+    }
+    .dlg-c__foot {
+      display: flex;
+      border-top: 1px solid #e8f0ec;
+    }
+    .dlg-c__btn {
+      flex: 1; height: 48px; border: 0; background: #fff;
+      font-size: 16px; font-weight: 500; color: var(--text);
+    }
+    .dlg-c__btn + .dlg-c__btn {
+      border-left: 1px solid #e8f0ec;
+    }
+    .dlg-c__btn.ok {
+      color: var(--brand); font-weight: 700;
+    }
+
+    .note {
+      margin-top: 14px; padding: 12px 14px; background: #fff; border-radius: 12px;
+      border: 1px solid #b7c9c0; font-size: 12px; color: #33443d; line-height: 1.6;
+    }
+    .note strong { color: var(--brand-dark); }
+  </style>
+</head>
+<body>
+  <div class="stage">
+    <div class="banner">
+      <strong>产品管理 · 删除确认弹窗美化 Demo</strong>
+      <ul>
+        <li>背景:当前 Vant <code>showConfirmDialog</code> 在 WebView 中呈现为纵向堆叠、无主次、圆角/间距差。</li>
+        <li>Token:三墙绿 <code>#0a9f6d</code> / <code>#087f57</code>;危险红 <code>#dc2626</code>;禁止 Fresh Mint <code>#10b981</code>。</li>
+        <li>推荐落地:方案 A(与客户管理 Dialog 确认色一致);删除场景也可选 B。</li>
+      </ul>
+    </div>
+
+    <div class="mode-bar">
+      <button type="button" class="active" data-mode="compare">三方案对照</button>
+      <button type="button" data-mode="old">仅现状</button>
+      <button type="button" data-mode="a">仅方案 A</button>
+      <button type="button" data-mode="b">仅方案 B</button>
+      <button type="button" data-mode="c">仅方案 C</button>
+    </div>
+
+    <div class="phones side-by-side-3" id="phones">
+      <!-- 现状 -->
+      <div class="phone-wrap show" data-id="old">
+        <div class="phone-label">现状 <span class="pill pill--old">丑</span></div>
+        <div class="phone">
+          <div class="status-bar"><span>9:41</span><span>产品管理</span></div>
+          <div class="header">
+            <div class="header-row">
+              <span class="header-back">‹</span>
+              <h1>产品管理</h1>
+            </div>
+          </div>
+          <div class="body">
+            <div class="card">
+              <div class="card-title">工业乙醇 95%</div>
+              <div class="card-meta">上架 · P20261018001</div>
+              <div class="card-actions">
+                <button class="mini-btn" type="button">下架</button>
+                <button class="mini-btn danger" type="button">删除</button>
+              </div>
+            </div>
+            <div class="mask">
+              <div class="dlg-old">
+                <div class="dlg-old__title">提示</div>
+                <div class="dlg-old__msg">确认删除该产品?删除后不可恢复。</div>
+                <button class="dlg-old__btn" type="button">取消</button>
+                <button class="dlg-old__btn" type="button">确认</button>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 方案 A -->
+      <div class="phone-wrap show" data-id="a">
+        <div class="phone-label">方案 A · 横排绿确认 <span class="pill pill--rec">推荐</span></div>
+        <div class="phone">
+          <div class="status-bar"><span>9:41</span><span>产品管理</span></div>
+          <div class="header">
+            <div class="header-row">
+              <span class="header-back">‹</span>
+              <h1>产品管理</h1>
+            </div>
+          </div>
+          <div class="body">
+            <div class="card">
+              <div class="card-title">工业乙醇 95%</div>
+              <div class="card-meta">上架 · P20261018001</div>
+              <div class="card-actions">
+                <button class="mini-btn" type="button">下架</button>
+                <button class="mini-btn danger" type="button">删除</button>
+              </div>
+            </div>
+            <div class="mask">
+              <div class="dlg-a">
+                <div class="dlg-a__head">
+                  <div class="dlg-a__title">删除产品</div>
+                </div>
+                <div class="dlg-a__msg">确认删除该产品?删除后不可恢复。</div>
+                <div class="dlg-a__foot">
+                  <button class="dlg-a__btn" type="button">取消</button>
+                  <button class="dlg-a__btn ok" type="button">确认删除</button>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 方案 B -->
+      <div class="phone-wrap show" data-id="b">
+        <div class="phone-label">方案 B · 横排红确认 <span class="pill pill--alt">危险强调</span></div>
+        <div class="phone">
+          <div class="status-bar"><span>9:41</span><span>产品管理</span></div>
+          <div class="header">
+            <div class="header-row">
+              <span class="header-back">‹</span>
+              <h1>产品管理</h1>
+            </div>
+          </div>
+          <div class="body">
+            <div class="card">
+              <div class="card-title">工业乙醇 95%</div>
+              <div class="card-meta">上架 · P20261018001</div>
+              <div class="card-actions">
+                <button class="mini-btn" type="button">下架</button>
+                <button class="mini-btn danger" type="button">删除</button>
+              </div>
+            </div>
+            <div class="mask">
+              <div class="dlg-b">
+                <div class="dlg-b__icon">!</div>
+                <div class="dlg-b__title">删除产品</div>
+                <div class="dlg-b__msg">确认删除该产品?删除后不可恢复。</div>
+                <div class="dlg-b__foot">
+                  <button class="dlg-b__btn" type="button">取消</button>
+                  <button class="dlg-b__btn ok" type="button">确认删除</button>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 方案 C(对照时隐藏,点「仅方案 C」或可在 compare 时通过 JS 切换) -->
+      <div class="phone-wrap" data-id="c">
+        <div class="phone-label">方案 C · 原生毛发线横排 <span class="pill pill--alt">轻量</span></div>
+        <div class="phone">
+          <div class="status-bar"><span>9:41</span><span>产品管理</span></div>
+          <div class="header">
+            <div class="header-row">
+              <span class="header-back">‹</span>
+              <h1>产品管理</h1>
+            </div>
+          </div>
+          <div class="body">
+            <div class="card">
+              <div class="card-title">工业乙醇 95%</div>
+              <div class="card-meta">上架 · P20261018001</div>
+              <div class="card-actions">
+                <button class="mini-btn" type="button">下架</button>
+                <button class="mini-btn danger" type="button">删除</button>
+              </div>
+            </div>
+            <div class="mask">
+              <div class="dlg-c">
+                <div class="dlg-c__title">删除产品</div>
+                <div class="dlg-c__msg">确认删除该产品?删除后不可恢复。</div>
+                <div class="dlg-c__foot">
+                  <button class="dlg-c__btn" type="button">取消</button>
+                  <button class="dlg-c__btn ok" type="button">确认</button>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="note">
+      <strong>落地说明(待你选方案后写进 spec):</strong>
+      产品页 <code>web-admin/src/views/m/crm/product/*.vue</code> 仍用 Vant <code>showConfirmDialog</code>,
+      在 <code>product.scss</code> 覆盖 Dialog 圆角/内边距/横排按钮样式(对齐客户页
+      <code>body .van-dialog .van-button</code>),删除调用传 <code>confirmButtonText: '确认删除'</code>、
+      标题改为「删除产品」。方案 B 额外设 <code>confirmButtonColor: '#dc2626'</code> 或 danger class。
+      上架/下架确认可共用同一外壳,确认色用品牌绿。
+    </div>
+  </div>
+
+  <script>
+    const phones = document.getElementById('phones')
+    const wraps = [...document.querySelectorAll('.phone-wrap')]
+    const buttons = [...document.querySelectorAll('.mode-bar button')]
+
+    function setMode(mode) {
+      buttons.forEach((b) => b.classList.toggle('active', b.dataset.mode === mode))
+      phones.classList.remove('side-by-side', 'side-by-side-3')
+      wraps.forEach((w) => w.classList.remove('show'))
+
+      if (mode === 'compare') {
+        phones.classList.add('side-by-side-3')
+        wraps.filter((w) => ['old', 'a', 'b'].includes(w.dataset.id)).forEach((w) => w.classList.add('show'))
+        return
+      }
+      wraps.find((w) => w.dataset.id === mode)?.classList.add('show')
+    }
+
+    buttons.forEach((b) => b.addEventListener('click', () => setMode(b.dataset.mode)))
+    setMode('compare')
+  </script>
+</body>
+</html>

+ 81 - 0
docs/superpowers/specs/2026-10-18-mp-crm-product-confirm-dialog-design.md

@@ -0,0 +1,81 @@
+# 小程序 WebView · 产品管理确认弹窗美化
+
+日期:2026-10-18  
+状态:已确认(删除 = 方案 B;其它确认 = 方案 A)  
+范围:`web-admin` 小程序 WebView 产品管理 `/m/crm/product*`
+
+## 1. 目标与边界
+
+### 1.1 目标
+
+1. 美化产品管理中 Vant `showConfirmDialog` 的默认纵向堆叠、无主次样式。
+2. **删除类确认**采用方案 B(危险红确认 + 警示图标感)。
+3. **非删除确认**(上架 / 下架)采用方案 A(三墙绿确认)。
+4. 文案与视觉对齐 Demo:`bak/mobile-ui/shanxia-mp-crm-product-delete-confirm-demo.html`。
+
+### 1.2 范式权威
+
+| 维度 | 权威 |
+|------|------|
+| 视觉 Token | 三墙绿 `#0a9f6d` / `#087f57` / `#e9f9f2`;危险红 `#dc2626`;禁止 Fresh Mint `#10b981` |
+| 交互示意 | `bak/mobile-ui/shanxia-mp-crm-product-delete-confirm-demo.html` 方案 A / B |
+| 客户页 Dialog 按钮覆盖先例 | `web-admin/src/views/m/crm/customer/customer.scss` 中 `body .van-dialog .van-button` |
+
+### 1.3 不做
+
+- 不改客户 / 报价 / 合同等其它 CRM 模块确认弹窗。
+- 不改 PC 端 `crm/product`。
+- 不更换组件库;继续使用 Vant `showConfirmDialog`。
+- 不引入独立自定义 Dialog 组件(除非 Vant 样式无法达到 Demo 效果,再单开变更)。
+
+## 2. 视觉规格
+
+### 2.1 公共外壳(A / B 共用)
+
+- 白底圆角卡片(约 14–16px)、居中标题与说明。
+- 遮罩半透明深色;无 backdrop-blur。
+- 底部**横排**两个按钮(取消 | 确认),等宽,圆角约 10px,高度约 40px。
+- 禁止纵向堆叠「取消 / 确认」。
+
+### 2.2 方案 A(非删除)
+
+- 确认按钮:三墙绿实心(`#0a9f6d`)+ 白字。
+- 取消按钮:白底 + `#dcebe4` 描边 + 深色字。
+- 无危险图标。
+
+### 2.3 方案 B(删除)
+
+- 标题上方或标题区呈现危险警示视觉(圆形浅红底 +「!」或等价警示符号,对齐 Demo 方案 B)。
+- 确认按钮:危险红实心(`#dc2626`)+ 白字;文案为「确认删除」。
+- 取消按钮:同方案 A。
+
+## 3. 调用点与文案
+
+| 文件 | 场景 | 方案 | 标题 | 确认文案 |
+|------|------|------|------|----------|
+| `product/index.vue` | 删除产品 | B | 删除产品 | 确认删除 |
+| `product/index.vue` | 上架 / 下架 | A | 上架产品 / 下架产品 | 确认 |
+| `product/detail.vue` | 删除产品 | B | 删除产品 | 确认删除 |
+| `product/detail.vue` | 上架 / 下架 | A | 上架产品 / 下架产品 | 确认 |
+| `product/categories.vue` | 删除分类 | B | 删除分类 | 确认删除 |
+
+说明文案保持现有语义(删除含「不可恢复」;上下架为「确认上架/下架该产品?」)。
+
+## 4. 实现要点
+
+说明:Vant 默认 `theme` 为横排文字按钮;`theme: 'round-button'` 会纵向堆叠(正是现状丑态,**禁止使用**)。`confirmButtonColor` 只改文字色,**不能单独达到 Demo 实心按钮**。落地必须以 CSS 覆盖 footer 为「内边距 + 横排实心键」。
+
+1. **样式入口**:在 `product.scss` 写 `body .van-dialog.mp-product-dialog`(及 `--danger` 修饰),只影响产品管理传入 `className` 的 Dialog,避免误伤客户等其它页。
+2. **公共外壳 CSS**:圆角约 16px、标题/正文居中、footer 去毛发线全宽拉伸,改为 `padding + gap` 横排两键(高约 40px、圆角约 10px),对齐 Demo A/B。
+3. **方案 A 调用**:`className: 'mp-product-dialog'`;确认键 CSS 实心 `#0a9f6d`;可选 `confirmButtonColor: '#0a9f6d'` 作兜底。
+4. **方案 B 调用**:`className: 'mp-product-dialog mp-product-dialog--danger'`;确认键 CSS 实心 `#dc2626`;`confirmButtonText: '确认删除'`;`confirmButtonColor: '#dc2626'` 作兜底。
+5. **警示图标**:删除 Dialog 用 `allowHtml: true`,在 `message` 内拼一段含圆形「!」图标 + 说明文字的 HTML(结构对齐 Demo B);非删除不传 HTML。
+6. **回归**:对照 Demo;列表/详情/分类删除为 B;列表/详情上下架为 A;确认无纵向堆叠。
+
+## 5. 验收标准
+
+1. 删除确认:横排按钮;确认键红色;有明显危险警示;文案为「删除产品/分类」+「确认删除」。
+2. 上架/下架确认:横排按钮;确认键三墙绿;无红色危险样式。
+3. 取消或点遮罩关闭不执行业务(保持 Vant 现有 reject 行为)。
+4. 不出现纵向堆叠的丑默认态(对照截图与 Demo「现状」)。
+5. 色值不混用 Fresh Mint `#10b981`。