Explorar el Código

按 PC 原型整体回写汇丰银行权益中台-FSD,并新增 Playwright 自动截图

- 用 PC 原型实际逻辑回写 FSD §2~§七全部模块(借记卡/信用卡/权益审核/人群管理/发放计划/页面管理/短信管理/报表中心)
- 借记卡/信用卡/审核列表分页统一为 10/20/50;权益来源/类型 radio 改用圆点样式
- 荣数商品池示例数据改为微信立减金面额;移动端关联中"跳转触发页"统一替换为"是否有详情页"
- 提示文案统一为"需在【发放计划】中选择权益发放,未发放的权益移动端不展示"
- 新增 Playwright 截图脚本 pc/scripts/capture-*,38 张本地截图替换至 FSD 对应章节

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fade hace 1 mes
padre
commit
776d7732ea
Se han modificado 66 ficheros con 2247 adiciones y 2543 borrados
  1. 3 1
      .claude/settings.local.json
  2. BIN
      .resources/image-1779202378440.png
  3. BIN
      .resources/image-1779202989364.png
  4. BIN
      .resources/image-1779203397662.png
  5. BIN
      .resources/image-1779204512366.png
  6. 0 1309
      Channel管理.html
  7. 48 0
      pc/package-lock.json
  8. 1 0
      pc/package.json
  9. 40 0
      pc/scripts/capture-one.js
  10. 120 0
      pc/scripts/capture-screenshots.js
  11. 171 0
      pc/scripts/capture-supplements.js
  12. 12 15
      pc/src/components/RongshuProductDialog.vue
  13. 4 72
      pc/src/mock/index.js
  14. 5 5
      pc/src/views/AddEquityProduct.vue
  15. 3 3
      pc/src/views/AuditDetail.vue
  16. 1 1
      pc/src/views/AuditEquityProduct.vue
  17. 4 4
      pc/src/views/AuditList.vue
  18. 2 2
      pc/src/views/CreditCardEquity.vue
  19. 6 8
      pc/src/views/EditEquityProduct.vue
  20. 2 4
      pc/src/views/EquityProducts.vue
  21. 5 5
      pc/src/views/ViewEquityProduct.vue
  22. BIN
      prd/汇丰银行/screenshots/2-1-equity-products-list.png
  23. BIN
      prd/汇丰银行/screenshots/2-2-equity-add-source-rongshu.png
  24. BIN
      prd/汇丰银行/screenshots/2-2-equity-add-step1.png
  25. BIN
      prd/汇丰银行/screenshots/2-2-equity-add-step2.png
  26. BIN
      prd/汇丰银行/screenshots/2-2-equity-add-step3.png
  27. BIN
      prd/汇丰银行/screenshots/2-2-equity-add-third-detail.png
  28. BIN
      prd/汇丰银行/screenshots/2-2-equity-add-third-list.png
  29. BIN
      prd/汇丰银行/screenshots/2-2-equity-add-third-miniprogram.png
  30. BIN
      prd/汇丰银行/screenshots/2-3-equity-edit.png
  31. BIN
      prd/汇丰银行/screenshots/2-4-equity-view.png
  32. BIN
      prd/汇丰银行/screenshots/3-credit-card-equity-list.png
  33. BIN
      prd/汇丰银行/screenshots/4-grant-campaign-add.png
  34. BIN
      prd/汇丰银行/screenshots/4-grant-campaign-list.png
  35. BIN
      prd/汇丰银行/screenshots/4-grant-campaign-view.png
  36. BIN
      prd/汇丰银行/screenshots/5-page-assembly.png
  37. BIN
      prd/汇丰银行/screenshots/5-page-home.png
  38. BIN
      prd/汇丰银行/screenshots/5-page-zone.png
  39. BIN
      prd/汇丰银行/screenshots/6-audit-detail.png
  40. BIN
      prd/汇丰银行/screenshots/6-audit-list.png
  41. BIN
      prd/汇丰银行/screenshots/6-sms-send-record.png
  42. BIN
      prd/汇丰银行/screenshots/6-sms-template-list.png
  43. BIN
      prd/汇丰银行/screenshots/7-cash-discount-grant.png
  44. BIN
      prd/汇丰银行/screenshots/7-customer-equity-grant.png
  45. BIN
      prd/汇丰银行/screenshots/7-points-detail.png
  46. BIN
      prd/汇丰银行/screenshots/campaign-management.png
  47. BIN
      prd/汇丰银行/screenshots/channel-management.png
  48. BIN
      prd/汇丰银行/screenshots/crowd-name-list-add-step1.png
  49. BIN
      prd/汇丰银行/screenshots/crowd-name-list-add-step2.png
  50. BIN
      prd/汇丰银行/screenshots/crowd-task-add-step1.png
  51. BIN
      prd/汇丰银行/screenshots/crowd-task-add-step2.png
  52. BIN
      prd/汇丰银行/screenshots/crowd-task-list.png
  53. BIN
      prd/汇丰银行/screenshots/crowd-task-view.png
  54. BIN
      prd/汇丰银行/screenshots/dialog-delete-confirm.png
  55. BIN
      prd/汇丰银行/screenshots/dialog-equity-link.png
  56. BIN
      prd/汇丰银行/screenshots/dialog-offshelf-confirm.png
  57. BIN
      prd/汇丰银行/screenshots/dialog-rongshu-product.png
  58. BIN
      prd/汇丰银行/screenshots/dialog-show-pages.png
  59. BIN
      prd/汇丰银行/screenshots/popover-adjust-order.png
  60. BIN
      prd/汇丰银行/screenshots/rule-group-add-step1.png
  61. BIN
      prd/汇丰银行/screenshots/rule-group-add-step2.png
  62. BIN
      prd/汇丰银行/screenshots/rule-group-list.png
  63. BIN
      prd/汇丰银行/screenshots/rule-group-view.png
  64. 24 0
      prd/汇丰银行/汇丰借记卡权益.md
  65. 1796 0
      prd/汇丰银行/汇丰银行权益中台-FSD.md
  66. 0 1114
      页面管理.html

+ 3 - 1
.claude/settings.local.json

@@ -17,7 +17,9 @@
       "Bash(echo \"exit=$?\")",
       "Bash(openssl s_client -connect hub.banwotech.cn:443 -servername hub.banwotech.cn)",
       "Bash(git add *)",
-      "Bash(git commit -m ' *)"
+      "Bash(git commit -m ' *)",
+      "Bash(python3 -c ' *)",
+      "Bash(python3)"
     ],
     "additionalDirectories": []
   }

BIN
.resources/image-1779202378440.png


BIN
.resources/image-1779202989364.png


BIN
.resources/image-1779203397662.png


BIN
.resources/image-1779204512366.png


+ 0 - 1309
Channel管理.html

@@ -1,1309 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-  <meta charset="UTF-8" />
-  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-  <title>Channel管理 - 管理后台</title>
-  <script src="https://unpkg.com/lucide@latest"></script>
-  <style>
-    * { box-sizing: border-box; }
-
-    body {
-      margin: 0;
-      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", Arial, sans-serif;
-      background: #f3f5f8;
-      color: #1f2d3d;
-      font-size: 14px;
-    }
-
-    .page {
-      min-height: 100vh;
-      display: flex;
-      background: #f3f5f8;
-      overflow: hidden;
-    }
-
-    /* === 侧边栏 === */
-    .sidebar {
-      width: 286px;
-      min-height: 100vh;
-      background: #2d3a4b;
-      color: #c6d0df;
-      flex-shrink: 0;
-      display: flex;
-      flex-direction: column;
-    }
-
-    .logo {
-      height: 78px;
-      display: flex;
-      align-items: center;
-      padding-left: 66px;
-      gap: 12px;
-      color: #fff;
-      font-size: 24px;
-      font-weight: 700;
-      background: #263445;
-      border-bottom: 1px solid rgba(255,255,255,.06);
-      flex-shrink: 0;
-    }
-
-    .logo-icon {
-      width: 24px;
-      height: 18px;
-      border: 2px solid #fff;
-      border-radius: 2px;
-      position: relative;
-      opacity: .92;
-    }
-
-    .logo-icon::after {
-      content: "";
-      position: absolute;
-      left: 7px;
-      bottom: -7px;
-      width: 9px;
-      height: 5px;
-      border-bottom: 2px solid #fff;
-    }
-
-    .menu { 
-      padding-top: 28px; 
-      flex: 1;
-      overflow-y: auto;
-    }
-
-    .menu-section-title,
-    .menu-item {
-      height: 46px;
-      display: flex;
-      align-items: center;
-      padding: 0 24px;
-      color: #d5deeb;
-      font-size: 14px;
-      letter-spacing: .2px;
-      cursor: pointer;
-      transition: all 0.2s;
-    }
-
-    .menu-section-title:hover, .menu-item:hover {
-      color: #fff;
-    }
-
-    .menu-section-title .left-icon {
-      width: 15px;
-      margin-right: 16px;
-      opacity: .95;
-    }
-
-    .menu-section-title .arrow {
-      margin-left: auto;
-      font-size: 14px;
-      color: #b6c1d3;
-      transition: transform 0.2s;
-    }
-
-    .bank-title {
-      margin-top: 8px;
-      color: #fff;
-    }
-
-    .submenu { padding-bottom: 12px; }
-
-    .submenu .menu-item {
-      height: 46px;
-      padding-left: 76px;
-      color: #bdc8d8;
-      font-size: 14px;
-    }
-
-    .submenu .menu-item.active {
-      background: rgba(23, 36, 54, .45);
-      color: #409eff;
-      font-weight: 500;
-    }
-
-    .rights-title { margin-top: 2px; }
-
-    /* === 主内容区 === */
-    .main {
-      flex: 1;
-      min-width: 0;
-      display: flex;
-      flex-direction: column;
-      height: 100vh;
-    }
-
-    .topbar {
-      height: 78px;
-      background: #fff;
-      border-bottom: 1px solid #e4e8ef;
-      box-shadow: 0 1px 4px rgba(30,45,70,.04);
-      display: flex;
-      align-items: center;
-      padding: 0 30px;
-      color: #2b3442;
-      font-size: 14px;
-      flex-shrink: 0;
-    }
-
-    .breadcrumb span {
-      margin: 0 11px;
-      color: #596579;
-    }
-
-    .content {
-      padding: 38px 30px 30px 30px;
-      max-width: 100%;
-      flex: 1;
-      overflow-y: auto;
-    }
-
-    .view-section { display: none; }
-    .view-section.active { display: block; }
-
-    .search-card {
-      background: #fff;
-      border: 1px solid #e2e7ef;
-      border-radius: 4px;
-      padding: 24px 24px 22px 24px;
-      box-shadow: 0 1px 4px rgba(30,45,70,.04);
-      width: 100%;
-      margin-bottom: 31px;
-    }
-
-    .form-grid {
-      display: grid;
-      grid-template-columns: repeat(3, minmax(260px, 1fr));
-      column-gap: 36px;
-      row-gap: 18px;
-      align-items: center;
-      margin-bottom: 24px;
-    }
-
-    /* 允许 label 根据需要设定特定的列宽 */
-    .form-item {
-      display: flex;
-      align-items: center;
-      min-width: 0;
-    }
-
-    /* 默认给 Channel 设置的标签宽度 */
-    .form-item label {
-      width: 106px;
-      flex-shrink: 0;
-      text-align: right;
-      padding-right: 14px;
-      color: #1f2937;
-      font-size: 14px;
-      white-space: nowrap;
-    }
-    
-    /* 针对 Campaign 长标签的扩展样式 */
-    .form-item.long-label label {
-        width: 130px;
-    }
-
-    input,
-    select {
-      flex: 1;
-      width: 100%;
-      height: 40px;
-      border: 1px solid #dfe4ec;
-      border-radius: 4px;
-      background: #fff;
-      color: #303a4b;
-      padding: 0 12px;
-      font-size: 14px;
-      outline: none;
-      transition: border-color 0.2s;
-    }
-
-    input:focus, select:focus {
-        border-color: #409eff;
-    }
-
-    input::placeholder { color: #9ba6b8; }
-
-    select:disabled,
-    input:disabled {
-      background: #f8fafc;
-      color: #a0a8b5;
-      cursor: not-allowed;
-    }
-
-    .divider {
-      height: 1px;
-      background: #e8edf3;
-      margin: 0 0 20px 0;
-    }
-
-    .toolbar {
-      display: flex;
-      justify-content: space-between;
-      align-items: center;
-    }
-
-    .toolbar-left,
-    .toolbar-right {
-      display: flex;
-      gap: 14px;
-      align-items: center;
-    }
-
-    .btn {
-      height: 40px;
-      padding: 0 24px;
-      border-radius: 4px;
-      border: 1px solid #d8dde6;
-      background: #fff;
-      color: #334155;
-      font-size: 14px;
-      cursor: pointer;
-      display: inline-flex;
-      align-items: center;
-      justify-content: center;
-      gap: 8px;
-      transition: all 0.2s;
-    }
-
-    .btn:hover { opacity: .92; border-color: #c0c4cc; color: #409eff; }
-    
-    .btn-primary {
-      background: #2463e8;
-      border-color: #2463e8;
-      color: #fff;
-      min-width: 152px;
-    }
-    .btn-primary:hover { border-color: #409eff; color: #fff; }
-
-    .btn-purple {
-      background: #5655e9;
-      border-color: #5655e9;
-      color: #fff;
-      min-width: 82px;
-    }
-    .btn-purple:hover { border-color: #7b7af0; color: #fff; }
-
-    .hide { display: none !important; }
-
-    /* === 表格区 === */
-    .table-card {
-      background: #fff;
-      border: 1px solid #e3e7ef;
-      border-radius: 1px;
-      overflow: auto;
-      box-shadow: 0 1px 3px rgba(30,45,70,.04);
-      max-width: 100%;
-    }
-
-    table {
-      width: max-content;
-      min-width: 100%;
-      border-collapse: separate;
-      border-spacing: 0;
-      table-layout: fixed;
-    }
-
-    thead th {
-      height: 55px;
-      background: #fbfcfe;
-      border-bottom: 1px solid #e5e9f1;
-      border-right: 1px solid #e5e9f1;
-      color: #344154;
-      font-size: 14px;
-      font-weight: 500;
-      text-align: center;
-      padding: 0 18px;
-      white-space: nowrap;
-    }
-
-    tbody td {
-      height: 58px;
-      border-bottom: 1px solid #edf0f5;
-      border-right: 1px solid #edf0f5;
-      color: #2f3c4e;
-      font-size: 14px;
-      padding: 0 18px;
-      vertical-align: middle;
-      text-align: center;
-      word-break: break-word;
-    }
-
-    tbody tr:last-child td { border-bottom: none; }
-    tbody tr:hover { background-color: #f5f7fa; }
-
-    /* 列宽 */
-    .col-action { width: 190px; }
-    .col-id { width: 120px; }
-    .col-type { width: 150px; }
-    .col-category { width: 190px; }
-    .col-name { width: 190px; }
-    .col-sub { width: 160px; }
-    .col-owner { width: 120px; }
-    .col-time { width: 205px; }
-
-    th.col-action,
-    td.action-links {
-      position: sticky;
-      left: 0;
-      z-index: 3;
-      background: #fff;
-      box-shadow: 1px 0 0 #e5e9f1;
-      white-space: nowrap;
-    }
-    
-    tbody tr:hover td.action-links {
-        background-color: #f5f7fa;
-    }
-
-    th.col-action {
-      z-index: 4;
-      background: #fbfcfe;
-    }
-
-    .action-links a {
-      text-decoration: none;
-      margin-right: 12px;
-      font-size: 14px;
-      cursor: pointer;
-    }
-
-    .action-links .edit,
-    .action-links .view { color: #2f7cf6; }
-    .action-links .delete { color: #ff4d4f; }
-    .empty-tip { color: #9aa4b4; }
-
-    /* === 弹窗 === */
-    .modal-mask {
-      position: fixed;
-      inset: 0;
-      background: rgba(15, 23, 42, .48);
-      display: none;
-      align-items: center;
-      justify-content: center;
-      padding: 0;
-      z-index: 50;
-    }
-
-    .modal-mask.show { display: flex; }
-
-    .modal {
-      width: 560px;
-      background: #fff;
-      border-radius: 4px;
-      box-shadow: 0 10px 28px rgba(0,0,0,.20);
-      overflow: hidden;
-      display: flex;
-      flex-direction: column;
-      max-height: 90vh;
-    }
-
-    .modal-header {
-      height: 56px;
-      display: flex;
-      align-items: center;
-      justify-content: space-between;
-      padding: 0 20px;
-      border-bottom: 1px solid #e5e7eb;
-      font-size: 16px;
-      font-weight: 500;
-      color: #172033;
-      background: #fff;
-      flex-shrink: 0;
-    }
-
-    .modal-close {
-      color: #9099a8;
-      font-size: 22px;
-      line-height: 1;
-      cursor: pointer;
-      transition: color 0.2s;
-    }
-    .modal-close:hover { color: #ff4d4f; }
-
-    .modal-body { 
-        padding: 22px 24px 18px 20px; 
-        overflow-y: auto;
-        flex: 1;
-    }
-
-    .modal-row {
-      display: flex;
-      align-items: center;
-      margin-bottom: 14px;
-    }
-
-    .modal-row > label {
-      width: 128px;
-      flex-shrink: 0;
-      text-align: right;
-      padding-right: 16px;
-      color: #1f2937;
-      font-size: 14px;
-      white-space: nowrap;
-    }
-
-    .modal-row > input, .modal-row > .inline-field {
-        flex: 1;
-        min-width: 0;
-    }
-
-    .required {
-      color: #f5222d;
-      margin-right: 5px;
-    }
-
-    .inline-field {
-      display: flex;
-      gap: 6px;
-      align-items: center;
-    }
-
-    .inline-field select,
-    .inline-field input { flex: 1; }
-
-    .small-btn {
-      width: 52px;
-      height: 34px;
-      border: 1px solid #2463e8;
-      background: #f7fbff;
-      color: #2463e8;
-      border-radius: 4px;
-      cursor: pointer;
-      font-size: 14px;
-      flex-shrink: 0;
-      transition: all 0.2s;
-    }
-
-    .small-btn:hover { background: #eaf1fb; }
-
-    .small-btn.edit-current {
-      border-color: #d8dde6;
-      color: #334155;
-      background: #fff;
-    }
-    .small-btn.edit-current:hover { border-color: #c0c4cc; color: #409eff; }
-
-    .small-btn:disabled {
-      border-color: #e5e7eb;
-      color: #b4bdca;
-      background: #f8fafc;
-      cursor: not-allowed;
-    }
-
-    .quick-panel {
-      margin: -4px 0 14px 128px;
-      padding: 10px;
-      border: 1px solid #dbeafe;
-      border-radius: 4px;
-      background: #f8fbff;
-      display: none;
-      grid-template-columns: 1fr auto auto;
-      gap: 8px;
-      align-items: center;
-    }
-
-    .quick-panel.show { display: grid; }
-
-    .quick-panel .btn {
-      height: 34px;
-      padding: 0 14px;
-      font-size: 14px;
-      min-width: 62px;
-    }
-
-    .modal-footer {
-      height: 58px;
-      border-top: 1px solid #e5e7eb;
-      background: #f8fafc;
-      display: flex;
-      justify-content: flex-end;
-      align-items: center;
-      gap: 10px;
-      padding: 0 18px;
-      flex-shrink: 0;
-    }
-
-    .modal-footer .btn {
-      min-width: 72px;
-      height: 38px;
-      padding: 0 20px;
-    }
-
-    .toast {
-      position: fixed;
-      top: 88px;
-      left: 50%;
-      transform: translateX(-50%);
-      background: rgba(15,23,42,.9);
-      color: #fff;
-      padding: 10px 16px;
-      border-radius: 4px;
-      font-size: 14px;
-      display: none;
-      z-index: 100;
-    }
-
-    .toast.show { display: block; }
-
-    @media (max-width: 1400px) {
-      .sidebar { width: 260px; }
-      .logo { padding-left: 50px; }
-      .form-grid {
-        grid-template-columns: repeat(2, minmax(260px, 1fr));
-        column-gap: 24px;
-      }
-    }
-
-    @media (max-width: 1100px) {
-      .form-grid {
-        grid-template-columns: 1fr;
-      }
-    }
-  </style>
-</head>
-<body>
-  <div class="page">
-    <!-- 侧边栏 -->
-    <aside class="sidebar">
-      <div class="logo"><div class="logo-icon"></div><div>管理后台</div></div>
-      <nav class="menu">
-        <div class="menu-section-title"><i data-lucide="settings" class="left-icon"></i><span>定制功能</span><i data-lucide="chevron-down" class="arrow"></i></div>
-        <div class="menu-section-title bank-title"><span>汇丰银行</span><i data-lucide="chevron-up" class="arrow"></i></div>
-        <div class="submenu">
-          <div class="menu-item">页面管理</div>
-          <div class="menu-item">权益介绍页</div>
-          <div class="menu-item">权益主页</div>
-          <div class="menu-item">组装页</div>
-          <div class="menu-item active" id="menu-channel" onclick="switchView('channel')">Channel管理</div>
-          <div class="menu-item" id="menu-campaign" onclick="switchView('campaign')">Campaign管理</div>
-        </div>
-        <div class="menu-section-title rights-title"><span>权益管理</span><i data-lucide="chevron-up" class="arrow"></i></div>
-        <div class="submenu">
-          <div class="menu-item">借记卡权益</div>
-          <div class="menu-item">信用卡权益</div>
-          <div class="menu-item">人群管理</div>
-        </div>
-      </nav>
-    </aside>
-
-    <!-- 主区域 -->
-    <main class="main">
-      <header class="topbar">
-        <div class="breadcrumb" id="breadcrumb">汇丰银行 <span>&gt;</span> 页面管理 <span>&gt;</span> Channel管理</div>
-      </header>
-
-      <!-- 视图:Channel -->
-      <section class="content view-section active" id="view-channel">
-        <div class="search-card">
-          <div class="form-grid">
-            <div class="form-item"><label>Channel ID</label><input type="text" id="s-ch-id" placeholder="支持精确查询" /></div>
-            <div class="form-item">
-              <label>Channel 类型</label>
-              <select id="s-ch-type"><option value="">全部</option></select>
-            </div>
-            <div class="form-item"><label>Channel 类别</label><input type="text" id="s-ch-category" placeholder="关键字" /></div>
-            <div class="form-item"><label>Channel 名称</label><input type="text" id="s-ch-name" placeholder="关键字" /></div>
-            <div class="form-item"><label>Channel 子名称</label><input type="text" id="s-ch-subname" placeholder="关键字" /></div>
-            <div class="form-item"><label>跟进员工</label><input type="text" id="s-ch-staff" placeholder="员工姓名" /></div>
-          </div>
-
-          <div class="divider"></div>
-
-          <div class="toolbar">
-            <div class="toolbar-left">
-              <button class="btn btn-primary" onclick="openChannelModal('create')">
-                  <i data-lucide="plus" style="width: 16px; height: 16px;"></i> 新建 Channel
-              </button>
-            </div>
-            <div class="toolbar-right">
-              <button class="btn btn-purple" onclick="renderChannelTable()">查询</button>
-              <button class="btn" onclick="resetSearch('channel')">重置</button>
-            </div>
-          </div>
-        </div>
-
-        <div class="table-card">
-          <table>
-            <thead>
-              <tr>
-                <th class="col-action">操作</th>
-                <th class="col-id">Channel ID</th>
-                <th class="col-type">类型</th>
-                <th class="col-category">类别</th>
-                <th class="col-name">名称</th>
-                <th class="col-sub">子名称</th>
-                <th class="col-owner">跟进员工</th>
-                <th class="col-time">创建时间</th>
-                <th class="col-time">更新时间</th>
-              </tr>
-            </thead>
-            <tbody id="channelTableBody"></tbody>
-          </table>
-        </div>
-      </section>
-
-      <!-- 视图:Campaign -->
-      <section class="content view-section" id="view-campaign">
-        <div class="search-card">
-          <div class="form-grid">
-            <div class="form-item long-label"><label>Campaign ID</label><input type="text" id="s-cp-id" placeholder="支持精确查询" /></div>
-            <div class="form-item long-label"><label>Campaign 名称</label><input type="text" id="s-cp-name" placeholder="模糊查询" /></div>
-            <div class="form-item long-label"><label>Campaign 负责人</label><input type="text" id="s-cp-owner" placeholder="关键字" /></div>
-            <div class="form-item long-label"><label>跟进员工</label><input type="text" id="s-cp-staff" placeholder="员工姓名" /></div>
-          </div>
-
-          <div class="divider"></div>
-
-          <div class="toolbar">
-            <div class="toolbar-left">
-              <button class="btn btn-primary" onclick="openCampaignModal('create')">
-                  <i data-lucide="plus" style="width: 16px; height: 16px;"></i> 新建 Campaign
-              </button>
-            </div>
-            <div class="toolbar-right">
-              <button class="btn btn-purple" onclick="renderCampaignTable()">查询</button>
-              <button class="btn" onclick="resetSearch('campaign')">重置</button>
-            </div>
-          </div>
-        </div>
-
-        <div class="table-card">
-          <table>
-            <thead>
-              <tr>
-                <th class="col-action" style="width: 150px;">操作</th>
-                <th class="col-id" style="width: 150px;">Campaign ID</th>
-                <th class="col-name">Campaign 名称</th>
-                <th class="col-owner" style="width: 150px;">Campaign 负责人</th>
-                <th class="col-owner" style="width: 150px;">跟进员工</th>
-                <th class="col-time">创建时间</th>
-                <th class="col-time">更新时间</th>
-              </tr>
-            </thead>
-            <tbody id="campaignTableBody"></tbody>
-          </table>
-        </div>
-      </section>
-    </main>
-  </div>
-
-  <!-- 弹窗:新建/编辑 Channel -->
-  <div class="modal-mask" id="channelModal">
-    <div class="modal">
-      <div class="modal-header">
-        <div id="modalTitle">新建 Channel</div>
-        <div class="modal-close" onclick="closeModal('channelModal')">×</div>
-      </div>
-
-      <div class="modal-body">
-        <div class="modal-row hide" id="chIdRow">
-          <label>Channel ID</label>
-          <input type="text" id="f-ch-id" disabled />
-        </div>
-
-        <div class="modal-row">
-          <label><span class="required">*</span>Channel 类型</label>
-          <div class="inline-field">
-            <select id="channelType">
-              <option value="">请选择类型</option>
-            </select>
-            <button class="small-btn ch-btn" type="button" data-target="type">新增</button>
-            <button class="small-btn edit-current ch-btn" type="button" data-target="type">编辑</button>
-          </div>
-        </div>
-        <div class="quick-panel" id="typePanel">
-          <input id="typeInput" type="text" placeholder="请输入类型" />
-          <button class="btn btn-primary" style="min-width: 62px;" type="button" data-save="type">保存</button>
-          <button class="btn" type="button" data-cancel="type">取消</button>
-        </div>
-
-        <div class="modal-row">
-          <label><span class="required">*</span>Channel 类别</label>
-          <div class="inline-field">
-            <select id="channelCategory" disabled>
-              <option value="">请先选择类型</option>
-            </select>
-            <button class="small-btn ch-btn" type="button" data-target="category">新增</button>
-            <button class="small-btn edit-current ch-btn" type="button" data-target="category">编辑</button>
-          </div>
-        </div>
-        <div class="quick-panel" id="categoryPanel">
-          <input id="categoryInput" type="text" placeholder="请输入类别" />
-          <button class="btn btn-primary" style="min-width: 62px;" type="button" data-save="category">保存</button>
-          <button class="btn" type="button" data-cancel="category">取消</button>
-        </div>
-
-        <div class="modal-row">
-          <label><span class="required">*</span>Channel 名称</label>
-          <div class="inline-field">
-            <select id="channelName" disabled>
-              <option value="">请先选择类别</option>
-            </select>
-            <button class="small-btn ch-btn" type="button" data-target="name">新增</button>
-            <button class="small-btn edit-current ch-btn" type="button" data-target="name">编辑</button>
-          </div>
-        </div>
-        <div class="quick-panel" id="namePanel">
-          <input id="nameInput" type="text" placeholder="请输入名称" />
-          <button class="btn btn-primary" style="min-width: 62px;" type="button" data-save="name">保存</button>
-          <button class="btn" type="button" data-cancel="name">取消</button>
-        </div>
-
-        <div class="modal-row">
-          <label>Channel 子名称</label>
-          <input id="subNameInput" type="text" placeholder="请输入Channel 子名称" />
-        </div>
-
-        <div class="modal-row">
-          <label>跟进员工</label>
-          <input id="ownerInput" type="text" placeholder="请输入跟进员工姓名" />
-        </div>
-      </div>
-
-      <div class="modal-footer" id="modal-ch-footer">
-        <button class="btn" onclick="closeModal('channelModal')">取消</button>
-        <button class="btn btn-primary" id="submitChannelBtn">确定</button>
-      </div>
-    </div>
-  </div>
-
-  <!-- 弹窗:新建/编辑 Campaign -->
-  <div class="modal-mask" id="campaignModal">
-    <div class="modal" style="width: 500px;">
-      <div class="modal-header">
-        <div id="modalCpTitle">新建 Campaign</div>
-        <div class="modal-close" onclick="closeModal('campaignModal')">×</div>
-      </div>
-      <div class="modal-body">
-        <div class="modal-row hide" id="cpIdRow">
-          <label style="width: 140px; padding-right: 12px;">Campaign ID</label>
-          <input type="text" id="f-cp-id" disabled />
-        </div>
-        <div class="modal-row">
-          <label style="width: 140px; padding-right: 12px;"><span class="required">*</span>Campaign 名称</label>
-          <input type="text" id="f-cp-name" placeholder="请输入名称" />
-        </div>
-        <div class="modal-row">
-          <label style="width: 140px; padding-right: 12px;"><span class="required">*</span>Campaign 负责人</label>
-          <input type="text" id="f-cp-owner" placeholder="请输入负责人" />
-        </div>
-        <div class="modal-row">
-          <label style="width: 140px; padding-right: 12px;">跟进员工 (可选)</label>
-          <input type="text" id="f-cp-staff" placeholder="请输入员工姓名" />
-        </div>
-      </div>
-      <div class="modal-footer" id="modal-cp-footer">
-        <button class="btn" onclick="closeModal('campaignModal')">取消</button>
-        <button class="btn btn-primary" onclick="submitCampaignModal()">确定</button>
-      </div>
-    </div>
-  </div>
-
-  <!-- 弹窗:确认删除 -->
-  <div class="modal-mask" id="confirmModal">
-      <div class="modal" style="width: 380px;">
-          <div class="modal-header" style="border-bottom: none; padding-top: 24px;">
-              <div style="font-weight: 600; font-size: 16px; color: #172033; display: flex; align-items: center;">
-                  <i data-lucide="alert-circle" style="color: #ff4d4f; margin-right: 8px;"></i> 确认删除
-              </div>
-              <div class="modal-close" onclick="closeModal('confirmModal')">×</div>
-          </div>
-          <div class="modal-body" style="padding: 16px 24px 32px 50px; font-size: 14px; color: #596579;">
-              您确定要删除此条数据吗?删除后将无法恢复。
-          </div>
-          <div class="modal-footer" style="padding: 12px 24px; justify-content: flex-end;">
-              <button class="btn" style="height: 32px; padding: 0 16px; min-width: auto;" onclick="closeModal('confirmModal')">取消</button>
-              <button class="btn btn-primary" style="height: 32px; padding: 0 16px; min-width: auto; background: #ff4d4f; border-color: #ff4d4f;" id="btn-confirm-delete">确认删除</button>
-          </div>
-      </div>
-  </div>
-
-  <div class="toast" id="toast"></div>
-
-  <script>
-    lucide.createIcons();
-
-    // ================= 数据初始化 =================
-    const channelDict = {
-      "Human-assisted": {
-        "汇丰中国客户服务号": ["汇丰中国客户服务号"],
-        "MGM": ["MGM - Mortgage", "MGM - Premier"],
-        "CCSS": ["CCSS SMS"],
-        "Referral": ["GPB Referral", "HK referral", "Non WPB staff Referral", "Pinnacle Referral", "RBB Refferal"],
-        "New staff": ["New staff"],
-        "IBC": ["IBC referral staff code"],
-        "Group Acct": ["HTC", "NIVIDA"]
-      },
-      "Unassisted": {
-        "汇丰中国APP": ["Mobile X"],
-        "汇丰中国官网": ["PWS site"],
-        "汇丰中国小程序": ["汇丰中国小程序"],
-        "汇丰中国订阅号": ["汇丰中国订阅号", "汇丰中国订阅号推文", "推文尾部入口"],
-        "Chatbot": ["Chatbot"],
-        "SMS": ["SMS"],
-        "Paid Media": ["汇丰中国订阅号推文", "others"],
-        "Others": ["others", "River APP"]
-      }
-    };
-
-    let channelRows = [
-      { id: "CH0001", type: "Human-assisted", category: "汇丰中国客户服务号", name: "汇丰中国客户服务号", subName: "", owner: "", createTime: "2025-11-20 09:15:00", updateTime: "2025-11-20 09:15:00" },
-      { id: "CH0002", type: "Human-assisted", category: "MGM", name: "MGM - Mortgage", subName: "", owner: "", createTime: "2025-11-20 09:15:00", updateTime: "2025-11-20 09:15:00" },
-      { id: "CH0003", type: "Human-assisted", category: "MGM", name: "MGM - Premier", subName: "双十一特惠", owner: "张经理", createTime: "2025-11-20 09:15:00", updateTime: "2025-11-21 14:30:00" },
-      { id: "CH0004", type: "Human-assisted", category: "CCSS", name: "CCSS SMS", subName: "", owner: "", createTime: "2025-11-20 09:15:00", updateTime: "2025-11-20 09:15:00" },
-      { id: "CH0005", type: "Human-assisted", category: "Referral", name: "GPB Referral", subName: "", owner: "", createTime: "2025-11-20 09:15:00", updateTime: "2025-11-20 09:15:00" },
-      { id: "CH0006", type: "Unassisted", category: "汇丰中国APP", name: "Mobile X", subName: "", owner: "", createTime: "2025-11-20 09:15:00", updateTime: "2025-11-20 09:15:00" },
-      { id: "CH0007", type: "Unassisted", category: "汇丰中国官网", name: "PWS site", subName: "私行沙龙引导", owner: "", createTime: "2025-11-20 09:15:00", updateTime: "2025-11-20 09:15:00" },
-      { id: "CH0008", type: "Unassisted", category: "汇丰中国订阅号", name: "汇丰中国订阅号", subName: "", owner: "", createTime: "2025-11-20 09:15:00", updateTime: "2025-11-20 09:15:00" },
-      { id: "CH0009", type: "Unassisted", category: "汇丰中国订阅号", name: "推文尾部入口", subName: "", owner: "", createTime: "2025-11-20 09:15:00", updateTime: "2025-11-20 09:15:00" }
-    ];
-
-    let campaigns = [
-      { id: 'CP9001', name: 'Q3 大客户回馈', owner: 'Market Team', staff: '王专员', createTime: '2025-10-01 10:00:00', updateTime: '2025-10-15 14:30:00' },
-      { id: 'CP9002', name: '新春开户活动', owner: 'Retail Team', staff: '李专员', createTime: '2025-11-20 09:15:00', updateTime: '2025-11-20 09:15:00' }
-    ];
-
-    let editingChannelId = null;
-    let editingCampaignId = null;
-    let panelMode = { type: "add", category: "add", name: "add" };
-    let deleteTarget = { type: null, id: null };
-
-    // ================= 工具 =================
-    function getNowString() {
-      const d = new Date();
-      const pad = n => n < 10 ? '0' + n : n;
-      return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
-    }
-
-    const toast = document.getElementById("toast");
-    function showToast(text) {
-      toast.textContent = text;
-      toast.classList.add("show");
-      setTimeout(() => toast.classList.remove("show"), 1600);
-    }
-
-    function closeModal(id) {
-      document.getElementById(id).classList.remove("show");
-      if(id === 'channelModal') {
-        editingChannelId = null;
-        closeAllPanels();
-      }
-    }
-
-    // ================= 视图切换 =================
-    function switchView(view) {
-      document.getElementById('menu-channel').classList.remove('active');
-      document.getElementById('menu-campaign').classList.remove('active');
-      document.getElementById('view-channel').classList.remove('active');
-      document.getElementById('view-campaign').classList.remove('active');
-
-      if(view === 'channel') {
-        document.getElementById('menu-channel').classList.add('active');
-        document.getElementById('view-channel').classList.add('active');
-        document.getElementById('breadcrumb').innerHTML = '汇丰银行 <span>&gt;</span> 页面管理 <span>&gt;</span> Channel管理';
-        updateSearchDropdowns();
-        renderChannelTable();
-      } else {
-        document.getElementById('menu-campaign').classList.add('active');
-        document.getElementById('view-campaign').classList.add('active');
-        document.getElementById('breadcrumb').innerHTML = '汇丰银行 <span>&gt;</span> 页面管理 <span>&gt;</span> Campaign管理';
-        renderCampaignTable();
-      }
-    }
-
-    // ================= Channel 逻辑 =================
-    const typeSelect = document.getElementById("channelType");
-    const categorySelect = document.getElementById("channelCategory");
-    const nameSelect = document.getElementById("channelName");
-    const subNameInput = document.getElementById("subNameInput");
-    const ownerInput = document.getElementById("ownerInput");
-
-    function updateSearchDropdowns() {
-      const types = Object.keys(channelDict);
-      const selType = document.getElementById('s-ch-type');
-      selType.innerHTML = '<option value="">全部</option>' + types.map(t => `<option value="${t}">${t}</option>`).join('');
-    }
-
-    function renderChannelTable() {
-      const sId = document.getElementById('s-ch-id').value.trim().toLowerCase();
-      const sType = document.getElementById('s-ch-type').value;
-      const sCategory = document.getElementById('s-ch-category').value.trim().toLowerCase();
-      const sName = document.getElementById('s-ch-name').value.trim().toLowerCase();
-      const sSubname = document.getElementById('s-ch-subname').value.trim().toLowerCase();
-      const sStaff = document.getElementById('s-ch-staff').value.trim().toLowerCase();
-
-      const tbody = document.getElementById("channelTableBody");
-      const filtered = channelRows.filter(c => {
-        return (!sId || c.id.toLowerCase().includes(sId)) &&
-               (!sType || c.type === sType) &&
-               (!sCategory || c.category.toLowerCase().includes(sCategory)) &&
-               (!sName || c.name.toLowerCase().includes(sName)) &&
-               (!sSubname || c.subName.toLowerCase().includes(sSubname)) &&
-               (!sStaff || c.owner.toLowerCase().includes(sStaff));
-      });
-
-      if (filtered.length === 0) {
-        tbody.innerHTML = '<tr><td colspan="9" class="text-center empty-tip" style="height:100px;">暂无数据</td></tr>';
-        return;
-      }
-
-      tbody.innerHTML = filtered.map(row => `
-        <tr>
-          <td class="action-links">
-            <a class="edit" onclick="openChannelModal('edit', '${row.id}')">编辑</a>
-            <a class="view" onclick="openChannelModal('view', '${row.id}')">查看</a>
-            <a class="delete" onclick="confirmDelete('channel', '${row.id}')">删除</a>
-          </td>
-          <td>${row.id}</td>
-          <td>${row.type}</td>
-          <td>${row.category}</td>
-          <td>${row.name}</td>
-          <td class="${row.subName ? "" : "empty-tip"}">${row.subName || "-"}</td>
-          <td class="${row.owner ? "" : "empty-tip"}">${row.owner || "-"}</td>
-          <td>${row.createTime}</td>
-          <td>${row.updateTime}</td>
-        </tr>
-      `).join("");
-    }
-
-    function fillSelect(select, placeholder, list, value = "") {
-      select.innerHTML = `<option value="">${placeholder}</option>`;
-      list.forEach(item => {
-        const option = document.createElement("option");
-        option.value = item;
-        option.textContent = item;
-        select.appendChild(option);
-      });
-      select.disabled = list.length === 0;
-      select.value = value;
-    }
-
-    function refreshTypes(value = "") {
-      fillSelect(typeSelect, "请选择类型", Object.keys(channelDict), value);
-    }
-
-    function refreshCategories(type, value = "") {
-      if (!type || !channelDict[type]) {
-        fillSelect(categorySelect, "请先选择类型", []);
-        fillSelect(nameSelect, "请先选择类别", []);
-        return;
-      }
-      fillSelect(categorySelect, "请选择类别", Object.keys(channelDict[type]), value);
-      refreshNames(type, value, "");
-    }
-
-    function refreshNames(type, category, value = "") {
-      if (!type || !category || !channelDict[type]?.[category]) {
-        fillSelect(nameSelect, "请先选择类别", []);
-        return;
-      }
-      fillSelect(nameSelect, "请选择名称", channelDict[type][category], value);
-    }
-
-    typeSelect.addEventListener("change", () => refreshCategories(typeSelect.value));
-    categorySelect.addEventListener("change", () => refreshNames(typeSelect.value, categorySelect.value));
-
-    function closeAllPanels() {
-      document.querySelectorAll(".quick-panel").forEach(p => p.classList.remove("show"));
-      document.querySelectorAll(".quick-panel input").forEach(i => i.value = "");
-    }
-
-    function openChannelModal(mode, id = null) {
-      editingChannelId = id;
-      closeAllPanels();
-      subNameInput.value = "";
-      ownerInput.value = "";
-      
-      document.querySelectorAll(".ch-btn").forEach(btn => btn.classList.remove("hide"));
-      document.getElementById('modal-ch-footer').classList.remove("hide");
-      typeSelect.disabled = false;
-      subNameInput.disabled = false;
-      ownerInput.disabled = false;
-
-      if (mode === "edit" || mode === "view") {
-        const row = channelRows.find(r => r.id === id);
-        document.getElementById('modalTitle').innerText = mode === "edit" ? "编辑 Channel" : "查看 Channel";
-        document.getElementById('chIdRow').classList.remove('hide');
-        document.getElementById('f-ch-id').value = row.id;
-
-        refreshTypes(row.type);
-        refreshCategories(row.type, row.category);
-        refreshNames(row.type, row.category, row.name);
-        subNameInput.value = row.subName;
-        ownerInput.value = row.owner;
-
-        if(mode === "view") {
-          document.querySelectorAll(".ch-btn").forEach(btn => btn.classList.add("hide"));
-          document.getElementById('modal-ch-footer').classList.add("hide");
-          typeSelect.disabled = true;
-          categorySelect.disabled = true;
-          nameSelect.disabled = true;
-          subNameInput.disabled = true;
-          ownerInput.disabled = true;
-        }
-      } else {
-        document.getElementById('modalTitle').innerText = "新建 Channel";
-        document.getElementById('chIdRow').classList.add('hide');
-        refreshTypes();
-        fillSelect(categorySelect, "请先选择类型", []);
-        fillSelect(nameSelect, "请先选择类别", []);
-      }
-      document.getElementById("channelModal").classList.add("show");
-    }
-
-    function openPanel(target, mode) {
-      const type = typeSelect.value;
-      const category = categorySelect.value;
-      
-      if (target === "category" && !type) return showToast("请先选择 Channel 类型");
-      if (target === "name" && (!type || !category)) return showToast("请先选择 Channel 类型和类别");
-      if (mode === "edit") {
-        if (target === "type" && !type) return showToast("请先选择要编辑的类型");
-        if (target === "category" && !category) return showToast("请先选择要编辑的类别");
-        if (target === "name" && !nameSelect.value) return showToast("请先选择要编辑的名称");
-      }
-
-      closeAllPanels();
-      panelMode[target] = mode;
-      const panel = document.getElementById(`${target}Panel`);
-      const input = document.getElementById(`${target}Input`);
-      input.value = mode === "edit" ? (target === "type" ? type : target === "category" ? category : nameSelect.value) : "";
-      input.placeholder = mode === "edit" ? "请输入新的内容" : `请输入新的${target === 'type' ? '类型' : target === 'category' ? '类别' : '名称'}`;
-      panel.classList.add("show");
-      input.focus();
-    }
-
-    document.querySelectorAll(".ch-btn[data-target]").forEach(btn => {
-      btn.addEventListener("click", () => openPanel(btn.dataset.target, btn.classList.contains("edit-current") ? "edit" : "add"));
-    });
-
-    function saveDict(target) {
-      const type = typeSelect.value;
-      const category = categorySelect.value;
-      const name = nameSelect.value;
-      const value = document.getElementById(`${target}Input`).value.trim();
-      const mode = panelMode[target];
-      if (!value) return showToast("请输入内容");
-
-      if (target === "type") {
-        if (mode === "add") {
-          if (channelDict[value]) return showToast("该类型已存在");
-          channelDict[value] = {};
-        } else {
-          if (value !== type && channelDict[value]) return showToast("该类型已存在");
-          channelDict[value] = channelDict[type];
-          delete channelDict[type];
-          channelRows.forEach(row => { if (row.type === type) row.type = value; });
-        }
-        refreshTypes(value);
-        refreshCategories(value);
-      }
-
-      if (target === "category") {
-        if (mode === "add") {
-          if (channelDict[type][value]) return showToast("该类别已存在");
-          channelDict[type][value] = [];
-        } else {
-          if (value !== category && channelDict[type][value]) return showToast("该类别已存在");
-          channelDict[type][value] = channelDict[type][category];
-          delete channelDict[type][category];
-          channelRows.forEach(row => { if (row.type === type && row.category === category) row.category = value; });
-        }
-        refreshCategories(type, value);
-      }
-
-      if (target === "name") {
-        const list = channelDict[type][category];
-        if (mode === "add") {
-          if (list.includes(value)) return showToast("该名称已存在");
-          list.push(value);
-        } else {
-          if (value !== name && list.includes(value)) return showToast("该名称已存在");
-          const index = list.indexOf(name);
-          if (index >= 0) list[index] = value;
-          channelRows.forEach(row => { if (row.type === type && row.category === category && row.name === name) row.name = value; });
-        }
-        refreshNames(type, category, value);
-      }
-
-      closeAllPanels();
-      updateSearchDropdowns();
-      renderChannelTable();
-      showToast(mode === "add" ? "新增成功" : "编辑成功");
-    }
-
-    document.querySelectorAll("[data-save]").forEach(btn => btn.addEventListener("click", () => saveDict(btn.dataset.save)));
-    document.querySelectorAll("[data-cancel]").forEach(btn => btn.addEventListener("click", closeAllPanels));
-
-    document.getElementById("submitChannelBtn").addEventListener("click", () => {
-      const type = typeSelect.value;
-      const category = categorySelect.value;
-      const name = nameSelect.value;
-      if (!type || !category || !name) return showToast("请完整选择类型、类别和名称");
-
-      const now = getNowString();
-      if (editingChannelId !== null) {
-        const idx = channelRows.findIndex(r => r.id === editingChannelId);
-        channelRows[idx] = {
-          ...channelRows[idx],
-          type, category, name,
-          subName: subNameInput.value.trim(),
-          owner: ownerInput.value.trim(),
-          updateTime: now
-        };
-        showToast("保存成功");
-      } else {
-        const nextId = `CH${String(channelRows.length + 1).padStart(4, "0")}`;
-        channelRows.unshift({
-          id: nextId,
-          type, category, name,
-          subName: subNameInput.value.trim(),
-          owner: ownerInput.value.trim(),
-          createTime: now,
-          updateTime: now
-        });
-        showToast("新建成功");
-      }
-      renderChannelTable();
-      closeModal('channelModal');
-    });
-
-    // ================= Campaign 逻辑 =================
-    function renderCampaignTable() {
-      const sId = document.getElementById('s-cp-id').value.trim().toLowerCase();
-      const sName = document.getElementById('s-cp-name').value.trim().toLowerCase();
-      const sOwner = document.getElementById('s-cp-owner').value.trim().toLowerCase();
-      const sStaff = document.getElementById('s-cp-staff').value.trim().toLowerCase();
-
-      const tbody = document.getElementById('campaignTableBody');
-      const filtered = campaigns.filter(c => {
-        return (!sId || c.id.toLowerCase().includes(sId)) &&
-               (!sName || c.name.toLowerCase().includes(sName)) &&
-               (!sOwner || c.owner.toLowerCase().includes(sOwner)) &&
-               (!sStaff || c.staff.toLowerCase().includes(sStaff));
-      });
-
-      if (filtered.length === 0) {
-        tbody.innerHTML = '<tr><td colspan="7" class="text-center empty-tip" style="height:100px;">暂无数据</td></tr>';
-        return;
-      }
-
-      tbody.innerHTML = filtered.map(row => `
-        <tr>
-          <td class="action-links">
-            <a class="edit" onclick="openCampaignModal('edit', '${row.id}')">编辑</a>
-            <a class="view" onclick="openCampaignModal('view', '${row.id}')">查看</a>
-            <a class="delete" onclick="confirmDelete('campaign', '${row.id}')">删除</a>
-          </td>
-          <td>${row.id}</td>
-          <td>${row.name}</td>
-          <td>${row.owner}</td>
-          <td class="${row.staff ? "" : "empty-tip"}">${row.staff || "-"}</td>
-          <td>${row.createTime}</td>
-          <td>${row.updateTime}</td>
-        </tr>
-      `).join("");
-    }
-
-    function openCampaignModal(mode, id = null) {
-      editingCampaignId = id;
-      const nameInp = document.getElementById('f-cp-name');
-      const ownerInp = document.getElementById('f-cp-owner');
-      const staffInp = document.getElementById('f-cp-staff');
-      
-      nameInp.disabled = false;
-      ownerInp.disabled = false;
-      staffInp.disabled = false;
-      document.getElementById('modal-cp-footer').classList.remove('hide');
-
-      if (mode === 'edit' || mode === 'view') {
-        const c = campaigns.find(x => x.id === id);
-        document.getElementById('modalCpTitle').innerText = mode === 'edit' ? '编辑 Campaign' : '查看 Campaign';
-        document.getElementById('cpIdRow').classList.remove('hide');
-        document.getElementById('f-cp-id').value = c.id;
-        nameInp.value = c.name;
-        ownerInp.value = c.owner;
-        staffInp.value = c.staff;
-
-        if (mode === 'view') {
-          nameInp.disabled = true;
-          ownerInp.disabled = true;
-          staffInp.disabled = true;
-          document.getElementById('modal-cp-footer').classList.add('hide');
-        }
-      } else {
-        document.getElementById('modalCpTitle').innerText = '新建 Campaign';
-        document.getElementById('cpIdRow').classList.add('hide');
-        nameInp.value = '';
-        ownerInp.value = '';
-        staffInp.value = '';
-      }
-      document.getElementById('campaignModal').classList.add('show');
-    }
-
-    function submitCampaignModal() {
-      const name = document.getElementById('f-cp-name').value.trim();
-      const owner = document.getElementById('f-cp-owner').value.trim();
-      const staff = document.getElementById('f-cp-staff').value.trim();
-      const now = getNowString();
-
-      if(!name || !owner) return showToast("名称和负责人是必填项");
-
-      if (editingCampaignId) {
-        const idx = campaigns.findIndex(x => x.id === editingCampaignId);
-        campaigns[idx] = { ...campaigns[idx], name, owner, staff, updateTime: now };
-        showToast("保存成功");
-      } else {
-        const newId = `CP${String(campaigns.length + 1).padStart(4, "0")}`;
-        campaigns.unshift({ id: newId, name, owner, staff, createTime: now, updateTime: now });
-        showToast("新建成功");
-      }
-      closeModal('campaignModal');
-      renderCampaignTable();
-    }
-
-    // ================= 通用功能 =================
-    function resetSearch(view) {
-      if (view === 'channel') {
-        document.getElementById('s-ch-id').value = '';
-        document.getElementById('s-ch-type').value = '';
-        document.getElementById('s-ch-category').value = '';
-        document.getElementById('s-ch-name').value = '';
-        document.getElementById('s-ch-subname').value = '';
-        document.getElementById('s-ch-staff').value = '';
-        renderChannelTable();
-      } else {
-        document.getElementById('s-cp-id').value = '';
-        document.getElementById('s-cp-name').value = '';
-        document.getElementById('s-cp-owner').value = '';
-        document.getElementById('s-cp-staff').value = '';
-        renderCampaignTable();
-      }
-    }
-
-    function confirmDelete(type, id) {
-      deleteTarget = { type, id };
-      document.getElementById('confirmModal').classList.add('show');
-    }
-
-    document.getElementById('btn-confirm-delete').onclick = function() {
-      if (deleteTarget.type === 'channel') {
-        channelRows = channelRows.filter(c => c.id !== deleteTarget.id);
-        renderChannelTable();
-      } else {
-        campaigns = campaigns.filter(c => c.id !== deleteTarget.id);
-        renderCampaignTable();
-      }
-      closeModal('confirmModal');
-      showToast("删除成功");
-    }
-
-    // 初始化
-    window.onload = function() {
-      switchView('channel');
-    };
-  </script>
-</body>
-</html>

+ 48 - 0
pc/package-lock.json

@@ -24,6 +24,7 @@
         "@vue/cli-service": "~5.0.0",
         "eslint": "^7.32.0",
         "eslint-plugin-vue": "^8.0.3",
+        "playwright": "^1.60.0",
         "vue-template-compiler": "^2.6.14"
       }
     },
@@ -9013,6 +9014,53 @@
         "node": ">=8"
       }
     },
+    "node_modules/playwright": {
+      "version": "1.60.0",
+      "resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.60.0.tgz",
+      "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "playwright-core": "1.60.0"
+      },
+      "bin": {
+        "playwright": "cli.js"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "fsevents": "2.3.2"
+      }
+    },
+    "node_modules/playwright-core": {
+      "version": "1.60.0",
+      "resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.60.0.tgz",
+      "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "playwright-core": "cli.js"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/playwright/node_modules/fsevents": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
+      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
     "node_modules/portfinder": {
       "version": "1.0.38",
       "resolved": "https://registry.npmmirror.com/portfinder/-/portfinder-1.0.38.tgz",

+ 1 - 0
pc/package.json

@@ -24,6 +24,7 @@
     "@vue/cli-service": "~5.0.0",
     "eslint": "^7.32.0",
     "eslint-plugin-vue": "^8.0.3",
+    "playwright": "^1.60.0",
     "vue-template-compiler": "^2.6.14"
   },
   "eslintConfig": {

+ 40 - 0
pc/scripts/capture-one.js

@@ -0,0 +1,40 @@
+/* eslint-disable */
+// 测试脚本:只截一张图(借记卡权益列表)
+const { chromium } = require('playwright')
+const path = require('path')
+const fs = require('fs')
+
+const BASE = 'http://localhost:8080'
+const OUT_DIR = path.resolve(__dirname, '../../prd/汇丰银行/screenshots')
+
+;(async () => {
+  console.log('启动 chromium...')
+  const browser = await chromium.launch({ headless: true })
+  const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2 })
+  const page = await context.newPage()
+
+  console.log('访问登录页注入 token...')
+  await page.goto(BASE + '/login', { waitUntil: 'domcontentloaded' })
+  await page.evaluate(() => {
+    localStorage.setItem('token', 'mock-token-' + Date.now())
+    localStorage.setItem('username', 'admin')
+  })
+
+  const url = BASE + '/#/customization/hsbc/equity-products'
+  console.log('访问', url)
+  try {
+    await page.goto(url, { waitUntil: 'networkidle', timeout: 15000 })
+  } catch (e) {
+    console.log('networkidle 超时,降级 domcontentloaded')
+    await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 })
+  }
+  await page.waitForTimeout(1500)
+
+  if (!fs.existsSync(OUT_DIR)) fs.mkdirSync(OUT_DIR, { recursive: true })
+  const file = path.join(OUT_DIR, '2-1-equity-products-list.png')
+  await page.screenshot({ path: file, fullPage: true })
+  const size = (fs.statSync(file).size / 1024).toFixed(0)
+  console.log(`✓ ${file}  (${size}KB)`)
+
+  await browser.close()
+})()

+ 120 - 0
pc/scripts/capture-screenshots.js

@@ -0,0 +1,120 @@
+/* eslint-disable */
+// 自动截图脚本:登录后逐页访问、等待加载、截图保存到 prd/汇丰银行/screenshots/
+// 用法:node pc/scripts/capture-screenshots.js
+// 前置:pc/ 下 npm run serve 已在 http://localhost:8080 运行
+
+const { chromium } = require('playwright')
+const path = require('path')
+const fs = require('fs')
+
+const BASE = 'http://localhost:8080'
+const OUT_DIR = path.resolve(__dirname, '../../prd/汇丰银行/screenshots')
+
+// 页面清单:{ name: 输出文件名(不带扩展名), path: 路由, step?: 多步骤页的某一步, wait?: 额外等待 ms, action?: 截图前执行的函数 }
+const PAGES = [
+  // §2 借记卡权益
+  { name: '2-1-equity-products-list', path: '/customization/hsbc/equity-products' },
+  { name: '2-2-equity-add-step1', path: '/customization/hsbc/equity-products/add' },
+  { name: '2-2-equity-add-step2', path: '/customization/hsbc/equity-products/add', step: 2 },
+  { name: '2-2-equity-add-step3', path: '/customization/hsbc/equity-products/add', step: 3 },
+  { name: '2-3-equity-edit', path: '/customization/hsbc/equity-products/edit/Q100006' },
+  { name: '2-4-equity-view', path: '/customization/hsbc/equity-products/view/Q100001' },
+  // §3 信用卡权益
+  { name: '3-credit-card-equity-list', path: '/customization/hsbc/credit-card-equity' },
+  // §6 权益审核
+  { name: '6-audit-list', path: '/customization/hsbc/audit' },
+  { name: '6-audit-detail', path: '/customization/hsbc/audit/AR202605150001' },
+  // §三 人群管理
+  { name: 'crowd-task-list', path: '/customization/hsbc/crowd-tasks' },
+  { name: 'crowd-task-add-step1', path: '/customization/hsbc/crowd-tasks/add' },
+  { name: 'crowd-task-add-step2', path: '/customization/hsbc/crowd-tasks/add', step: 2 },
+  { name: 'crowd-name-list-add-step1', path: '/customization/hsbc/crowd-name-lists/add' },
+  { name: 'crowd-name-list-add-step2', path: '/customization/hsbc/crowd-name-lists/add', step: 2 },
+  { name: 'rule-group-list', path: '/customization/hsbc/rule-groups' },
+  { name: 'rule-group-add-step1', path: '/customization/hsbc/rule-groups/add' },
+  { name: 'rule-group-add-step2', path: '/customization/hsbc/rule-groups/add', step: 2 },
+  // §四 发放计划
+  { name: '4-grant-campaign-list', path: '/customization/hsbc/equity-grant-campaign' },
+  { name: '4-grant-campaign-add', path: '/customization/hsbc/equity-grant-campaign/add' },
+  // §五 页面管理
+  { name: '5-page-home', path: '/customization/hsbc/pages/home' },
+  { name: '5-page-zone', path: '/customization/hsbc/pages/zone' },
+  { name: '5-page-assembly', path: '/customization/hsbc/pages/assembly' },
+  // Channel/Campaign
+  { name: 'channel-management', path: '/customization/hsbc/channel' },
+  { name: 'campaign-management', path: '/customization/hsbc/campaign' },
+  // §六 短信管理
+  { name: '6-sms-template-list', path: '/customization/hsbc/sms-templates' },
+  { name: '6-sms-send-record', path: '/customization/hsbc/sms-send-records' },
+  // §七 报表中心
+  { name: '7-points-detail', path: '/customization/hsbc/points-detail' },
+  { name: '7-customer-equity-grant', path: '/customization/hsbc/equity-grant' },
+  { name: '7-cash-discount-grant', path: '/customization/hsbc/cash-discount-grant' }
+]
+
+async function waitDevServer() {
+  const fetchUrl = BASE + '/'
+  for (let i = 0; i < 60; i++) {
+    try {
+      const res = await fetch(fetchUrl)
+      if (res.ok || res.status === 200) return true
+    } catch (_) { /* not ready */ }
+    await new Promise(r => setTimeout(r, 1000))
+  }
+  throw new Error('dev server 未在 60s 内就绪,请确认 npm run serve 已启动')
+}
+
+async function gotoStep(page, stepIdx) {
+  // 三步骤向导(添加/编辑/规则组/客群任务)通过点击「下一步」翻到目标 step
+  // stepIdx 是 1-based 业务步骤;首屏已是 step 1,则点击 stepIdx-1 次「下一步」
+  for (let i = 1; i < stepIdx; i++) {
+    const nextBtn = page.locator('button:has-text("下一步")').first()
+    if (await nextBtn.count() === 0) break
+    await nextBtn.click().catch(() => {})
+    await page.waitForTimeout(400)
+  }
+}
+
+;(async () => {
+  console.log('[1/4] 等待 dev server 就绪...')
+  await waitDevServer()
+  console.log('     dev server OK')
+
+  console.log('[2/4] 启动 chromium...')
+  const browser = await chromium.launch({ headless: true })
+  // 视口宽高按 1/0.8 放大,让一屏容纳更多内容(等价于"缩放 80%"的视觉效果)
+  const context = await browser.newContext({ viewport: { width: 1800, height: 1125 }, deviceScaleFactor: 2 })
+  const page = await context.newPage()
+
+  console.log('[3/4] 注入 token 跳过登录...')
+  await page.goto(BASE + '/#/login', { waitUntil: 'domcontentloaded' })
+  await page.evaluate(() => {
+    localStorage.setItem('token', 'mock-token-' + Date.now())
+    localStorage.setItem('username', 'admin')
+  })
+
+  console.log(`[4/4] 开始截图(共 ${PAGES.length} 张)...`)
+  const results = []
+  for (const item of PAGES) {
+    const url = BASE + '/#' + item.path
+    try {
+      await page.goto(url, { waitUntil: 'networkidle', timeout: 15000 })
+    } catch (_) {
+      // networkidle 在 dev server 热更新场景下可能 timeout,降级用 domcontentloaded
+      await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 })
+    }
+    await page.waitForTimeout(800)
+    if (item.step && item.step > 1) {
+      await gotoStep(page, item.step)
+      await page.waitForTimeout(400)
+    }
+    const file = path.join(OUT_DIR, item.name + '.png')
+    await page.screenshot({ path: file, fullPage: true })
+    const size = (fs.statSync(file).size / 1024).toFixed(0)
+    console.log(`     ✓ ${item.name}.png  (${size}KB)`)
+    results.push({ name: item.name, file })
+  }
+
+  await browser.close()
+  console.log(`\n完成。共 ${results.length} 张,输出目录:${OUT_DIR}`)
+})()

+ 171 - 0
pc/scripts/capture-supplements.js

@@ -0,0 +1,171 @@
+/* eslint-disable */
+// B 类补截:每张图独立 try-catch
+const { chromium } = require('playwright')
+const path = require('path')
+
+const BASE = 'http://localhost:8080'
+const OUT_DIR = path.resolve(__dirname, '../../prd/汇丰银行/screenshots')
+
+async function shot(page, name, full = false) {
+  await page.screenshot({ path: path.join(OUT_DIR, name + '.png'), fullPage: full })
+  console.log('✓', name)
+}
+
+async function safe(label, fn) {
+  try { await fn() } catch (e) { console.log('✗', label, '—', e.message.split('\n')[0]) }
+}
+
+;(async () => {
+  const browser = await chromium.launch({ headless: true })
+  const context = await browser.newContext({ viewport: { width: 1800, height: 1125 }, deviceScaleFactor: 2 })
+  const page = await context.newPage()
+
+  await page.goto(BASE + '/#/login', { waitUntil: 'domcontentloaded' })
+  await page.evaluate(() => {
+    localStorage.setItem('token', 'mock-token-' + Date.now())
+    localStorage.setItem('username', 'admin')
+  })
+
+  // ---- 详情页(3 张)----
+  for (const item of [
+    { name: 'crowd-task-view', url: '/#/customization/hsbc/crowd-tasks/view/00000120251224' },
+    { name: 'rule-group-view', url: '/#/customization/hsbc/rule-groups/view/RG20260401001' },
+    { name: '4-grant-campaign-view', url: '/#/customization/hsbc/equity-grant-campaign/view/P000001' }
+  ]) {
+    await safe(item.name, async () => {
+      await page.goto(BASE + item.url, { waitUntil: 'domcontentloaded' })
+      await page.waitForTimeout(1200)
+      await shot(page, item.name, true)
+    })
+  }
+
+  // ---- 添加权益 4 个分支 ----
+  const goAdd = async () => {
+    await page.goto(BASE + '/#/customization/hsbc/equity-products/add', { waitUntil: 'domcontentloaded' })
+    await page.waitForTimeout(800)
+  }
+
+  await safe('2-2-equity-add-source-rongshu', async () => {
+    await goAdd()
+    await page.locator('label.el-radio:has-text("荣数商品")').first().click()
+    await page.waitForTimeout(400)
+    await shot(page, '2-2-equity-add-source-rongshu', true)
+  })
+
+  await safe('2-2-equity-add-third-list', async () => {
+    await goAdd()
+    await page.locator('label.el-radio:has-text("第三方")').first().click()
+    await page.waitForTimeout(400)
+    await shot(page, '2-2-equity-add-third-list', true)
+  })
+
+  await safe('2-2-equity-add-third-detail', async () => {
+    await goAdd()
+    await page.locator('label.el-radio:has-text("第三方")').first().click()
+    await page.waitForTimeout(300)
+    await page.locator('label.el-radio-button:has-text("有")').first().click()
+    await page.waitForTimeout(400)
+    await shot(page, '2-2-equity-add-third-detail', true)
+  })
+
+  await safe('2-2-equity-add-third-miniprogram', async () => {
+    await goAdd()
+    await page.locator('label.el-radio:has-text("第三方")').first().click()
+    await page.waitForTimeout(300)
+    await page.locator('.el-form-item:has-text("外部权益链接") .el-select').click()
+    await page.waitForTimeout(300)
+    await page.locator('.el-select-dropdown__item:has-text("小程序链接")').click()
+    await page.waitForTimeout(400)
+    await shot(page, '2-2-equity-add-third-miniprogram', true)
+  })
+
+  // ---- 关键弹窗 ----
+  // 下架确认(权益A 已上架 + 审核通过)
+  await safe('dialog-offshelf-confirm', async () => {
+    await page.goto(BASE + '/#/customization/hsbc/equity-products', { waitUntil: 'domcontentloaded' })
+    await page.waitForTimeout(1000)
+    // 通过权益A 这一行的"下架"按钮
+    await page.locator('tr:has-text("权益A") button:has-text("下架")').first().click({ force: true })
+    await page.waitForTimeout(500)
+    await shot(page, 'dialog-offshelf-confirm', false)
+    await page.locator('.el-message-box__btns button:has-text("取消")').click({ force: true }).catch(() => {})
+    await page.waitForTimeout(300)
+  })
+
+  // 删除确认(权益F 草稿状态)
+  await safe('dialog-delete-confirm', async () => {
+    await page.goto(BASE + '/#/customization/hsbc/equity-products', { waitUntil: 'domcontentloaded' })
+    await page.waitForTimeout(1000)
+    await page.locator('tr:has-text("权益F(草稿)") button.el-button--text:has(i.el-icon-more)').first().click({ force: true })
+    await page.waitForTimeout(500)
+    await page.evaluate(() => {
+      const el = [...document.querySelectorAll('.more-actions-item')]
+        .find(e => e.textContent.trim() === '删除' && e.offsetParent !== null)
+      el && el.click()
+    })
+    await page.waitForTimeout(500)
+    await shot(page, 'dialog-delete-confirm', false)
+    await page.locator('.el-message-box__btns button:has-text("取消")').click({ force: true }).catch(() => {})
+    await page.waitForTimeout(300)
+  })
+
+  // 调整顺序气泡(权益A 行)
+  await safe('popover-adjust-order', async () => {
+    await page.goto(BASE + '/#/customization/hsbc/equity-products', { waitUntil: 'domcontentloaded' })
+    await page.waitForTimeout(1000)
+    await page.locator('tr:has-text("权益A") button.el-button--text:has(i.el-icon-more)').first().click({ force: true })
+    await page.waitForTimeout(500)
+    await page.evaluate(() => {
+      const el = [...document.querySelectorAll('.more-actions-item')]
+        .find(e => e.textContent.trim() === '调整显示顺序' && e.offsetParent !== null)
+      el && el.click()
+    })
+    await page.waitForTimeout(500)
+    await shot(page, 'popover-adjust-order', false)
+    // 关闭:点空白处
+    await page.mouse.click(10, 10).catch(() => {})
+    await page.waitForTimeout(300)
+  })
+
+  // 设置显示页面对话框(信用卡列表)
+  await safe('dialog-show-pages', async () => {
+    await page.goto(BASE + '/#/customization/hsbc/credit-card-equity', { waitUntil: 'domcontentloaded' })
+    await page.waitForTimeout(1000)
+    await page.locator('button:has-text("设置显示页面")').first().click({ force: true })
+    await page.waitForTimeout(700)
+    await shot(page, 'dialog-show-pages', false)
+    await page.locator('.el-dialog__footer button:has-text("取消")').click({ force: true }).catch(() => {})
+    await page.waitForTimeout(300)
+  })
+
+  // 生成链接弹窗
+  await safe('dialog-equity-link', async () => {
+    await page.goto(BASE + '/#/customization/hsbc/equity-products', { waitUntil: 'domcontentloaded' })
+    await page.waitForTimeout(1000)
+    await page.locator('tr:has-text("权益A") button.el-button--text:has(i.el-icon-more)').first().click({ force: true })
+    await page.waitForTimeout(500)
+    await page.evaluate(() => {
+      const el = [...document.querySelectorAll('.more-actions-item')]
+        .find(e => e.textContent.trim() === '生成链接' && e.offsetParent !== null)
+      el && el.click()
+    })
+    await page.waitForTimeout(700)
+    await shot(page, 'dialog-equity-link', false)
+    await page.locator('.el-dialog__headerbtn').click({ force: true }).catch(() => {})
+    await page.waitForTimeout(300)
+  })
+
+  // 荣数商品选择弹窗
+  await safe('dialog-rongshu-product', async () => {
+    await page.goto(BASE + '/#/customization/hsbc/equity-products/add', { waitUntil: 'domcontentloaded' })
+    await page.waitForTimeout(1000)
+    await page.locator('label.el-radio:has-text("荣数商品")').first().click()
+    await page.waitForTimeout(500)
+    await page.locator('button:has-text("选择")').first().click({ force: true })
+    await page.waitForTimeout(800)
+    await shot(page, 'dialog-rongshu-product', false)
+  })
+
+  await browser.close()
+  console.log('\nB 类截图完成。')
+})()

+ 12 - 15
pc/src/components/RongshuProductDialog.vue

@@ -49,21 +49,18 @@
 
 <script>
 const MOCK_PRODUCTS = [
-  { name: '荣数机场贵宾厅', merchant: '环亚机场贵宾', settlePrice: 88, stock: 200, mainImage: [{ name: 'rongshu-a.png', url: 'https://placehold.co/200x200/4096ff/ffffff?text=A' }] },
-  { name: '荣数高尔夫体验', merchant: '尊享高尔夫俱乐部', settlePrice: 388, stock: 50, mainImage: [{ name: 'rongshu-b.png', url: 'https://placehold.co/200x200/52c41a/ffffff?text=B' }] },
-  { name: '荣数洗车服务', merchant: '车之家精洗', settlePrice: 48, stock: 800, mainImage: [{ name: 'rongshu-c.png', url: 'https://placehold.co/200x200/faad14/ffffff?text=C' }] },
-  { name: '荣数体检套餐', merchant: '美年大健康', settlePrice: 988, stock: 120, mainImage: [{ name: 'rongshu-d.png', url: 'https://placehold.co/200x200/eb2f96/ffffff?text=D' }] },
-  { name: '荣数咖啡券', merchant: 'Starbucks', settlePrice: 28, stock: 1000, mainImage: [] },
-  { name: '荣数视频会员月卡', merchant: '腾讯视频', settlePrice: 25, stock: 500, mainImage: [] },
-  { name: '荣数音乐会员季卡', merchant: 'QQ音乐', settlePrice: 45, stock: 600, mainImage: [] },
-  { name: '荣数电影票券', merchant: '猫眼电影', settlePrice: 38, stock: 300, mainImage: [] },
-  { name: '荣数家政服务券', merchant: '58到家', settlePrice: 158, stock: 200, mainImage: [] },
-  { name: '荣数美甲服务券', merchant: '河狸家', settlePrice: 88, stock: 100, mainImage: [] },
-  { name: '荣数车辆保养券', merchant: '途虎养车', settlePrice: 268, stock: 80, mainImage: [] },
-  { name: '荣数读书会员', merchant: '微信读书', settlePrice: 18, stock: 600, mainImage: [] },
-  { name: '荣数花艺礼盒', merchant: '花点时间', settlePrice: 199, stock: 50, mainImage: [] },
-  { name: '荣数瑜伽课程券', merchant: 'Keep', settlePrice: 128, stock: 90, mainImage: [] },
-  { name: '荣数智能家居体验', merchant: '小米之家', settlePrice: 358, stock: 30, mainImage: [] }
+  { name: '微信立减金 5元(满10减5)', merchant: '微信支付', settlePrice: 5, stock: 2000, mainImage: [{ name: 'wx-5.png', url: 'https://placehold.co/200x200/07c160/ffffff?text=%C2%A55' }] },
+  { name: '微信立减金 8元(满30减8)', merchant: '微信支付', settlePrice: 8, stock: 1500, mainImage: [{ name: 'wx-8.png', url: 'https://placehold.co/200x200/07c160/ffffff?text=%C2%A58' }] },
+  { name: '微信立减金 10元(满50减10)', merchant: '微信支付', settlePrice: 10, stock: 1200, mainImage: [{ name: 'wx-10.png', url: 'https://placehold.co/200x200/07c160/ffffff?text=%C2%A510' }] },
+  { name: '微信立减金 18元(满88减18)', merchant: '微信支付', settlePrice: 18, stock: 1000, mainImage: [{ name: 'wx-18.png', url: 'https://placehold.co/200x200/07c160/ffffff?text=%C2%A518' }] },
+  { name: '微信立减金 20元(满100减20)', merchant: '微信支付', settlePrice: 20, stock: 800, mainImage: [{ name: 'wx-20.png', url: 'https://placehold.co/200x200/07c160/ffffff?text=%C2%A520' }] },
+  { name: '微信立减金 30元(满150减30)', merchant: '微信支付', settlePrice: 30, stock: 600, mainImage: [{ name: 'wx-30.png', url: 'https://placehold.co/200x200/07c160/ffffff?text=%C2%A530' }] },
+  { name: '微信立减金 50元(满200减50)', merchant: '微信支付', settlePrice: 50, stock: 400, mainImage: [{ name: 'wx-50.png', url: 'https://placehold.co/200x200/07c160/ffffff?text=%C2%A550' }] },
+  { name: '微信立减金 66元(满300减66)', merchant: '微信支付', settlePrice: 66, stock: 300, mainImage: [{ name: 'wx-66.png', url: 'https://placehold.co/200x200/07c160/ffffff?text=%C2%A566' }] },
+  { name: '微信立减金 88元(满388减88)', merchant: '微信支付', settlePrice: 88, stock: 200, mainImage: [{ name: 'wx-88.png', url: 'https://placehold.co/200x200/07c160/ffffff?text=%C2%A588' }] },
+  { name: '微信立减金 100元(满500减100)', merchant: '微信支付', settlePrice: 100, stock: 150, mainImage: [{ name: 'wx-100.png', url: 'https://placehold.co/200x200/07c160/ffffff?text=%C2%A5100' }] },
+  { name: '微信立减金 128元(满600减128)', merchant: '微信支付', settlePrice: 128, stock: 100, mainImage: [{ name: 'wx-128.png', url: 'https://placehold.co/200x200/07c160/ffffff?text=%C2%A5128' }] },
+  { name: '微信立减金 200元(满800减200)', merchant: '微信支付', settlePrice: 200, stock: 80, mainImage: [{ name: 'wx-200.png', url: 'https://placehold.co/200x200/07c160/ffffff?text=%C2%A5200' }] }
 ]
 
 export default {

+ 4 - 72
pc/src/mock/index.js

@@ -366,13 +366,12 @@ Mock.mock(/\/api\/campaign\/delete/, 'post', (options) => {
 // =============================================
 
 // 商品状态:未上架 / 已上架
-// 审核状态:草稿 / 上架待审核 / 编辑待审核 / 下架待审核 / 审核通过
-// 合法组合(6 种):
+// 审核状态:草稿 / 上架待审核 / 下架待审核 / 审核通过
+// 合法组合(5 种):
 //   未上架+草稿        = 草稿
 //   未上架+上架待审核   = 新增待审核
 //   未上架+审核通过     = 已下架
 //   已上架+审核通过     = 已上架
-//   已上架+编辑待审核   = 已上架·变更待审核
 //   已上架+下架待审核   = 已上架·下架待审核
 const equityList = [
   {
@@ -391,7 +390,6 @@ const equityList = [
     reviewTime: '2026-04-15 10:30:00',
     shelfStatus: '已上架',
     auditStatus: '审核通过',
-    pendingData: null,
     showOnHome: '显示',
     showOnAssembly: '不显示',
     assemblyPages: []
@@ -412,7 +410,6 @@ const equityList = [
     reviewTime: '',
     shelfStatus: '未上架',
     auditStatus: '上架待审核',
-    pendingData: null,
     showOnHome: '显示',
     showOnAssembly: '不显示',
     assemblyPages: []
@@ -432,8 +429,7 @@ const equityList = [
     reviewer: '王五',
     reviewTime: '2026-04-14 16:45:00',
     shelfStatus: '已上架',
-    auditStatus: '编辑待审核',
-    pendingData: { equityName: '权益C(变更)', subTitle: '调整后的副标题' },
+    auditStatus: '审核通过',
     showOnHome: '显示',
     showOnAssembly: '不显示',
     assemblyPages: []
@@ -454,7 +450,6 @@ const equityList = [
     reviewTime: '2026-04-13 09:15:00',
     shelfStatus: '已上架',
     auditStatus: '下架待审核',
-    pendingData: null,
     showOnHome: '显示',
     showOnAssembly: '不显示',
     assemblyPages: []
@@ -475,7 +470,6 @@ const equityList = [
     reviewTime: '2026-04-12 11:20:00',
     shelfStatus: '未上架',
     auditStatus: '审核通过',
-    pendingData: null,
     showOnHome: '显示',
     showOnAssembly: '不显示',
     assemblyPages: []
@@ -496,7 +490,6 @@ const equityList = [
     reviewTime: '',
     shelfStatus: '未上架',
     auditStatus: '草稿',
-    pendingData: null,
     showOnHome: '不显示',
     showOnAssembly: '不显示',
     assemblyPages: []
@@ -533,33 +526,6 @@ const auditList = [
     auditStatus: 'pending',
     auditComment: ''
   },
-  {
-    id: 'AR202605160001',
-    auditType: 'change',
-    bizModule: 'equity_product',
-    bizModuleName: '借记卡权益',
-    bizId: 'Q100003',
-    bizName: '权益C',
-    snapshot: {
-      equityName: '权益C(变更)',
-      source: '第三方',
-      jumpMethod: '有',
-      jumpLink: '龙腾',
-      productType: '',
-      skuNo: 'SKU003',
-      merchantName: '商户C',
-      subTitle: '调整后的副标题'
-    },
-    submitReason: '调整副标题与名称',
-    submitterId: 'u002',
-    submitterName: '李运营',
-    submitTime: '2026-05-16 14:30:00',
-    auditorId: null,
-    auditorName: '',
-    auditTime: '',
-    auditStatus: 'pending',
-    auditComment: ''
-  },
   {
     id: 'AR202605170001',
     auditType: 'offshelf',
@@ -700,7 +666,6 @@ Mock.mock(/\/api\/equity\/save-draft/, 'post', (options) => {
     reviewTime: '',
     shelfStatus: '未上架',
     auditStatus: '草稿',
-    pendingData: null,
     showOnHome: body.showOnHome || '不显示',
     showOnAssembly: body.showOnAssembly || '不显示',
     assemblyPages: body.assemblyPages || [],
@@ -744,7 +709,6 @@ Mock.mock(/\/api\/equity\/submit/, 'post', (options) => {
       reviewTime: '',
       shelfStatus: '未上架',
       auditStatus: '上架待审核',
-      pendingData: null,
       showOnHome: body.showOnHome || '不显示',
       showOnAssembly: body.showOnAssembly || '不显示',
       assemblyPages: body.assemblyPages || [],
@@ -821,33 +785,7 @@ Mock.mock(/\/api\/equity\/submit/, 'post', (options) => {
     return { code: 200, data: { productId: cur.productId }, msg: '已提交审核' }
   }
 
-  // 已上架 → 编辑待审核(change),写入 pendingData,不动主表运行字段
-  if (cur.shelfStatus === '已上架' && cur.auditStatus === '审核通过') {
-    const pending = { ...body }
-    delete pending.submitReason
-    delete pending.submitterName
-    delete pending.productId
-    equityList[idx] = { ...cur, auditStatus: '编辑待审核', pendingData: pending }
-    auditList.unshift({
-      id: nextAuditId(),
-      auditType: 'change',
-      bizModule: 'equity_product',
-      bizModuleName: '借记卡权益',
-      bizId: cur.productId,
-      bizName: cur.equityName,
-      snapshot: { ...snapshotOf(cur), ...pending },
-      submitReason,
-      submitterId: 'u001',
-      submitterName: submitter,
-      submitTime: nowStr(),
-      auditorId: null,
-      auditorName: '',
-      auditTime: '',
-      auditStatus: 'pending',
-      auditComment: ''
-    })
-    return { code: 200, data: { productId: cur.productId }, msg: '已提交审核' }
-  }
+  // 已上架商品禁止直接编辑(需先走下架审核)
 
   return { code: 400, data: null, msg: '当前状态不可提交审核' }
 })
@@ -932,10 +870,6 @@ Mock.mock(/\/api\/audit\/approve/, 'post', (options) => {
 
   if (audit.auditType === 'new') {
     equityList[eqIdx] = { ...eq, shelfStatus: '已上架', auditStatus: '审核通过', reviewer: auditor, reviewTime: now }
-  } else if (audit.auditType === 'change') {
-    // 合并 pendingData 到主表
-    const merged = { ...eq, ...(eq.pendingData || {}) }
-    equityList[eqIdx] = { ...merged, shelfStatus: '已上架', auditStatus: '审核通过', pendingData: null, reviewer: auditor, reviewTime: now }
   } else if (audit.auditType === 'offshelf') {
     equityList[eqIdx] = { ...eq, shelfStatus: '未上架', auditStatus: '审核通过', reviewer: auditor, reviewTime: now }
   }
@@ -961,8 +895,6 @@ Mock.mock(/\/api\/audit\/reject/, 'post', (options) => {
   // 驳回后权益主表回退到提审前的状态
   if (audit.auditType === 'new') {
     equityList[eqIdx] = { ...eq, shelfStatus: '未上架', auditStatus: '草稿' }
-  } else if (audit.auditType === 'change') {
-    equityList[eqIdx] = { ...eq, shelfStatus: '已上架', auditStatus: '审核通过', pendingData: null }
   } else if (audit.auditType === 'offshelf') {
     equityList[eqIdx] = { ...eq, shelfStatus: '已上架', auditStatus: '审核通过' }
   }

+ 5 - 5
pc/src/views/AddEquityProduct.vue

@@ -22,9 +22,9 @@
               <el-col :span="8">
                 <el-form-item label="权益来源" required>
                   <el-radio-group v-model="form.source">
-                    <el-radio-button label="银行自有"></el-radio-button>
-                    <el-radio-button label="荣数商品"></el-radio-button>
-                    <el-radio-button label="第三方"></el-radio-button>
+                    <el-radio label="银行自有"></el-radio>
+                    <el-radio label="荣数商品"></el-radio>
+                    <el-radio label="第三方"></el-radio>
                   </el-radio-group>
                 </el-form-item>
               </el-col>
@@ -42,7 +42,7 @@
               <el-col :span="8" v-if="form.source === '银行自有'">
                 <el-form-item label="权益类型" required>
                   <el-radio-group v-model="form.productType">
-                    <el-radio-button label="券码"></el-radio-button>
+                    <el-radio label="券码"></el-radio>
                   </el-radio-group>
                 </el-form-item>
               </el-col>
@@ -417,7 +417,7 @@
                 <template v-if="form.claimGroupEnabled">
                   <div style="width: 600px;background: #f1f2f5;padding: 20px;margin-top: 10px;">
                     <el-alert
-                      title="需在【发放计划】中选择发放计划,未发放的权益移动端不展示"
+                      title="需在【发放计划】中选择权益发放,未发放的权益移动端不展示"
                       type="info"
                       :closable="false"
                       show-icon

+ 3 - 3
pc/src/views/AuditDetail.vue

@@ -365,7 +365,7 @@ export default {
     canAudit() {
       if (!this.audit || this.audit.auditStatus !== 'pending') return false
       const t = this.audit.auditType
-      if (t === 'new' || t === 'change') return this.activeStep === 2
+      if (t === 'new') return this.activeStep === 2
       return true
     }
   },
@@ -397,10 +397,10 @@ export default {
       }
     },
     auditTypeLabel(t) {
-      return { new: '新增', change: '变更', offshelf: '下架' }[t] || t
+      return { new: '新增', offshelf: '下架' }[t] || t
     },
     auditTypeTag(t) {
-      return { new: 'success', change: '', offshelf: 'danger' }[t] || ''
+      return { new: 'success', offshelf: 'danger' }[t] || ''
     },
     auditStatusLabel(s) {
       return { pending: '待审核', approved: '已通过', rejected: '已驳回' }[s] || s

+ 1 - 1
pc/src/views/AuditEquityProduct.vue

@@ -310,7 +310,7 @@
                 <template v-if="form.claimGroupEnabled">
                   <div style="width: 600px;background: #f1f2f5;padding: 20px;margin-top: 10px;">
                     <el-alert
-                      title="需在【发放计划】中选择发放计划,未发放的权益移动端不展示"
+                      title="需在【发放计划】中选择权益发放,未发放的权益移动端不展示"
                       type="info"
                       :closable="false"
                       show-icon

+ 4 - 4
pc/src/views/AuditList.vue

@@ -65,8 +65,8 @@
         <span>Total {{ tableData.length }}</span>
         <el-pagination
           layout="prev, pager, next, sizes, jumper"
-          :page-size="100"
-          :page-sizes="[100, 200, 500]"
+          :page-size="10"
+          :page-sizes="[10, 20, 50]"
           :total="tableData.length"
         ></el-pagination>
       </div>
@@ -115,10 +115,10 @@ export default {
       this.loadData()
     },
     auditTypeLabel(t) {
-      return { new: '新增', change: '变更', offshelf: '下架' }[t] || t
+      return { new: '新增', offshelf: '下架' }[t] || t
     },
     auditTypeTag(t) {
-      return { new: 'success', change: '', offshelf: 'danger' }[t] || ''
+      return { new: 'success', offshelf: 'danger' }[t] || ''
     },
     auditStatusLabel(s) {
       return { pending: '待审核', approved: '已通过', rejected: '已驳回' }[s] || s

+ 2 - 2
pc/src/views/CreditCardEquity.vue

@@ -85,8 +85,8 @@
         <span>Total {{ filteredTableData.length }}</span>
         <el-pagination
           layout="prev, pager, next, sizes, jumper"
-          :page-size="100"
-          :page-sizes="[100, 200, 500]"
+          :page-size="10"
+          :page-sizes="[10, 20, 50]"
           :total="filteredTableData.length"
         ></el-pagination>
       </div>

+ 6 - 8
pc/src/views/EditEquityProduct.vue

@@ -23,9 +23,9 @@
               <el-form-item label="权益来源" required>
                 <!-- 编辑时不允许修改权益来源 -->
                 <el-radio-group v-model="form.source" disabled>
-                  <el-radio-button label="银行自有"></el-radio-button>
-                  <el-radio-button label="荣数商品"></el-radio-button>
-                  <el-radio-button label="第三方"></el-radio-button>
+                  <el-radio label="银行自有"></el-radio>
+                  <el-radio label="荣数商品"></el-radio>
+                  <el-radio label="第三方"></el-radio>
                 </el-radio-group>
               </el-form-item>
             </el-col>
@@ -43,7 +43,7 @@
             <el-col :span="8" v-if="form.source === '银行自有'">
               <el-form-item label="权益类型" required>
                 <el-radio-group v-model="form.productType">
-                  <el-radio-button label="券码"></el-radio-button>
+                  <el-radio label="券码"></el-radio>
                 </el-radio-group>
               </el-form-item>
             </el-col>
@@ -415,7 +415,7 @@
             <template v-if="form.claimGroupEnabled">
               <div style="width: 600px;background: #f1f2f5;padding: 20px;margin-top: 10px;">
                 <el-alert
-                  title="需在【发放计划】中选择发放计划,未发放的权益移动端不展示"
+                  title="需在【发放计划】中选择权益发放,未发放的权益移动端不展示"
                   type="info"
                   :closable="false"
                   show-icon
@@ -697,9 +697,7 @@ export default {
       }).catch(() => {})
     },
     handleSubmitAudit() {
-      const tip = this.currentShelfStatus === '已上架'
-        ? '提交变更审核后,移动端仍展示当前线上版本,待审核通过后变更生效。'
-        : '提交后将进入审核流程,期间不可再编辑。'
+      const tip = '提交后将进入审核流程,期间不可再编辑。'
       this.$prompt(tip + '\n请填写提交说明(可选)', '提交审核', {
         confirmButtonText: '提交',
         cancelButtonText: '取消',

+ 2 - 4
pc/src/views/EquityProducts.vue

@@ -21,7 +21,6 @@
               <el-select v-model="searchForm.auditStatus" placeholder="全部" clearable style="width: 150px">
                 <el-option label="草稿" value="草稿"></el-option>
                 <el-option label="上架待审核" value="上架待审核"></el-option>
-                <el-option label="编辑待审核" value="编辑待审核"></el-option>
                 <el-option label="下架待审核" value="下架待审核"></el-option>
                 <el-option label="审核通过" value="审核通过"></el-option>
               </el-select>
@@ -163,8 +162,8 @@
         <span>Total {{ tableData.length }}</span>
         <el-pagination
           layout="prev, pager, next, sizes, jumper"
-          :page-size="100"
-          :page-sizes="[100, 200, 500]"
+          :page-size="10"
+          :page-sizes="[10, 20, 50]"
           :total="tableData.length"
         ></el-pagination>
       </div>
@@ -242,7 +241,6 @@ export default {
       if (s === '审核通过') return 'success'
       if (s === '草稿') return 'info'
       if (s === '上架待审核') return ''
-      if (s === '编辑待审核') return 'warning'
       if (s === '下架待审核') return 'danger'
       return ''
     },

+ 5 - 5
pc/src/views/ViewEquityProduct.vue

@@ -22,9 +22,9 @@
               <el-col :span="8">
                 <el-form-item label="权益来源">
                   <el-radio-group v-model="form.source" disabled>
-                    <el-radio-button label="银行自有"></el-radio-button>
-                    <el-radio-button label="荣数商品"></el-radio-button>
-                    <el-radio-button label="第三方"></el-radio-button>
+                    <el-radio label="银行自有"></el-radio>
+                    <el-radio label="荣数商品"></el-radio>
+                    <el-radio label="第三方"></el-radio>
                   </el-radio-group>
                 </el-form-item>
               </el-col>
@@ -36,7 +36,7 @@
               <el-col :span="8" v-if="form.source === '银行自有'">
                 <el-form-item label="权益类型">
                   <el-radio-group v-model="form.productType" disabled>
-                    <el-radio-button label="券码"></el-radio-button>
+                    <el-radio label="券码"></el-radio>
                   </el-radio-group>
                 </el-form-item>
               </el-col>
@@ -313,7 +313,7 @@
                 <template v-if="form.claimGroupEnabled">
                   <div style="width: 600px;background: #f1f2f5;padding: 20px;margin-top: 10px;">
                     <el-alert
-                      title="需在【发放计划】中选择发放计划,未发放的权益移动端不展示"
+                      title="需在【发放计划】中选择权益发放,未发放的权益移动端不展示"
                       type="info"
                       :closable="false"
                       show-icon

BIN
prd/汇丰银行/screenshots/2-1-equity-products-list.png


BIN
prd/汇丰银行/screenshots/2-2-equity-add-source-rongshu.png


BIN
prd/汇丰银行/screenshots/2-2-equity-add-step1.png


BIN
prd/汇丰银行/screenshots/2-2-equity-add-step2.png


BIN
prd/汇丰银行/screenshots/2-2-equity-add-step3.png


BIN
prd/汇丰银行/screenshots/2-2-equity-add-third-detail.png


BIN
prd/汇丰银行/screenshots/2-2-equity-add-third-list.png


BIN
prd/汇丰银行/screenshots/2-2-equity-add-third-miniprogram.png


BIN
prd/汇丰银行/screenshots/2-3-equity-edit.png


BIN
prd/汇丰银行/screenshots/2-4-equity-view.png


BIN
prd/汇丰银行/screenshots/3-credit-card-equity-list.png


BIN
prd/汇丰银行/screenshots/4-grant-campaign-add.png


BIN
prd/汇丰银行/screenshots/4-grant-campaign-list.png


BIN
prd/汇丰银行/screenshots/4-grant-campaign-view.png


BIN
prd/汇丰银行/screenshots/5-page-assembly.png


BIN
prd/汇丰银行/screenshots/5-page-home.png


BIN
prd/汇丰银行/screenshots/5-page-zone.png


BIN
prd/汇丰银行/screenshots/6-audit-detail.png


BIN
prd/汇丰银行/screenshots/6-audit-list.png


BIN
prd/汇丰银行/screenshots/6-sms-send-record.png


BIN
prd/汇丰银行/screenshots/6-sms-template-list.png


BIN
prd/汇丰银行/screenshots/7-cash-discount-grant.png


BIN
prd/汇丰银行/screenshots/7-customer-equity-grant.png


BIN
prd/汇丰银行/screenshots/7-points-detail.png


BIN
prd/汇丰银行/screenshots/campaign-management.png


BIN
prd/汇丰银行/screenshots/channel-management.png


BIN
prd/汇丰银行/screenshots/crowd-name-list-add-step1.png


BIN
prd/汇丰银行/screenshots/crowd-name-list-add-step2.png


BIN
prd/汇丰银行/screenshots/crowd-task-add-step1.png


BIN
prd/汇丰银行/screenshots/crowd-task-add-step2.png


BIN
prd/汇丰银行/screenshots/crowd-task-list.png


BIN
prd/汇丰银行/screenshots/crowd-task-view.png


BIN
prd/汇丰银行/screenshots/dialog-delete-confirm.png


BIN
prd/汇丰银行/screenshots/dialog-equity-link.png


BIN
prd/汇丰银行/screenshots/dialog-offshelf-confirm.png


BIN
prd/汇丰银行/screenshots/dialog-rongshu-product.png


BIN
prd/汇丰银行/screenshots/dialog-show-pages.png


BIN
prd/汇丰银行/screenshots/popover-adjust-order.png


BIN
prd/汇丰银行/screenshots/rule-group-add-step1.png


BIN
prd/汇丰银行/screenshots/rule-group-add-step2.png


BIN
prd/汇丰银行/screenshots/rule-group-list.png


BIN
prd/汇丰银行/screenshots/rule-group-view.png


+ 24 - 0
prd/汇丰银行/汇丰借记卡权益.md

@@ -162,6 +162,12 @@
 -   **保存:** 校验必填项,通过后更新权益商品记录,状态变为【待审核】。
     
 
+<br>
+
+<br>
+
+<br>
+
 <br>
 
 <br>
@@ -330,6 +336,12 @@
                 
                 <br>
                 
+                <br>
+                
+                <br>
+                
+                <br>
+                
                 ![](../../.resources/image-1776678728870.png)![](../../.resources/image-1776678755981.png)
         -   **龙腾:** 点击后弹出授权提醒
             
@@ -423,6 +435,12 @@
         
         <br>
         
+        <br>
+        
+        <br>
+        
+        <br>
+        
         ![](../../.resources/image-1776678923546.png)![](../../.resources/image-1776678942001.png)
 
 **(2)领取权益**
@@ -455,6 +473,12 @@
 -   **按钮跳转:** 跳转权益订单详情页
     
 
+<br>
+
+<br>
+
+<br>
+
 <br>
 
 <br>

+ 1796 - 0
prd/汇丰银行/汇丰银行权益中台-FSD.md

@@ -0,0 +1,1796 @@
+# 汇丰银行权益中台FSD
+
+# 一、角色和用户管理
+
+## 流程如下
+
+1.  使用管理员账号,登录【统一门户】
+    
+2.  点击“新商城外跳联登”,进入【商城】角色管理配置角色
+    
+3.  回到【统一门户】系统管理->用户管理->添加用户并分配角色
+    
+
+## 角色管理
+
+入口:【统一门户】->【新商城外跳联登】
+
+![](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=YzRlNjQ3MjQzMTJmODY2NWY2YmY3OGY1MWU1MTRjNzBfZDI1NDA4NzIzYjAwNzRkNzFlZmE5Zjc4ODBlZmI3YjVfSUQ6NzY0MTU2NTM1ODU3ODg0NjkyMV8xNzc5MjAwMzM5OjE3NzkyMDM5MzlfVjM)
+
+### 角色列表
+
+展示全部已创建的【普通角色】类型的角色。
+
+![](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=MTkwNjA3MDgzZDRjOTY2OWMwNjQ5MzAyNTRkNThjZWVfYTJlMWFjNGYyODg1YmMwMzgzOGQ5NGM4OTA0NGZmYmVfSUQ6NzY0MTU2NTQwMTUyODY1MDk1MF8xNzc5MjAwMzQwOjE3NzkyMDM5NDBfVjM)
+
+### 添加/编辑角色
+
+![](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=YzNhMGZiYThjMTY4MGRkNmU3OGFkOTA4MDZhNzRiNmFfMDg5NWM5NzNkYTY1MTQzYWU5NjJjYmNmMWNiNTczYmRfSUQ6NzY0MTU2NTM2MTcyNDY3MzIzNl8xNzc5MjAwMzM5OjE3NzkyMDM5MzlfVjM)
+
+点击【添加】可添加角色。
+
+-   **角色名称:**可自定义角色名称
+    
+-   **角色状态:**包含启用 / 不启用,启用时可登录,不启用时不可登录
+    
+-   **菜单:**角色菜单权限,可选择到最后一级菜单
+    
+-   **按钮权限:**选了最后一级菜单后,会展示对应页面的【按钮权限】,每个菜单的【按钮权限】都会在上线前配置完成。
+    
+
+![](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=ZmY3YmM1NWJiODIzYjc0N2UxMThiYmQyOGNiODlkYzBfZWEwMjM1ZmE4ODIzYTljMzNlNmNkZGUwNWFkNWZhZmJfSUQ6NzY0MTU2NTM2OTg1NzUyNjk2NV8xNzc5MjAwMzM5OjE3NzkyMDM5MzlfVjM)
+
+## 用户管理
+
+### 用户列表
+
+![](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=OWI3MjIzMWE4OGViYzQ3ZTgxNDliMDhlYmRkNzE0ZjlfYzM2NzkzYzZkZjlmYzNmNDNhZWE4ODFjMDMzMTczZjBfSUQ6NzY0MTU2NTM4NDg4NzE0MzY0Nl8xNzc5MjAwMzM5OjE3NzkyMDM5MzlfVjM)
+
+### 添加用户
+
+> **一期不需要添加用户,我们会批量导入用户进去。**
+
+![](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=MTA4NDRkMjkzOGY3NGZkOTc4YmRlNjliNDhkMTU1MDdfY2MxZDQxNWQ5NDFjOGY3YTI3OGY0OGI1OGJjYWRiMWFfSUQ6NzY0MTU2NTQyNTY1NDM1MzA4Nl8xNzc5MjAwMzM5OjE3NzkyMDM5MzlfVjM)
+
+点击【添加】可添加用户。
+
+-   昵称:必填,可输入用户姓名
+    
+-   用户名:必填
+    
+-   用户类别:必填,需选择【普通用户】
+    
+-   一级和二级分行:相当于组织架构,权益列表根据一级分行进行数据的隔离,二级分行没有作用。如下
+    
+    -   一级分行A(对应A部门)
+        
+    -   一级分行B(对应B部门)
+        
+-   手机号:必填,用于登录发送验证码校验添加完成后,会给该用户的手机号发送包含 账号名称和初始密码 的短信。
+    
+
+![](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=NjMxZDY2ZDYyNjFkNzg4NGZjOTRmNzY1YzlkNTFmOGRfYTY3OGU1NTgwNmExNDg1MTE0N2MwNTcxODhiM2E5OTlfSUQ6NzY0MTU2NTM2Njk3NTgyNzEyMl8xNzc5MjAwMzM5OjE3NzkyMDM5MzlfVjM)
+
+### 分配角色
+
+<br>
+
+<br>
+
+<br>
+
+![](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=OTNlY2UyOTMyMDQ5NDM1ZjMyMThlOGFhNjhhMDc1ZjJfMzc2ZGEyOGQ5YjliNjFhZjgyY2EzN2Y4M2IyMWM0NzZfSUQ6NzY0MTU2NTM1NjQyMTc0NTg3NF8xNzc5MjAwMzQwOjE3NzkyMDM5NDBfVjM)![](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=MjBhYzA0NzgxNGFhYWQxODZiYzM2YmE5MzRlYmNjN2VfY2QyMGE0OGM2MzU4MTVlNGE4ZGJhZTY2NTczOWNkNTJfSUQ6NzY0MTU2NTM0NTE5NDcyNDU0MF8xNzc5MjAwMzQwOjE3NzkyMDM5NDBfVjM)
+
+## 用户登录
+
+登录页填写账号、密码即可登录。
+
+![](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=ZmUyNTA5NWI2ZGYxMmY4MmU2ZTVhYmZjYTY3Njc2ZjdfYmI2MWIyZjQ2MGUwODYxMThiZDU1YTU0MTQ1NzE4OWZfSUQ6NzY0MTU2NTM4Nzc5MjM4NzI3M18xNzc5MjAwMzM5OjE3NzkyMDM5MzlfVjM)
+
+## 用户配置计划
+
+-   一期:在CUPD中独立配置,我们会批量导入行内账号,可通过账号和密码登录。
+    
+
+![](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=NGZkZmNjZTUzMDIwNzE4Y2ViNTQ1NTYxYjY2NDc3YzFfZjA3OGQwOTY0MDNhYzBiNzhmNTIzOTU1OWVhZWJiNTNfSUQ6NzY0MTYwNTAxNDMzNTAxNTg4N18xNzc5MjAwMzM5OjE3NzkyMDM5MzlfVjM)
+
+-   二期:接行内AD,然后和行内的用户匹配关联。
+    
+
+# 二、权益管理
+
+<div class="joplin-table-wrapper"><table style="min-width: 75px;"><thead><tr><th colspan="1" rowspan="1"><p data-id="e1093df7-c7c1-4454-bbf1-ae328331678b">变更日期</p></th><th colspan="1" rowspan="1"><p data-id="cee93dda-3a7b-427c-ae45-ca654191fdd8">变更内容</p></th><th colspan="1" rowspan="1"><p data-id="9c6d30fb-cf93-4e25-b629-53efcd53bafe">变更人</p></th></tr></thead><tbody><tr><td colspan="1" rowspan="1"><p data-id="7dcb972a-3b53-46a3-baae-f8e8b2a4ade0">2026-05-11</p></td><td colspan="1" rowspan="1"><ol data-id="9565e3ac-1a83-4a3e-9408-643b09afba1c"><li><p data-id="77f882a5-4d41-4ee2-a856-c7b104ec6330">添加权益商品调整为三步骤向导(填写商品信息 → 设置规则 → 更多设置)</p></li><li><p data-id="41ab9ff4-f66d-4ba4-af61-ecb3e6ccd7ef">商品来源新增"荣数商品"</p></li><li><p data-id="286268ea-4536-4590-97a3-0ea9ce3aa92f">银行自有商品类型拆分为"券码"和"微信立减金"</p></li><li><p data-id="013780a6-7801-4fb1-8d20-5758a50f56d1">新增"信用卡权益"列表页(仅支持设置显示页面与调整顺序)</p></li><li><p data-id="0ba15c4d-b5e6-42d2-a740-8f59629679a6">新增"券码管理"功能(批次导入、编辑、回收、导出失败券码)</p></li><li><p data-id="efa7a8f3-c2db-4a38-9f28-ccb23af5799f">列表新增"调整显示顺序"、"设置显示页面"、"删除(仅已下架)"操作</p></li><li><p data-id="694ddab5-53da-409d-b337-adef3a895834">领取客群限制新增"是否潜客领取"字段</p></li><li><p data-id="169ca733-1f05-418d-b909-edf211087cd9">新增"分享设置"模块</p></li></ol></td><td colspan="1" rowspan="1"><p data-id="fe879af0-8ec0-4215-93ec-6d7c80512e4a">陈生坤</p></td></tr></tbody></table></div>
+
+## 1\. 概述
+
+### 1.1 业务背景
+
+汇丰银行此前仅支持信用卡权益,本次升级新增**借记卡权益**,将权益范围拓展至全行用户,提升用户活跃度。
+
+### 1.2 权益分类
+
+按**商品来源**分为三类:
+
+| 商品来源 | 说明  |
+| --- | --- |
+| **银行自有** | 银行自有商户权益商品,细分仅为"券码"商品 |
+| **荣数商品** | 由荣数侧统一提供的权益商品池(仅微信立减金),选择后自动回填名称与主图 |
+| **第三方** | 第三方合作商家权益(太古地产、龙腾、h5链接、小程序链接) |
+
+### 1.3 权益商品状态流转
+
+-   **商品状态**:`未上架` / `已上架` — 决定前台是否展示
+    
+-   **审核状态**:`草稿` / `上架待审核` / `下架待审核` / `审核通过` — 决定后台审核工作流
+    
+
+1.3.1 双状态组合矩阵
+
+| 商品状态 | 审核状态 | 业务含义 | 可执行操作 |
+| --- | --- | --- | --- |
+| 未上架 | 草稿  | 新建后未提审,仅自己可见 | 查看、编辑、删除 |
+| 未上架 | 上架待审核 | 新建提审,等待审核 | 查看、编辑 |
+| 已上架 | 审核通过 | 正式上架中,前台可见 | 查看、下架、券码管理、设置显示页面、调整显示顺序 |
+| 已上架 | 下架待审核 | 已上架但已发起下架,等待审核(仍在线) | 查看  |
+| 未上架 | 审核通过 | 历史已下架,前台不可见 | 查看、编辑、删除 |
+
+### 1.4 操作入口
+
+**Liso 后台 → 定制功能 → 汇丰银行 → 权益管理**,包含两个子菜单:
+
+-   **借记卡权益**:权益商品完整增删改查、审核、券码管理
+    
+-   **信用卡权益**:仅维护显示页面与展示顺序,权益从信用卡权益平台自动同步过来
+    
+
+* * *
+
+## 2\. 借记卡权益
+
+权限控制说明:数据权限按照【一级分行】隔离,相同一级分行可见,不同一级分行不可见。
+
+![](screenshots/2-1-equity-products-list.png)
+
+### 2.1 权益列表
+
+![](../../../.resources/image-1779202378440.png)
+
+2.1.1 筛选条件
+
+| 字段  | 类型  | 说明  |
+| --- | --- | --- |
+| 权益名称 | 文本输入 | 模糊匹配 |
+| 权益编号 | 文本输入 | 模糊匹配(系统生成的 `Q` + 6 位序号,如 `Q000001`) |
+| 商品状态 | 下拉单选 | 选项:未上架 / 已上架;不选视为全部 |
+| 审核状态 | 下拉单选 | 选项:草稿 / 上架待审核 / 下架待审核 / 审核通过;不选视为全部 |
+
+**操作按钮**:
+
+-   【查询】:按筛选条件查询
+    
+-   【重置】:清空筛选条件
+    
+-   【添加权益】:跳转至新增向导(位于查询条件下方,作为列表主操作按钮)
+    
+
+2.1.2 列表字段
+
+| 字段  | 说明  |
+| --- | --- |
+| 操作  | 行内主操作按钮 +「更多」下拉菜单,按"商品状态 + 审核状态"显隐对应操作,详见 2.1.3 操作可见性矩阵 |
+| 权益名称 | 权益名称,超过一行时换行显示 |
+| 商品状态 | 使用色块 Tag 展示  <br>  <br>可选值:未上架(灰)/ 已上架(绿) |
+| 审核状态 | 使用色块 Tag 展示  <br>  <br>可选值:草稿(灰)/ 上架待审核(默认)/ 下架待审核(红)/ 审核通过(绿) |
+| 权益来源 | 银行自有 / 荣数商品 / 第三方 |
+| 外部权益链接 | 第三方权益的链接来源:太古地产 / 龙腾 / h5链接 / 小程序链接;非第三方显示"-" |
+| 是否有详情页 | 仅第三方权益使用,标识跳转触发位:"有" = 用户进入权益详情页后再跳出;"没有" = 用户在首页/列表页/组装页“点击后直接跳出;非第三方显示"-" |
+| 权益编号 | 系统生成,统一 `Q` + 6 位序号格式(如 `Q000001`),全局唯一 |
+| 显示页面 | 使用 Tag 展示当前权益启用的展示位:开启"权益首页/列表页"时显示一枚「权益首页/列表页」Tag;开启"组装页"时按所选组装页(组装页 A/B/C/D…)追加 Tag。两项均未开启时显示"-" |
+| 创建人 | 创建该权益的用户昵称 |
+| 创建时间 | Y-m-d H:i:s |
+| 审核人 | 最近一次审核操作的执行人昵称;未审核时为空 |
+| 审核时间 | 最近一次审核操作的时间(Y-m-d H:i:s);未审核时为空 |
+
+2.1.3 操作可见性矩阵
+
+行内固定显示「查看」按钮,再按状态显示「编辑」/「下架」,最右侧为【更多】下拉菜单。状态命中规则如下:
+
+| 操作  | 显示条件 | 行为  |
+| --- | --- | --- |
+| 查看  | 任意状态 | 进入查看页,字段与编辑一致但所有输入态禁用,并展示操作日志 |
+| 编辑  | 商品状态 = `未上架` 且 审核状态 = `草稿` | 进入编辑向导继续完善草稿 |
+| 下架  | 商品状态 = `已上架` 且 审核状态 = `审核通过` | 二次确认弹窗("确定提交'XXX'的下架审核吗?审核通过后才会下架。")→ 确认后生成下架审核单,权益审核状态切到 `下架待审核` |
+| 更多 → 调整显示顺序 | 任意状态 | 弹出气泡:"调整到第 N 位",输入框默认值=当前位次,范围 1 ~ 当前列表总条数;提示文案"移动端权益首页和列表页按照该顺序展示";【确定】后列表局部刷新到目标位置 |
+| 更多 → 复制 | 任意状态 | 进入【添加权益】向导,自动回填源权益的基本信息、规则、显示页面等字段;保存时由系统生成新权益编号 |
+| 更多 → 生成链接 | 任意状态 | 弹出"生成借记卡权益链接"对话框,给出可分享的借记卡权益链接 |
+| 更多 → 导入券码 | 权益来源 = `银行自有` 且 权益类型 = `券码` | 新开页面跳转至统一资源管理后台的"采购导入券码"页面 |
+| 更多 → 券码管理 | 权益来源 = `银行自有` 且 权益类型 = `券码` | 新开页面跳转至统一资源管理后台的"码库管理"页面 |
+| 更多 → 删除 | 商品状态 = `未上架` 且 审核状态 ∈ { `草稿`, `审核通过` } | 二次确认弹窗("确定删除权益'XXX'吗?"),确认后从列表移除 |
+
+生成借记卡权益链接对话框:
+
+![](screenshots/dialog-equity-link.png)
+
+2.1.4 分页
+
+-   默认每页 10 条,可切换 10 / 20 / 50
+    
+-   支持上一页、下一页、页码、跳转
+    
+-   左下角展示 `Total N` 总条数
+    
+
+* * *
+
+### 2.2 添加权益
+
+![](screenshots/2-2-equity-add-step1.png)
+
+权益添加采用**三步骤向导**:填写权益信息 → 设置规则 → 更多设置。步骤间通过页面底部的【上一步】/【下一步】导航,最后一步可【保存草稿】或【提交审核】。
+
+**复制场景**:列表「更多 → 复制」进入本向导后,自动回填源权益的基本信息、规则、显示页面等内容,权益编号留空(保存时由系统生成新编号),同时给出提示 `已复制「XXX」的内容,请确认后保存`。
+
+* * *
+
+2.2.1 第一步:填写权益信息
+
+**分支表单示意(自上而下:荣数商品 / 第三方-没有详情页 / 第三方-有详情页 / 第三方-小程序链接)**:
+
+<br>
+
+![](screenshots/2-2-equity-add-source-rongshu.png)
+
+<br>
+
+![](screenshots/2-2-equity-add-third-list.png)
+
+<br>
+
+![](screenshots/2-2-equity-add-third-detail.png)
+
+<br>
+
+![](screenshots/2-2-equity-add-third-miniprogram.png)
+
+荣数商品分支点击「选择」打开商品池:  
+
+![](screenshots/dialog-rongshu-product.png)
+
+第一步包含两个卡片:**基本信息**(始终显示)与 **详情信息**(按"权益来源 / 是否有详情页"条件显示)。
+
+(1)基本信息
+
+页面顶部先选择**权益来源**(必填,圆点单选,默认"银行自有"):
+
+-   银行自有(默认)
+    
+-   荣数商品
+    
+-   第三方
+    
+
+不同权益来源展开不同字段:
+
+**A. 权益来源 = 银行自有**
+
+| 字段  | 必填  | 说明  |
+| --- | --- | --- |
+| 权益类型 | 是   | 圆点单选,目前仅"券码" |
+| 权益名称 | 是   | 最长 40 字符 |
+| 权益副标题 | 否   | 最长 40 字符 |
+| 权益图标 | 是   | 上传 1 张,建议尺寸 80×80px。Tooltip:"展示在权益首页和权益列表页" |
+
+**B. 权益来源 = 荣数商品**
+
+| 字段  | 必填  | 说明  |
+| --- | --- | --- |
+| 选择权益 | 是   | 只读输入框 + 【选择】按钮。弹窗"选择荣数商品"中按商品名称模糊搜索,每页 10 条;选中后自动回填权益名称、商户名称、库存数、权益主图。**荣数商品池均为微信立减金商品**(如"微信立减金 10元(满50减10)"等) |
+| 权益名称 | 是   | 最长 40 字符,由"选择权益"自动回填,仍可手动编辑 |
+| 权益副标题 | 否   | 最长 40 字符 |
+| 库存数 | 是   | 整数,范围 0 ~ 100000,由"选择权益"自动回填 |
+| 权益图标 | 是   | 上传 1 张,建议尺寸 80×80px |
+
+**C. 权益来源 = 第三方**
+
+先选择"外部权益链接"与"是否有详情页"两个关键字段;当外部权益链接为 h5链接 / 小程序链接时还需补充对应地址:
+
+| 字段  | 必填  | 说明  |
+| --- | --- | --- |
+| 外部权益链接 | 是   | 下拉单选:太古地产 / 龙腾 / h5链接 / 小程序链接 |
+| h5链接 | 是   | 仅"外部权益链接 = h5链接"时显示,输入完整 URL |
+| 小程序appid | 是   | 仅"外部权益链接 = 小程序链接"时显示 |
+| 小程序页面地址 | 是   | 仅"外部权益链接 = 小程序链接"时显示 |
+| 是否有详情页 | 是   | 圆点单选:"没有" / "有"。Tooltip:"没有详情页时直接从首页/列表页跳转" |
+| 权益名称 | 是   | 最长 40 字符 |
+| 权益副标题 | 否   | 最长 40 字符 |
+| 权益图标 | 是   | 上传 1 张,建议尺寸 80×80px |
+
+注:第三方权益没有"权益类型"与"库存数"字段。
+
+* * *
+
+(2)详情信息
+
+**显示条件**:
+
+权益来源 = 银行自有 / 荣数商品;或 权益来源 = 第三方 **且** "是否有详情页 = 有"。
+
+当权益来源 = 第三方 **且** "是否有详情页 = 没有"时,整个详情信息卡片不显示。
+
+| 字段  | 必填  | 说明  |
+| --- | --- | --- |
+| 权益主图 | 是   | 上传 1 张,建议尺寸 716×400px |
+| 权益介绍 | 是   | 多行文本,最长 500 字符,带字数实时提示 |
+| 权益有效期 | 是   | • 权益来源 = 银行自有 / 荣数商品:圆点单选「取自发放计划 / 自定义输入」,选"自定义输入"时下方出现文本框(最长 50 字符,带字数提示)  <br>• 权益来源 = 第三方:直接输入文本框(最长 50 字符) |
+| 权益内容 | 是   | 富文本编辑器 |
+| 权益说明 | 是   | 富文本编辑器 |
+| 下单成功短信 | 否   | **仅银行自有 / 荣数商品显示。** 开关 + 短信模板下拉。开启后选择已配置的短信模板,模板内容会在下方实时回显。模板支持占位符:`${productName}` 权益名称、`${expireDate}` 结束日期、`${exchangeNum}` 卡号、`${exchangePwd}` 卡密、`${qrCodeDownPaths}` 券码路径 |
+| 领取成功按钮 | 是   | 默认值"马上去使用" |
+
+2.2.2 第二步:设置规则
+
+![](screenshots/2-2-equity-add-step2.png)
+
+第二步包含**展示规则**与**领取规则**两组开关,所有开关默认关闭,关闭时不做任何限制。
+
+(1)展示规则(控制权益在 App 首页/列表中是否可见)
+
+| 字段  | 类型  | 说明  |
+| --- | --- | --- |
+| 展示时间限制 | 开关  | 开启后配置开始/结束时间(精确到秒),超出区间则不展示 |
+| 展示客群限制 | 开关  | 开启后多选客群(示例选项:客群A / 客群B / 客群C),命中的用户才可见 |
+
+(2)领取规则(控制用户能否领取)
+
+| 字段  | 类型  | 说明  |
+| --- | --- | --- |
+| 领取时间限制 | 开关  | 开启后配置开始/结束时间(精确到秒)。 |
+| 领取客群限制 | 开关  | 开启后展开顶部提示卡(信息蓝):"需在【发放计划】中选择权益发放,未发放的权益移动端不展示"。  <br>  <br>下方为**不满足提示**配置:  <br>• 提示内容(最长 200 字符)  <br>• 跳转按钮文案(最长 10 字符)  <br>• 跳转按钮链接(最长 200 字符) |
+
+注:**领取客群不再在权益处直接选定**,统一由【发放计划】维护"哪些客群可领"。本开关仅用于配置"不满足时的提示与跳转链接"。
+
+* * *
+
+2.2.3 第三步:更多设置
+
+第三步包含**显示页面**与**分享设置**两组配置。
+
+(1)显示页面(决定权益在移动端哪些页面展示)
+
+![](screenshots/2-2-equity-add-step3.png)
+
+| 字段  | 类型  | 说明  |
+| --- | --- | --- |
+| 权益首页/列表页 | 圆点单选 | 显示 / 不显示,默认"显示" |
+| 组装页 | 圆点单选 + 多选 | 显示 / 不显示,默认"不显示"。选"显示"时下方展开多选框,可选组装页 A / B / C / D… |
+
+(2)分享设置
+
+| 字段  | 类型  | 说明  |
+| --- | --- | --- |
+| 是否支持分享 | 开关  | 默认关闭。开启后展开下列字段: |
+| · 分享名称 | 必填  | 最长 40 字符。开关由关→开时,若分享名称为空则自动回填当前的"权益名称" |
+| · 分享图片 | 必填  | 上传 1 张 |
+
+* * *
+
+2.2.4 操作按钮
+
+页面底部固定操作栏,按当前步骤显隐。校验集中在【提交审核】时执行;【保存草稿】仅校验"权益名称"非空,【上一步】/【下一步】不做任何校验。
+
+| 按钮  | 显示条件 | 行为  |
+| --- | --- | --- |
+| 上一步 | 当前步骤 > 1 | 回到上一个步骤 |
+| 下一步 | 当前步骤 < 3 | 进入下一个步骤 |
+| 取消  | 始终显示 | 直接返回权益列表,不做提示 |
+| 保存草稿 | 始终显示 | 校验"权益名称"非空后,落地一条记录:商品状态 = `未上架`、审核状态 = `草稿`。后续可在列表点击「编辑」继续完善 |
+| 提交审核 | 当前步骤 = 3(仅第三步显示) | 校验必填后,弹出文本框 `请填写提交说明(可选)`,提交后创建记录:商品状态 = `未上架`、审核状态 = `上架待审核`。审核通过后切换为"已上架 + 审核通过",新创建的商品默认排在列表第一位 |
+
+* * *
+
+### 2.3 编辑权益
+
+![](screenshots/2-3-equity-edit.png)
+
+**入口与可见性**:权益列表行【操作】→【编辑】。【编辑】按钮仅在 **商品状态 =** `未上架` **且 审核状态 =** `草稿` 时显示,其他状态下该按钮不出现。
+
+**页面结构**:与"添加权益"的三步骤向导完全一致(填写权益信息 → 设置规则 → 更多设置),表单字段、显示条件、规则提示文案、分享设置、显示页面等全部相同。
+
+**数据加载**:进入页面时按权益编号请求详情接口,回填权益数据,并同步当前的"商品状态/审核状态"用于按钮显隐。
+
+**字段编辑约束**:
+
+-   **权益来源** 不可编辑(圆点单选禁用),保持创建时选定的来源;其余字段(含第三方分支的"外部权益链接 / 是否有详情页"、银行自有的"权益类型")均可正常编辑
+    
+
+**操作按钮**:
+
+| 按钮  | 显示条件 | 行为  |
+| --- | --- | --- |
+| 上一步 | 当前步骤 > 1 | 回到上一个步骤 |
+| 下一步 | 当前步骤 < 3 | 进入下一个步骤 |
+| 取消  | 始终显示 | 直接返回权益列表 |
+| 保存草稿 | 审核状态 = `草稿` | 不做完整校验,覆盖原记录并保持 `未上架 + 草稿` |
+| 提交审核 | 当前步骤 = 3 | 弹出文本框 `提交后将进入审核流程,期间不可再编辑。请填写提交说明(可选)`,确认后落地为 `未上架 + 上架待审核` 并生成新的审核单 |
+
+* * *
+
+### 2.4 查看权益
+
+**入口与可见性**:权益列表行【操作】→【查看】,按钮在所有状态下均显示。
+
+**页面结构**:复用"添加权益"三步骤向导布局(填写权益信息 → 设置规则 → 更多设置),所有输入框、单选 / 复选、上传、富文本、开关等控件均处于**禁用态**,仅作信息展示。
+
+**操作日志卡片**:下方追加"操作日志"表格,按操作时间倒序展示:
+
+| 列   | 说明  |
+| --- | --- |
+| 操作人 | 触发该次操作的用户名 |
+| 操作日期 | Y-m-d H:i:s |
+| 操作  | Tag 形式展示:`创建`/ `审核`/ `下架`等 |
+
+**操作按钮**(页面底部,按当前步骤显隐):
+
+| 按钮  | 显示条件 | 行为  |
+| --- | --- | --- |
+| 上一步 | 当前步骤 > 1 | 回到上一个步骤 |
+| 下一步 | 当前步骤 < 3 | 进入下一个步骤 |
+| 返回  | 当前步骤 = 3 | 返回权益列表 |
+| 下架  | 当前步骤 = 3 且 商品状态 = `已上架` 且 审核状态 = `审核通过` | 二次确认后提交下架审核(交互同 §2.6) |
+
+<br>
+
+![](screenshots/2-4-equity-view.png)
+
+* * *
+
+### 2.5 审核权益
+
+**入口与可见性**:审核入口统一收敛至独立子菜单【权益审核】(详见 §6 权益审核),从待审核列表点击对应审核单的「审核」按钮进入审核页面。借记卡权益列表中**不直接提供**「审核」按钮。
+
+**页面结构**:与"查看权益"完全一致 —— 三步骤只读向导(填写权益信息 → 设置规则 → 更多设置),所有控件禁用,用于审核人完整查看待审记录的快照内容。
+
+**操作按钮**(页面底部,按步骤显隐):
+
+| 按钮  | 显示条件 | 行为  |
+| --- | --- | --- |
+| 上一步 | 当前步骤 > 1 | 回到上一个步骤 |
+| 下一步 | 当前步骤 < 3 | 进入下一个步骤 |
+| 返回  | 始终显示 | 返回上一页(审核列表) |
+| 审核拒绝 | 当前步骤 = 3 | 二次确认弹窗 `确定要审核拒绝吗?` → 提示 `审核拒绝成功`,返回权益列表。商品状态保持原值,审核状态回退到上一稳定态(详见 §6.3) |
+| 审核通过 | 当前步骤 = 3 | 二次确认弹窗 `确定要审核通过吗?` → 提示 `审核通过成功`,返回权益列表。上架审核通过 → 商品状态切到 `已上架`、审核状态 = `审核通过`;下架审核通过 → 商品状态切到 `未上架`、审核状态 = `审核通过`(详见 §6.3) |
+
+**审核通过后直接生效**,前台立即可见或下线,无需额外的上架/下架操作。
+
+![](screenshots/6-audit-detail.png)
+
+* * *
+
+### 2.6 下架权益
+
+![](screenshots/dialog-offshelf-confirm.png)
+
+**入口与可见性**:商品状态 = `已上架` 且 审核状态 = `审核通过` 时显示【下架】按钮,触发位置有两个:
+
+1.  权益列表行【操作】列 →【下架】
+    
+2.  权益查看页第三步底部 →【下架】
+    
+
+**操作流程**:
+
+1.  点击【下架】,弹出二次确认弹窗:`确定提交"XXX"的下架审核吗?审核通过后才会下架。`(类型:warning)
+    
+2.  按钮:
+    
+
+| 按钮  | 行为  |
+| --- | --- |
+| 提交  | 调用下架接口,生成一条**下架审核单**:审核状态切到 `下架待审核`,商品状态保持 `已上架`(线上仍可见);提示"已提交下架审核"。审核通过后切换为"未上架 + 审核通过"(等价于历史下架),前台不再展示且无法兑换下单 |
+| 取消  | 关闭弹窗,状态不变 |
+
+* * *
+
+### 2.7 调整显示顺序
+
+![](screenshots/popover-adjust-order.png)
+
+**入口与可见性**:列表行【操作】→【更多】→【调整显示顺序】。所有状态均可操作。
+
+**交互**:
+
+-   点击后弹出小气泡(卡片样式),顶部为标题"调整显示顺序"
+    
+-   提示文案:"移动端权益首页和列表页按照该顺序展示"
+    
+-   输入:`调整到第 [ N ] 位`,N 默认值 = 当前所在行号,校验范围 `1 ~ 当前列表总条数`
+    
+-   按钮:
+    
+    -   【取消】:关闭气泡
+        
+    -   【确定】:校验 N 有效后将当前行移动至目标位置,列表局部刷新并提示"顺序已调整";若输入无效则提示"请输入有效的顺序位置"
+        
+
+### 2.9 删除
+
+![](screenshots/dialog-delete-confirm.png)
+
+**入口与可见性**:列表行【操作】→【更多】→【删除】。【删除】仅在 **商品状态 =** `未上架` **且 审核状态 ∈ {** `草稿`**,** `审核通过`**(即历史下架) }** 时显示,其他状态下不出现。
+
+**交互**:弹出二次确认弹窗 `确定删除权益"XXX"吗?`(类型:warning),按钮:
+
+| 按钮  | 行为  |
+| --- | --- |
+| 删除  | 调用删除接口,列表移除该记录并提示"删除成功" |
+| 取消  | 关闭弹窗,状态不变 |
+
+### 2.10 权益测试说明
+
+1.  创建白名单人群
+    
+2.  创建权益
+    
+3.  在发放计划中选择权益进行发放
+    
+
+* * *
+
+## 3\. 信用卡权益
+
+![](screenshots/3-credit-card-equity-list.png)
+
+信用卡权益由营销系统统一维护,本系统 **不支持** 新建 / 编辑 / 审核 / 下架,仅维护「显示页面」「显示顺序」以及「生成链接」三项轻量操作。
+
+权限控制说明:数据权限按照【一级分行】隔离,相同一级分行可见,不同一级分行不可见。
+
+### 3.1 列表入口
+
+**Liso 后台 → 定制功能 → 汇丰银行 → 信用卡权益**
+
+### 3.2 筛选条件
+
+| 字段  | 类型  | 说明  |
+| --- | --- | --- |
+| 权益名称 | 文本输入 | 模糊匹配 |
+
+**操作按钮**:
+
+-   【查询】:根据筛选条件查询
+    
+-   【重置】:清空筛选条件
+    
+
+### 3.3 列表字段
+
+| 字段  | 说明  |
+| --- | --- |
+| 操作  | 行内"调整显示顺序"、"设置显示页面"两个文字按钮,最右侧为【更多】下拉菜单;详见 §3.4 操作 |
+| 权益名称 | 权益名称,来源于营销系统;过长时省略号 + 悬浮 tooltip 展示 |
+| 显示页面 | 使用 Tag 展示当前权益启用的展示位:开启"权益首页/列表页"时显示一枚「权益首页/列表页」Tag;开启"组装页"时按所选组装页(组装页 A/B/C/D…)追加 Tag。两项均未开启时显示"-" |
+
+### 3.4 操作
+
+![](screenshots/dialog-show-pages.png)
+
+| 操作  | 显示条件 | 行为  |
+| --- | --- | --- |
+| 调整显示顺序 | 任意状态 | 行内文字按钮。点击后弹气泡,交互、提示文案、校验范围与按钮均与 §2.7 一致 |
+| 设置显示页面 | 任意状态 | 行内文字按钮。点击后弹出"设置显示页面"对话框,与添加权益第三步「显示页面」字段一致:  <br>• **权益首页/列表页**:圆点单选 显示 / 不显示  <br>• **组装页**:圆点单选 显示 / 不显示,选"显示"时下方多选组装页 A/B/C/D  <br>底部按钮【取消】/【确定】。点击【确定】后立即生效,列表"显示页面"列以 Tag 形式回显,并提示"显示页面已更新" |
+| 更多 → 生成链接 | 任意状态 | 弹出"生成信用卡权益链接"对话框,给出对应 `equity/credit?id=<权益编号>` 形式的可分享链接 |
+
+### 3.5 分页
+
+-   默认每页 10 条,可切换 10 / 20 / 50
+    
+-   支持上一页、下一页、页码、跳转
+    
+-   左下角展示 `Total N` 总条数
+    
+
+* * *
+
+## 4\. 券码管理
+
+**适用范围**:仅对**商品来源 = 银行自有**且**商品类型 = 券码**的权益开放。**操作入口**:借记卡权益列表 → 行【操作】→【更多】→【券码管理】点击后跳转至统一资源管理后台现有【码库管理】模块,默认填充商品编号。
+
+### 4.1 批次列表字段
+
+![](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=NGQwNzkzMmMwMTNkMmExZGQ3ZWNlNzhiZDQ5M2M1MDJfNzg2NmU1NGQ3ODJhNjRhNzQwODQ3YjhmMzViNTA0NjdfSUQ6NzY0MTU2NTUwNTE5NDkwNDc2Nl8xNzc5MjAwMzM5OjE3NzkyMDM5MzlfVjM)
+
+| 字段  | 说明  |
+| --- | --- |
+| 导入数量 | Excel 文件中的券码总数 |
+| 实际导入数量 | 去重、格式校验后成功入库的券码数 |
+| 导入失败数量 | 未通过校验的券码数。> 0 时,操作列展示**【导出失败券码】** |
+| 券码剩余数量 | 未发放 + 未回收的券码数。> 0 时,操作列展示**【回收】** |
+| 生效时间 | Y-m-d H:i:s |
+| 过期时间 | Y-m-d H:i:s |
+| 停止发放时间 | Y-m-d H:i:s。达到此时间后该批次不再对外发放 |
+| 文件处理状态 | 处理中 / 处理成功 / 处理失败,Tag 形式展示 |
+| 导入时间 | 文件提交时间 |
+| 创建人 | 上传文件的操作员 |
+
+### 4.2 新增券码批次
+
+![](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=MDczZmUxMWNjMTQxZDdlM2Q3NmIyMDRmNzI1MzlmZDlfMjQ0MDZhMWZkNDAxOTNmOWU4MWUzNDE2NDI5NTE3MWNfSUQ6NzY0MTU2NTUxNDQ2ODYwOTIyMl8xNzc5MjAwMzM5OjE3NzkyMDM5MzlfVjM)
+
+点击列表上方【新增券码批次】按钮,显示页面:
+
+| 字段  | 必填  | 说明  |
+| --- | --- | --- |
+| **导入方式** | 是   | 选择【新增采购导入】 |
+| **SKU商品和商户简称** | 是   | 跳转后自动选择 |
+| **生效时间** | 是   | Y-m-d H:i:s |
+| **过期时间** | 是   | Y-m-d H:i:s |
+| **停止发放时间** | 是   | Y-m-d H:i:s |
+| **券码文件** | 是   | 支持 .xlsx / .csv,单文件上传。配套提供【模板下载】链接 |
+
+**操作按钮**:【取消】关闭页面;【确定】提交,后台异步处理,列表新增一条批次记录(文件处理状态初始为【处理中】)。
+
+### 4.3 编辑批次
+
+**操作入口**:批次行【操作】→【编辑】仅可修改三个时间字段:生效时间、过期时间、停止发放时间。**操作按钮**:【取消】关闭;【确定】保存。
+
+![](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=NTA1OTQxODFkMTI0Mjg3MjlkMWJmNjVjNDhmNTg4Y2RfMjcwNzY2NDdlY2UzYWYxMjg5ZDg2MTFhZWZjMmVjZTBfSUQ6NzY0MTU2NTQ1MzgzOTY2NjM3NF8xNzc5MjAwMzM5OjE3NzkyMDM5MzlfVjM)
+
+### 4.4 回收
+
+![](https://internal-api-drive-stream.feishu.cn/space/api/box/stream/download/authcode/?code=OGFkY2M5YmQwMzE1MTU1ZWM1OWFlNTJhZTQ1NzYzYmRfM2EwMzBkOTMxZmJjZDljNTRlY2M1MTUzMjZmMjRmZjBfSUQ6NzY0MTU2NTQzMzUzOTc0MjkwMF8xNzc5MjAwMzM5OjE3NzkyMDM5MzlfVjM)
+
+**展示条件**:批次"券码剩余数量 > 0"时显示【回收】按钮。**交互**:弹出对话框,先选择**回收类型**(单选三选一):
+
+| 回收类型 | 说明  |
+| --- | --- |
+| 未发放券码回收 | 回收尚未发放给用户的券码 |
+| 已过期券码回收 | 回收已过期但尚未清理的券码 |
+| 导错码券码回收 | 回收因导入错误而入库的券码 |
+
+**回收数量**:输入框输入整数,校验范围 1 ~ 当前类型最大可回收数(页面动态提示"本批次 XXX 最多可回收数量:N")。**操作按钮**:【取消】关闭;【提交】执行回收,列表"券码剩余数量"对应减少。
+
+### 4.5 导出失败券码
+
+**展示条件**:批次"导入失败数量 > 0"时显示【导出失败券码】按钮。**交互**:直接下载该批次的失败明细文件,便于运营修正后重新导入。
+
+## 5\. 与移动端的关联
+
+-   移动端权益列表的可见范围 = 借记卡权益(已上架)∩ 展示规则命中
+    
+-   移动端"显示位置"由后台【设置显示页面】决定(首页 / 组装页)
+    
+-   移动端"展示顺序"由后台【调整显示顺序】决定
+    
+-   第三方权益的"是否有详情页"决定用户在 App 列表层级还是详情层级跳出("没有" = 首页/列表页直接跳出;"有" = 进入权益详情页后再跳出)
+    
+-   微信立减金在单独的微信立减金列表展示,统一通过"我的立减金"入口领用
+    
+
+## 6\. 权益审核
+
+### 6.1 入口
+
+**Liso 后台 → 定制功能 → 汇丰银行 → 权益审核**,作为权益模块下的独立子菜单,集中处理借记卡权益所有审核单(新增上架 / 下架)。
+
+### 6.2 列表页
+
+![](screenshots/6-audit-list.png)
+
+按审核状态分为三个 Tab,切换 Tab 时重新加载列表:
+
+| Tab | 含义  | 操作列按钮 |
+| --- | --- | --- |
+| 待审核 | 审核状态 = `上架待审核` / `下架待审核` 的审核单 | 审批  |
+| 审核通过 | 历史审核通过的记录 | 查看  |
+| 审核驳回 | 历史审核驳回的记录 | 查看  |
+
+**筛选条件**:
+
+| 字段  | 类型  | 说明  |
+| --- | --- | --- |
+| 权益编号 | 文本输入 | 模糊匹配 |
+
+**操作按钮**:
+
+-   【查询】:根据筛选条件查询
+    
+-   【重置】:清空筛选条件
+    
+
+**列表字段**:
+
+| 字段  | 说明  |
+| --- | --- |
+| 操作  | 待审核 Tab 显示【审批】;审核通过 / 驳回 Tab 显示【查看】 |
+| 权益名称 | 过长时省略号 + 悬浮 tooltip 展示 |
+| 权益编号 | 系统生成的 `Q` + 6 位序号 |
+| 审核类型 | Tag 展示,可选值:`新增`(绿)/ `下架`(红) |
+| 业务模块 | 当前固定为"借记卡权益" |
+| 提交人 | 审核单提交人姓名 |
+| 提交时间 | Y-m-d H:i:s |
+| 审核人 | 审核人姓名;未审核时显示"-" |
+| 审核时间 | Y-m-d H:i:s;未审核时显示"-" |
+
+**分页**:默认每页 10 条,可切换 10 / 20 / 50;支持上一页、下一页、页码、跳转;左下角展示 `Total N` 总条数。
+
+### 6.3 审核详情
+
+点击行内【审批】/【查看】进入审核详情页。
+
+**页面结构**:三步骤步骤条 —— `权益信息 → 设置规则 → 更多设置`,全部控件 disabled,按"添加权益"的同款向导只读展示提交时的快照内容:
+
+-   **步骤 1 权益信息**:基本信息(权益来源、权益类型 / 外部权益链接 / 是否有详情页、权益名称、副标题、SIP 商品编号、商户名称、权益图标);详情信息(权益主图、权益介绍、权益有效期、权益内容、权益说明、领取成功按钮)。详情信息卡片显示条件同 §2.2.1(银行自有 / 荣数商品;或第三方 + 有详情页)
+    
+-   **步骤 2 设置规则**:展示规则(展示时间限制、展示客群限制)+ 领取规则(领取时间限制、领取客群限制)开关 + 已配置内容只读回显
+    
+-   **步骤 3 更多设置**:显示页面(权益首页/列表页、组装页 + 组装页列表)+ 分享设置(开关回显)
+    
+
+**已审核记录卡片**(仅 `审核状态 ≠ 待审核` 时展示,置于步骤导航下方):
+
+| 字段  | 说明  |
+| --- | --- |
+| 审核人 | 该审核单的审核执行人 |
+| 审核时间 | Y-m-d H:i:s |
+| 审核结果 | Tag 展示,可选值:`已通过`(绿)/ `已驳回`(红) |
+
+**底部操作按钮**(按当前步骤与审核类型显隐):
+
+| 按钮  | 显示条件 | 行为  |
+| --- | --- | --- |
+| 上一步 | 当前步骤 > 1 | 回到上一个步骤 |
+| 下一步 | 当前步骤 < 3 | 进入下一个步骤 |
+| 返回  | 始终显示 | 返回审核列表 |
+| 通过  | 审核状态 = `待审核` 时显示。**新增类型须当前步骤 = 3 才显示**;下架类型不限制步骤 | 二次确认弹窗 `确定通过该审核单吗?`(按钮 通过/取消) → 提示 `审核通过`,返回审核列表 |
+| 驳回  | 显示条件同【通过】 | 二次确认弹窗 `确定驳回该审核单吗?`(按钮 驳回/取消) → 提示 `已驳回`,返回审核列表 |
+
+### 6.4 状态联动
+
+-   **审核通过**:
+    
+    -   新增上架审核 → 权益主表 `商品状态 = 已上架`、`审核状态 = 审核通过`
+        
+    -   下架审核 → 权益主表 `商品状态 = 未上架`、`审核状态 = 审核通过`(等价于历史下架)
+        
+-   **审核驳回**:
+    
+    -   新增上架审核 → 权益主表 `商品状态` 保持 `未上架`、`审核状态` 回退到 `草稿`,可在借记卡权益列表点击【编辑】继续完善后重新提交
+        
+    -   下架审核 → 权益主表 `商品状态` 保持 `已上架`、`审核状态` 回退到 `审核通过`,权益继续正常在线
+        
+-   同一权益**同时只能存在 1 条进行中的审核单**
+    
+-   审核动作产生的状态变化在借记卡权益列表实时刷新
+    
+
+# 三、人群管理
+
+### 1\. 模块概述
+
+人群管理由两个独立列表构成:**客群任务管理**与**规则组管理**。
+
+-   **客群任务管理**:维护"对客群体"。任务有两种类型 —— ① **规则组圈选**(通过规则组运行 `count / sample / fulllist` 三阶段流程产出客户名单)② **指定客户号**(由运营人员直接维护一份固定的客户号清单)。每个任务由「客群编号」唯一标识,"客群编号 + 名称"在后续【发放计划】、权益领取限制等场景被引用
+    
+-   **规则组管理**:维护可复用的"规则集合"。规则组由一组元规则按"且 / 或"逻辑组合形成,配置完成并审核通过后才可被客群任务(规则组圈选类型)引用
+    
+-   **审核**:涉及新建、编辑、停止任务等行为均需另一账号审核后才生效(demo 中默认通过)
+    
+-   **三层逻辑结构**:客群任务 → 规则组 → 元规则。高维可对低维设置「且 / 或」组合,例如:`(A 且 B) 且 (C) 且 (D 或 E 或 F)`
+    
+
+* * *
+
+### 2\. 客群任务管理
+
+2.1 列表页
+
+![](screenshots/crowd-task-list.png)
+
+**入口**:Liso 后台 → 定制功能 → 汇丰银行 → 客群任务管理。
+
+**头部按钮**:
+
+| 按钮  | 说明  |
+| --- | --- |
+| 新增任务 | 下拉菜单,二选一:  <br>• **规则组圈选**:跳转至 `crowd-tasks/add` 走两步骤新建向导  <br>• **指定客户号**:跳转至 `crowd-name-lists/add` 走两步骤新建向导 |
+
+**筛选条件**:
+
+| 字段  | 类型  | 说明  |
+| --- | --- | --- |
+| 客群编号 | 文本输入 | 模糊匹配 |
+| 客群名称 | 文本输入 | 模糊匹配 |
+| 类型  | 下拉单选 | 选项:全部 / 规则组圈选 / 指定客户号 |
+
+操作按钮:【查询】【重置】。
+
+**列表字段**:
+
+| 字段  | 说明  |
+| --- | --- |
+| 操作  | 行内按钮,按"类型 + 状态"显隐,详见 §2.4 操作可见性矩阵 |
+| 客群编号 | 系统生成,全局唯一 |
+| 客群名称 | 过长省略号 + tooltip |
+| 类型  | Tag 形式展示,可选值:`规则组圈选` / `指定客户号` |
+| 状态  | Tag 形式展示。规则组圈选状态:`新建` / `校验` / `上线` / `已停止`;指定客户号状态:`新建` / `已生效` / `已停止` |
+| 人数  | **仅"指定客户号"任务展示**,规则组圈选类型该列显示"-" |
+| 创建人 | 任务创建人 |
+| 审核人 | 最近一次审核执行人;未审核时为空 |
+| 任务创建时间 | Y-m-d H:i:s |
+| 任务开始日期 / 任务结束日期 | **仅"规则组圈选"任务展示**,指定客户号类型显示"-" |
+| 更新时间 | 最近一次更新时间 |
+
+**分页**:默认每页 10 条,可切换 10 / 20 / 50。
+
+* * *
+
+2.2 新增任务
+
+2.2.1 规则组圈选
+
+![](screenshots/crowd-task-add-step1.png)
+
+**入口**:列表头部「新增任务 → 规则组圈选」。两步骤向导:
+
+**第一步:基本信息**
+
+| 字段  | 必填  | 说明  |
+| --- | --- | --- |
+| 客群名称 | 是   | 文本,最长 40 字符,带字数提示 |
+| 客群编号 | —   | 只读,系统自动生成,保证唯一 |
+| 客群描述 | 是   | 多行文本,最长 200 字符 |
+| 客群任务开始日期 | 是   | 日期选择 |
+| 客群任务结束日期 | 是   | 日期选择,不可早于开始日期 |
+
+**第二步:规则组 / 奖励信息**
+
+| 字段  | 必填  | 说明  |
+| --- | --- | --- |
+| 规则组间运算逻辑 | 是   | 下拉单选:`AND(且)` / `OR(或)` |
+| 规则组表格 | —   | 列:规则组编号 / 规则组名称 / 规则组描述 / 组内逻辑 / 规则个数;行内操作:删除 |
+| 【添加规则组】按钮 | —   | 弹窗选择已上线的规则组,支持按规则组编号、名称模糊搜索 |
+
+**底部按钮**:
+
+| 按钮  | 显示条件 | 行为  |
+| --- | --- | --- |
+| 取消  | 步骤 = 1 | 返回客群任务列表 |
+| 下一步 | 步骤 = 1 | 进入第二步 |
+| 上一步 | 步骤 = 2 | 返回第一步 |
+| 完成  | 步骤 = 2 | 校验后提交,落地为 `新建` 状态,需审核通过后才能进入运行流程 |
+
+2.2.2 指定客户号
+
+![](screenshots/crowd-name-list-add-step1.png)
+
+**入口**:列表头部「新增任务 → 指定客户号」。两步骤向导:
+
+**第一步:基本信息**
+
+| 字段  | 必填  | 说明  |
+| --- | --- | --- |
+| 客群名称 | 是   | 文本,最长 40 字符 |
+| 客群编号 | —   | 只读,系统自动生成 |
+| 客群描述 | 是   | 多行文本,最长 200 字符 |
+
+**第二步:初始人员**(仅新增时显示,编辑时跳过)
+
+输入模式按钮组切换:
+
+| 输入模式 | 说明  |
+| --- | --- |
+| 手动输入 | 多行 textarea,每行一个客户号(示例占位符:"00001 / 00002 / 00003") |
+| 上传文件 | 拖拽或点击上传 `.txt` / `.csv`,UTF-8 编码,每行一个客户号;解析成功后展示已选文件名 + 提示 `已识别 N 个有效客户号` |
+
+**校验**:至少添加 1 个有效客户号,否则不可完成。
+
+**底部按钮**:取消 / 上一步 / 下一步 / 完成(按步骤显隐,规则同 2.2.1)。
+
+* * *
+
+2.3 编辑 / 复制 / 启用
+
+**编辑(规则组圈选)**:允许编辑全部字段;编辑过程中若任务当前状态为 `校验`,保存后会回退至 `新建`,并重置 count / sample / fulllist 三段运行信息。
+
+**复制(规则组圈选)**:默认继承源任务的基本信息与规则组信息,状态为 `新建`,客群编号留空,由系统在保存时生成新的唯一编号。
+
+**编辑(指定客户号)**:仅可修改基本信息(客群名称 / 描述),不再展示"初始人员"步骤。已停止名单不可编辑。
+
+**启用(指定客户号)**:仅 `新建` 状态的指定客户号任务行内有【启用】按钮,点击后切换为 `已生效`。
+
+* * *
+
+2.4 操作可见性矩阵
+
+任务行的操作按状态 + 类型动态展示:
+
+**A. 规则组圈选**
+
+| 状态  | 可执行操作 |
+| --- | --- |
+| 新建 (`draft`) | 编辑 / 运行 / 停止 / 详情 / 复制 |
+| 校验 (`verifying`) | 编辑 / 上线 / 停止 / 详情 / 复制 |
+| 上线 (`online`) | 停止 / 详情 / 复制(编辑仅限基本信息) |
+| 已停止 (`stopped`) | 详情 / 复制 |
+
+各动作含义:
+
+| 操作  | 说明  |
+| --- | --- |
+| 运行  | 上送 count + sample 至行内,状态从 `新建` 流转至 `校验`;运行期间显示"正在运行"且按钮不可点 |
+| 上线  | 上送 fulllist 至行内,状态从 `校验` 流转至 `上线`;该动作需审核 |
+| 停止  | 状态切到 `已停止`,**不可逆**;该动作需审核 |
+| 编辑  | 重新编辑基本信息与规则组;编辑后审核通过,状态回到 `新建` 并清空 count / sample / fulllist |
+| 复制  | 进入新建向导,自动回填源任务的基本信息与规则组,权益编号留空(保存时由系统生成新编号) |
+| 详情  | 跳转至详情页(§2.5) |
+
+**B. 指定客户号**
+
+| 状态  | 可执行操作 |
+| --- | --- |
+| 新建 (`draft`) | 启用 / 编辑 / 人员调整 / 上传调整文件 / 停止 |
+| 已生效 (`active`) | 编辑 / 人员调整 / 上传调整文件 / 停止 |
+| 已停止 (`stopped`) | 无可执行操作(仅展示) |
+
+* * *
+
+2.5 任务详情
+
+![](screenshots/crowd-task-view.png)
+
+**入口**:列表行内【详情】。
+
+页面分为三块:
+
+**(1)基本信息**:客群名称 / 客群编号 / 状态 Tag / 客群描述 / 任务开始日期 / 任务结束日期 / 任务创建时间 / 任务创建用户 / 审核人 / 最近更新时间。
+
+**(2)规则组信息**:
+
+-   规则组间逻辑:Tag 展示 `AND(且)` / `OR(或)`
+    
+-   规则组明细表:规则组编号 / 规则组名称 / 规则组描述 / 组内逻辑 / 规则个数(只读)
+    
+
+**(3)运行信息**(黄色背景卡片):
+
+| 阶段  | 显示形态 |
+| --- | --- |
+| 总数 (count) | "未运行" / "正在运行"(灰)/ 具体数字 |
+| 校验 (sample) | "详情"链接(可点击打开弹窗查看 sample 数据)/ "正在运行"(灰)/ "未运行"(灰);右侧附【重新校验】按钮,仅当任务状态允许重跑时可点。重新校验仅重跑 count + sample,不影响 fulllist |
+| 全量圈客 (fulllist) | "未运行"(红)/ "正在运行"(灰)/ "已完成"(绿) |
+
+**sample 抽样规则**(行内返回):count < 30000 取 5 个;30000~~50000 取 8 个;50000~~150000 取 12 个;> 150000 取 15 个;结果不足 5 个则全部返回。
+
+**底部按钮**:返回。
+
+* * *
+
+2.6 人员调整 / 上传调整文件(指定客户号专属)
+
+**人员调整(行级)**
+
+点击列表行的【人员调整】按钮唤起弹窗:
+
+-   客群编号自动绑定为该行 ID,不可改
+    
+-   输入字段:客户号 + 调整方向(`A` = 调增;`D` = 调减)
+    
+-   已停止名单不可调整
+    
+
+**上传调整文件(行级)**
+
+点击列表行的【上传调整文件】按钮唤起弹窗:
+
+-   顶部展示目标客群(`<编号>-<名称>`),由所在行隐式绑定
+    
+-   文件格式(两段,使用 `|+|` 分隔):
+    
+
+```Plain
+客户号|+|调整方向
+00001|+|A
+00002|+|D
+```
+
+-   解析后展示预览表(行号 / 客户号 / 调整方向 / 状态),无效行单独标红
+    
+-   客群编号无需写入文件;行内若与目标客群冲突,按目标客群处理
+    
+
+* * *
+
+### 3\. 规则组管理
+
+3.1 列表页
+
+![](screenshots/rule-group-list.png)
+
+**入口**:Liso 后台 → 定制功能 → 汇丰银行 → 规则组管理。
+
+**头部按钮**:【新增规则组】。
+
+**筛选条件**:
+
+| 字段  | 类型  | 说明  |
+| --- | --- | --- |
+| 规则组编号 | 文本输入 | 模糊匹配 |
+| 规则组名称 | 文本输入 | 模糊匹配 |
+| 规则组类型 | 下拉单选 | 选项由系统维护的字典动态加载 |
+
+操作按钮:【查询】【重置】。
+
+**列表字段**:
+
+| 字段  | 说明  |
+| --- | --- |
+| 操作  | 左侧固定列:查看详情 / 编辑 / 提交审核 / 删除,按状态显隐 |
+| 规则组编号 | 系统生成 |
+| 规则组名称 | 过长省略 + tooltip |
+| 规则组类型 | 来自字典 |
+| 规则逻辑 | Tag 展示,可选值:`AND(且)` / `OR(或)` |
+| 规则数量 | 该规则组下的元规则条数 |
+| 状态  | Tag 展示,可选值:`新建` / `已上线` |
+| 创建人 | 规则组创建人 |
+| 创建时间 | Y-m-d H:i:s |
+
+**行内操作可见性**:
+
+| 操作  | 显示条件 |
+| --- | --- |
+| 查看详情 | 任意状态 |
+| 编辑  | 状态 = `新建` |
+| 提交审核 | 状态 = `新建` |
+| 删除  | 状态 = `新建`(红色) |
+
+**分页**:默认每页 10 条,可切换 10 / 20 / 50。
+
+* * *
+
+3.2 新增 / 编辑规则组
+
+![](screenshots/rule-group-add-step1.png)
+
+**入口**:列表头部【新增规则组】 / 行内【编辑】。两步骤向导:
+
+**第一步:基本信息**
+
+| 字段  | 必填  | 说明  |
+| --- | --- | --- |
+| 规则组名称 | 是   | 文本,最长 40 字符,带字数提示 |
+| 规则组类型 | 是   | 下拉,选项由系统维护的字典动态加载 |
+| 规则逻辑 | 是   | 圆点单选:`AND(且,全部满足)` / `OR(或,任一满足)` |
+| 备注说明 | 否   | 多行文本,最长 200 字符 |
+
+**第二步:规则配置**
+
+-   规则表格:元规则编号 / 元规则名称 / 规则名称 / 规则描述 / 规则参数(等宽字体);行内操作:编辑 / 查看 / 删除
+    
+-   【添加规则】按钮:弹出"添加规则"对话框选择元规则,选中元规则后自动展开该元规则对应的"待配置项"(带 `*` 为必配项,否则可选),保存后落入规则表格
+    
+
+**校验**:至少添加 1 条规则;状态 = `已上线` 的规则组不可编辑。
+
+**底部按钮**:取消 / 上一步 / 下一步 / 完成(按步骤显隐)。
+
+* * *
+
+3.3 规则组详情
+
+![](screenshots/rule-group-view.png)
+
+**入口**:列表行内【查看详情】。
+
+页面展示两个只读描述区:
+
+**(1)基本信息**:规则组编号 / 规则组名称 / 规则组类型 / 规则逻辑 Tag / 状态 Tag / 规则数量 / 创建人 / 创建时间 / 最后更新人 / 最后更新时间 / 备注说明
+
+**(2)规则组配置**:规则逻辑提示文案"规则之间使用「且 / 或」逻辑组合";明细表:元规则编号 / 元规则名称 / 规则名称 / 规则描述 / 规则参数(等宽字体)
+
+**底部按钮**:返回。
+
+* * *
+
+### 4\. 具体规则与逻辑配置
+
+逻辑运算支持「且 / 或(AND / OR)」组合。客群任务范围内分为三层(从高到低):**客群任务 → 规则组 → 元规则**(借记卡元规则约 50+ 项,信用卡元规则若干项)。高维可对低维设置 AND / OR 组合,例如:
+
+```Plain
+(A 且 B) 且 (C) 且 (D 或 E 或 F)
+```
+
+# 四、发放计划
+
+## 1\. 概述
+
+发放计划用于将已上架的借记卡权益按计划下发到客群名下。一个发放计划对应「一组客群 × 一个或多个权益」,按周期统计实际可领取 / 已发放数。
+
+-   **入口**:Liso 后台 → 定制功能 → 汇丰银行 → 发放计划
+    
+-   **发放计划 ID**:系统自动生成,统一 `P` + 6 位序号格式(如 `P000001`),全局唯一
+    
+-   **状态**:由"开始时间 / 结束时间 + 当前时间"实时推算 —— `未开始`(当前 < 开始时间)、`进行中`(开始 ≤ 当前 ≤ 结束)、`已结束`(当前 > 结束时间)
+    
+
+## 2\. 列表页
+
+![](screenshots/4-grant-campaign-list.png)
+
+### 2.1 筛选条件
+
+| 字段  | 类型  | 说明  |
+| --- | --- | --- |
+| 发放计划名称 | 文本输入 | 关键字模糊匹配,宽度 220px |
+| 发放计划时间 | 日期范围 | 选择 `开始日期 至 结束日期`,按计划开始/结束时间过滤 |
+| 状态  | 下拉单选 | 选项:未开始 / 进行中 / 已结束;不选视为全部 |
+
+**操作按钮**:
+
+-   【查询】:按筛选条件查询
+    
+-   【重置】:清空筛选条件
+    
+-   【新建发放计划】(位于筛选下方):跳转新建表单
+    
+
+### 2.2 列表字段
+
+| 字段  | 说明  |
+| --- | --- |
+| 操作  | 行内按钮:查看 / 编辑 / 结束 / 删除,按状态显隐,详见 §2.3 |
+| 发放计划 ID | 系统生成,`P` + 6 位序号(如 `P000001`) |
+| 发放计划名称 | 最长 50 字符,过长省略号 + tooltip |
+| 开始时间 | Y-m-d H:i:s |
+| 结束时间 | Y-m-d H:i:s |
+| 状态  | Tag 展示,可选值:`未开始`(灰)/ `进行中`(绿)/ `已结束`(默认) |
+| 获取方式 | Tag 展示,可选值:`用户领取`(绿,对应 `claim`)/ `直接发放`(黄,对应 `direct`) |
+| 权益数量 | 含周期前缀,例如 `每月 2 个`、`整个周期 1 个`、`每季度 5 个`、`每年 10 个` |
+| 权益有效期 | 固定日期:`YYYY-MM-DD 至 YYYY-MM-DD`;相对日期:`自发放起 N 天内有效` |
+| 创建时间 | Y-m-d H:i:s |
+
+### 2.3 操作可见性矩阵
+
+| 操作  | 显示条件 | 行为  |
+| --- | --- | --- |
+| 查看  | 任意状态 | 跳转 `equity-grant-campaign/view/:id` 查看页 |
+| 编辑  | 状态 ≠ `进行中`(即 `未开始` / `已结束`) | 跳转 `equity-grant-campaign/edit/:id` 编辑页;若进入后发现该计划已切到"进行中",会强制提示 `进行中的发放计划不可编辑` 并退回列表 |
+| 结束  | 状态 = `进行中`(黄色文字) | 二次确认弹窗 `确定结束发放计划「XXX」吗?结束后将立即生效,且不再继续发放权益。`(按钮 确认结束 / 取消) → 调用接口,提示 `已结束` |
+| 删除  | 状态 ≠ `进行中`(红色文字) | 二次确认弹窗 `您确定要删除此条发放计划吗?删除后将无法恢复。`(按钮 确认删除 / 取消) → 调用接口,提示 `删除成功` |
+
+* * *
+
+## 3\. 新建 / 编辑发放计划
+
+**入口**:
+
+-   新建:列表页【新建发放计划】
+    
+-   编辑:列表行【编辑】,仅 `未开始` / `已结束` 状态可进入;`进行中` 进入会强制退回
+    
+
+**页面结构**:单页表单(非步骤向导),整体水平居中,字段标签左对齐。字段如下:
+
+| 字段  | 必填  | 说明  |
+| --- | --- | --- |
+| 发放计划 ID | —   | **仅编辑时显示**,只读,禁用输入框 |
+| 发放计划名称 | 是   | 文本输入,placeholder `请输入发放计划名称`,最长 50 字符,带字数实时提示 |
+| 发放计划开始时间 | 是   | datetime 选择器,精确到秒,格式 `YYYY-MM-DD HH:mm:ss` |
+| 发放计划结束时间 | 是   | datetime 选择器,精确到秒;提交时校验「结束时间必须晚于开始时间」 |
+| 选择客群 | 是   | 多选下拉(支持搜索),已选项以可折叠 Tag 展示;选项格式 `客群名称 (客群编号)`,数据源为人群管理中的客群任务(含规则组圈选与指定客户号两种类型);校验文案 `请至少选择一个客群` |
+| 获取方式 | 是   | 圆点单选,默认 `用户领取`:  <br>• **用户领取**(`claim`):权益发放到客户名下后由用户主动领取  <br>• **直接发放**(`direct`):权益自动到账,无需领取动作 |
+| 选择权益 | 是   | 数据源为「已上架」的借记卡权益。选项格式 `权益名称 (权益编号)`  <br>• 用户领取模式:多选下拉(支持搜索,已选项以可折叠 Tag 展示)  <br>• 直接发放模式:单选下拉 + 搜索  <br>切换 `用户领取 → 直接发放` 时,若已选多个权益,自动只保留第一个;校验文案 `请至少选择一个权益` / `请选择一个权益` |
+| 权益数量 | 是   | 行内组合控件:左侧周期下拉(整个周期 / 每月 / 每季度 / 每年),右侧数字输入(1 ~ 9999),后缀"个" |
+| 权益有效期 | 是   | 圆点单选「固定日期 / 相对日期」:  <br>• **固定日期**:选择起止日期(必须二选完整)  <br>• **相对日期**:数字输入 1 ~ 3650,单位"天(自发放起算)",默认值 30 |
+| 短信通知 | 是   | 短信模板下拉,宽 420px。选项与权益侧"下单成功短信"是两套独立模板,**专用于发放成功短信**:  <br>• `GRANT_SMS_001` 发放成功通用模板  <br>• `GRANT_SMS_002` 发放成功-含查看入口  <br>• `GRANT_SMS_003` 发放成功-简洁版  <br>选中后下方实时展示模板内容预览;占位符:`${customerName}` 客户名、`${productName}` 权益名称、`${grantTime}` 发放时间、`${expireDate}` 有效期 |
+
+**底部按钮**(页脚居中):
+
+| 按钮  | 行为  |
+| --- | --- |
+| 取消  | 直接返回发放计划列表 |
+| 保存  | 触发所有字段必填校验 + 业务校验(结束晚于开始 / 固定日期需选完整 / 相对日期 ≥ 1 / 权益数量 ≥ 1 / 直接发放模式仅允许 1 个权益 / 短信模板必选)。校验通过后调用保存接口,新建提示 `新建成功`,编辑提示 `保存成功`,并返回列表页 |
+
+* * *
+
+## 4\. 查看发放计划
+
+![](screenshots/4-grant-campaign-view.png)
+
+**入口**:列表行【查看】,所有状态均可进入。
+
+**页面结构**:采用 2 列描述列表布局展示所有字段,全部只读;权益、客群、短信模板内容以 Tag / 预览块形式展示。
+
+| 字段  | 展示形态 |
+| --- | --- |
+| 发放计划 ID | 文本  |
+| 发放计划名称 | 文本  |
+| 状态  | Tag(同列表口径) |
+| 获取方式 | Tag(同列表口径) |
+| 权益数量 | 含周期前缀的描述文本 |
+| 开始时间 / 结束时间 | 文本(缺省时显示"-") |
+| 权益有效期 | 文本(固定/相对两种描述,跨 2 列) |
+| 选择客群 | Tag 列表(info 灰色),每个 Tag 显示 `客群名称 (客群编号)`;无值显示"-" |
+| 选择权益 | Tag 列表,每个 Tag 显示 `权益名称 (权益编号)`;无值显示"-" |
+| 短信通知 | 第一行:模板名称(如"发放成功通用模板");下方虚线框展示该模板的完整内容预览 |
+| 创建时间 / 更新时间 | 文本  |
+
+**底部按钮**:【返回】,回到发放计划列表。
+
+# 五、页面管理
+
+## 权益介绍页
+
+### 等级配置
+
+按“客户等级”进行隔离配置。需在页面顶部 Tab 切换不同等级(卓越理财 / 卓越理财·尊尚 / 环球私人银行),分别配置各等级在移动端的专属展示内容。
+
+### Tab名称配置模块
+
+![](screenshots/5-page-home.png)
+
+-   **Tab名称(输入框)**:
+    
+    -   **作用**:定义当前等级在移动端顶部切换栏的展示文字(如将“卓越理财”改为“卓越尊享”)。
+        
+    -   **移动端交互**:移动端顶部导航栏的文本会随之动态改变。后台上方的配置 Tab 也会实时同步。
+        
+
+### 头部配置
+
+对应移动端最上方的大图与欢迎文案。
+
+-   **头图配图**:点击虚线框上传本地图片。建议图片尺寸:686\*296 px。
+    
+-   **主标题/副标题**:配置移动端大图下方展示的加粗主标题和小字副标题文本。
+    
+
+### 按钮配置模块
+
+-   **已解锁按钮文案(输入框)**:
+    
+    -   **移动端交互**:针对**已解锁**当前等级的客户展示的按钮文案(如“进入权益专区”)。点击直接拉起“权益首页”。
+        
+-   **待解锁按钮文案(输入框)**:
+    
+    -   **移动端交互**:针对**未解锁**该等级的客户,按钮将呈现置灰状态,并回显此文案(如“查看达标条件”)。点击会拉起“如何达标”半屏说明弹窗。
+        
+
+### 权益列表配置
+
+-   **配图(上传组件)**:
+    
+    -   **移动端交互**:此项为**非必填**。若未上传图片,移动端不会出现小图标,而是自动降级展示为一个标准的“圆点”符号兜底,确保排版整洁对齐。
+        
+-   **标题 / 权益文字描述(输入框)**:填写单行小标题与详细的权益介绍。支持通过列表右上角的气泡排序功能调整顺序。
+    
+
+## 权益主页
+
+### 客户身份模块配置
+
+![](screenshots/5-page-zone.png)
+
+前端将根据登录客户的实际客群 (MSC) 自动匹配展示此处配置的对应身份卡片。支持四个客群的独立配置:运筹理财、卓越理财、卓越理财·尊尚、环球私人银行。
+
+-   **客群背景图(上传组件)**:支持为每个客群上传独立的顶部横向卡片背景图。建议尺寸:`750 * 185 px`。
+    
+-   **身份标识文字(输入框)**:显示于移动端卡片左上角的客群称呼。
+    
+-   **显示合格状态 / 显示 PB 权益等级及编号(复选框)**:控制是否在卡片上展示客户的达标状态及专属编号。
+    
+-   **显示“账户月内日均余额”(复选框 & 强联动逻辑)**:
+    
+    -   若开启此开关,卡片将展示余额,**并且下方会自动显示“日均余额计算规则”的富文本配置框**,前端同时出现 `?` 问号规则查看入口。
+        
+    -   若关闭此开关,“计算规则”配置框将被强制隐藏,前端也同步隐藏该规则入口。
+        
+    -   _注:PB 客群卡片底层逻辑不显示月内日均余额,故不提供此项配置。_
+        
+
+### 活动广告轮播图
+
+对应移动端中部的精选活动卡片滑动区。
+
+-   **多端跳转配置(输入框组)**:
+    
+    -   支持同时配置 **H5 跳转链接 / APP 跳转链接 / 微信小程序 AppID / 微信小程序页面路径**。
+        
+    -   **移动端交互**:移动端内核会智能探测当前用户的运行环境,精准拉起对应的原生页面,避免跨环境跳转失败。
+        
+-   **可见人群标签(下拉多选)**:千人千面控制开关。勾选对应标签后,该广告仅对含有相应标签的客群展示。
+    
+
+### 常用兑换配置
+
+-   **常用兑换**:配置底部横滑图标卡片,支持图片、名称及跳转 URL 的录入。
+    
+
+### 积分规则弹窗配置
+
+-   **积分规则弹窗**:支持通过富文本工具的 **“附件”** 按钮上传本地 PDF。移动端会自动生成带回形针图标的下载链接,点击可调用原生阅读器全屏预览该 PDF 规则细则。
+    
+
+## 组装页
+
+点击“编辑”进入页面配置。顶部需填写内部管理的**组装页名称**。 页面由上至下通过添加不同的“数据模块”组合而成,页面最下方有四个模块添加按钮,支持无限叠加与自由排序:
+
+### 列表管理
+
+![](screenshots/5-page-assembly.png)
+
+-   **列表视图**:展示所有组装页的页面ID、名称、发布状态(草稿/已上架)及更新时间。支持按页面ID或名称精准查询。
+    
+-   **获取链接**:在列表视图的每一行操作列中,提供了专属的“链接”按钮,点击可直接唤起获取该单页专属分发链接的面板。支持在弹窗的下拉列表中选择 `Campaign ID` 和 `Channel ID`。选择后,会自动将这些参数拼接在 URL 末尾(例如拼接 `&CampaignId=CAMP202511&Channelid=CH_APP`)。若未选择任何下拉值,则不拼接相关参数。
+    
+-   **页面基础信息**:点击“新建”或“编辑”进入详情后,需设置内部管理的“组装页名称”。页面 ID 则由系统自动分配生成,不可修改。
+    
+-   **状态与链接区分**:若页面处于“草稿”状态,弹窗中仅提供用于内部预览测试的“草稿预览链接”;当页面正式“发布”后,弹窗会同时展示正式投放使用的“正式环境链接”和草稿预览链接。
+    
+
+### 创建页面与基础设置
+
+-   **进入列表与创建**:在组装页列表中,您可以查看到所有历史配置的页面。点击右上角的【新建组装页】按钮,或点击已有页面的【编辑】进入配置界面。
+    
+-   **设置组装页名称(必填)**:在页面顶部输入框填写该页面的名称。
+    
+    -   注意:不仅用于后台列表查找,还会**直接在移动端最顶部的导航栏(Page Title)作为标题向客户展示**。
+        
+-   **页面 ID 生成**:系统会自动为当前页面生成一个唯一的“页面 ID”,无需您手动配置。
+    
+
+### 页面模块配置
+
+-   模块新增:在配置区最下方,可以看到四个功能按钮。点击对应的按钮即可在页面最末尾新增该模块:
+    
+-   **模块位置调整与管理**:每个已添加的模块右上角都有一个统一的操作控制台:
+    
+    -   **气泡排序**:点击带有数字的列表图标,在弹出的悬浮气泡中输入目标序号(如:1),点击确定,该模块将立即移动到对应位置。
+        
+    -   **复制**:点击“复制”可一键克隆当前模块及内部所有数据(_注:为防止数据冲突,系统已强行锁死并隐藏了“权益商品模块”的复制功能_)。
+        
+    -   **删除**:点击红色的垃圾桶图标并确认,即可移除不需要的模块。
+        
+-   **图片模块**:
+    
+    -   **配置**:通栏视觉展示模块。
+        
+    -   **移动端交互**:强烈建议图片宽度为 `750px`,移动端将 `100%` 自动拉通铺满屏幕宽度。高度无需受限,系统将根据上传图片的原始比例自动计算并拉伸展示,防止变形。
+        
+-   **权益商品模块**:**全局唯一,一个页面仅限添加一个,且不支持复制**。可配置一个大区标题,并在其下方添加多个带图标的权益商品明细。
+    
+-   **图文模块**:提供包含完整工具栏的富文本编辑器,支持上传附件和插入外链。
+    
+-   **按钮模块**:
+    
+    -   **样式选择(下拉框)**:支持“突出显示 (红色背景)”与“次要操作 (白色背景)”两种视觉层级。
+        
+    -   动作 - 跳转链接(输入框):配置按钮直接跳转至指定 URL。提交保存时系统会进行强校验,若动作为跳转链接,则链接地址字段绝对不能为空。
+        
+    -   动作 - 弹窗提示 (Dialog):配置点击按钮后唤起屏幕中央的弹窗。支持配置弹窗标题、描述正文,以及右侧“确认”按钮的文案与后续跳转链接。若未配置确认按钮的跳转链接,则点击确认仅执行关闭弹窗的动作。弹窗左侧固定展示取消按钮。
+        
+
+## Channel管理
+
+### 新建 Channel(标准流程)
+
+-   点击页面左中部的蓝色按钮 **【+ 新建 Channel】**,系统将弹出新建表单。
+    
+
+![](screenshots/channel-management.png)
+
+-   **Channel ID**:系统会在保存时**自动生成**唯一的 ID(如 CH0001),无需手动填写。
+    
+-   **选择内置层级属性**(严格的级联选择):
+    
+-   **Channel 类型 (Type)**:点击下拉框,选择内置的类型(如 `Human-assisted` 或 `Unassisted`)。
+    
+-   **Channel 类别 (Category)**:**必须先选择类型**,此处才会解锁并仅显示属于该类型下的类别(如选了 `Human-assisted` 才会出现 `MGM`)。
+    
+-   **Channel 名称 (Name)**:**必须先选择类别**,此处才会解锁并显示对应的具体名称。
+    
+-   **补充业务信息**:
+    
+    -   **Channel 子名称 (Subname)**:自由文本输入,例如输入具体的活动名称“私行沙龙引导”。
+        
+    -   **跟进员工 (Follow-up staff)**:输入负责跟进此渠道的员工姓名。
+        
+-   点击 **【确定】** 保存,系统将在列表中生成一条全新的 Channel 记录。
+    
+
+### 新增全新的 类型/类别/名称(字典维护功能)
+
+如果下拉列表中没有需要的选项,**无需跳出当前页面**,可以直接在弹窗内“边填边建”。
+
+-   **新增「Channel 类型」**:
+    
+    -   点击【Channel 类型】下拉框右侧的“新增”按钮。
+        
+    -   下方会展开一个快捷面板。在输入框中输入全新的类型名称(例如:新的类型)。
+        
+    -   点击“保存”。系统会自动将该类型存入底层字典,并在下拉框中自动选中它。
+        
+-   **新增「Channel 类别」**:
+    
+    -   **前提**:必须先在上方选择或新建好【Channel 类型】。如果未选择类型直接点击新增类别,系统会拦截并提示“_请先选择 Channel 类型_”。
+        
+    -   点击【Channel 类别】右侧的“新增”,输入新类别并保存。该新类别将自动归属于当前选中的类型之下。
+        
+-   **新增「Channel 名称」**:
+    
+    -   **前提**:必须先确定好【类型】和【类别】。点击右侧”新增“,输入后保存。该名称将牢牢绑定在当前的“类型+类别”路径下。**💡 核心逻辑提示**: 如果新增了一个顶级节点(类型),那么它下面必然是空的。此时必须继续使用”新增“功能,依次为其创建下属的【类别】和【名称】,才能完成整条链路的配置。
+        
+
+### 重复新增的校验与提示
+
+为了保证字典数据的整洁,系统排重校验:
+
+-   **同级查重**:在通过快捷面板保存新的 类型/类别/名称 时,系统会实时校验。如果输入的名称在当前层级已经存在,系统会弹出红色提示 **“该类型/类别/名称已存在”**,并拒绝保存。此时需要修改输入,或者直接取消新增,去下拉框中选择已有的那一项。
+    
+
+### 列表数据的操作
+
+成功创建 Channel 后,列表会展示所有数据,并在最右侧自动记录 **创建时间** 和 **更新时间**。在最左侧的”操作“列,可以进行:**【快捷查询】** 在页面顶部的筛选区,可以通过多维度组合条件快速定位数据:
+
+-   支持 **Channel ID** 的精确查询。
+    
+-   支持按 **Channel 类型** 进行下拉框筛选。
+    
+-   支持按 **Channel 类别**、**Channel 名称**、**Channel 子名称** 以及 **跟进员工** 进行关键字模糊搜索。
+    
+-   输入条件后点击 **【查询】** 即可过滤列表,点击 **【重置】** 会清空所有输入条件并恢复展示所有数据。**【数据操作】** 在最左侧的「操作」列,可以进行:
+    
+-   **【编辑】**:点击后弹出表单,回显原有数据。可以重新选择下拉项,或修改子名称和跟进员工。也可以再次使用”新增“按钮扩展字典。
+    
+-   **【查看】**:点击后弹出表单,但所有字段均置灰(只读模式),只能查看不可修改,防止误触。底部也没有确定/取消按钮。
+    
+-   **【删除】**:点击后,系统会弹出一个醒目的**确认删除提示窗**,提示“确定要删除此条数据吗?删除后将无法恢复”。点击确认后才会在列表中彻底移除该数据。
+    
+
+## Campaign管理
+
+### 新建 Campaign
+
+-   点击 **【+ 新建 Campaign】**
+    
+
+![](screenshots/campaign-management.png)
+
+-   **Campaign ID**:同样由系统自动生成(如 CP9001)。
+    
+-   依次手动输入以下信息:
+    
+    -   **Campaign 名称**:(必填项)当前活动的正式名称。
+        
+    -   **Campaign 负责人 (Owner)**:(必填项)负责该活动的团队或主管名称。
+        
+    -   **跟进员工 (Follow-up staff)**:(选填项)具体执行跟进的员工。
+        
+-   如果遗漏必填项,点击确定时系统会提示“_Campaign 名称 和 负责人 是必填项_”。
+    
+
+### 列表操作与查询
+
+Campaign 的列表操作与 Channel 保持一致:
+
+-   支持 **查看**、**编辑**(再次编辑会自动更新“更新时间”)、**删除**(带有防误删的二次确认弹窗)。
+    
+-   **顶部筛选区**:支持根据 ID 精确搜索,或根据名称、负责人、跟进员工进行模糊匹配查询。输入条件后点击 **【查询】** 即可过滤列表,点击 **【重置】** 会清空条件并展示所有数据。
+    
+
+## 通用操作说明
+
+### 全局页面状态指示器
+
+位于顶部独立工具栏左侧,实时侦测并防呆:
+
+-   **草稿 (灰色)**:从未发布至生产环境的内部版本。
+    
+-   **有未发布的修改 (黄色)**:您对页面进行了任意字符的修改但还未点击发布,系统立即亮起黄灯提醒。
+    
+-   **正式线上环境 (绿色)**:当前后台配置与线上移动端用户所见100%一致。
+    
+
+### 保存与发布
+
+界面右上方固定提供两个全局核心操作:
+
+-   **保存配置**:将当前所有修改保存为“草稿”状态。该操作仅更新后台数据库,不会影响移动端线上真实用户的所见内容,适用于未完成配置的暂存。
+    
+-   **发布配置**:将当前修改正式提交上架。发布成功后,最新数据将立刻同步至正式生产环境,移动端线上用户将实时看到更新后的内容与页面。
+    
+
+# 六、短信管理
+
+| 变更日期 | 变更内容 | 变更人 |
+| --- | --- | --- |
+| 2026-05-11 | 初稿:短信模板管理 + 发送记录查询 | 陈生坤 |
+
+## 1\. 概述
+
+### 1.1 业务背景
+
+为支撑权益商品的"下单成功短信"等通知场景,系统需提供一套独立的短信模板维护与发送追溯能力。本模块面向运营人员,统一管理短信模板与发送记录。
+
+### 1.2 模块组成
+
+| 子模块 | 职责  | 使用方 |
+| --- | --- | --- |
+| **短信模板管理** | 维护短信模板的内容、占位符、生命周期(新建/编辑/删除) | 被《二、权益管理》的"下单成功短信"引用 |
+| **发送记录查询** | 查看每条短信的发送明细(接收号、模板、内容、状态) | 运营 / 客服查问题、做对账 |
+
+### 1.3 操作入口
+
+**Liso 后台 → 定制功能 → 汇丰银行**,包含两个子菜单:
+
+-   **短信模板管理**
+    
+-   **发送记录查询**
+    
+
+* * *
+
+## 2\. 短信模板管理
+
+### 2.1 列表页
+
+![](screenshots/6-sms-template-list.png)
+
+2.1.1 筛选条件
+
+| 字段  | 类型  | 说明  |
+| --- | --- | --- |
+| 模板名称 | 文本输入 | 模糊匹配,支持清空 |
+| 模板 ID | 文本输入 | 精确匹配,支持清空 |
+
+**操作按钮**:
+
+-   【查询】:根据筛选条件查询
+    
+-   【重置】:清空筛选条件并回到第 1 页
+    
+-   【新建短信模板】:打开新建弹窗
+    
+
+2.1.2 列表字段
+
+| 字段  | 说明  |
+| --- | --- |
+| 操作  | 行内按钮:编辑(蓝色文字)、删除(红色文字) |
+| 模板 ID | 系统生成,格式形如 `SMS_TPL_001` |
+| 模板名称 | 用户可见的模板名(如"通用权益支付成功模板");过长省略号 + tooltip |
+| 模板内容 | 完整的短信文案(含占位符);过长省略号 + tooltip |
+| 创建人 | 首次创建的操作员 |
+| 创建时间 | Y-m-d H:i:s |
+| 最后更新人 | 最近一次保存的操作员 |
+| 更新时间 | 最近一次保存的时间(Y-m-d H:i:s) |
+
+2.1.3 分页
+
+-   默认每页 10 条,可切换 10 / 20 / 50
+    
+-   支持上一页、下一页、页码、跳转
+    
+-   左下角展示 `共 N 条` 总条数
+    
+
+* * *
+
+### 2.2 新建 / 编辑短信模板
+
+**入口**:列表【新建短信模板】或列表行【编辑】。**形态**:模态对话框,宽度 600px,标题随入口切换为 `新建短信模板` / `编辑短信模板`;点击遮罩不关闭,关闭后自动重置表单。
+
+| 字段  | 必填  | 说明  |
+| --- | --- | --- |
+| 模板名称 | 是   | 文本输入框,placeholder `请输入模板名称`,最长 40 字符,带字数实时提示;校验文案 `请输入模板名称` / `不超过 40 字` |
+| 模板内容 | 是   | 多行文本框(6 行),placeholder `请输入模板内容,支持下方占位符`,最长 500 字符,带字数实时提示;校验文案 `请输入模板内容` / `不超过 500 字`。下方常驻"占位符说明"虚线提示卡片 |
+
+**占位符说明**(编辑区固定提示,便于运营直接复用,占位符以蓝色 `code` 样式展示):
+
+| 变量  | 占位符 |
+| --- | --- |
+| 商品名称 | `${productName}` |
+| 结束日期 | `${expireDate}` |
+| 卡号  | `${exchangeNum}` |
+| 卡密  | `${exchangePwd}` |
+| 券码路径 | `${qrCodeDownPaths}` |
+
+**底部按钮**:
+
+| 按钮  | 行为  |
+| --- | --- |
+| 取消  | 关闭弹窗,丢弃输入 |
+| 保存  | 触发校验,通过后写入:  <br>• 新建:生成模板 ID(`SMS_TPL_xxx`),提示 `新建成功`  <br>• 编辑:保留原 ID 并刷新"最后更新人 / 更新时间",提示 `保存成功` |
+
+* * *
+
+### 2.3 删除短信模板
+
+**入口**:列表行【删除】按钮(红色文字)。**交互**:弹出二次确认(warning 类型)`确定要删除模板「XXX」吗?删除后无法恢复`。
+
+| 按钮  | 行为  |
+| --- | --- |
+| 确定  | 删除该模板并从列表移除,提示 `删除成功` |
+| 取消  | 关闭弹窗,列表不变 |
+
+* * *
+
+## 3\. 发送记录查询
+
+### 3.1 列表页
+
+![](screenshots/6-sms-send-record.png)
+
+3.1.1 筛选条件
+
+| 字段  | 类型  | 说明  |
+| --- | --- | --- |
+| 接收手机号 | 文本输入 | 模糊匹配(号码已脱敏,可直接对脱敏值检索) |
+| 模板名称 | 下拉单选 | 选项来源于"短信模板管理"。空值表示"全部" |
+| 发送状态 | 下拉单选 | 成功 / 失败 / 全部 |
+| 发送时间 | 日期区间 | 按"开始日期 ~ 结束日期"过滤(按天比较) |
+
+**操作按钮**:
+
+-   【查询】:根据筛选条件查询并回到第 1 页
+    
+-   【重置】:清空所有筛选条件
+    
+
+3.1.2 列表字段
+
+| 字段  | 说明  |
+| --- | --- |
+| 发送 ID | 系统生成,格式形如 `SMS20260510001` |
+| 接收手机号 | 脱敏展示(如 `1385821`) |
+| 模板名称 | 引用的模板名;超长以 tooltip 展示 |
+| 发送内容 | 占位符已替换为真实值的最终文案;超长以 tooltip 展示 |
+| 关联权益商品 | 触发本条短信的权益商品名(可为空) |
+| 发送状态 | 成功(绿色 Tag)/ 失败(红色 Tag) |
+| 发送时间 | Y-m-d H:i:s |
+
+3.1.3 分页
+
+-   默认每页 10 条,可切换 10 / 20 / 50
+    
+-   支持上一页、下一页、页码、跳转
+    
+-   左下角展示 `共 N 条` 总条数
+    
+
+* * *
+
+## 4\. 与权益管理的关联
+
+-   《二、权益管理》→ 添加 / 编辑权益商品 → 第一步【下单成功短信】开关开启后,引用本模块维护的短信模板(仅银行自有 / 荣数商品来源可见)
+    
+-   **发放计划侧的"短信通知"** 使用独立的发放短信模板(`GRANT_SMS_001` 系列),不与本模块的模板池共享,详见 §四 发放计划 §3
+    
+-   用户完成权益下单后,系统使用模板内容 + 该笔订单上下文生成最终文案,调用短信通道发送,并在【发送记录查询】中沉淀一条记录
+    
+
+# 七、报表中心
+
+报表中心面向运营 / 客服,统一提供积分、权益、立减金三类数据明细的查询与导出能力。三个子模块均为"筛选 + 列表 + 分页 + 导出"的标准结构;**导出**统一异步触发,提示 `导出任务已提交,请稍后在下载中心查看`。
+
+## 1\. 积分明细查询
+
+![](screenshots/7-points-detail.png)
+
+**入口**:Liso 后台 → 定制功能 → 汇丰银行 → 积分明细查询。
+
+### 1.1 筛选条件
+
+| 字段  | 类型  | 说明  |
+| --- | --- | --- |
+| 客户号 | 文本输入 | 模糊匹配,clearable |
+| 变动类型 | 下拉单选 | 选项:获取积分 / 使用积分 / 积分过期;空值视为全部 |
+| 查询日期范围 | 日期范围 | 按"变动日期"过滤 |
+
+**操作按钮**:
+
+-   【查询】:按筛选条件查询并回到第 1 页
+    
+-   【重置】:清空筛选条件
+    
+-   【导出】(绿色 + 下载图标):触发异步导出,按当前筛选结果导出,字段与列表一致
+    
+
+### 1.2 列表字段
+
+| 字段  | 说明  |
+| --- | --- |
+| 客户号 | 客户唯一标识 |
+| 变动日期 | 格式 `Y/m/d` |
+| 变动积分金额 | 整数  |
+| 积分总数 | 变动后客户的积分总数 |
+| HYH 积分 | 借记卡积分余额 |
+| 信用卡积分 | 信用卡积分余额 |
+| 变动类别 | Tag 展示,可选值:`获取积分`(绿)/ `使用积分`(默认)/ `积分过期`(灰) |
+| 积分项目类别 | 具体的积分项目名称,过长省略号 + tooltip。"积分过期"类无项目,显示 `/` |
+
+### 1.3 分页
+
+-   默认每页 10 条
+    
+
+* * *
+
+## 2\. 客户权益发放
+
+![](screenshots/7-customer-equity-grant.png)
+
+**入口**:Liso 后台 → 定制功能 → 汇丰银行 → 客户权益发放。查询客户维度的权益发放记录。
+
+### 2.1 筛选条件(两行 3 列布局)
+
+| 字段  | 类型  | 说明  |
+| --- | --- | --- |
+| 客户号 | 文本输入 | 模糊匹配 |
+| 权益编号 | 文本输入 | 模糊匹配(编号格式 `Q` + 6 位) |
+| 发放时间 | 日期范围 | 按发放时间过滤 |
+| Channel 名称 | 下拉单选 | 支持输入搜索;选项来源于【Channel 管理】(示例如"汇丰中国客户服务号"、"MGM - Mortgage" 等) |
+| Campaign 名称 | 下拉单选 | 支持输入搜索;选项来源于【Campaign 管理】 |
+| 客户账户类型 | 下拉单选 | 选项:Premier / Elite / PB / Prospect |
+
+**操作按钮**:查询 / 重置 / 导出(同 §1)。
+
+### 2.2 列表字段
+
+| 字段  | 说明  |
+| --- | --- |
+| 发放时间 | `Y-m-d H:i:s` |
+| 权益名称 | 发放的权益名称 |
+| 权益编号 | 权益唯一编号(`Q` + 6 位) |
+| Campaign ID | 关联营销活动 ID(如 `CP9001`) |
+| Campaign 名称 | 关联营销活动名称,过长省略号 + tooltip |
+| Channel ID | 投放渠道 ID(如 `CH0001`) |
+| Channel 名称 | 投放渠道名称 |
+| 客户号 | 客户唯一标识 |
+| 客户账户类型 | Tag 展示,可选值:`Premier`(默认)/ `Elite`(绿)/ `PB`(黄)/ `Prospect`(灰) |
+
+### 2.3 分页
+
+-   默认每页 10 条,布局为"总数 / 上一页 / 页码 / 下一页 / 跳转"
+    
+
+* * *
+
+## 3\. 立减金发放明细
+
+![](screenshots/7-cash-discount-grant.png)
+
+**入口**:Liso 后台 → 定制功能 → 汇丰银行 → 立减金发放明细。查询立减金发放及使用情况。
+
+### 3.1 筛选条件(两行 3 列布局)
+
+| 字段  | 类型  | 说明  |
+| --- | --- | --- |
+| 客户号 | 文本输入 | 模糊匹配 |
+| 立减金类型 | 下拉单选 | 选项:借记卡 / 信用卡 |
+| 发放时间 | 日期范围 | 按发放日期过滤 |
+| 是否领取 | 下拉单选 | 是 / 否 |
+| 是否使用 | 下拉单选 | 是 / 否 |
+
+**操作按钮**:查询 / 重置 / 导出(同 §1)。
+
+### 3.2 列表字段
+
+| 字段  | 说明  |
+| --- | --- |
+| 发放时间 | `Y-m-d H:i:s` |
+| 客户号 | 客户唯一标识 |
+| 立减金类型 | 借记卡 / 信用卡 |
+| 是否有 bin 卡限制 | Tag 展示,可选值:`是`(黄)/ `否`(灰) |
+| 发放金额 | `¥` 前缀的金额(如 `¥50`) |
+| 发放事由 | 发放原因说明(如"新开户礼遇"、"月度消费达标奖励"等),过长省略号 + tooltip |
+| 是否领取 | Tag 展示,可选值:`是`(绿)/ `否`(红) |
+| 领取时间 | `Y-m-d H:i:s`;未领取时显示 `-` |
+| 使用有效期 | `YYYY-MM-DD 至 YYYY-MM-DD`;未领取时显示 `-` |
+| 是否使用 | Tag 展示,可选值:`是`(绿)/ `否`(红) |
+| 使用时间 | `Y-m-d H:i:s`;未使用时显示 `-` |
+
+### 3.3 分页
+
+-   默认每页 10 条,布局为"总数 / 上一页 / 页码 / 下一页 / 跳转"
+    
+
+<br>

+ 0 - 1114
页面管理.html

@@ -1,1114 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>后台管理 - 汇丰权益配置</title>
-    <script src="https://cdn.tailwindcss.com"></script>
-    <script src="https://unpkg.com/@phosphor-icons/web"></script>
-    <style>
-        ::-webkit-scrollbar { width: 8px; height: 8px; }
-        ::-webkit-scrollbar-track { background: #f1f1f1; }
-        ::-webkit-scrollbar-thumb { background: #ccc; border-radius: 4px; }
-        ::-webkit-scrollbar-thumb:hover { background: #aaa; }
-        .menu-item { transition: all 0.2s; }
-        .menu-item:hover { color: #fff; }
-    </style>
-</head>
-<body class="bg-gray-100 h-screen flex overflow-hidden font-sans text-gray-800">
-
-    <!-- 左侧菜单栏 -->
-    <aside class="w-[240px] bg-[#364252] text-gray-300 flex flex-col h-full flex-shrink-0 shadow-xl z-20">
-        <div class="py-6 text-2xl font-bold text-white flex justify-center items-center gap-3 bg-[#2b3644] border-b border-[#2b3644]">
-            <i class="ph ph-desktop"></i>
-            管理后台
-        </div>
-        
-        <div class="flex-1 overflow-y-auto py-4 text-[15px]">
-            <div>
-                <div class="px-5 py-3 flex justify-between items-center cursor-pointer text-gray-200 hover:text-white transition">
-                    <div class="flex items-center gap-3"><i class="ph ph-gear text-lg"></i><span>定制功能</span></div>
-                    <i class="ph ph-caret-up text-sm text-gray-400"></i>
-                </div>
-                
-                <div class="flex flex-col">
-                    <div class="px-5 py-3 pl-11 flex justify-between items-center cursor-pointer text-gray-200 hover:text-white transition">
-                        <span>汇丰银行</span>
-                        <i class="ph ph-caret-up text-sm text-gray-400"></i>
-                    </div>
-                    
-                    <div class="flex flex-col">
-                        <div class="px-5 py-3 pl-16 text-gray-200 hover:text-white transition cursor-default">页面管理</div>
-                        <div class="flex flex-col text-sm">
-                            <a href="#" onclick="switchMenu('home')" id="menu-home" class="py-2.5 pl-[72px] pr-5 text-blue-400 bg-[#2b3644] font-medium block transition">
-                                权益介绍页
-                            </a>
-                            <a href="#" onclick="switchMenu('zone')" id="menu-zone" class="py-2.5 pl-[72px] pr-5 text-gray-400 hover:text-white block transition">
-                                权益首页
-                            </a>
-                            <a href="#" onclick="switchMenu('assembly')" id="menu-assembly" class="py-2.5 pl-[72px] pr-5 text-gray-400 hover:text-white block transition">
-                                组装页
-                            </a>
-                        </div>
-
-                        <div class="px-5 py-3 pl-16 flex justify-between items-center cursor-pointer text-gray-200 hover:text-white transition mt-2">
-                            <span>权益管理</span>
-                            <i class="ph ph-caret-up text-sm text-gray-400"></i>
-                        </div>
-                        <div class="flex flex-col text-[14px]">
-                            <a href="#" class="py-2.5 pl-[72px] pr-5 text-gray-400 hover:text-white transition block">借记卡权益</a>
-                            <a href="#" class="py-2.5 pl-[72px] pr-5 text-gray-400 hover:text-white transition block">信用卡权益</a>
-                            <a href="#" class="py-2.5 pl-[72px] pr-5 text-gray-400 hover:text-white transition block">人群管理</a>
-                        </div>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </aside>
-
-    <!-- 右侧主内容区 -->
-    <main class="flex-1 flex flex-col h-full bg-[#f4f6f8] relative">
-        
-        <!-- 1. 极简面包屑 -->
-        <div class="bg-white h-10 border-b border-gray-100 flex items-center px-6 flex-shrink-0 text-[13px] text-gray-500">
-            <span>页面管理</span>
-            <i class="ph ph-caret-right mx-1.5 text-xs"></i>
-            <span class="text-gray-900" id="breadcrumb-title">权益介绍页配置</span>
-        </div>
-
-        <!-- 2. 独立操作工具栏 (悬浮吸顶) -->
-        <div class="bg-white px-6 py-3 border-b border-gray-200 shadow-sm flex items-center justify-between flex-shrink-0 z-20 relative">
-            <div class="flex items-center gap-4">
-                <h1 class="text-[20px] font-bold text-gray-900 tracking-wide" id="toolbar-title">权益介绍页配置</h1>
-                <!-- 全局状态指示器 -->
-                <div id="global-status-indicator" class="hidden items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border">
-                    <!-- 动态内容渲染 -->
-                </div>
-            </div>
-            <div id="header-actions" class="flex items-center gap-3">
-                <button onclick="showLinkModal()" class="bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 px-4 py-2 rounded-md text-sm font-medium transition shadow-sm flex items-center gap-1.5">
-                    <i class="ph ph-link text-lg"></i> 获取链接
-                </button>
-                <div class="w-px h-5 bg-gray-300 mx-1"></div>
-                <button onclick="handleSaveAction('draft')" class="bg-white border border-blue-600 text-blue-600 hover:bg-blue-50 px-4 py-2 rounded-md text-sm font-medium transition shadow-sm flex items-center gap-1.5">
-                    <i class="ph ph-floppy-disk text-lg"></i> 保存草稿
-                </button>
-                <button onclick="handleSaveAction('publish')" class="bg-blue-600 border border-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition shadow-sm flex items-center gap-1.5">
-                    <i class="ph ph-paper-plane-tilt text-lg"></i> 发布上线
-                </button>
-            </div>
-        </div>
-
-        <div id="tier-tabs-container" class="bg-white px-6 pt-4 border-b border-gray-200 flex-shrink-0">
-            <div class="flex gap-6" id="tier-tabs">
-                <button onclick="switchTier('premier')" id="tab-premier" class="pb-3 px-2 border-b-2 border-blue-600 text-blue-600 font-medium transition">卓越理财</button>
-                <button onclick="switchTier('elite')" id="tab-elite" class="pb-3 px-2 border-b-2 border-transparent text-gray-500 hover:text-gray-800 transition">卓越理财·尊尚</button>
-                <button onclick="switchTier('global')" id="tab-global" class="pb-3 px-2 border-b-2 border-transparent text-gray-500 hover:text-gray-800 transition">环球私人银行</button>
-            </div>
-        </div>
-
-        <div class="flex-1 overflow-y-auto p-6" id="form-container"></div>
-    </main>
-
-    <!-- 通用弹窗通知 -->
-    <div id="toast" class="fixed top-20 right-10 bg-gray-900 text-white px-6 py-3 rounded shadow-lg transform transition-all duration-300 translate-x-full opacity-0 z-[100] flex items-center gap-2">
-        <i class="ph ph-check-circle text-green-400 text-xl" id="toast-icon"></i>
-        <span id="toast-text">配置已保存</span>
-    </div>
-
-    <!-- 获取链接弹窗 -->
-    <div id="link-modal-backdrop" class="fixed inset-0 bg-black/50 z-[80] hidden transition-opacity" onclick="closeLinkModal()"></div>
-    <div id="link-modal" class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white rounded-xl shadow-2xl z-[90] w-[700px] hidden flex-col overflow-hidden">
-        <div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50">
-            <h3 class="text-lg font-bold text-gray-800 flex items-center gap-2"><i class="ph ph-link text-blue-600"></i> 获取页面链接</h3>
-            <button onclick="closeLinkModal()" class="text-gray-400 hover:text-gray-600"><i class="ph ph-x text-xl"></i></button>
-        </div>
-        <div class="p-6" id="link-modal-content">
-            <!-- 链接内容动态渲染 -->
-        </div>
-    </div>
-
-    <!-- 富文本插入超链接弹窗 -->
-    <div id="rt-link-modal-backdrop" class="fixed inset-0 bg-black/50 z-[110] hidden transition-opacity" onclick="closeRtLinkModal()"></div>
-    <div id="rt-link-modal" class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white rounded-xl shadow-2xl z-[120] w-[450px] hidden flex-col overflow-hidden">
-        <div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50">
-            <h3 class="text-base font-bold text-gray-800 flex items-center gap-2"><i class="ph ph-link text-blue-600"></i> 插入超链接</h3>
-            <button onclick="closeRtLinkModal()" class="text-gray-400 hover:text-gray-600"><i class="ph ph-x text-xl"></i></button>
-        </div>
-        <div class="p-6 space-y-4">
-            <div>
-                <label class="block text-sm font-medium text-gray-700 mb-1">链接地址 (URL) <span class="text-red-500">*</span></label>
-                <input type="text" id="rt-link-url" class="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:border-blue-500 outline-none" placeholder="https://">
-            </div>
-            <div>
-                <label class="block text-sm font-medium text-gray-700 mb-1">展示文字</label>
-                <input type="text" id="rt-link-text" class="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:border-blue-500 outline-none" placeholder="例如:点击查看详情">
-            </div>
-        </div>
-        <div class="px-6 py-4 bg-gray-50 border-t border-gray-100 flex justify-end gap-3">
-            <button onclick="closeRtLinkModal()" class="px-4 py-2 border border-gray-300 text-gray-700 rounded text-sm hover:bg-gray-100 transition">取消</button>
-            <button onclick="confirmRtLink()" class="px-4 py-2 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 transition font-medium">确认插入</button>
-        </div>
-    </div>
-
-    <script>
-        // ==========================================
-        // 核心数据模型与状态
-        // ==========================================
-        let currentMenu = 'home'; 
-        let currentTier = 'premier';
-        let currentLinkAssemblyId = null;
-
-        let pageStatus = {
-            home: 'published',
-            zone: 'published'
-        };
-
-        const availableTags = Array.from({length: 8}, (_, i) => ({ id: `t${i+1}`, name: `人群标签${i+1}` }));
-
-        // 1. 权益介绍页数据 
-        const homeData = {
-            premier: createEmptyTierData('卓越理财'),
-            elite: createEmptyTierData('卓越理财·尊尚'),
-            global: createEmptyTierData('环球私人银行')
-        };
-
-        function createEmptyTierData(defaultTitle) {
-            return {
-                tabName: defaultTitle,
-                buttons: { unlocked: '进入权益专区', locked: '待解锁' },
-                header: { imageUrl: 'https://images.unsplash.com/photo-1518893494013-481c1d8ed3fd?w=800', mainTitle: defaultTitle, subTitle: '为您和您的家人提供尊崇专属礼遇和权益' },
-                privileges: [
-                    { imageUrl: '', title: '国际教育', desc: '享专属国际化学校优惠及背景提升项目' },
-                    { imageUrl: '', title: '酒店礼遇', desc: '高端酒店及商场会籍匹配' }
-                ]
-            };
-        }
-
-        // 2. 权益首页数据 (原权益专区)
-        const zoneData = {
-            adCarousel: [{ 
-                imageUrl: 'https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?w=800', 
-                linkH5: '', linkApp: '', wechatAppId: '', wechatPath: '', title: '汇丰卓越理财新户达标礼', subtitle: '报名参加汇丰活动,体验更多精彩礼遇', tags: ['t1', 't2']
-            }],
-            exchangeConfig: {
-                items: [
-                    { imageUrl: 'https://images.unsplash.com/photo-1436491865332-7a61a109cc05?w=200', title: '航空里程', link: '/flight' },
-                    { imageUrl: 'https://images.unsplash.com/photo-1518893494013-481c1d8ed3fd?w=200', title: '贵宾厅', link: '/lounge' }
-                ]
-            },
-            ruleModal: { title: '积分规则', content: '<p>本积分使用规则为汇丰银行(中国)有限公司...</p>' }
-        };
-
-        // 3. 组装页数据
-        let assemblyViewState = 'list'; 
-        let editAssemblyIndex = null; 
-        let tempAssemblyData = null;  
-
-        function generateBlockId() { return 'b_' + Math.random().toString(36).substr(2, 9); }
-
-        function createEmptyAssembly(name) {
-            const now = new Date().toLocaleString('zh-CN', { hour12: false });
-            return {
-                id: 'A' + Math.floor(Math.random() * 10000),
-                name: name || '', status: 'draft', createTime: now, updateTime: now,
-                blocks: [
-                    { type: 'image', id: generateBlockId(), data: { imageUrl: '' } },
-                    { type: 'privilege', id: generateBlockId(), data: { title: '权益标题', items: [ { id: '1', title: '权益名称', desc: '权益简介' } ] } },
-                    { type: 'richtext', id: generateBlockId(), data: { content: '<p>组装页富文本区块内容...</p>' } },
-                    { type: 'buttonGroup', id: generateBlockId(), data: { buttons: [ { text: '点击配置', link: '', style: 'primary', actionType: 'link' } ] } }
-                ]
-            };
-        }
-
-        const assemblyList = [
-            {
-                id: 'A7538', name: '双十一迎新专属活动页', status: 'published', createTime: '2025-10-01 10:00:00', updateTime: '2025-10-15 14:30:00',
-                blocks: [
-                    { type: 'image', id: generateBlockId(), data: { imageUrl: 'https://images.unsplash.com/photo-1557683316-973673baf926?w=800' } },
-                    { type: 'privilege', id: generateBlockId(), data: { title: '专属权益', items: [{ id: '1', title: '机场贵宾厅服务', desc: '全年免费使用6次指定机场贵宾厅' }, { id: '2', title: '境内礼宾车', desc: '免费2次接送' }] } },
-                    { type: 'richtext', id: generateBlockId(), data: { content: '<p>欢迎参与双十一专属福利活动...</p>' } },
-                    { type: 'buttonGroup', id: generateBlockId(), data: { buttons: [ { text: '立即参与', link: '/join', style: 'primary', actionType: 'link' } ] } }
-                ]
-            },
-            {
-                id: 'A2091', name: '私行高净值沙龙引导页', status: 'draft', createTime: '2025-11-20 09:15:00', updateTime: '2025-11-20 09:15:00',
-                blocks: [
-                    { type: 'image', id: generateBlockId(), data: { imageUrl: 'https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?w=800' } },
-                    { type: 'privilege', id: generateBlockId(), data: { title: '尊享体验', items: [{ id: '1', title: '高尔夫权益', desc: 'VIP高尔夫俱乐部免会籍预约' }] } }
-                ]
-            }
-        ];
-
-        let openTagDropdownIndex = null; 
-
-        function getCurrentAssemblyData() { return tempAssemblyData; }
-
-        // ==========================================
-        // 渲染与切换
-        // ==========================================
-
-        function switchMenu(menuId) {
-            currentMenu = menuId;
-            const activeClass = 'py-2.5 pl-[72px] pr-5 text-blue-400 bg-[#2b3644] font-medium block transition';
-            const inactiveClass = 'py-2.5 pl-[72px] pr-5 text-gray-400 hover:text-white block transition';
-
-            document.getElementById('menu-home').className = (menuId === 'home') ? activeClass : inactiveClass;
-            document.getElementById('menu-zone').className = (menuId === 'zone') ? activeClass : inactiveClass;
-            document.getElementById('menu-assembly').className = (menuId === 'assembly') ? activeClass : inactiveClass;
-            
-            const titles = { 'home': '权益介绍页配置', 'zone': '权益首页配置', 'assembly': '组装页管理' };
-            document.getElementById('breadcrumb-title').innerText = titles[menuId];
-            document.getElementById('toolbar-title').innerText = titles[menuId];
-            document.getElementById('tier-tabs-container').style.display = (menuId === 'home') ? 'block' : 'none';
-
-            if (menuId === 'home') {
-                document.getElementById('tab-premier').innerText = homeData.premier.tabName || '卓越理财';
-                document.getElementById('tab-elite').innerText = homeData.elite.tabName || '卓越理财·尊尚';
-                document.getElementById('tab-global').innerText = homeData.global.tabName || '环球私人银行';
-            }
-
-            if (menuId === 'assembly') assemblyViewState = 'list';
-            openTagDropdownIndex = null;
-            
-            renderLayout();
-            updateStatusIndicator();
-        }
-
-        function switchTier(tier) {
-            currentTier = tier;
-            const tabs = ['premier', 'elite', 'global'];
-            tabs.forEach(t => {
-                const el = document.getElementById(`tab-${t}`);
-                el.className = (t === tier) ? "pb-3 px-2 border-b-2 border-blue-600 text-blue-600 font-medium transition" : "pb-3 px-2 border-b-2 border-transparent text-gray-500 hover:text-gray-800 transition";
-            });
-            renderLayout();
-        }
-
-        function renderLayout() {
-            const actions = document.getElementById('header-actions');
-            actions.style.display = (currentMenu === 'assembly' && assemblyViewState === 'list') ? 'none' : 'flex';
-
-            if (currentMenu === 'home') renderHomeConfig();
-            else if (currentMenu === 'zone') renderZoneConfig();
-            else renderAssemblyController();
-
-            updateStatusIndicator();
-        }
-
-        function markAsModified() {
-            if (currentMenu === 'home') pageStatus.home = 'modified';
-            if (currentMenu === 'zone') pageStatus.zone = 'modified';
-            if (currentMenu === 'assembly' && assemblyViewState === 'edit' && tempAssemblyData) {
-                tempAssemblyData._isModified = true; 
-            }
-            updateStatusIndicator();
-        }
-
-        function updateStatusIndicator() {
-            const indicator = document.getElementById('global-status-indicator');
-            
-            if (currentMenu === 'assembly' && assemblyViewState === 'list') {
-                indicator.classList.add('hidden');
-                return;
-            }
-
-            indicator.classList.remove('hidden', 'bg-green-50', 'text-green-700', 'border-green-200', 'bg-yellow-50', 'text-yellow-700', 'border-yellow-200', 'bg-gray-100', 'text-gray-600', 'border-gray-200');
-
-            let currentStatus = '';
-
-            if (currentMenu === 'home') currentStatus = pageStatus.home;
-            else if (currentMenu === 'zone') currentStatus = pageStatus.zone;
-            else if (currentMenu === 'assembly' && assemblyViewState === 'edit') {
-                 if (tempAssemblyData._isModified) {
-                     currentStatus = 'modified';
-                 } else {
-                     currentStatus = tempAssemblyData.status;
-                 }
-            }
-
-            if (currentStatus === 'published') {
-                indicator.classList.add('bg-green-50', 'text-green-700', 'border-green-200');
-                indicator.innerHTML = `<span class="w-1.5 h-1.5 rounded-full bg-green-500"></span> 正式线上环境`;
-            } else if (currentStatus === 'modified') {
-                indicator.classList.add('bg-yellow-50', 'text-yellow-700', 'border-yellow-200');
-                indicator.innerHTML = `<span class="w-1.5 h-1.5 rounded-full bg-yellow-500"></span> 有未发布的修改`;
-            } else {
-                indicator.classList.add('bg-gray-100', 'text-gray-600', 'border-gray-200');
-                indicator.innerHTML = `<span class="w-1.5 h-1.5 rounded-full bg-gray-400"></span> 草稿 (未发布)`;
-            }
-        }
-
-        function handleSaveAction(type) {
-            const pageName = (currentMenu === 'home') ? '权益介绍页' : (currentMenu === 'zone' ? '权益首页' : '组装页');
-            
-            if (currentMenu === 'home' || currentMenu === 'zone') {
-                if (currentMenu === 'home') pageStatus.home = (type === 'publish') ? 'published' : 'draft';
-                if (currentMenu === 'zone') pageStatus.zone = (type === 'publish') ? 'published' : 'draft';
-            }
-
-            if (currentMenu === 'assembly' && assemblyViewState === 'edit') {
-                // 校验:如果按钮模块的点击动作是“跳转链接”,则链接不能为空
-                let hasError = false;
-                tempAssemblyData.blocks.forEach(block => {
-                    if (block.type === 'buttonGroup') {
-                        block.data.buttons.forEach(btn => {
-                            if (btn.actionType === 'link' && !btn.link.trim()) {
-                                hasError = true;
-                            }
-                        });
-                    }
-                });
-
-                if (hasError) {
-                    showToast('保存失败:按钮模块中存在未填写的跳转链接!', 'error');
-                    return; 
-                }
-
-                if (!tempAssemblyData.name.trim()) tempAssemblyData.name = '未命名组装页';
-                tempAssemblyData.status = (type === 'publish') ? 'published' : 'draft';
-                tempAssemblyData.updateTime = new Date().toLocaleString('zh-CN', { hour12: false });
-                delete tempAssemblyData._isModified; 
-
-                if (editAssemblyIndex === null) {
-                    assemblyList.unshift(tempAssemblyData);
-                    if (type === 'draft') editAssemblyIndex = 0;
-                } else {
-                    assemblyList[editAssemblyIndex] = tempAssemblyData;
-                }
-                
-                if (type === 'publish') {
-                    editAssemblyIndex = null; 
-                    tempAssemblyData = null; 
-                    assemblyViewState = 'list';
-                }
-                
-                renderLayout();
-            }
-
-            showToast(type === 'publish' ? `已发布${pageName}配置!` : `已保存${pageName}配置草稿!`);
-            updateStatusIndicator();
-        }
-
-        // ==========================================
-        // 1. 权益介绍页渲染 
-        // ==========================================
-        function renderHomeConfig() {
-            const data = homeData[currentTier];
-
-            // 兼容历史数据,确保字段存在
-            if (!data.tabName) data.tabName = currentTier === 'premier' ? '卓越理财' : (currentTier === 'elite' ? '卓越理财·尊尚' : '环球私人银行');
-            if (!data.buttons) data.buttons = { unlocked: '进入权益专区', locked: '待解锁' };
-
-            document.getElementById('form-container').innerHTML = `
-                <div class="max-w-4xl mx-auto space-y-8 pb-10">
-                    
-                    <section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
-                        <div class="bg-gray-50 px-5 py-3 border-b border-gray-200 font-medium flex items-center gap-2"><i class="ph ph-folders text-gray-500 text-lg"></i> Tab名称配置</div>
-                        <div class="p-6">
-                            <label class="block text-sm font-medium text-gray-700 mb-1">Tab名称</label>
-                            <input type="text" value="${data.tabName}" oninput="homeData[currentTier].tabName=this.value; document.getElementById('tab-' + currentTier).innerText = this.value; markAsModified();" class="w-1/2 px-3 py-2 border rounded text-sm" placeholder="例如:卓越理财">
-                        </div>
-                    </section>
-
-                    <section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
-                        <div class="bg-gray-50 px-5 py-3 border-b border-gray-200 font-medium flex items-center gap-2"><i class="ph ph-image text-gray-500 text-lg"></i> 头部配置</div>
-                        <div class="p-6 space-y-5">
-                            <div><label class="block text-sm font-medium mb-2">头图配图</label>
-                                <div class="flex items-center gap-4">
-                                    <label class="w-[360px] h-[160px] bg-[#eef6fc] rounded border border-gray-300 relative group overflow-hidden cursor-pointer flex items-center justify-center shrink-0 shadow-sm">
-                                        ${data.header.imageUrl ? `<img src="${data.header.imageUrl}" class="w-full h-full object-cover">` : `<div class="text-gray-400 flex flex-col items-center"><i class="ph ph-image text-3xl"></i><span class="text-xs mt-1">未上传图片</span></div>`}
-                                        <div class="absolute inset-0 bg-black/50 hidden group-hover:flex flex-col items-center justify-center text-white"><i class="ph ph-upload-simple text-3xl mb-2"></i><span class="text-sm">点击上传本地图片</span></div>
-                                        <input type="file" class="hidden" accept="image/*" onchange="uploadImageCallback(event, (url) => {homeData[currentTier].header.imageUrl=url; markAsModified(); renderLayout();})">
-                                    </label>
-                                    <div class="text-xs text-gray-500 bg-gray-50 p-2 rounded border border-gray-100">建议图片尺寸:<br><span class="font-bold text-gray-700">686 * 296 px</span></div>
-                                </div>
-                            </div>
-                            <div class="space-y-4 pt-2">
-                                <div><label class="block text-sm font-medium mb-1">主标题(粗体)</label><input type="text" value="${data.header.mainTitle}" oninput="homeData[currentTier].header.mainTitle=this.value; markAsModified();" class="w-full px-3 py-2 border rounded text-sm"></div>
-                                <div><label class="block text-sm font-medium mb-1">副标题(小字)</label><input type="text" value="${data.header.subTitle}" oninput="homeData[currentTier].header.subTitle=this.value; markAsModified();" class="w-full px-3 py-2 border rounded text-sm"></div>
-                            </div>
-                        </div>
-                    </section>
-
-                    <section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
-                        <div class="bg-gray-50 px-5 py-3 border-b border-gray-200 font-medium flex items-center gap-2"><i class="ph ph-hand-tap text-gray-500 text-lg"></i> 按钮配置</div>
-                        <div class="p-6">
-                            <div class="grid grid-cols-2 gap-5">
-                                <div>
-                                    <label class="block text-sm font-medium text-gray-700 mb-1">已解锁按钮文案</label>
-                                    <input type="text" value="${data.buttons.unlocked}" oninput="homeData[currentTier].buttons.unlocked=this.value; markAsModified();" class="w-full px-3 py-2 border rounded text-sm" placeholder="例如:进入权益专区">
-                                </div>
-                                <div>
-                                    <label class="block text-sm font-medium text-gray-700 mb-1">待解锁按钮文案</label>
-                                    <input type="text" value="${data.buttons.locked}" oninput="homeData[currentTier].buttons.locked=this.value; markAsModified();" class="w-full px-3 py-2 border rounded text-sm" placeholder="例如:待解锁">
-                                </div>
-                            </div>
-                        </div>
-                    </section>
-
-                    <section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
-                        <div class="bg-gray-50 px-5 py-3 border-b border-gray-200 font-medium flex justify-between items-center">
-                            <div class="flex items-center gap-2"><i class="ph ph-list-bullets text-gray-500 text-lg"></i> 权益列表配置</div>
-                            <button onclick="homeData[currentTier].privileges.push({ imageUrl: '', title: '新权益', desc: '描述...' }); markAsModified(); renderLayout();" class="text-blue-600 text-sm flex items-center gap-1 hover:text-blue-800"><i class="ph ph-plus-circle text-lg"></i> 添加权益</button>
-                        </div>
-                        <div class="p-6 space-y-4">
-                            ${data.privileges.map((item, index) => renderHomePrivilegeItem(item, index)).join('') || `<div class="text-center text-gray-400 py-6 border border-dashed rounded">暂无权益项</div>`}
-                        </div>
-                    </section>
-                </div>`;
-        }
-
-        function renderHomePrivilegeItem(item, index) {
-            return `
-                <div class="flex gap-4 items-start p-5 pt-8 border border-gray-200 rounded-lg bg-white relative shadow-sm mt-2">
-                    <div class="absolute left-0 top-0 bg-blue-600 text-white text-xs font-bold px-3 py-1 rounded-tl-lg rounded-br-lg shadow-sm">NO. ${index + 1}</div>
-                    <div class="absolute right-4 top-4 flex items-center gap-2 z-20">
-                        ${renderSortPopover('homePrivilege', index, homeData[currentTier].privileges.length)}
-                        <button onclick="homeData[currentTier].privileges.splice(${index}, 1); markAsModified(); renderLayout();" class="text-red-500 hover:text-red-700 bg-red-50 hover:bg-red-100 p-1.5 rounded transition shadow-sm"><i class="ph ph-trash text-lg"></i></button>
-                    </div>
-                    
-                    <div class="flex flex-col items-center gap-1 shrink-0 mt-1">
-                        <div class="w-14 h-14 bg-[#eef6fc] rounded border border-gray-300 relative group overflow-hidden flex items-center justify-center cursor-pointer shadow-inner">
-                            ${item.imageUrl ? `<img src="${item.imageUrl}" class="w-full h-full object-cover">` : `<div class="text-gray-400 flex flex-col items-center"><i class="ph ph-image text-xl mb-0.5"></i><span class="text-[10px]">未上传</span></div>`}
-                            <label class="absolute inset-0 bg-black/50 hidden group-hover:flex flex-col items-center justify-center text-white cursor-pointer"><i class="ph ph-upload-simple text-lg"></i><input type="file" class="hidden" accept="image/*" onchange="uploadImageCallback(event, (url) => {homeData[currentTier].privileges[${index}].imageUrl = url; markAsModified(); renderLayout();})"></label>
-                        </div>
-                        <div class="text-[10px] text-gray-400 text-center leading-tight">非必填<br>无图展示圆点</div>
-                    </div>
-
-                    <div class="flex-1 grid grid-cols-1 md:grid-cols-3 gap-4 pr-24 mt-1">
-                        <div class="col-span-1">
-                            <label class="block text-xs text-gray-500 mb-1">标题</label>
-                            <input type="text" value="${item.title}" oninput="homeData[currentTier].privileges[${index}].title=this.value; markAsModified();" class="w-full px-3 py-1.5 border border-gray-300 rounded text-sm">
-                        </div>
-                        <div class="col-span-2">
-                            <label class="block text-xs text-gray-500 mb-1">权益文字描述</label>
-                            <input type="text" value="${item.desc}" oninput="homeData[currentTier].privileges[${index}].desc=this.value; markAsModified();" class="w-full px-3 py-1.5 border border-gray-300 rounded text-sm">
-                        </div>
-                    </div>
-                </div>`;
-        }
-
-        // ==========================================
-        // 2. 权益首页渲染 (Zone)
-        // ==========================================
-        function renderZoneConfig() {
-            const container = document.getElementById('form-container');
-            container.innerHTML = `
-                <div class="max-w-4xl mx-auto space-y-8 pb-10">
-                    <section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-visible mt-2">
-                        <div class="bg-gray-50 px-5 py-3 border-b font-medium flex justify-between items-center rounded-t-xl">
-                            <div class="flex items-center gap-2"><i class="ph ph-slideshow text-gray-500 text-lg"></i> 活动广告轮播图</div>
-                            <button onclick="zoneData.adCarousel.push({imageUrl:'',linkH5:'',linkApp:'',wechatAppId:'',wechatPath:'',title:'新广告',subtitle:'',tags:[]}); markAsModified(); renderLayout();" class="text-blue-600 text-sm flex items-center gap-1"><i class="ph ph-plus-circle text-lg"></i> 添加轮播</button>
-                        </div>
-                        <div class="p-6 space-y-6">${zoneData.adCarousel.map((item, index) => renderZoneCarouselItem(item, index)).join('') || `<div class="text-center text-gray-400 py-6 border border-dashed rounded">暂无广告轮播图</div>`}</div>
-                    </section>
-                    <section class="bg-white rounded-xl shadow-sm border border-gray-200">
-                        <div class="bg-gray-50 px-5 py-3 border-b font-medium flex justify-between items-center rounded-t-xl"><div class="flex items-center gap-2"><i class="ph ph-squares-four text-gray-500 text-lg"></i> 常用兑换配置</div><button onclick="zoneData.exchangeConfig.items.push({imageUrl:'',title:'新兑换项',link:''}); markAsModified(); renderLayout();" class="text-blue-600 text-sm flex items-center gap-1 hover:text-blue-800"><i class="ph ph-plus-circle text-lg"></i> 添加兑换项</button></div>
-                        <div class="p-6">
-                            <div class="space-y-4">${zoneData.exchangeConfig.items.map((item, index) => renderZoneExchangeItem(item, index)).join('') || `<div class="text-center text-gray-400 py-6 border border-dashed rounded">暂无常用兑换项</div>`}</div>
-                        </div>
-                    </section>
-                    <section class="bg-white rounded-xl shadow-sm border border-gray-200">
-                        <div class="bg-gray-50 px-5 py-3 border-b font-medium flex items-center rounded-t-xl"><i class="ph ph-article text-gray-500 text-lg mr-2"></i> 积分规则弹窗配置</div>
-                        <div class="p-6 space-y-4">
-                            <div><label class="block text-sm font-medium mb-1">弹窗顶部名称</label><input type="text" value="${zoneData.ruleModal.title}" oninput="zoneData.ruleModal.title=this.value; markAsModified();" class="w-1/2 px-3 py-2 border rounded text-sm"></div>
-                            <div>
-                                <label class="block text-sm font-medium mb-1">规则内容 (富文本编辑器)</label>
-                                <div class="border rounded shadow-sm flex flex-col">
-                                    ${getRichTextToolbar('zone')}
-                                    <textarea id="rt-zone" oninput="zoneData.ruleModal.content=this.value; markAsModified();" class="w-full px-4 py-3 text-sm h-48 focus:outline-none resize-y">${zoneData.ruleModal.content}</textarea>
-                                </div>
-                            </div>
-                        </div>
-                    </section>
-                </div>`;
-        }
-
-        function renderZoneCarouselItem(item, index) {
-            return `
-                <div class="border rounded-xl p-5 pt-8 bg-white relative shadow-sm overflow-visible">
-                    <div class="absolute left-0 top-0 bg-blue-600 text-white text-xs font-bold px-3 py-1 rounded-tl-xl rounded-br-lg shadow-sm">NO. ${index + 1}</div>
-                    <div class="absolute right-5 top-5 flex items-center gap-2">
-                        ${renderSortPopover('zoneCarousel', index, zoneData.adCarousel.length)}
-                        <button onclick="zoneData.adCarousel.splice(${index},1); markAsModified(); renderLayout();" class="text-red-500 bg-red-50 p-1.5 rounded shadow-sm"><i class="ph ph-trash"></i></button>
-                    </div>
-                    
-                    <div class="flex flex-col gap-4 mt-3">
-                        <div class="flex items-center gap-4">
-                            <div class="w-64 h-36 bg-[#eef6fc] rounded-md relative group overflow-hidden border flex items-center justify-center shrink-0">
-                                ${item.imageUrl ? `<img src="${item.imageUrl}" class="w-full h-full object-cover">` : `<div class="text-gray-400 flex flex-col items-center"><i class="ph ph-image text-3xl mb-1"></i><span class="text-xs">未上传</span></div>`}
-                                <label class="absolute inset-0 bg-black/50 hidden group-hover:flex flex-col items-center justify-center text-white cursor-pointer"><i class="ph ph-upload-simple text-3xl mb-1"></i><span class="text-sm">点击上传</span><input type="file" class="hidden" accept="image/*" onchange="uploadImageCallback(event, (url) => {zoneData.adCarousel[${index}].imageUrl=url; markAsModified(); renderLayout();})"></label>
-                            </div>
-                            <div class="text-xs text-gray-500 bg-gray-50 p-2 rounded border border-gray-100">建议图片尺寸:<br><span class="font-bold text-gray-700">686 * 296 px</span></div>
-                        </div>
-
-                        <div class="space-y-3">
-                            <div><label class="block text-xs text-gray-600 mb-1">主标题</label><input type="text" value="${item.title}" oninput="zoneData.adCarousel[${index}].title=this.value; markAsModified();" class="w-full px-3 py-1.5 border rounded text-sm"></div>
-                            <div><label class="block text-xs text-gray-600 mb-1">副标题</label><input type="text" value="${item.subtitle}" oninput="zoneData.adCarousel[${index}].subtitle=this.value; markAsModified();" class="w-full px-3 py-1.5 border rounded text-sm"></div>
-                        </div>
-
-                        <div class="tag-dropdown-container relative">
-                            <label class="block text-xs text-gray-600 mb-1">可见人群标签 (多选)</label>
-                            <div class="min-h-[38px] p-1.5 border border-blue-300 bg-blue-50 rounded flex flex-wrap gap-1.5 cursor-pointer" onclick="toggleTagDropdown(event, ${index})">
-                                ${(item.tags || []).map(tid => `<div class="bg-blue-600 text-white text-[12px] px-2 py-0.5 rounded flex items-center gap-1">${availableTags.find(t=>t.id===tid)?.name || ''}<i class="ph ph-x" onclick="event.stopPropagation(); zoneData.adCarousel[${index}].tags = zoneData.adCarousel[${index}].tags.filter(id=>id!=='${tid}'); markAsModified(); renderLayout();"></i></div>`).join('')}
-                                <div class="text-gray-400 text-sm">${(!item.tags || item.tags.length === 0) ? '点击选择...' : ''}</div>
-                            </div>
-                            <div id="tag-dropdown-${index}" class="${openTagDropdownIndex === index ? 'block' : 'hidden'} absolute top-full left-0 w-full mt-1 bg-white border rounded shadow-lg z-50 overflow-hidden">
-                                <div class="p-2 bg-gray-50 border-b"><input type="text" placeholder="搜索..." class="w-full px-2 py-1 text-sm border rounded outline-none" onkeyup="filterTags(event, ${index})" onclick="event.stopPropagation()"></div>
-                                <div class="max-h-48 overflow-y-auto p-1" id="tag-list-${index}">${availableTags.map(tag => `<label class="flex items-center gap-2 px-3 py-2 hover:bg-blue-50 cursor-pointer text-sm"><input type="checkbox" ${(item.tags||[]).includes(tag.id)?'checked':''} onclick="event.stopPropagation(); const ts = zoneData.adCarousel[${index}].tags || []; if(this.checked) ts.push('${tag.id}'); else zoneData.adCarousel[${index}].tags=ts.filter(id=>id!=='${tag.id}'); zoneData.adCarousel[${index}].tags = ts; markAsModified(); renderLayout();"><span>${tag.name}</span></label>`).join('')}</div>
-                            </div>
-                        </div>
-
-                        <div class="bg-gray-50 p-3 rounded-lg border border-gray-200 space-y-3 mt-2">
-                            <div class="text-xs font-bold text-gray-700 border-b border-gray-200 pb-2"><i class="ph ph-link"></i> 跳转链接配置</div>
-                            <div><label class="block text-xs text-gray-600 mb-1">H5 跳转链接</label><input type="text" value="${item.linkH5 || ''}" oninput="zoneData.adCarousel[${index}].linkH5=this.value; markAsModified();" class="w-full px-3 py-1.5 border rounded text-sm"></div>
-                            <div><label class="block text-xs text-gray-600 mb-1">APP 跳转链接</label><input type="text" value="${item.linkApp || ''}" oninput="zoneData.adCarousel[${index}].linkApp=this.value; markAsModified();" class="w-full px-3 py-1.5 border rounded text-sm"></div>
-                            <div class="grid grid-cols-2 gap-3">
-                                <div><label class="block text-xs text-gray-600 mb-1">微信小程序 AppID</label><input type="text" value="${item.wechatAppId || ''}" oninput="zoneData.adCarousel[${index}].wechatAppId=this.value; markAsModified();" class="w-full px-3 py-1.5 border rounded text-sm" placeholder="例如:wx1234567890abcdef"></div>
-                                <div><label class="block text-xs text-gray-600 mb-1">微信小程序 页面路径</label><input type="text" value="${item.wechatPath || ''}" oninput="zoneData.adCarousel[${index}].wechatPath=this.value; markAsModified();" class="w-full px-3 py-1.5 border rounded text-sm" placeholder="例如:pages/index/index"></div>
-                            </div>
-                        </div>
-
-                    </div>
-                </div>`;
-        }
-
-        function renderZoneExchangeItem(item, index) {
-            return `<div class="flex gap-4 items-center p-4 pt-6 border rounded-lg bg-white relative shadow-sm mt-3"><div class="absolute left-0 top-0 bg-blue-600 text-white text-xs font-bold px-3 py-1 rounded-tl-lg rounded-br-lg">NO. ${index + 1}</div><div class="absolute right-4 top-4 flex items-center gap-2">${renderSortPopover('zoneExchange', index, zoneData.exchangeConfig.items.length)}<button onclick="zoneData.exchangeConfig.items.splice(${index},1); markAsModified(); renderLayout();" class="text-red-500 bg-red-50 p-1.5 rounded"><i class="ph ph-trash"></i></button></div><div class="w-20 h-20 bg-[#eef6fc] rounded border relative group flex items-center justify-center shrink-0 overflow-hidden shadow-inner">${item.imageUrl ? `<img src="${item.imageUrl}" class="w-full h-full object-cover">` : `<div class="text-gray-400 flex flex-col items-center"><i class="ph ph-image text-xl mb-0.5"></i><span class="text-[10px]">未上传</span></div>`}<label class="absolute inset-0 bg-black/50 hidden group-hover:flex flex-col items-center justify-center text-white cursor-pointer"><i class="ph ph-upload-simple text-xl"></i><input type="file" class="hidden" accept="image/*" onchange="uploadImageCallback(event, (url)=>{zoneData.exchangeConfig.items[${index}].imageUrl=url; markAsModified(); renderLayout();})"></label></div><div class="flex-1 grid grid-cols-2 gap-4 mt-1 pr-16"><div><label class="block text-xs text-gray-600 mb-1">展示文字</label><input type="text" value="${item.title}" oninput="zoneData.exchangeConfig.items[${index}].title=this.value; markAsModified();" class="w-full px-3 py-2 border rounded text-sm"></div><div><label class="block text-xs text-gray-600 mb-1">跳转链接 (URL)</label><input type="text" value="${item.link}" oninput="zoneData.exchangeConfig.items[${index}].link=this.value; markAsModified();" class="w-full px-3 py-2 border rounded text-sm"></div></div></div>`;
-        }
-
-
-        // ==========================================
-        // 3. 组装页渲染逻辑
-        // ==========================================
-        function renderAssemblyController() {
-            if (assemblyViewState === 'list') renderAssemblyList();
-            else renderAssemblyEdit();
-        }
-
-        function renderAssemblyList() {
-            const container = document.getElementById('form-container');
-            let tableRows = assemblyList.map((item, index) => `
-                <tr class="border-b hover:bg-gray-50 transition">
-                    <td class="px-5 py-3 text-sm border-r flex gap-3 whitespace-nowrap"><button onclick="editAssembly(${index})" class="text-blue-600 font-medium">编辑</button><button onclick="showLinkModal(null, ${index})" class="text-indigo-600 font-medium">链接</button><button onclick="deleteAssembly(${index})" class="text-red-500 font-medium">删除</button></td>
-                    <td class="border px-5 py-3 text-sm text-gray-600 text-center">${item.id}</td>
-                    <td class="border px-5 py-3 text-sm font-medium text-gray-900">${item.name || '未命名'}</td>
-                    <td class="border px-5 py-3 text-center"><span class="px-2 py-1 text-xs rounded font-medium ${item.status === 'published' ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600'}">${item.status === 'published' ? '已上架' : '草稿'}</span></td>
-                    <td class="border px-5 py-3 text-sm text-gray-500 text-center whitespace-nowrap">${item.createTime}</td>
-                    <td class="border px-5 py-3 text-sm text-gray-500 text-center whitespace-nowrap">${item.updateTime}</td>
-                </tr>`).join('');
-
-            container.innerHTML = `
-                <div class="w-full space-y-6">
-                    <div class="bg-white p-5 rounded shadow-sm border mb-4 mt-2">
-                        <div class="grid grid-cols-2 gap-6 mb-5 w-2/3"><div class="flex items-center gap-3"><label class="text-sm font-medium w-16 text-right">页面ID</label><input type="text" class="flex-1 px-3 py-1.5 border rounded text-sm focus:outline-none focus:border-blue-500"></div><div class="flex items-center gap-3"><label class="text-sm font-medium w-16 text-right">页面名称</label><input type="text" class="flex-1 px-3 py-1.5 border rounded text-sm focus:outline-none focus:border-blue-500"></div></div>
-                        <div class="flex justify-between items-center border-t pt-4 mt-2"><button onclick="handleCreateNewAssembly()" class="bg-blue-600 text-white px-4 py-1.5 rounded text-sm flex items-center gap-1.5 font-medium hover:bg-blue-700"><i class="ph ph-plus text-lg"></i> 新建组装页</button><div class="flex gap-3"><button class="bg-indigo-500 text-white px-5 py-1.5 rounded text-sm font-medium hover:bg-indigo-600">查询</button><button class="bg-white border text-gray-700 px-5 py-1.5 rounded text-sm hover:bg-gray-50">重置</button></div></div>
-                    </div>
-                    <div class="bg-white rounded shadow-sm border overflow-hidden"><table class="w-full border-collapse"><thead><tr class="bg-gray-50 text-gray-600 text-sm"><th class="border px-5 py-3 font-medium text-left w-48">操作</th><th class="border px-5 py-3 font-medium">页面ID</th><th class="border px-5 py-3 font-medium text-left">组装页名称</th><th class="border px-5 py-3 font-medium">发布状态</th><th class="border px-5 py-3 font-medium">创建时间</th><th class="border px-5 py-3 font-medium">更新时间</th></tr></thead><tbody>${tableRows || '<tr><td colspan="6" class="p-10 text-center text-gray-400">暂无数据</td></tr>'}</tbody></table></div>
-                </div>`;
-        }
-
-        function renderAssemblyEdit() {
-            const data = getCurrentAssemblyData();
-            let html = `
-                <div class="max-w-4xl mx-auto space-y-6 pb-20 mt-2">
-                    <div class="flex items-center justify-between border-b pb-4"><div class="flex items-center gap-4"><button onclick="backToAssemblyList()" class="text-gray-600 hover:text-blue-600 flex items-center gap-1 font-medium"><i class="ph ph-arrow-left text-lg"></i> 返回</button><h2 class="text-xl font-bold">${editAssemblyIndex === null ? '新建组装页' : '编辑组装页'}</h2></div></div>
-                    <section class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 flex gap-6 items-center">
-                        <div class="shrink-0 flex items-center gap-2 h-[38px]">
-                            <span class="text-sm font-medium text-gray-500">页面 ID:</span>
-                            <span class="text-sm font-bold text-gray-800 bg-gray-100 px-3 py-1 rounded border border-gray-200 leading-none flex items-center">${data.id}</span>
-                        </div>
-                        <div class="flex-1 flex items-center gap-3 border-l border-gray-200 pl-6 h-[38px]">
-                            <label class="text-sm font-medium text-gray-700 shrink-0">组装页名称<span class="text-red-500 ml-0.5">*</span></label>
-                            <input type="text" value="${data.name}" oninput="updateAssemblyName(this.value)" class="flex-1 px-3 py-1.5 border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500 text-sm font-medium h-[34px]" placeholder="请为该页面设置一个名称 (必填)">
-                        </div>
-                    </section>
-            `;
-
-            const hasPrivilege = data.blocks.some(b => b.type === 'privilege');
-
-            data.blocks.forEach((block, index) => {
-                html += `<section class="bg-white rounded-xl shadow-sm border overflow-hidden relative mb-6">
-                    <div class="absolute right-0 top-0 flex items-center z-10 bg-gray-50 border-b border-l rounded-bl-lg shadow-sm">
-                        ${renderSortPopover('assemblyBlock', index, data.blocks.length)}`;
-                        
-                if (block.type !== 'privilege') {
-                    html += `<div class="w-px h-4 bg-gray-300"></div><button onclick="duplicateAssemblyBlock(${index})" class="p-1.5 px-3 text-blue-600 hover:bg-blue-100 text-sm font-medium transition" title="复制模块"><i class="ph ph-copy mr-1"></i>复制</button>`;
-                }
-
-                html += `<div class="w-px h-4 bg-gray-300"></div><button onclick="removeAssemblyBlock(${index})" class="p-1.5 px-3 text-red-500 hover:bg-red-100 text-sm font-medium rounded-bl-lg transition" title="删除模块"><i class="ph ph-trash mr-1"></i>删除</button></div>`;
-
-                if (block.type === 'image') {
-                    html += `<div class="bg-gray-50 px-5 py-3 border-b font-medium flex items-center gap-2"><i class="ph ph-image text-gray-500 text-lg"></i> 图片模块</div>
-                        <div class="p-6">
-                            <div class="flex gap-4 items-center">
-                                <label class="w-[360px] h-[160px] bg-[#eef6fc] rounded border relative group overflow-hidden cursor-pointer flex items-center justify-center shrink-0 shadow-sm">
-                                    ${block.data.imageUrl ? `<img src="${block.data.imageUrl}" class="w-full h-full object-cover">` : `<div class="text-gray-400 flex flex-col items-center"><i class="ph ph-image text-3xl mb-1"></i><span class="text-xs">未上传图片</span></div>`}
-                                    <div class="absolute inset-0 bg-black/50 hidden group-hover:flex flex-col items-center justify-center text-white"><i class="ph ph-upload-simple text-3xl mb-2"></i><span class="text-sm font-medium">点击上传图片</span></div>
-                                    <input type="file" class="hidden" accept="image/*" onchange="uploadImageCallback(event, (url)=>{tempAssemblyData.blocks[${index}].data.imageUrl=url; markAsModified(); renderLayout();})">
-                                </label>
-                                <div class="text-xs text-gray-500 space-y-1 bg-gray-50 p-3 rounded border"><p class="font-medium text-gray-700">建议宽度:750px</p><p>高度不限,系统将按比例自适应展示</p></div>
-                            </div>
-                        </div>`;
-                }
-                else if (block.type === 'privilege') {
-                    html += `<div class="bg-gray-50 px-5 py-3 border-b font-medium flex items-center gap-2"><i class="ph ph-list-dashes text-gray-500 text-lg"></i> 权益商品模块 <span class="text-xs text-gray-400 ml-2 font-normal">(当前页面仅限添加一个)</span></div>
-                        <div class="p-6">
-                            <div class="mb-5 border-b pb-5"><label class="block text-sm font-medium mb-2">可配置大标题</label><input type="text" value="${block.data.title}" oninput="tempAssemblyData.blocks[${index}].data.title=this.value; markAsModified();" class="w-1/2 px-3 py-2 border rounded text-sm"></div>
-                            <div class="space-y-3">${block.data.items.map((item, idx) => `<div class="flex items-center gap-4 p-3 border bg-gray-50 rounded-lg relative group"><div class="bg-white border rounded text-xs font-bold text-gray-500 px-2 py-1">NO. ${idx + 1}</div><div class="w-10 h-10 rounded-full border bg-white flex items-center justify-center text-gray-500 shrink-0"><i class="ph ph-file-text text-xl"></i></div><div class="flex-1 opacity-80 pointer-events-none"><div class="text-sm font-medium text-gray-900">${item.title}</div><div class="text-xs text-gray-500 mt-0.5 truncate">${item.desc}</div></div><div class="pr-2">${renderSortPopover(`assemblyPrivilege-${index}`, idx, block.data.items.length)}</div></div>`).join('')}</div>
-                        </div>`;
-                }
-                else if (block.type === 'richtext') {
-                    html += `<div class="bg-gray-50 px-5 py-3 border-b font-medium flex items-center gap-2"><i class="ph ph-text-align-left text-gray-500 text-lg"></i> 图文模块</div>
-                        <div class="p-6">
-                            <label class="block text-sm font-medium mb-1">图文内容 (富文本编辑器)</label>
-                            <div class="border rounded flex flex-col shadow-sm">
-                                ${getRichTextToolbar('assembly', index)}
-                                <textarea id="rt-assembly-${index}" oninput="tempAssemblyData.blocks[${index}].data.content=this.value; markAsModified();" class="w-full px-4 py-3 text-sm h-32 focus:outline-none resize-y">${block.data.content}</textarea>
-                            </div>
-                        </div>`;
-                }
-                else if (block.type === 'buttonGroup') {
-                    html += `<div class="bg-gray-50 px-5 py-3 border-b font-medium flex items-center gap-4"><div class="flex items-center gap-2"><i class="ph ph-hand-tap text-gray-500 text-lg"></i> 按钮模块</div><button onclick="addAssemblyButton(${index})" class="text-blue-600 bg-blue-50 hover:bg-blue-100 px-2 py-1 rounded text-sm flex items-center gap-1 transition"><i class="ph ph-plus-circle text-lg"></i> 添加按钮</button></div>
-                        <div class="p-6 space-y-4">`;
-                    
-                    block.data.buttons.forEach((btn, idx) => {
-                        const actionType = btn.actionType || 'link';
-                        html += `<div class="flex gap-4 items-start p-4 border rounded-lg bg-gray-50 relative">
-                            <div class="absolute left-0 top-0 bg-gray-600 text-white text-[10px] font-bold px-2 py-0.5 rounded-tl-lg rounded-br-lg">BTN ${idx + 1}</div>
-                            <div class="flex-1 pt-2 space-y-4">
-                                <div class="grid grid-cols-3 gap-4">
-                                    <div><label class="block text-xs mb-1">按钮文案</label><input type="text" value="${btn.text}" oninput="tempAssemblyData.blocks[${index}].data.buttons[${idx}].text=this.value; markAsModified();" class="w-full px-3 py-1.5 border rounded text-sm"></div>
-                                    <div><label class="block text-xs mb-1">点击动作</label>
-                                        <select onchange="tempAssemblyData.blocks[${index}].data.buttons[${idx}].actionType=this.value; markAsModified(); renderLayout();" class="w-full px-3 py-1.5 border rounded text-sm bg-white">
-                                            <option value="link" ${actionType === 'link' ? 'selected' : ''}>跳转链接</option>
-                                            <option value="dialog" ${actionType === 'dialog' ? 'selected' : ''}>弹窗提示</option>
-                                        </select>
-                                    </div>
-                                    <div><label class="block text-xs mb-1">按钮样式</label><select onchange="tempAssemblyData.blocks[${index}].data.buttons[${idx}].style=this.value; markAsModified();" class="w-full px-3 py-1.5 border rounded text-sm bg-white"><option value="primary" ${btn.style === 'primary' ? 'selected' : ''}>突出显示 (红色)</option><option value="secondary" ${btn.style === 'secondary' ? 'selected' : ''}>次要操作 (白色)</option></select></div>
-                                </div>`;
-                                
-                        if (actionType === 'link') {
-                            html += `<div><label class="block text-xs mb-1">跳转链接 <span class="text-red-500">*</span></label><input type="text" value="${btn.link || ''}" oninput="tempAssemblyData.blocks[${index}].data.buttons[${idx}].link=this.value; markAsModified();" class="w-full px-3 py-1.5 border rounded text-sm" placeholder="必填,输入跳转URL"></div>`;
-                        } else {
-                            const d = btn.dialog || { title: '', desc: '', confirmText: '确认', confirmLink: '' };
-                            html += `<div class="bg-white p-4 border border-gray-200 rounded text-sm space-y-3 shadow-sm">
-                                <div class="font-medium text-gray-800 mb-2 border-b border-gray-100 pb-2"><i class="ph ph-browser"></i> 弹窗内容配置 (包含取消、确认按钮)</div>
-                                <div><label class="block text-xs text-gray-600 mb-1">弹窗标题</label><input type="text" value="${d.title}" oninput="updateAssemblyButtonDialog(${index}, ${idx}, 'title', this.value)" class="w-full px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-blue-500 outline-none" placeholder="弹窗标题"></div>
-                                <div><label class="block text-xs text-gray-600 mb-1">弹窗描述</label><textarea oninput="updateAssemblyButtonDialog(${index}, ${idx}, 'desc', this.value)" class="w-full px-3 py-1.5 border border-gray-300 rounded text-sm h-16 focus:border-blue-500 outline-none" placeholder="弹窗正文提示...">${d.desc}</textarea></div>
-                                <div class="grid grid-cols-2 gap-4">
-                                    <div><label class="block text-xs text-gray-600 mb-1">右侧确认按钮文案</label><input type="text" value="${d.confirmText}" oninput="updateAssemblyButtonDialog(${index}, ${idx}, 'confirmText', this.value)" class="w-full px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-blue-500 outline-none" placeholder="例如:继续前往"></div>
-                                    <div><label class="block text-xs text-gray-600 mb-1">确认按钮跳转链接</label><input type="text" value="${d.confirmLink}" oninput="updateAssemblyButtonDialog(${index}, ${idx}, 'confirmLink', this.value)" class="w-full px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-blue-500 outline-none" placeholder="选填,无链接则仅关闭弹窗"></div>
-                                </div>
-                            </div>`;
-                        }
-                        
-                        html += `</div>
-                            <div class="flex items-center gap-2 shrink-0 pb-0.5 pt-2">
-                                ${renderSortPopover(`assemblyButton-${index}`, idx, block.data.buttons.length)}
-                                <button onclick="tempAssemblyData.blocks[${index}].data.buttons.splice(${idx}, 1); markAsModified(); renderLayout();" class="text-red-500 bg-red-100 p-1.5 rounded shadow-sm"><i class="ph ph-trash text-lg"></i></button>
-                            </div>
-                        </div>`;
-                    });
-
-                    if(block.data.buttons.length === 0) {
-                        html += `<div class="text-center text-gray-400 py-4 border border-dashed rounded">暂无按钮</div>`;
-                    }
-                    html += `</div>`;
-                }
-
-                html += `</section>`;
-            });
-
-            html += `
-                <div class="flex gap-4 justify-center py-6 border-t border-gray-200 mt-8 border-dashed border-2 rounded-xl bg-white shadow-sm">
-                    <button onclick="addAssemblyBlock('image')" class="flex items-center gap-1.5 text-blue-600 font-medium hover:bg-blue-50 px-4 py-2 rounded transition"><i class="ph ph-image text-lg"></i>图片模块</button>
-                    ${!hasPrivilege ? `<button onclick="addAssemblyBlock('privilege')" class="flex items-center gap-1.5 text-blue-600 font-medium hover:bg-blue-50 px-4 py-2 rounded transition"><i class="ph ph-list-dashes text-lg"></i>权益商品模块</button>` : ''}
-                    <button onclick="addAssemblyBlock('richtext')" class="flex items-center gap-1.5 text-blue-600 font-medium hover:bg-blue-50 px-4 py-2 rounded transition"><i class="ph ph-text-align-left text-lg"></i>图文模块</button>
-                    <button onclick="addAssemblyBlock('buttonGroup')" class="flex items-center gap-1.5 text-blue-600 font-medium hover:bg-blue-50 px-4 py-2 rounded transition"><i class="ph ph-hand-tap text-lg"></i>按钮模块</button>
-                </div>
-            </div>`;
-
-            document.getElementById('form-container').innerHTML = html;
-        }
-
-        // ==========================================
-        // 富文本功能 & 工具函数 (排序、上传、Toast)
-        // ==========================================
-        
-        let rtTarget = { type: null, index: null };
-
-        function getRichTextToolbar(type, index = null) {
-            return `
-                <div class="bg-gray-50 border-b border-gray-300 px-3 py-2 flex gap-1 flex-wrap text-gray-400 items-center">
-                    <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="字体大小"><i class="ph ph-text-t"></i></button>
-                    <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="字体颜色"><i class="ph ph-text-a-under"></i></button>
-                    <div class="w-px h-5 bg-gray-300 mx-1"></div>
-                    <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="加粗"><i class="ph ph-text-b"></i></button>
-                    <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="斜体"><i class="ph ph-text-italic"></i></button>
-                    <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="下划线"><i class="ph ph-text-underline"></i></button>
-                    <div class="w-px h-5 bg-gray-300 mx-1"></div>
-                    <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="左对齐"><i class="ph ph-text-align-left"></i></button>
-                    <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="居中对齐"><i class="ph ph-text-align-center"></i></button>
-                    <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="右对齐"><i class="ph ph-text-align-right"></i></button>
-                    <div class="w-px h-5 bg-gray-300 mx-1"></div>
-                    <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="无序列表"><i class="ph ph-list-bullets"></i></button>
-                    <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="有序列表"><i class="ph ph-list-numbers"></i></button>
-                    <div class="w-px h-5 bg-gray-300 mx-1"></div>
-                    
-                    <!-- 实际可用功能 -->
-                    <button type="button" onclick="openRtLinkModal('${type}', ${index})" class="p-1.5 rounded hover:bg-gray-200 text-blue-600 transition font-medium" title="插入超链接"><i class="ph ph-link"></i> 链接</button>
-                    <label class="p-1.5 rounded hover:bg-gray-200 cursor-pointer text-blue-600 transition font-medium flex items-center gap-1" title="插入图片">
-                        <i class="ph ph-image"></i> 图片
-                        <input type="file" class="hidden" accept="image/*" onchange="handleRichTextImage(event, '${type}', ${index})">
-                    </label>
-                    <label class="p-1.5 rounded hover:bg-gray-200 cursor-pointer text-blue-600 transition font-medium flex items-center gap-1" title="插入附件文件">
-                        <i class="ph ph-paperclip"></i> 附件
-                        <input type="file" class="hidden" onchange="handleRichTextFile(event, '${type}', ${index})">
-                    </label>
-                </div>
-            `;
-        }
-
-        function openRtLinkModal(type, index) {
-            rtTarget = { type, index };
-            document.getElementById('rt-link-url').value = '';
-            document.getElementById('rt-link-text').value = '';
-            document.getElementById('rt-link-modal-backdrop').classList.remove('hidden');
-            document.getElementById('rt-link-modal').classList.remove('hidden');
-            document.getElementById('rt-link-modal').classList.add('flex');
-        }
-
-        function closeRtLinkModal() {
-            document.getElementById('rt-link-modal-backdrop').classList.add('hidden');
-            document.getElementById('rt-link-modal').classList.add('hidden');
-            document.getElementById('rt-link-modal').classList.remove('flex');
-        }
-
-        function confirmRtLink() {
-            const url = document.getElementById('rt-link-url').value.trim();
-            const text = document.getElementById('rt-link-text').value.trim() || '点击查看';
-            if(!url) { showToast('请输入链接地址', 'error'); return; }
-            
-            const html = `<a href="${url}" target="_blank" style="color:#2563eb; text-decoration:underline;">${text}</a>`;
-            appendToRichText(html);
-            closeRtLinkModal();
-        }
-
-        function handleRichTextImage(event, type, index) {
-            const file = event.target.files[0];
-            if (!file) return;
-            const html = `\n<img src="[图片: ${file.name}]" style="max-width:100%; border-radius:4px; margin-top:8px;" />\n`;
-            rtTarget = { type, index };
-            appendToRichText(html);
-            event.target.value = ''; 
-        }
-        
-        function handleRichTextFile(event, type, index) {
-            const file = event.target.files[0];
-            if (!file) return;
-            const html = `\n<a href="[附件: ${file.name}]" target="_blank" style="color:#2563eb; text-decoration:underline; display:inline-block; margin-top:8px;">📎 ${file.name}</a>\n`;
-            rtTarget = { type, index };
-            appendToRichText(html);
-            event.target.value = ''; 
-        }
-
-        function appendToRichText(html) {
-            if (rtTarget.type === 'zone') {
-                zoneData.ruleModal.content += html;
-            } else if (rtTarget.type === 'assembly') {
-                tempAssemblyData.blocks[rtTarget.index].data.content += html;
-            }
-            markAsModified();
-            renderLayout();
-        }
-
-        function renderSortPopover(type, index, maxCount) {
-            return `<div class="relative inline-block"><button onclick="toggleSortPopover('${type}', ${index}, event)" class="text-blue-500 bg-blue-50 p-1.5 rounded shadow-sm"><i class="ph ph-list-numbers"></i></button>
-            <div id="popover-${type}-${index}" class="hidden absolute right-0 top-full mt-2 z-50 w-48 bg-white border rounded-lg shadow-xl p-3 text-left">
-                <label class="block text-xs text-gray-700 mb-2">移动到序号:</label>
-                <div class="flex gap-2"><input type="number" id="sort-input-${type}-${index}" min="1" max="${maxCount}" value="${index + 1}" class="w-full px-2 py-1 border rounded text-sm outline-none"><button onclick="confirmSort('${type}', ${index})" class="bg-blue-600 text-white px-3 py-1 rounded text-sm">确定</button></div>
-            </div></div>`;
-        }
-
-        function toggleSortPopover(type, index, e) {
-            e.stopPropagation();
-            const pid = `popover-${type}-${index}`;
-            document.querySelectorAll('[id^="popover-"]').forEach(el => { if(el.id !== pid) el.classList.add('hidden'); });
-            const p = document.getElementById(pid);
-            p.classList.toggle('hidden');
-            if(!p.classList.contains('hidden')) setTimeout(()=>document.getElementById(`sort-input-${type}-${index}`).select(), 50);
-        }
-
-        function confirmSort(type, oldIdx) {
-            const val = parseInt(document.getElementById(`sort-input-${type}-${oldIdx}`).value) - 1;
-            let arr;
-            if (type === 'homePrivilege') arr = homeData[currentTier].privileges;
-            else if (type === 'zoneCarousel') arr = zoneData.adCarousel;
-            else if (type === 'zoneExchange') arr = zoneData.exchangeConfig.items;
-            else if (type === 'echoPrivileges') arr = zoneData.echoPrivileges;
-            else if (type === 'echoCreditCards') arr = zoneData.echoCreditCards;
-            else if (type === 'assemblyBlock') arr = getCurrentAssemblyData().blocks;
-            else if (type.startsWith('assemblyPrivilege-')) arr = getCurrentAssemblyData().blocks[parseInt(type.split('-')[1])].data.items;
-            else if (type.startsWith('assemblyButton-')) arr = getCurrentAssemblyData().blocks[parseInt(type.split('-')[1])].data.buttons;
-
-            if (arr && val >= 0 && val < arr.length && val !== oldIdx) {
-                const item = arr.splice(oldIdx, 1)[0];
-                arr.splice(val, 0, item);
-                markAsModified();
-            }
-            renderLayout();
-        }
-
-        function uploadImageCallback(event, updateFn) {
-            const file = event.target.files[0];
-            if (file) { const r = new FileReader(); r.onload = e => updateFn(e.target.result); r.readAsDataURL(file); }
-        }
-
-        function showToast(msg, type = 'success') {
-            const t = document.getElementById('toast');
-            document.getElementById('toast-text').innerText = msg;
-            
-            const icon = document.getElementById('toast-icon');
-            if(type === 'error') {
-                icon.className = 'ph ph-x-circle text-red-500 text-xl';
-            } else {
-                icon.className = 'ph ph-check-circle text-green-400 text-xl';
-            }
-
-            t.classList.remove('translate-x-full', 'opacity-0');
-            setTimeout(() => t.classList.add('translate-x-full', 'opacity-0'), 3000);
-        }
-
-        // ==========================================
-        // 链接弹窗动态处理 
-        // ==========================================
-        function showLinkModal(menuType = null, listIndex = null) {
-            
-            const targetMenu = menuType || currentMenu;
-            
-            if (targetMenu === 'home') {
-                currentLinkAssemblyId = 'home';
-            } else if (targetMenu === 'zone') {
-                currentLinkAssemblyId = 'zone';
-            } else if (targetMenu === 'assembly') {
-                if (assemblyViewState === 'list' && listIndex !== null) {
-                    currentLinkAssemblyId = assemblyList[listIndex].id;
-                } else if (assemblyViewState === 'edit' && tempAssemblyData) {
-                    if (!tempAssemblyData.id) {
-                         showToast('请先保存组装页再获取链接!', 'error');
-                         return;
-                    }
-                    currentLinkAssemblyId = tempAssemblyData.id;
-                }
-            }
-            
-            if (!currentLinkAssemblyId) return;
-
-            document.getElementById('link-modal-content').innerHTML = `
-                <div class="flex gap-4 mb-2">
-                    <div class="flex-1">
-                        <label class="block text-xs font-medium text-gray-700 mb-1">Campaign ID</label>
-                        <select id="modal-campaign-id" onchange="updateModalLinks()" class="w-full px-3 py-2 border border-gray-300 rounded text-sm outline-none focus:border-blue-500 bg-white">
-                            <option value="">请选择 (无)</option>
-                            <option value="CAMP202511">CAMP202511</option>
-                            <option value="CAMP_WINTER">CAMP_WINTER</option>
-                        </select>
-                    </div>
-                    <div class="flex-1">
-                        <label class="block text-xs font-medium text-gray-700 mb-1">Channel ID</label>
-                        <select id="modal-channel-id" onchange="updateModalLinks()" class="w-full px-3 py-2 border border-gray-300 rounded text-sm outline-none focus:border-blue-500 bg-white">
-                            <option value="">请选择 (无)</option>
-                            <option value="CH_APP">APP推送</option>
-                            <option value="CH_WECHAT">微信公众号</option>
-                        </select>
-                    </div>
-                </div>
-                <div id="modal-links-container" class="space-y-4"></div>
-            `;
-            
-            updateModalLinks(); 
-            
-            document.getElementById('link-modal-backdrop').classList.remove('hidden');
-            document.getElementById('link-modal').classList.remove('hidden');
-            document.getElementById('link-modal').classList.add('flex');
-        }
-
-        function updateModalLinks() {
-            if(!currentLinkAssemblyId) return;
-            
-            const campId = document.getElementById('modal-campaign-id').value;
-            const chanId = document.getElementById('modal-channel-id').value;
-            
-            let queryParams = [];
-            if(campId) queryParams.push(`CampaignId=${campId}`);
-            if(chanId) queryParams.push(`Channelid=${chanId}`);
-            const queryStr = queryParams.length > 0 ? (currentLinkAssemblyId === 'home' || currentLinkAssemblyId === 'zone' ? '?' : '&') + queryParams.join('&') : '';
-
-            let basePath = '';
-            let isPublished = false;
-
-            if (currentLinkAssemblyId === 'home') {
-                basePath = 'home';
-                isPublished = pageStatus.home === 'published';
-            } else if (currentLinkAssemblyId === 'zone') {
-                basePath = 'zone';
-                isPublished = pageStatus.zone === 'published';
-            } else {
-                basePath = `assembly?id=${currentLinkAssemblyId}`;
-                const asm = assemblyList.find(a => a.id === currentLinkAssemblyId) || tempAssemblyData;
-                isPublished = asm && asm.status === 'published';
-            }
-
-            const prodH5Url = `https://hsbc.com/${basePath}${queryStr}`;
-            const prodAppUrl = `hsbc-app://${basePath}${queryStr}`;
-            
-            const draftJoiner = (basePath === 'home' || basePath === 'zone') ? '?' : '&';
-            const testUrl = `https://test.hsbc.com/${basePath}${draftJoiner}draft=1${queryStr.replace('?', '&')}`;
-            
-            let html = '';
-            if (isPublished) {
-                html += `
-                    <div class="bg-white border border-gray-200 rounded-lg p-4 shadow-sm relative">
-                        <div class="flex items-center gap-2 mb-3"><span class="w-2 h-2 rounded-full bg-green-500"></span><span class="font-bold text-gray-800">正式环境链接</span></div>
-                        <div class="space-y-2">
-                            <div class="flex gap-2 items-center">
-                                <span class="text-xs text-gray-500 w-16">H5 链接</span>
-                                <input type="text" readonly value="${prodH5Url}" class="flex-1 bg-gray-50 px-3 py-2 text-sm border border-gray-200 rounded text-gray-700 outline-none">
-                                <button onclick="copyToClipboard('${prodH5Url}')" class="bg-blue-50 border border-blue-100 text-blue-600 px-4 py-2 rounded text-sm hover:bg-blue-100 font-medium whitespace-nowrap transition">复制</button>
-                            </div>
-                            <div class="flex gap-2 items-center">
-                                <span class="text-xs text-gray-500 w-16">小程序/APP</span>
-                                <input type="text" readonly value="${prodAppUrl}" class="flex-1 bg-gray-50 px-3 py-2 text-sm border border-gray-200 rounded text-gray-700 outline-none">
-                                <button onclick="copyToClipboard('${prodAppUrl}')" class="bg-blue-50 border border-blue-100 text-blue-600 px-4 py-2 rounded text-sm hover:bg-blue-100 font-medium whitespace-nowrap transition">复制</button>
-                            </div>
-                        </div>
-                    </div>
-                `;
-            }
-            html += `
-                <div class="bg-white border border-gray-200 rounded-lg p-4 shadow-sm relative">
-                    <div class="flex items-center gap-2 mb-3"><span class="w-2 h-2 rounded-full bg-blue-500"></span><span class="font-bold text-gray-800">草稿预览链接 (测试)</span></div>
-                    <div class="flex gap-2 items-center">
-                        <input type="text" readonly value="${testUrl}" class="flex-1 bg-gray-50 px-3 py-2 text-sm border border-gray-200 rounded text-gray-700 outline-none">
-                        <button onclick="copyToClipboard('${testUrl}')" class="bg-blue-50 border border-blue-100 text-blue-600 px-4 py-2 rounded text-sm hover:bg-blue-100 font-medium whitespace-nowrap transition">复制</button>
-                    </div>
-                </div>
-            `;
-            document.getElementById('modal-links-container').innerHTML = html;
-        }
-
-        function closeLinkModal() {
-            document.getElementById('link-modal-backdrop').classList.add('hidden');
-            document.getElementById('link-modal').classList.add('hidden');
-            document.getElementById('link-modal').classList.remove('flex');
-            currentLinkAssemblyId = null;
-        }
-
-        function copyToClipboard(text) {
-            navigator.clipboard.writeText(text).then(() => showToast('链接已复制到剪贴板!'));
-        }
-
-        function toggleTagDropdown(e, i) { e.stopPropagation(); openTagDropdownIndex = (openTagDropdownIndex === i) ? null : i; renderLayout(); }
-        function filterTags(e, i) {
-            const k = e.target.value.toLowerCase();
-            document.querySelectorAll(`#tag-list-${i} label`).forEach(l => {
-                l.style.display = l.innerText.toLowerCase().includes(k) ? 'flex' : 'none';
-            });
-        }
-
-        // ==========================================
-        // 组装页模块增删复制逻辑
-        // ==========================================
-        function handleCreateNewAssembly() {
-            editAssemblyIndex = null;
-            tempAssemblyData = createEmptyAssembly('');
-            assemblyViewState = 'edit';
-            markAsModified();
-            renderLayout();
-        }
-
-        function editAssembly(index) {
-            editAssemblyIndex = index;
-            tempAssemblyData = JSON.parse(JSON.stringify(assemblyList[index]));
-            assemblyViewState = 'edit';
-            renderLayout();
-        }
-
-        function deleteAssembly(index) {
-            if(confirm('确认要删除该组装页吗?此操作不可恢复。')) {
-                assemblyList.splice(index, 1);
-                showToast('删除成功');
-                renderLayout();
-            }
-        }
-
-        function backToAssemblyList() {
-            editAssemblyIndex = null;
-            tempAssemblyData = null;
-            assemblyViewState = 'list';
-            renderLayout();
-        }
-
-        function updateAssemblyName(val) {
-            if (getCurrentAssemblyData()) {
-                getCurrentAssemblyData().name = val;
-                markAsModified();
-            }
-        }
-
-        function addAssemblyBlock(type) {
-            let newBlock = { type: type, id: generateBlockId(), data: {} };
-            if (type === 'image') newBlock.data = { imageUrl: '' };
-            if (type === 'privilege') newBlock.data = { title: '新权益标题', items: [{ id:'1', title:'模拟权益', desc:'描述' }] };
-            if (type === 'richtext') newBlock.data = { content: '<p>输入富文本内容...</p>' };
-            if (type === 'buttonGroup') newBlock.data = { buttons: [{ text: '点击跳转', link: '', style: 'primary', actionType: 'link' }] };
-            getCurrentAssemblyData().blocks.push(newBlock);
-            markAsModified();
-            renderLayout();
-        }
-
-        function duplicateAssemblyBlock(index) {
-            let block = getCurrentAssemblyData().blocks[index];
-            if (block.type === 'privilege') {
-                alert('权益商品模块每页仅限配置一个,无法复制!');
-                return;
-            }
-            let newBlock = JSON.parse(JSON.stringify(block));
-            newBlock.id = generateBlockId(); // 新ID防止重复
-            getCurrentAssemblyData().blocks.splice(index + 1, 0, newBlock);
-            markAsModified();
-            renderLayout();
-        }
-
-        function removeAssemblyBlock(index) {
-            if(confirm('确定删除该模块吗?')) {
-                getCurrentAssemblyData().blocks.splice(index, 1);
-                markAsModified();
-                renderLayout();
-            }
-        }
-
-        function addAssemblyButton(blockIndex) { 
-            tempAssemblyData.blocks[blockIndex].data.buttons.push({ 
-                text: '新按钮', link: '', style: 'secondary', 
-                actionType: 'link', dialog: { title: '', desc: '', confirmText: '确认', confirmLink: '' } 
-            }); 
-            markAsModified();
-            renderLayout(); 
-        }
-
-        function updateAssemblyButtonDialog(blockIndex, btnIndex, field, value) {
-            let block = tempAssemblyData.blocks[blockIndex];
-            if (!block.data.buttons[btnIndex].dialog) {
-                block.data.buttons[btnIndex].dialog = { title: '', desc: '', confirmText: '确认', confirmLink: '' };
-            }
-            block.data.buttons[btnIndex].dialog[field] = value;
-            markAsModified();
-        }
-
-        switchMenu('home'); // Init
-    </script>
-</body>
-</html>