Просмотр исходного кода

新增人群管理三大模块:客群任务管理、规则组管理、客群名单管理

- 客群任务管理:基本信息+规则组配置 2 步向导,含运行/校验/上线状态机、复制任务、sample 结果详情、重新校验等
- 规则组管理:4 个元规则(开户时间/MSC/Avg TRB/月末余额)的可视化配置,自动生成规则描述和规则参数
- 客群名单管理:人员调整弹窗、上传文件批量调整(|+| 分隔格式),含解析预览和结果汇总
- 菜单结构:将「人群管理」改为子菜单,并入上述三个模块
- 权益产品相关页面同步微调

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fade 2 месяцев назад
Родитель
Сommit
b3bc2abe1c
36 измененных файлов с 7079 добавлено и 765 удалено
  1. 7 1
      .claude/settings.local.json
  2. 1309 0
      Channel管理.html
  3. 1 0
      pc/.gitignore
  4. 138 0
      pc/src/components/EquityLinkDialog.vue
  5. 50 4
      pc/src/components/Layout.vue
  6. 122 0
      pc/src/components/RongshuProductDialog.vue
  7. 2 2
      pc/src/components/ShowPagesDialog.vue
  8. 119 0
      pc/src/components/crowdNameList/AdjustMemberDialog.vue
  9. 192 0
      pc/src/components/crowdNameList/UploadAdjustDialog.vue
  10. 131 0
      pc/src/components/crowdTask/AddRuleGroupDialog.vue
  11. 40 0
      pc/src/components/crowdTask/SampleDetailDialog.vue
  12. 268 0
      pc/src/components/ruleGroup/RuleEditDialog.vue
  13. 200 0
      pc/src/config/metaRules.js
  14. 18 0
      pc/src/mock/index.js
  15. 92 8
      pc/src/router/index.js
  16. 129 0
      pc/src/store/crowdNameListStore.js
  17. 203 0
      pc/src/store/crowdTaskStore.js
  18. 163 0
      pc/src/store/ruleGroupStore.js
  19. 214 204
      pc/src/views/AddEquityProduct.vue
  20. 167 133
      pc/src/views/AuditEquityProduct.vue
  21. 36 2
      pc/src/views/CreditCardEquity.vue
  22. 0 22
      pc/src/views/CrowdManagement.vue
  23. 170 0
      pc/src/views/CrowdNameList.vue
  24. 207 0
      pc/src/views/CrowdNameListEdit.vue
  25. 328 0
      pc/src/views/CrowdTaskEdit.vue
  26. 218 0
      pc/src/views/CrowdTaskList.vue
  27. 205 0
      pc/src/views/CrowdTaskView.vue
  28. 237 209
      pc/src/views/EditEquityProduct.vue
  29. 5 5
      pc/src/views/EquityProductCoupons.vue
  30. 89 32
      pc/src/views/EquityProducts.vue
  31. 132 0
      pc/src/views/Login.vue
  32. 291 0
      pc/src/views/RuleGroupEdit.vue
  33. 175 0
      pc/src/views/RuleGroupList.vue
  34. 127 0
      pc/src/views/RuleGroupView.vue
  35. 180 143
      pc/src/views/ViewEquityProduct.vue
  36. 1114 0
      页面管理.html

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

@@ -6,7 +6,13 @@
       "Bash(lark-cli docs *)",
       "Bash(lark-cli api *)",
       "Bash(cp \"/Users/apple/workspace/pm_agent/prd/汇丰银行/汇丰借记卡权益.md\" /tmp/hsbc_debit_card.md)",
-      "Read(//tmp/**)"
+      "Read(//tmp/**)",
+      "Bash(lsof -ti:8080)",
+      "Bash(curl -s http://localhost:8080/#/customization/hsbc/pages/home)",
+      "Bash(npm run *)",
+      "Bash(grep -n \"  },$\\\\|computed:\\\\|methods:\\\\|^  }\" /Users/apple/workspace/rongshu/pc/src/views/AddEquityProduct.vue)",
+      "Bash(sed -i '' 's/label=\"首页\"/label=\"权益首页\\\\/列表页\"/g; s/pages.push\\(.首页.\\)/pages.push\\(\"权益首页\\\\/列表页\"\\)/g' components/ShowPagesDialog.vue views/AddEquityProduct.vue views/EditEquityProduct.vue views/ViewEquityProduct.vue views/AuditEquityProduct.vue views/EquityProducts.vue views/CreditCardEquity.vue)",
+      "Bash(grep -rn \"label=\\\\\"首页\\\\\"\\\\|pages.push\\('首页'\\)\\\\|pages.push\\(\\\\\"首页\\\\\"\\)\" .)"
     ],
     "additionalDirectories": []
   }

+ 1309 - 0
Channel管理.html

@@ -0,0 +1,1309 @@
+<!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>

+ 1 - 0
pc/.gitignore

@@ -21,3 +21,4 @@ pnpm-debug.log*
 *.njsproj
 *.sln
 *.sw?
+.vercel

+ 138 - 0
pc/src/components/EquityLinkDialog.vue

@@ -0,0 +1,138 @@
+<template>
+  <el-dialog
+    :title="title"
+    :visible.sync="dialogVisible"
+    width="680px"
+    @open="resetForm"
+  >
+    <el-form label-width="100px" inline>
+      <el-form-item label="Campaign ID">
+        <el-select v-model="form.campaignId" clearable placeholder="请选择 (无)">
+          <el-option
+            v-for="opt in campaignOptions"
+            :key="opt.value"
+            :label="opt.label"
+            :value="opt.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="Channel ID">
+        <el-select v-model="form.channelId" clearable placeholder="请选择 (无)">
+          <el-option
+            v-for="opt in channelOptions"
+            :key="opt.value"
+            :label="opt.label"
+            :value="opt.value"
+          />
+        </el-select>
+      </el-form-item>
+    </el-form>
+    <div class="link-section">
+      <div class="link-label"><span class="dot"></span> 权益链接</div>
+      <div class="link-row">
+        <span class="link-type">H5 链接</span>
+        <el-input :value="h5Url" readonly size="small" />
+        <el-button size="small" type="primary" plain @click="copyLink(h5Url)">复制</el-button>
+      </div>
+      <div class="link-row">
+        <span class="link-type">小程序/APP</span>
+        <el-input :value="appUrl" readonly size="small" />
+        <el-button size="small" type="primary" plain @click="copyLink(appUrl)">复制</el-button>
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+export default {
+  name: 'EquityLinkDialog',
+  props: {
+    visible: { type: Boolean, default: false },
+    id: { type: [String, Number], default: '' },
+    pathName: { type: String, default: 'equity' },
+    title: { type: String, default: '生成链接' }
+  },
+  data() {
+    return {
+      form: { campaignId: '', channelId: '' },
+      campaignOptions: [
+        { label: 'CAMP202511', value: 'CAMP202511' },
+        { label: 'CAMP_WINTER', value: 'CAMP_WINTER' }
+      ],
+      channelOptions: [
+        { label: 'APP推送', value: 'CH_APP' },
+        { label: '微信公众号', value: 'CH_WECHAT' }
+      ]
+    }
+  },
+  computed: {
+    dialogVisible: {
+      get() { return this.visible },
+      set(v) { this.$emit('update:visible', v) }
+    },
+    queryStr() {
+      const params = []
+      if (this.form.campaignId) params.push(`CampaignId=${this.form.campaignId}`)
+      if (this.form.channelId) params.push(`Channelid=${this.form.channelId}`)
+      return params.length ? '&' + params.join('&') : ''
+    },
+    h5Url() {
+      return `https://hsbc.com/${this.pathName}?id=${this.id}${this.queryStr}`
+    },
+    appUrl() {
+      return `hsbc-app://${this.pathName}?id=${this.id}${this.queryStr}`
+    }
+  },
+  methods: {
+    resetForm() {
+      this.form = { campaignId: '', channelId: '' }
+    },
+    copyLink(text) {
+      if (!text) return
+      navigator.clipboard.writeText(text).then(() => {
+        this.$message.success('链接已复制到剪贴板')
+      }).catch(() => {
+        this.$message.error('复制失败,请手动选取')
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.link-section {
+  margin-top: 8px;
+  padding: 12px;
+  background: #fafafa;
+  border-radius: 6px;
+  border: 1px solid #ebeef5;
+}
+.link-label {
+  font-weight: 600;
+  margin-bottom: 10px;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  color: #303133;
+}
+.link-row {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 8px;
+}
+.link-row:last-child { margin-bottom: 0; }
+.link-type {
+  font-size: 12px;
+  color: #909399;
+  width: 70px;
+  flex-shrink: 0;
+}
+.dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  display: inline-block;
+  background: #67C23A;
+}
+</style>

+ 50 - 4
pc/src/components/Layout.vue

@@ -42,9 +42,18 @@
                 信用卡权益
               </el-menu-item>
             </el-submenu>
-            <el-menu-item index="/customization/hsbc/crowds">
-              人群管理
-            </el-menu-item>
+            <el-submenu index="customization-hsbc-crowd">
+              <template slot="title">人群管理</template>
+              <el-menu-item index="/customization/hsbc/crowd-tasks">
+                客群任务管理
+              </el-menu-item>
+              <el-menu-item index="/customization/hsbc/rule-groups">
+                规则组管理
+              </el-menu-item>
+              <el-menu-item index="/customization/hsbc/crowd-name-lists">
+                客群名单管理
+              </el-menu-item>
+            </el-submenu>
             <el-submenu index="customization-hsbc-sms">
               <template slot="title">短信管理</template>
               <el-menu-item index="/customization/hsbc/sms-templates">
@@ -103,6 +112,16 @@
             {{ currentTitle }}
           </el-breadcrumb-item>
         </el-breadcrumb>
+        <el-dropdown trigger="click" @command="handleUserCommand">
+          <span class="user-info">
+            <i class="el-icon-user-solid"></i>
+            {{ username }}
+            <i class="el-icon-arrow-down"></i>
+          </span>
+          <el-dropdown-menu slot="dropdown">
+            <el-dropdown-item command="logout">退出登录</el-dropdown-item>
+          </el-dropdown-menu>
+        </el-dropdown>
       </header>
 
       <!-- 页面内容 -->
@@ -118,13 +137,29 @@ export default {
   name: 'Layout',
   data() {
     return {
-      collapsed: false
+      collapsed: false,
+      username: localStorage.getItem('username') || '用户'
     }
   },
   computed: {
     currentTitle() {
       return this.$route.meta?.title || ''
     }
+  },
+  methods: {
+    handleUserCommand(cmd) {
+      if (cmd === 'logout') {
+        this.$confirm('确定要退出登录吗?', '提示', {
+          confirmButtonText: '退出',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          localStorage.removeItem('token')
+          localStorage.removeItem('username')
+          this.$router.replace('/login')
+        }).catch(() => {})
+      }
+    }
   }
 }
 </script>
@@ -178,6 +213,17 @@ export default {
 
 .breadcrumb { flex: 1; }
 
+.user-info {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  color: #606266;
+  cursor: pointer;
+  font-size: 14px;
+}
+.user-info:hover { color: #409EFF; }
+.user-info .el-icon-user-solid { font-size: 18px; }
+
 .content {
   flex: 1;
   padding: 20px;

+ 122 - 0
pc/src/components/RongshuProductDialog.vue

@@ -0,0 +1,122 @@
+<template>
+  <el-dialog
+    title="选择荣数商品"
+    :visible.sync="dialogVisible"
+    width="900px"
+    @open="onOpen"
+  >
+    <el-form :model="filter" inline>
+      <el-form-item label="商品名称">
+        <el-input
+          v-model="filter.name"
+          placeholder="请输入"
+          clearable
+          style="width: 220px"
+          @keyup.enter.native="handleSearch"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="handleSearch">查询</el-button>
+        <el-button @click="handleReset">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-table :data="pagedData" border stripe size="small" style="width: 100%">
+      <el-table-column prop="name" label="商品名称" min-width="180" show-overflow-tooltip />
+      <el-table-column prop="merchant" label="商户名称" width="160" show-overflow-tooltip />
+      <el-table-column label="银行结算价" width="120" align="right">
+        <template slot-scope="{ row }">¥{{ row.settlePrice.toFixed(2) }}</template>
+      </el-table-column>
+      <el-table-column prop="stock" label="库存数" width="100" align="right" />
+      <el-table-column label="操作" width="80" align="center" fixed="right">
+        <template slot-scope="{ row }">
+          <el-button type="text" size="small" @click="handleSelect(row)">选择</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <div class="pagination-bar">
+      <el-pagination
+        background
+        layout="total, prev, pager, next"
+        :page-size="10"
+        :current-page.sync="currentPage"
+        :total="filteredData.length"
+      />
+    </div>
+  </el-dialog>
+</template>
+
+<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: [] }
+]
+
+export default {
+  name: 'RongshuProductDialog',
+  props: {
+    visible: { type: Boolean, default: false }
+  },
+  data() {
+    return {
+      filter: { name: '' },
+      currentPage: 1,
+      products: MOCK_PRODUCTS
+    }
+  },
+  computed: {
+    dialogVisible: {
+      get() { return this.visible },
+      set(v) { this.$emit('update:visible', v) }
+    },
+    filteredData() {
+      if (!this.filter.name) return this.products
+      const kw = this.filter.name.trim()
+      return this.products.filter(p => p.name.includes(kw))
+    },
+    pagedData() {
+      const start = (this.currentPage - 1) * 10
+      return this.filteredData.slice(start, start + 10)
+    }
+  },
+  methods: {
+    onOpen() {
+      this.filter = { name: '' }
+      this.currentPage = 1
+    },
+    handleSearch() {
+      this.currentPage = 1
+    },
+    handleReset() {
+      this.filter = { name: '' }
+      this.currentPage = 1
+    },
+    handleSelect(row) {
+      this.$emit('select', { ...row })
+      this.dialogVisible = false
+    }
+  }
+}
+</script>
+
+<style scoped>
+.pagination-bar {
+  margin-top: 12px;
+  display: flex;
+  justify-content: flex-end;
+}
+</style>

+ 2 - 2
pc/src/components/ShowPagesDialog.vue

@@ -5,8 +5,8 @@
     width="560px"
     @open="onOpen"
   >
-    <el-form :model="form" label-width="100px" label-position="left">
-      <el-form-item label="首页">
+    <el-form :model="form" label-width="140px" label-position="left">
+      <el-form-item label="权益页/列表页">
         <el-radio-group v-model="form.showOnHome">
           <el-radio label="显示">显示</el-radio>
           <el-radio label="不显示">不显示</el-radio>

+ 119 - 0
pc/src/components/crowdNameList/AdjustMemberDialog.vue

@@ -0,0 +1,119 @@
+<template>
+  <el-dialog
+    title="人员调整"
+    :visible.sync="innerVisible"
+    width="520px"
+    :close-on-click-modal="false"
+    @close="reset"
+  >
+    <el-form label-width="100px" label-position="right">
+      <el-form-item label="客群编号" required>
+        <el-select
+          v-model="form.listId"
+          placeholder="请选择"
+          filterable
+          style="width: 100%"
+        >
+          <el-option
+            v-for="item in availableLists"
+            :key="item.id"
+            :label="`${item.id}-${item.name}`"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="客户号" required>
+        <el-input
+          v-model.trim="form.customerId"
+          placeholder="请输入客户号(5 位数字)"
+          maxlength="10"
+        />
+      </el-form-item>
+
+      <el-form-item label="调整方向" required>
+        <el-select v-model="form.direction" placeholder="请选择" style="width: 100%">
+          <el-option label="调增 - A" value="A" />
+          <el-option label="调减 - D" value="D" />
+        </el-select>
+      </el-form-item>
+    </el-form>
+
+    <div slot="footer" class="dialog-footer">
+      <el-button @click="handleCancel">取 消</el-button>
+      <el-button type="primary" @click="handleSave">保 存</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import crowdNameListStore from '@/store/crowdNameListStore'
+
+export default {
+  name: 'AdjustMemberDialog',
+  props: {
+    visible: { type: Boolean, default: false },
+    defaultListId: { type: String, default: '' }
+  },
+  data() {
+    return {
+      form: { listId: '', customerId: '', direction: '' }
+    }
+  },
+  computed: {
+    innerVisible: {
+      get() { return this.visible },
+      set(v) { this.$emit('update:visible', v) }
+    },
+    availableLists() {
+      return crowdNameListStore.list.filter((l) => l.status !== 'stopped')
+    }
+  },
+  watch: {
+    visible(v) {
+      if (v) {
+        this.reset()
+        if (this.defaultListId) this.form.listId = this.defaultListId
+      }
+    }
+  },
+  methods: {
+    reset() {
+      this.form = { listId: '', customerId: '', direction: '' }
+    },
+    handleSave() {
+      if (!this.form.listId) {
+        this.$message.error('请选择客群编号')
+        return
+      }
+      if (!this.form.customerId) {
+        this.$message.error('请输入客户号')
+        return
+      }
+      if (!this.form.direction) {
+        this.$message.error('请选择调整方向')
+        return
+      }
+      const r = crowdNameListStore.adjustOne({
+        listId: this.form.listId,
+        customerId: this.form.customerId,
+        direction: this.form.direction
+      })
+      if (r.ok) {
+        this.$message.success(`调整提交成功(${r.action === 'add' ? '已新增' : '已移除'}),审核通过后生效(demo 默认通过)`)
+        this.innerVisible = false
+        this.$emit('saved')
+      } else {
+        this.$message.error(r.reason)
+      }
+    },
+    handleCancel() {
+      this.innerVisible = false
+    }
+  }
+}
+</script>
+
+<style scoped>
+.dialog-footer { text-align: center; }
+</style>

+ 192 - 0
pc/src/components/crowdNameList/UploadAdjustDialog.vue

@@ -0,0 +1,192 @@
+<template>
+  <el-dialog
+    title="上传调整文件"
+    :visible.sync="innerVisible"
+    width="640px"
+    :close-on-click-modal="false"
+    @close="reset"
+  >
+    <div class="format-block">
+      <div class="format-title">文件格式说明</div>
+      <div class="format-line">每行一条调整记录,使用 <code>|+|</code> 作为分隔符:</div>
+      <pre class="format-sample">客户号|+|客群编号|+|调整方向
+00001|+|A0000120251224|+|A
+00002|+|A0000120251224|+|D</pre>
+      <div class="format-line">调整方向:<code>A</code> = 调增,<code>D</code> = 调减</div>
+    </div>
+
+    <el-upload
+      class="upload-area"
+      drag action="#"
+      :auto-upload="false"
+      :show-file-list="false"
+      :on-change="handleFile"
+      accept=".txt,.csv"
+    >
+      <i class="el-icon-upload"></i>
+      <div class="el-upload__text">将文件拖到此处,或<em>点击选择文件</em></div>
+      <div slot="tip" class="el-upload__tip">仅支持 .txt / .csv,建议 UTF-8 编码</div>
+    </el-upload>
+
+    <div v-if="rawLines.length > 0" class="preview">
+      <div class="preview-title">解析预览(共 {{ rawLines.length }} 行,有效 {{ validItems.length }} 行,无效 {{ invalidLines.length }} 行)</div>
+      <el-table :data="previewRows" border size="small" max-height="240">
+        <el-table-column prop="line" label="行号" width="60" align="center" />
+        <el-table-column prop="customerId" label="客户号" width="100" />
+        <el-table-column prop="listId" label="客群编号" />
+        <el-table-column label="调整方向" width="100">
+          <template slot-scope="scope">
+            {{ scope.row.direction === 'A' ? '调增-A' : scope.row.direction === 'D' ? '调减-D' : scope.row.direction }}
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" width="160">
+          <template slot-scope="scope">
+            <el-tag v-if="scope.row.error" type="danger" size="small">{{ scope.row.error }}</el-tag>
+            <el-tag v-else type="success" size="small">有效</el-tag>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <div v-if="result" class="result-block">
+      <el-alert
+        :title="`处理完成:成功 ${result.success} 条,失败 ${result.failed} 条`"
+        :type="result.failed === 0 ? 'success' : 'warning'"
+        :closable="false"
+        show-icon
+      />
+      <div v-if="result.failures.length > 0" class="result-failures">
+        <div>失败明细:</div>
+        <ul>
+          <li v-for="(f, i) in result.failures" :key="i">
+            第 {{ f.line }} 行({{ f.item.customerId }} → {{ f.item.listId }} / {{ f.item.direction }}):{{ f.reason }}
+          </li>
+        </ul>
+      </div>
+    </div>
+
+    <div slot="footer" class="dialog-footer">
+      <el-button @click="handleCancel">取 消</el-button>
+      <el-button
+        type="primary"
+        :disabled="validItems.length === 0"
+        @click="handleApply"
+      >确认上传({{ validItems.length }} 条)</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import crowdNameListStore from '@/store/crowdNameListStore'
+
+export default {
+  name: 'UploadAdjustDialog',
+  props: {
+    visible: { type: Boolean, default: false }
+  },
+  data() {
+    return {
+      rawLines: [],
+      previewRows: [],
+      result: null
+    }
+  },
+  computed: {
+    innerVisible: {
+      get() { return this.visible },
+      set(v) { this.$emit('update:visible', v) }
+    },
+    validItems() {
+      return this.previewRows.filter((r) => !r.error)
+    },
+    invalidLines() {
+      return this.previewRows.filter((r) => r.error)
+    }
+  },
+  watch: {
+    visible(v) {
+      if (v) this.reset()
+    }
+  },
+  methods: {
+    reset() {
+      this.rawLines = []
+      this.previewRows = []
+      this.result = null
+    },
+    handleFile(file) {
+      const reader = new FileReader()
+      reader.onload = (e) => {
+        const text = e.target.result || ''
+        const lines = text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l)
+        this.rawLines = lines
+        this.parseLines(lines)
+      }
+      reader.readAsText(file.raw, 'utf-8')
+    },
+    parseLines(lines) {
+      this.previewRows = lines.map((line, idx) => {
+        const parts = line.split('|+|')
+        if (parts.length !== 3) {
+          return { line: idx + 1, raw: line, error: '格式错误(应为3段,使用 |+| 分隔)' }
+        }
+        const [customerId, listId, direction] = parts.map((s) => s.trim())
+        if (!customerId) return { line: idx + 1, customerId, listId, direction, error: '客户号为空' }
+        const list = crowdNameListStore.getById(listId)
+        if (!list) return { line: idx + 1, customerId, listId, direction, error: '客群编号不存在' }
+        if (list.status === 'stopped') return { line: idx + 1, customerId, listId, direction, error: '名单已停止' }
+        if (direction !== 'A' && direction !== 'D') {
+          return { line: idx + 1, customerId, listId, direction, error: '调整方向只能为 A 或 D' }
+        }
+        return { line: idx + 1, customerId, listId, direction }
+      })
+    },
+    handleApply() {
+      if (this.validItems.length === 0) {
+        this.$message.warning('没有有效记录可上传')
+        return
+      }
+      const items = this.validItems.map((r) => ({
+        listId: r.listId,
+        customerId: r.customerId,
+        direction: r.direction
+      }))
+      this.result = crowdNameListStore.applyBatch(items)
+      this.$message.success(`已提交 ${items.length} 条调整,审核通过后生效(demo 默认通过)`)
+      this.$emit('applied')
+    },
+    handleCancel() {
+      this.innerVisible = false
+    }
+  }
+}
+</script>
+
+<style scoped>
+.format-block {
+  background: #f5f7fa; border-radius: 4px;
+  padding: 12px 16px; margin-bottom: 16px; font-size: 13px;
+}
+.format-title { font-weight: 600; color: #303133; margin-bottom: 6px; }
+.format-line { color: #606266; margin: 4px 0; }
+.format-line code { background: #fff; padding: 1px 4px; border-radius: 2px; color: #F56C6C; }
+.format-sample {
+  background: #fff; padding: 8px 12px; border: 1px solid #ebeef5; border-radius: 2px;
+  font-family: 'Menlo', 'Consolas', monospace; font-size: 12px;
+  margin: 6px 0; white-space: pre;
+}
+
+.upload-area { margin-bottom: 16px; }
+
+.preview { margin-top: 12px; }
+.preview-title { font-size: 13px; color: #606266; margin-bottom: 8px; }
+
+.result-block { margin-top: 16px; }
+.result-failures {
+  margin-top: 8px; font-size: 12px; color: #909399;
+  max-height: 120px; overflow-y: auto;
+}
+.result-failures ul { padding-left: 18px; margin: 4px 0; }
+
+.dialog-footer { text-align: center; }
+</style>

+ 131 - 0
pc/src/components/crowdTask/AddRuleGroupDialog.vue

@@ -0,0 +1,131 @@
+<template>
+  <el-dialog
+    title="新增规则组"
+    :visible.sync="innerVisible"
+    width="560px"
+    :close-on-click-modal="false"
+    @close="reset"
+  >
+    <el-form ref="form" :model="form" label-width="140px" label-position="right">
+      <el-form-item label="规则组卡种类型" required>
+        <el-select
+          v-model="form.cardType"
+          placeholder="请选择"
+          style="width: 100%"
+          @change="form.ruleGroupId = ''"
+        >
+          <el-option label="借记卡圈客规则" value="debitCard" />
+          <el-option label="信用卡圈客规则" value="creditCard" />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="规则组" required>
+        <el-select
+          v-model="form.ruleGroupId"
+          placeholder="请选择"
+          filterable
+          :filter-method="filterRuleGroup"
+          style="width: 100%"
+          no-data-text="暂无可选规则组"
+          no-match-text="未找到匹配的规则组"
+        >
+          <el-option
+            v-for="rg in filteredGroups"
+            :key="rg.id"
+            :label="`${rg.id}(${rg.name})`"
+            :value="rg.id"
+          />
+        </el-select>
+        <div class="hint">仅展示「已上线」状态且与卡种类型匹配的规则组</div>
+      </el-form-item>
+    </el-form>
+
+    <div slot="footer" class="dialog-footer">
+      <el-button @click="handleCancel">取 消</el-button>
+      <el-button type="primary" @click="handleSave">保 存</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import ruleGroupStore from '@/store/ruleGroupStore'
+
+export default {
+  name: 'AddRuleGroupDialog',
+  props: {
+    visible: { type: Boolean, default: false },
+    excludeIds: { type: Array, default: () => [] }
+  },
+  data() {
+    return {
+      form: { cardType: '', ruleGroupId: '' },
+      filterKeyword: ''
+    }
+  },
+  computed: {
+    innerVisible: {
+      get() { return this.visible },
+      set(v) { this.$emit('update:visible', v) }
+    },
+    filteredGroups() {
+      const all = ruleGroupStore.list.filter((rg) => rg.status === 'online')
+      return all.filter((rg) => {
+        if (this.form.cardType && rg.type !== this.form.cardType) return false
+        if (this.excludeIds.includes(rg.id)) return false
+        if (this.filterKeyword) {
+          const k = this.filterKeyword.toLowerCase()
+          if (!rg.id.toLowerCase().includes(k) && !rg.name.toLowerCase().includes(k)) {
+            return false
+          }
+        }
+        return true
+      })
+    }
+  },
+  watch: {
+    visible(v) {
+      if (v) this.reset()
+    }
+  },
+  methods: {
+    reset() {
+      this.form = { cardType: '', ruleGroupId: '' }
+      this.filterKeyword = ''
+    },
+    filterRuleGroup(k) {
+      this.filterKeyword = k
+    },
+    handleSave() {
+      if (!this.form.cardType) {
+        this.$message.error('请选择规则组卡种类型')
+        return
+      }
+      if (!this.form.ruleGroupId) {
+        this.$message.error('请选择规则组')
+        return
+      }
+      const rg = ruleGroupStore.getById(this.form.ruleGroupId)
+      if (!rg) {
+        this.$message.error('所选规则组不存在')
+        return
+      }
+      this.$emit('save', {
+        id: rg.id,
+        name: rg.name,
+        description: rg.description || '-',
+        logic: rg.logic,
+        ruleCount: (rg.rules || []).length
+      })
+      this.innerVisible = false
+    },
+    handleCancel() {
+      this.innerVisible = false
+    }
+  }
+}
+</script>
+
+<style scoped>
+.hint { color: #909399; font-size: 12px; margin-top: 4px; }
+.dialog-footer { text-align: center; }
+</style>

+ 40 - 0
pc/src/components/crowdTask/SampleDetailDialog.vue

@@ -0,0 +1,40 @@
+<template>
+  <el-dialog
+    title="sample结果详情"
+    :visible.sync="innerVisible"
+    width="520px"
+  >
+    <div class="dialog-body">
+      <span class="label"><span class="required">*</span> sample 结果</span>
+      <el-table :data="rows" border stripe size="small" style="margin-top: 8px">
+        <el-table-column prop="index" label="序号" width="100" align="center" />
+        <el-table-column prop="customerId" label="客户号" align="center" />
+      </el-table>
+    </div>
+    <div slot="footer" class="dialog-footer">
+      <el-button type="primary" @click="innerVisible = false">返 回</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+export default {
+  name: 'SampleDetailDialog',
+  props: {
+    visible: { type: Boolean, default: false },
+    rows: { type: Array, default: () => [] }
+  },
+  computed: {
+    innerVisible: {
+      get() { return this.visible },
+      set(v) { this.$emit('update:visible', v) }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.dialog-body .label { font-weight: 600; color: #303133; }
+.required { color: #F56C6C; margin-right: 4px; }
+.dialog-footer { text-align: center; }
+</style>

+ 268 - 0
pc/src/components/ruleGroup/RuleEditDialog.vue

@@ -0,0 +1,268 @@
+<template>
+  <el-dialog
+    :title="dialogTitle"
+    :visible.sync="innerVisible"
+    width="640px"
+    :close-on-click-modal="false"
+    @close="handleCancel"
+  >
+    <el-form
+      ref="form"
+      :model="form"
+      label-width="140px"
+      label-position="right"
+    >
+      <el-form-item label="元标签规则" required>
+        <el-select
+          v-model="form.metaCode"
+          placeholder="请选择"
+          :disabled="readonly"
+          style="width: 100%"
+          @change="handleMetaChange"
+        >
+          <el-option
+            v-for="meta in metaRules"
+            :key="meta.code"
+            :label="`${meta.code}-${meta.name}`"
+            :value="meta.code"
+          />
+        </el-select>
+      </el-form-item>
+
+      <template v-if="currentMeta">
+        <el-divider />
+
+        <el-form-item label="规则名称">
+          <el-input
+            v-model="form.ruleName"
+            :disabled="readonly"
+            placeholder="便于识别该规则用途"
+            maxlength="40"
+            show-word-limit
+            style="width: 100%"
+          />
+        </el-form-item>
+
+        <el-form-item
+          v-for="field in currentMeta.fields"
+          :key="field.key"
+          :label="field.label"
+          :required="field.required"
+        >
+          <!-- 单选 -->
+          <el-select
+            v-if="field.type === 'select'"
+            v-model="form.config[field.key]"
+            :disabled="readonly"
+            placeholder="请选择"
+            clearable
+            style="width: 100%"
+          >
+            <el-option
+              v-for="opt in field.options"
+              :key="String(opt.value)"
+              :label="opt.label"
+              :value="opt.value"
+            />
+          </el-select>
+          <!-- 多选 -->
+          <el-select
+            v-else-if="field.type === 'multiSelect'"
+            v-model="form.config[field.key]"
+            :disabled="readonly"
+            multiple
+            collapse-tags
+            placeholder="请选择"
+            clearable
+            style="width: 100%"
+          >
+            <el-option
+              v-for="opt in field.options"
+              :key="String(opt.value)"
+              :label="opt.label"
+              :value="opt.value"
+            />
+          </el-select>
+          <!-- 日期 -->
+          <el-date-picker
+            v-else-if="field.type === 'datePicker'"
+            v-model="form.config[field.key]"
+            :disabled="readonly"
+            type="date"
+            placeholder="请选择日期"
+            value-format="yyyy-MM-dd"
+            style="width: 100%"
+          />
+          <!-- 月份 -->
+          <el-date-picker
+            v-else-if="field.type === 'monthPicker'"
+            v-model="form.config[field.key]"
+            :disabled="readonly"
+            type="month"
+            placeholder="请选择日期"
+            value-format="yyyy-MM"
+            style="width: 100%"
+          />
+          <!-- 数字 -->
+          <el-input
+            v-else-if="field.type === 'number'"
+            v-model.number="form.config[field.key]"
+            :disabled="readonly"
+            type="number"
+            :min="field.min"
+            :max="field.max"
+            placeholder="请输入"
+            style="width: 100%"
+          />
+          <!-- 连续达标月数 / 时间范围 -->
+          <div v-else-if="field.type === 'nm'" class="nm-input">
+            <el-input
+              v-model.number="form.config[field.key].n"
+              :disabled="readonly"
+              type="number"
+              :min="1"
+              style="width: 90px"
+            />
+            <span class="nm-sep">/</span>
+            <el-input
+              v-model.number="form.config[field.key].m"
+              :disabled="readonly"
+              type="number"
+              :min="1"
+              style="width: 90px"
+            />
+          </div>
+          <!-- 文本 -->
+          <el-input
+            v-else
+            v-model="form.config[field.key]"
+            :disabled="readonly"
+            placeholder="请输入"
+            style="width: 100%"
+          />
+        </el-form-item>
+      </template>
+    </el-form>
+
+    <div slot="footer" class="dialog-footer">
+      <el-button @click="handleCancel">取 消</el-button>
+      <el-button v-if="!readonly" type="primary" @click="handleSave">保 存</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { META_RULES, getMetaRule } from '@/config/metaRules'
+
+const emptyForm = () => ({
+  id: '',
+  metaCode: '',
+  ruleName: '',
+  config: {}
+})
+
+export default {
+  name: 'RuleEditDialog',
+  props: {
+    visible: { type: Boolean, default: false },
+    rule: { type: Object, default: null },
+    mode: { type: String, default: 'add' } // add | edit | view
+  },
+  data() {
+    return {
+      form: emptyForm(),
+      metaRules: META_RULES
+    }
+  },
+  computed: {
+    innerVisible: {
+      get() { return this.visible },
+      set(v) { this.$emit('update:visible', v) }
+    },
+    dialogTitle() {
+      if (this.mode === 'edit') return '编辑规则信息'
+      if (this.mode === 'view') return '查看规则信息'
+      return '新增规则信息'
+    },
+    readonly() {
+      return this.mode === 'view'
+    },
+    currentMeta() {
+      return this.form.metaCode ? getMetaRule(this.form.metaCode) : null
+    }
+  },
+  watch: {
+    visible(v) {
+      if (v) this.initForm()
+    }
+  },
+  methods: {
+    initForm() {
+      if (this.rule) {
+        this.form = {
+          id: this.rule.id,
+          metaCode: this.rule.metaCode,
+          ruleName: this.rule.ruleName || '',
+          config: JSON.parse(JSON.stringify(this.rule.config || {}))
+        }
+      } else {
+        this.form = emptyForm()
+      }
+    },
+    handleMetaChange(code) {
+      const meta = getMetaRule(code)
+      const config = {}
+      if (meta) {
+        meta.fields.forEach((f) => {
+          if (f.type === 'multiSelect') config[f.key] = []
+          else if (f.type === 'nm') config[f.key] = f.defaultValue ? { ...f.defaultValue } : { n: 3, m: 6 }
+          else config[f.key] = ''
+        })
+      }
+      this.form.config = config
+      if (!this.form.ruleName && meta) {
+        this.form.ruleName = meta.name
+      }
+    },
+    validateConfig() {
+      if (!this.form.metaCode) {
+        this.$message.error('请选择元标签规则')
+        return false
+      }
+      const meta = this.currentMeta
+      if (!meta) return false
+      for (const f of meta.fields) {
+        if (!f.required) continue
+        const v = this.form.config[f.key]
+        const empty = v === '' || v === null || v === undefined ||
+          (Array.isArray(v) && v.length === 0)
+        if (empty) {
+          this.$message.error(`请填写「${f.label}」`)
+          return false
+        }
+      }
+      return true
+    },
+    handleSave() {
+      if (!this.validateConfig()) return
+      this.$emit('save', {
+        id: this.form.id || `r_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
+        metaCode: this.form.metaCode,
+        ruleName: this.form.ruleName || this.currentMeta.name,
+        config: JSON.parse(JSON.stringify(this.form.config))
+      })
+      this.innerVisible = false
+    },
+    handleCancel() {
+      this.innerVisible = false
+      this.$emit('cancel')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.nm-input { display: flex; align-items: center; }
+.nm-sep { margin: 0 12px; color: #303133; font-size: 16px; }
+.dialog-footer { text-align: center; }
+</style>

+ 200 - 0
pc/src/config/metaRules.js

@@ -0,0 +1,200 @@
+// 元规则(Meta Rule)字典
+// 每个元规则定义编号、名称、配置字段,并提供 buildDescription / buildParams
+// 用于在规则列表中自动渲染"规则描述"和"规则参数"
+
+// 规则组类型选项
+export const GROUP_TYPES = [
+  { value: 'debitCard', label: '借记卡圈客规则' },
+  { value: 'creditCard', label: '信用卡圈客规则' }
+]
+
+// 规则逻辑
+export const LOGIC_OPTIONS = [
+  { value: 'AND', label: '且(AND)' },
+  { value: 'OR', label: '或(OR)' }
+]
+
+// 状态
+export const STATUS_OPTIONS = [
+  { value: 'draft', label: '新建' },
+  { value: 'online', label: '已上线' }
+]
+
+// 常用比较运算符(用于 TRB / 余额 / 年龄等数值字段)
+const NUMERIC_OPS = [
+  { value: '>', label: '大于 >' },
+  { value: '<', label: '小于 <' },
+  { value: '=', label: '等于 =' },
+  { value: '>=', label: '大于等于 >=' },
+  { value: '<=', label: '小于等于 <=' },
+  { value: '!=', label: '不等于 !=' }
+]
+
+// 日期比较运算符
+const DATE_OPS = [
+  { value: '<', label: '在...前' },
+  { value: '>', label: '在...后' },
+  { value: '=', label: '在...当日' }
+]
+
+// 集合运算符
+const SET_OPS = [
+  { value: 'in', label: '属于' },
+  { value: 'not_in', label: '不属于' },
+  { value: '=', label: '等于' },
+  { value: '!=', label: '不等于' }
+]
+
+// =============================================
+// 工具函数
+// =============================================
+function formatMonthCN(yyyymm) {
+  if (!yyyymm) return ''
+  const [y, m] = yyyymm.split('-')
+  return `${y}年${parseInt(m, 10)}月`
+}
+function formatMonthCompact(yyyymm) {
+  return yyyymm ? yyyymm.replace('-', '') : ''
+}
+function formatDateSlash(yyyymmdd) {
+  if (!yyyymmdd) return ''
+  const [y, m, d] = yyyymmdd.split('-')
+  return `${y}/${parseInt(m, 10)}/${parseInt(d, 10)}`
+}
+function formatAmount(num) {
+  if (num === '' || num === null || num === undefined) return ''
+  const n = Number(num)
+  if (Number.isNaN(n)) return String(num)
+  if (n >= 10000) return `${n / 10000}万`
+  return String(n)
+}
+function dateOpSuffix(op) {
+  return { '<': '前', '>': '后', '=': '当日' }[op] || op || ''
+}
+
+// =============================================
+// 元规则列表
+// =============================================
+export const META_RULES = [
+  {
+    code: '001',
+    name: '开户时间(open_date)',
+    paramKey: 'open_date',
+    fields: [
+      {
+        key: 'accountType', label: '账户类型', type: 'select', required: true,
+        options: [
+          { value: 'I', label: 'I类账户' },
+          { value: 'II', label: 'II类账户' },
+          { value: 'III', label: 'III类账户' }
+        ]
+      },
+      { key: 'operator', label: '比较方式', type: 'select', required: true, options: DATE_OPS },
+      { key: 'date', label: '开户时间', type: 'datePicker', required: true }
+    ],
+    buildDescription(c) {
+      if (!c.accountType || !c.date) return ''
+      return `开户时间(${c.accountType}类账户)在${formatDateSlash(c.date)}${dateOpSuffix(c.operator)}`
+    },
+    buildParams(c) {
+      if (!c.operator || !c.date) return ''
+      return `{open_date,${c.operator},(${c.date})}`
+    }
+  },
+  {
+    code: '010',
+    name: 'MSC',
+    paramKey: 'MSC',
+    fields: [
+      { key: 'operator', label: 'MSC表达式', type: 'select', required: true, options: SET_OPS },
+      {
+        key: 'values', label: 'MSC值', type: 'multiSelect', required: true,
+        options: [
+          { value: 'premier', label: 'premier(卓越理财)' },
+          { value: 'advance', label: 'advance(运筹理财)' },
+          { value: 'excellence', label: 'excellence(卓越理财·尊尚)' },
+          { value: 'global', label: 'global(环球私人银行)' }
+        ]
+      }
+    ],
+    buildDescription(c) {
+      const vals = (c.values || []).join('/')
+      if (!vals) return ''
+      const opText = c.operator === 'not_in' || c.operator === '!=' ? '≠' : '='
+      return `MSC${opText}${vals}`
+    },
+    buildParams(c) {
+      const vals = (c.values || []).join('/')
+      if (!c.operator || !vals) return ''
+      return `{MSC,${c.operator},(${vals})}`
+    }
+  },
+  {
+    code: '050',
+    name: 'TRB值-Avg TRB口径(不含家庭账户余额)',
+    paramKey: 'Avg_TRB',
+    fields: [
+      { key: 'month', label: '开始月份', type: 'monthPicker', required: true },
+      { key: 'nm', label: '连续达标月数/时间范围(月)', type: 'nm', required: false, defaultValue: { n: 3, m: 6 } },
+      { key: 'operator', label: 'Avg TRB表达式', type: 'select', required: true, options: NUMERIC_OPS },
+      { key: 'amount', label: 'Avg TRB值', type: 'number', required: true, min: 0 }
+    ],
+    buildDescription(c) {
+      if (!c.month || !c.operator || c.amount === '' || c.amount == null) return ''
+      const n = c.nm && c.nm.n ? `(连续${c.nm.n}个月达标)` : ''
+      return `${formatMonthCN(c.month)}月末 TRB值-Avg TRB口径(不含家庭账户余额)${c.operator}${formatAmount(c.amount)}${n}`
+    },
+    buildParams(c) {
+      if (!c.month || !c.operator) return ''
+      const month = formatMonthCompact(c.month)
+      const nm = c.nm && c.nm.n ? `;{${c.nm.n}/${c.nm.m || ''}}` : ''
+      return `{month,=,${month}};{Avg_TRB,${c.operator},${c.amount}}${nm}`
+    }
+  },
+  {
+    code: '051',
+    name: '月末余额(End of day balance)',
+    paramKey: 'balance',
+    fields: [
+      { key: 'month', label: '开始月份', type: 'monthPicker', required: true },
+      { key: 'nm', label: '连续达标月数/时间范围(月)', type: 'nm', required: false, defaultValue: { n: 3, m: 6 } },
+      { key: 'operator', label: '余额表达式', type: 'select', required: true, options: NUMERIC_OPS },
+      { key: 'amount', label: '余额值', type: 'number', required: true, min: 0 }
+    ],
+    buildDescription(c) {
+      if (!c.month || !c.operator || c.amount === '' || c.amount == null) return ''
+      const n = c.nm && c.nm.n ? `(连续${c.nm.n}个月达标)` : ''
+      return `${formatMonthCN(c.month)}月末 End of day balance (所有资金含产品)${c.operator}${formatAmount(c.amount)}${n}`
+    },
+    buildParams(c) {
+      if (!c.month || !c.operator) return ''
+      const month = formatMonthCompact(c.month)
+      const nm = c.nm && c.nm.n ? `;{${c.nm.n}/${c.nm.m || ''}}` : ''
+      return `{month,=,${month}};{balance,${c.operator},${c.amount}}${nm}`
+    }
+  }
+]
+
+// 元规则下拉显示文本:编号-名称
+export function metaRuleLabel(meta) {
+  return `${meta.code}-${meta.name}`
+}
+
+// 根据 code 查询元规则
+export function getMetaRule(code) {
+  return META_RULES.find((m) => m.code === code)
+}
+
+// 计算一条规则的描述
+export function buildRuleDescription(metaCode, config) {
+  const meta = getMetaRule(metaCode)
+  if (!meta || !meta.buildDescription) return ''
+  return meta.buildDescription(config || {})
+}
+
+// 计算一条规则的参数
+export function buildRuleParams(metaCode, config) {
+  const meta = getMetaRule(metaCode)
+  if (!meta || !meta.buildParams) return ''
+  return meta.buildParams(config || {})
+}

+ 18 - 0
pc/src/mock/index.js

@@ -1,5 +1,23 @@
 import Mock from 'mockjs'
 
+// =============================================
+// 登录
+// =============================================
+Mock.mock(/\/api\/login/, 'post', (options) => {
+  const { username, password } = JSON.parse(options.body || '{}')
+  if (!username || !password) {
+    return { code: 400, data: null, msg: '账号或密码不能为空' }
+  }
+  if (username === 'admin' && password === '123456') {
+    return {
+      code: 200,
+      data: { token: 'mock-token-' + Date.now(), username },
+      msg: '登录成功'
+    }
+  }
+  return { code: 401, data: null, msg: '账号或密码错误' }
+})
+
 // =============================================
 // 页面管理 - 权益介绍页配置
 // =============================================

+ 92 - 8
pc/src/router/index.js

@@ -5,6 +5,12 @@ import Layout from '@/components/Layout.vue'
 Vue.use(VueRouter)
 
 const routes = [
+  {
+    path: '/login',
+    name: 'Login',
+    component: () => import('@/views/Login.vue'),
+    meta: { public: true }
+  },
   {
     path: '/',
     component: Layout,
@@ -46,34 +52,100 @@ const routes = [
         meta: { title: '信用卡权益', icon: 'el-icon-bank-card' }
       },
       {
-        path: 'customization/hsbc/crowds',
-        name: 'CrowdManagement',
-        component: () => import('@/views/CrowdManagement.vue'),
-        meta: { title: '人群管理', icon: 'el-icon-user' }
+        path: 'customization/hsbc/crowd-tasks',
+        name: 'CrowdTaskList',
+        component: () => import('@/views/CrowdTaskList.vue'),
+        meta: { title: '客群任务管理', icon: 'el-icon-user' }
+      },
+      {
+        path: 'customization/hsbc/crowd-tasks/add',
+        name: 'CrowdTaskAdd',
+        component: () => import('@/views/CrowdTaskEdit.vue'),
+        meta: { title: '新增客群规则任务' }
+      },
+      {
+        path: 'customization/hsbc/crowd-tasks/edit/:id',
+        name: 'CrowdTaskEdit',
+        component: () => import('@/views/CrowdTaskEdit.vue'),
+        meta: { title: '编辑客群规则任务' }
+      },
+      {
+        path: 'customization/hsbc/crowd-tasks/copy/:id',
+        name: 'CrowdTaskCopy',
+        component: () => import('@/views/CrowdTaskEdit.vue'),
+        meta: { title: '复制客群规则任务' }
+      },
+      {
+        path: 'customization/hsbc/crowd-tasks/view/:id',
+        name: 'CrowdTaskView',
+        component: () => import('@/views/CrowdTaskView.vue'),
+        meta: { title: '客群规则任务详情' }
+      },
+      {
+        path: 'customization/hsbc/crowd-name-lists',
+        name: 'CrowdNameList',
+        component: () => import('@/views/CrowdNameList.vue'),
+        meta: { title: '客群名单管理', icon: 'el-icon-document' }
+      },
+      {
+        path: 'customization/hsbc/crowd-name-lists/add',
+        name: 'CrowdNameListAdd',
+        component: () => import('@/views/CrowdNameListEdit.vue'),
+        meta: { title: '新增客群' }
+      },
+      {
+        path: 'customization/hsbc/crowd-name-lists/edit/:id',
+        name: 'CrowdNameListEdit',
+        component: () => import('@/views/CrowdNameListEdit.vue'),
+        meta: { title: '编辑客群' }
+      },
+      {
+        path: 'customization/hsbc/rule-groups',
+        name: 'RuleGroupList',
+        component: () => import('@/views/RuleGroupList.vue'),
+        meta: { title: '规则组管理', icon: 'el-icon-set-up' }
+      },
+      {
+        path: 'customization/hsbc/rule-groups/add',
+        name: 'RuleGroupAdd',
+        component: () => import('@/views/RuleGroupEdit.vue'),
+        meta: { title: '新增规则组' }
+      },
+      {
+        path: 'customization/hsbc/rule-groups/edit/:id',
+        name: 'RuleGroupEdit',
+        component: () => import('@/views/RuleGroupEdit.vue'),
+        meta: { title: '编辑规则组' }
+      },
+      {
+        path: 'customization/hsbc/rule-groups/view/:id',
+        name: 'RuleGroupView',
+        component: () => import('@/views/RuleGroupView.vue'),
+        meta: { title: '规则组详情' }
       },
       {
         path: 'customization/hsbc/equity-products/add',
         name: 'AddEquityProduct',
         component: () => import('@/views/AddEquityProduct.vue'),
-        meta: { title: '添加权益商品', icon: 'el-icon-edit' }
+        meta: { title: '添加权益', icon: 'el-icon-edit' }
       },
       {
         path: 'customization/hsbc/equity-products/view/:id',
         name: 'ViewEquityProduct',
         component: () => import('@/views/ViewEquityProduct.vue'),
-        meta: { title: '查看权益商品', icon: 'el-icon-view' }
+        meta: { title: '查看权益', icon: 'el-icon-view' }
       },
       {
         path: 'customization/hsbc/equity-products/edit/:id',
         name: 'EditEquityProduct',
         component: () => import('@/views/EditEquityProduct.vue'),
-        meta: { title: '编辑权益商品', icon: 'el-icon-edit' }
+        meta: { title: '编辑权益', icon: 'el-icon-edit' }
       },
       {
         path: 'customization/hsbc/equity-products/audit/:id',
         name: 'AuditEquityProduct',
         component: () => import('@/views/AuditEquityProduct.vue'),
-        meta: { title: '审核权益商品', icon: 'el-icon-check' }
+        meta: { title: '审核权益', icon: 'el-icon-check' }
       },
       {
         path: 'customization/hsbc/equity-products/coupons/:id',
@@ -156,4 +228,16 @@ const router = new VueRouter({
   routes
 })
 
+router.beforeEach((to, from, next) => {
+  const token = localStorage.getItem('token')
+  if (to.meta && to.meta.public) {
+    if (token && to.path === '/login') return next('/')
+    return next()
+  }
+  if (!token) {
+    return next({ path: '/login', query: { redirect: to.fullPath } })
+  }
+  next()
+})
+
 export default router

+ 129 - 0
pc/src/store/crowdNameListStore.js

@@ -0,0 +1,129 @@
+// 客群名单 store(demo 用,刷新后重置为初始种子数据)
+import Vue from 'vue'
+
+const now = () => {
+  const d = new Date()
+  const pad = (n) => String(n).padStart(2, '0')
+  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
+}
+
+// A 开头 + 6 位序列 + 8 位日期 = 15 位
+function genId(seq) {
+  const d = new Date()
+  const pad = (n) => String(n).padStart(2, '0')
+  const stamp = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`
+  return `A${String(seq).padStart(6, '0')}${stamp}`
+}
+
+const state = Vue.observable({
+  list: [
+    {
+      id: 'A0000120251224',
+      name: '名单1',
+      description: '层层礼活动黑名单',
+      status: 'draft',
+      members: ['00001', '00002', '00003', '00004'],
+      creator: '运维A',
+      reviewer: '审核B',
+      createdAt: '2025-12-24 12:52:30',
+      updatedAt: '2025-12-31 15:31:12'
+    }
+  ],
+  seq: 2,
+  // 调整流水(demo 仅用于上传文件结果展示)
+  adjustments: []
+})
+
+function findById(id) {
+  return state.list.find((t) => t.id === id)
+}
+
+export default {
+  get list() {
+    return state.list
+  },
+  query({ id = '', name = '' } = {}) {
+    return state.list.filter((t) => {
+      if (id && !t.id.includes(id)) return false
+      if (name && !t.name.includes(name)) return false
+      return true
+    })
+  },
+  getById(id) {
+    return findById(id)
+  },
+  create(payload) {
+    const initialMembers = Array.isArray(payload.members)
+      ? Array.from(new Set(payload.members.filter((m) => m)))
+      : []
+    const record = {
+      id: genId(state.seq++),
+      name: payload.name,
+      description: payload.description || '',
+      status: 'draft',
+      members: initialMembers,
+      creator: '当前用户',
+      reviewer: '审核B',
+      createdAt: now(),
+      updatedAt: now()
+    }
+    state.list.unshift(record)
+    return record
+  },
+  update(id, payload) {
+    const target = findById(id)
+    if (!target) return null
+    target.name = payload.name
+    target.description = payload.description || ''
+    target.updatedAt = now()
+    return target
+  },
+  activate(id) {
+    const target = findById(id)
+    if (!target) return
+    target.status = 'active'
+    target.updatedAt = now()
+  },
+  stop(id) {
+    const target = findById(id)
+    if (!target) return
+    target.status = 'stopped'
+    target.updatedAt = now()
+  },
+  // 单条人员调整
+  adjustOne({ listId, customerId, direction }) {
+    const target = findById(listId)
+    if (!target) return { ok: false, reason: '名单不存在' }
+    if (target.status === 'stopped') return { ok: false, reason: '已停止名单不可调整' }
+    if (direction === 'A') {
+      if (target.members.includes(customerId)) {
+        return { ok: false, reason: '客户号已存在名单中' }
+      }
+      target.members.push(customerId)
+      target.updatedAt = now()
+      return { ok: true, action: 'add' }
+    }
+    if (direction === 'D') {
+      const idx = target.members.indexOf(customerId)
+      if (idx < 0) return { ok: false, reason: '客户号不在名单中' }
+      target.members.splice(idx, 1)
+      target.updatedAt = now()
+      return { ok: true, action: 'remove' }
+    }
+    return { ok: false, reason: '调整方向非法' }
+  },
+  // 批量调整(来自上传文件)
+  applyBatch(items) {
+    const result = { success: 0, failed: 0, failures: [] }
+    items.forEach((item, idx) => {
+      const r = this.adjustOne(item)
+      if (r.ok) {
+        result.success++
+      } else {
+        result.failed++
+        result.failures.push({ line: idx + 1, item, reason: r.reason })
+      }
+    })
+    return result
+  }
+}

+ 203 - 0
pc/src/store/crowdTaskStore.js

@@ -0,0 +1,203 @@
+// 客群任务 store(demo 用,刷新后重置为初始种子数据)
+import Vue from 'vue'
+
+const now = () => {
+  const d = new Date()
+  const pad = (n) => String(n).padStart(2, '0')
+  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
+}
+
+// 14 位编号:6 位序列 + 8 位日期
+function genTaskId(seq) {
+  const d = new Date()
+  const pad = (n) => String(n).padStart(2, '0')
+  const stamp = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`
+  return `${String(seq).padStart(6, '0')}${stamp}`
+}
+
+// sample 数量规则
+function sampleSize(count) {
+  if (count < 30000) return 5
+  if (count < 50000) return 8
+  if (count < 150000) return 12
+  return 15
+}
+
+function generateSample(count) {
+  const size = Math.min(sampleSize(count), Math.max(count, 0))
+  const arr = []
+  const used = new Set()
+  while (arr.length < size) {
+    const cid = String(Math.floor(Math.random() * 99999) + 1).padStart(5, '0')
+    if (!used.has(cid)) {
+      used.add(cid)
+      arr.push({ index: arr.length + 1, customerId: cid })
+    }
+  }
+  return arr
+}
+
+function randomCount() {
+  // 随机分布在 1000 - 200000
+  return Math.floor(Math.random() * 199000) + 1000
+}
+
+const state = Vue.observable({
+  list: [
+    {
+      id: '00000120251224',
+      name: '层层礼圈选客群1',
+      description: '圈选层层礼报名客群',
+      type: 'display', // display | reward
+      cycle: 'once',
+      cycleConfig: null,
+      startDate: '2025-12-25',
+      endDate: '2025-12-31',
+      ruleGroupLogic: 'AND',
+      ruleGroups: [
+        { id: 'RG20260401001', name: '层层礼-借记卡新客圈选', description: '2025/9/30 前开户 + MSC 属于卓越/运筹', logic: 'AND', ruleCount: 4 },
+        { id: 'RG20260410002', name: '信用卡-高净值持卡客户', description: 'MSC 属于卓越/尊尚 且 月末余额 ≥ 100 万', logic: 'AND', ruleCount: 2 }
+      ],
+      status: 'verifying',
+      creator: '运维A',
+      reviewer: '审核B',
+      createdAt: '2025-12-24 12:52:30',
+      updatedAt: '2025-12-31 15:31:12',
+      run: {
+        count: 20000,
+        sample: generateSample(20000),
+        fulllist: null
+      }
+    }
+  ],
+  seq: 2
+})
+
+function findById(id) {
+  return state.list.find((t) => t.id === id)
+}
+
+// 模拟"行内"异步返回
+function asyncRun(task, kind, delay = 2000) {
+  if (kind === 'countSample') {
+    Vue.set(task.run, 'count', 'running')
+    Vue.set(task.run, 'sample', 'running')
+    setTimeout(() => {
+      const c = randomCount()
+      Vue.set(task.run, 'count', c)
+      Vue.set(task.run, 'sample', generateSample(c))
+      task.status = 'verifying'
+      task.updatedAt = now()
+    }, delay)
+  } else if (kind === 'fulllist') {
+    Vue.set(task.run, 'fulllist', 'running')
+    setTimeout(() => {
+      Vue.set(task.run, 'fulllist', 'completed')
+      task.status = 'online'
+      task.updatedAt = now()
+    }, delay + 1000)
+  }
+}
+
+export default {
+  get list() {
+    return state.list
+  },
+  query({ id = '', name = '' } = {}) {
+    return state.list.filter((t) => {
+      if (id && !t.id.includes(id)) return false
+      if (name && !t.name.includes(name)) return false
+      return true
+    })
+  },
+  getById(id) {
+    return findById(id)
+  },
+  create(payload) {
+    const record = {
+      id: genTaskId(state.seq++),
+      name: payload.name,
+      description: payload.description || '',
+      type: payload.type,
+      cycle: payload.cycle || 'once',
+      cycleConfig: payload.cycleConfig || null,
+      startDate: payload.startDate,
+      endDate: payload.endDate,
+      ruleGroupLogic: payload.ruleGroupLogic || 'AND',
+      ruleGroups: payload.ruleGroups || [],
+      status: 'draft',
+      creator: '当前用户',
+      reviewer: '',
+      createdAt: now(),
+      updatedAt: now(),
+      run: { count: null, sample: null, fulllist: null }
+    }
+    state.list.unshift(record)
+    return record
+  },
+  copyFrom(sourceId) {
+    const src = findById(sourceId)
+    if (!src) return null
+    return this.create({
+      name: src.name + '-副本',
+      description: src.description,
+      type: src.type,
+      cycle: src.cycle,
+      cycleConfig: src.cycleConfig ? { ...src.cycleConfig } : null,
+      startDate: src.startDate,
+      endDate: src.endDate,
+      ruleGroupLogic: src.ruleGroupLogic,
+      ruleGroups: src.ruleGroups.map((r) => ({ ...r }))
+    })
+  },
+  update(id, payload, opts = {}) {
+    const target = findById(id)
+    if (!target) return null
+    target.name = payload.name
+    target.description = payload.description || ''
+    target.type = payload.type
+    target.cycle = payload.cycle || 'once'
+    target.cycleConfig = payload.cycleConfig || null
+    target.startDate = payload.startDate
+    target.endDate = payload.endDate
+    target.ruleGroupLogic = payload.ruleGroupLogic || 'AND'
+    target.ruleGroups = payload.ruleGroups || []
+    target.updatedAt = now()
+    // 编辑校验状态的任务 → 回退到新建并重置运行信息
+    if (opts.resetRun) {
+      target.status = 'draft'
+      target.run = { count: null, sample: null, fulllist: null }
+    }
+    return target
+  },
+  stop(id) {
+    const target = findById(id)
+    if (!target) return
+    target.status = 'stopped'
+    target.updatedAt = now()
+  },
+  run(id) {
+    const target = findById(id)
+    if (!target || target.status !== 'draft') return
+    asyncRun(target, 'countSample')
+  },
+  rerunCountSample(id) {
+    const target = findById(id)
+    if (!target) return
+    asyncRun(target, 'countSample')
+  },
+  publish(id) {
+    const target = findById(id)
+    if (!target || target.status !== 'verifying') return
+    target.reviewer = target.reviewer || '审核B'
+    asyncRun(target, 'fulllist')
+  },
+  // 给视图用:当前任务允许的操作列表
+  allowedActions(task) {
+    if (!task) return []
+    if (task.status === 'draft') return ['edit', 'stop', 'detail', 'run']
+    if (task.status === 'verifying') return ['edit', 'stop', 'detail', 'publish']
+    if (task.status === 'online') return ['stop', 'detail']
+    return ['detail']
+  }
+}

+ 163 - 0
pc/src/store/ruleGroupStore.js

@@ -0,0 +1,163 @@
+// 规则组数据 store(demo 用,刷新后重置为初始种子数据)
+import Vue from 'vue'
+
+const now = () => {
+  const d = new Date()
+  const pad = (n) => String(n).padStart(2, '0')
+  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
+}
+
+const state = Vue.observable({
+  list: [
+    {
+      id: 'RG20260401001',
+      name: '层层礼-借记卡新客圈选',
+      type: 'debitCard',
+      logic: 'AND',
+      description: '2025/9/30 前开户 + MSC 属于卓越/运筹 + TRB < 50万',
+      status: 'online',
+      creator: '张运营',
+      createdAt: '2026-04-01 09:15:30',
+      updater: '王审核',
+      updatedAt: '2026-04-05 11:20:18',
+      rules: [
+        {
+          id: 'r1',
+          metaCode: '001',
+          ruleName: '层层礼开户时间',
+          config: { accountType: 'I', operator: '<', date: '2025-09-30' }
+        },
+        {
+          id: 'r2',
+          metaCode: '010',
+          ruleName: '层层礼,MSC',
+          config: { operator: 'in', values: ['premier', 'advance'] }
+        },
+        {
+          id: 'r3',
+          metaCode: '050',
+          ruleName: 'TRB值不含家庭账户余额(连续3个月达标)',
+          config: { month: '2025-12', nm: { n: 3, m: 6 }, operator: '<', amount: 500000 }
+        },
+        {
+          id: 'r4',
+          metaCode: '051',
+          ruleName: '2025年12月月末余额(连续3个月达标)',
+          config: { month: '2025-12', nm: { n: 3, m: 6 }, operator: '<', amount: 500000 }
+        }
+      ]
+    },
+    {
+      id: 'RG20260410002',
+      name: '信用卡-高净值持卡客户',
+      type: 'creditCard',
+      logic: 'AND',
+      description: 'MSC 属于卓越/尊尚 且 月末余额 ≥ 100 万',
+      status: 'online',
+      creator: '李产品',
+      createdAt: '2026-04-10 16:55:02',
+      updater: '李产品',
+      updatedAt: '2026-04-10 16:55:02',
+      rules: [
+        {
+          id: 'r1',
+          metaCode: '010',
+          ruleName: '信用卡MSC',
+          config: { operator: 'in', values: ['premier', 'excellence'] }
+        },
+        {
+          id: 'r2',
+          metaCode: '051',
+          ruleName: '月末余额 ≥ 100万',
+          config: { month: '2026-03', nm: { n: 3, m: 6 }, operator: '>=', amount: 1000000 }
+        }
+      ]
+    },
+    {
+      id: 'RG20260420003',
+      name: '借记卡-跨境候选客群',
+      type: 'debitCard',
+      logic: 'OR',
+      description: 'TRB ≥ 50 万 或 月末余额 ≥ 50 万',
+      status: 'draft',
+      creator: '赵市场',
+      createdAt: '2026-04-20 11:48:33',
+      updater: '赵市场',
+      updatedAt: '2026-04-25 09:30:01',
+      rules: [
+        {
+          id: 'r1',
+          metaCode: '050',
+          ruleName: 'TRB ≥ 50万',
+          config: { month: '2026-04', nm: { n: 1, m: 1 }, operator: '>=', amount: 500000 }
+        }
+      ]
+    }
+  ]
+})
+
+let seq = state.list.length + 1
+
+function genId() {
+  const d = new Date()
+  const pad = (n) => String(n).padStart(2, '0')
+  const stamp = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`
+  return `RG${stamp}${String(seq++).padStart(3, '0')}`
+}
+
+export default {
+  get list() {
+    return state.list
+  },
+  query({ id = '', name = '', type = '' } = {}) {
+    return state.list.filter((t) => {
+      if (id && !t.id.includes(id)) return false
+      if (name && !t.name.includes(name)) return false
+      if (type && t.type !== type) return false
+      return true
+    })
+  },
+  getById(id) {
+    return state.list.find((t) => t.id === id)
+  },
+  create(payload) {
+    const record = {
+      id: genId(),
+      name: payload.name,
+      type: payload.type,
+      logic: payload.logic || 'AND',
+      description: payload.description || '',
+      status: 'draft',
+      creator: '当前用户',
+      createdAt: now(),
+      updater: '当前用户',
+      updatedAt: now(),
+      rules: payload.rules || []
+    }
+    state.list.unshift(record)
+    return record
+  },
+  update(id, payload) {
+    const target = state.list.find((t) => t.id === id)
+    if (!target) return null
+    target.name = payload.name
+    target.type = payload.type
+    target.logic = payload.logic || 'AND'
+    target.description = payload.description || ''
+    target.rules = payload.rules || []
+    target.updater = '当前用户'
+    target.updatedAt = now()
+    return target
+  },
+  remove(id) {
+    const idx = state.list.findIndex((t) => t.id === id)
+    if (idx >= 0) state.list.splice(idx, 1)
+  },
+  publish(id) {
+    const target = state.list.find((t) => t.id === id)
+    if (!target) return
+    target.status = 'online'
+    target.updater = '当前用户'
+    target.updatedAt = now()
+  }
+}

+ 214 - 204
pc/src/views/AddEquityProduct.vue

@@ -2,25 +2,25 @@
   <div class="add-equity-product">
     <el-card>
       <div slot="header" class="header-title">
-        <span>添加权益商品</span>
+        <span>添加权益</span>
       </div>
 
       <!-- 步骤条 -->
       <el-steps :active="activeStep" simple style="margin-bottom: 20px;">
-        <el-step title="填写商品信息" icon="el-icon-info"></el-step>
+        <el-step title="填写权益信息" icon="el-icon-info"></el-step>
         <el-step title="设置规则" icon="el-icon-setting"></el-step>
         <el-step title="更多设置" icon="el-icon-monitor"></el-step>
       </el-steps>
 
       <!-- 步骤内容 -->
       <div v-show="activeStep === 0">
-        <!-- 填写商品信息 -->
+        <!-- 填写权益信息 -->
         <el-card shadow="never" class="section-card">
           <div slot="header" class="section-title">基本信息</div>
           <el-form :model="form" label-width="120px" label-position="left">
             <el-row :gutter="20">
               <el-col :span="8">
-                <el-form-item label="商品来源" required>
+                <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>
@@ -29,94 +29,59 @@
                 </el-form-item>
               </el-col>
               <el-col :span="8" v-if="form.source === '荣数商品'">
-                <el-form-item label="选择商品" required>
-                  <el-select
+                <el-form-item label="选择权益" required>
+                  <el-input
                     v-model="form.rongshuProduct"
-                    placeholder="请选择商品"
-                    filterable
-                    style="width: 100%"
-                    @change="handleRongshuProductChange"
+                    placeholder="请选择权益"
+                    readonly
                   >
-                    <el-option label="荣数商品A" value="荣数商品A"></el-option>
-                    <el-option label="荣数商品B" value="荣数商品B"></el-option>
-                    <el-option label="荣数商品C" value="荣数商品C"></el-option>
-                    <el-option label="荣数商品D" value="荣数商品D"></el-option>
-                  </el-select>
+                    <el-button slot="append" @click="rongshuDialogVisible = true">选择</el-button>
+                  </el-input>
                 </el-form-item>
               </el-col>
               <el-col :span="8" v-if="form.source === '银行自有'">
-                <el-form-item label="商品类型" required>
+                <el-form-item label="权益类型" required>
                   <el-radio-group v-model="form.productType">
                     <el-radio-button label="券码"></el-radio-button>
-                    <el-radio-button label="微信立减金"></el-radio-button>
                   </el-radio-group>
                 </el-form-item>
               </el-col>
-              <el-col :span="8" v-if="form.source === '银行自有'">
-                <el-form-item label="商户名称" required>
-                  <el-select v-model="form.merchantName" placeholder="请选择商户名称" filterable style="width: 100%">
-                    <el-option label="商户A" value="商户A"></el-option>
-                    <el-option label="商户B" value="商户B"></el-option>
-                    <el-option label="商户C" value="商户C"></el-option>
-                    <el-option label="商户D" value="商户D"></el-option>
-                    <el-option label="商户E" value="商户E"></el-option>
-                  </el-select>
-                </el-form-item>
-              </el-col>
-            </el-row>
-            <el-row :gutter="20" v-if="form.source === '银行自有' && form.productType === '微信立减金'">
-              <el-col :span="8">
-                <el-form-item label="面额" required>
-                  <div style="display: flex;align-items: center;gap: 6px;">
-                    <span>满</span>
-                    <el-input
-                      v-model="form.faceValueThreshold"
-                      placeholder="金额"
-                      style="width: 100px;"
-                      oninput="value=value.replace(/[^\d]/g,'')"
-                    ></el-input>
-                    <span>减</span>
-                    <el-input
-                      v-model="form.faceValueDiscount"
-                      placeholder="金额"
-                      style="width: 100px;"
-                      oninput="value=value.replace(/[^\d]/g,'')"
-                    ></el-input>
-                  </div>
-                </el-form-item>
-              </el-col>
-              <el-col :span="8">
-                <el-form-item label="库存数" required>
-                  <el-input-number
-                    v-model="form.stockCount"
-                    :min="0"
-                    :max="100000"
-                    style="width: 100%"
-                  ></el-input-number>
-                </el-form-item>
-              </el-col>
             </el-row>
             <el-row :gutter="20">
               <el-col :span="8" v-if="form.source === '第三方'">
-                <el-form-item label="外部商品链接" required>
-                  <el-select v-model="form.externalLink" placeholder="请选择外部商品链接">
+                <el-form-item label="外部权益链接" required>
+                  <el-select v-model="form.externalLink" placeholder="请选择外部权益链接">
                     <el-option label="太古地产" value="太古地产"></el-option>
                     <el-option label="龙腾" value="龙腾"></el-option>
-                    <el-option label="自定义" value="自定义"></el-option>
+                    <el-option label="h5链接" value="h5链接"></el-option>
+                    <el-option label="小程序链接" value="小程序链接"></el-option>
                   </el-select>
                 </el-form-item>
               </el-col>
+              <el-col :span="8" v-if="form.source === '第三方' && form.externalLink === 'h5链接'" :gutter="20">
+                <el-form-item label="h5链接" required>
+                  <el-input v-model="form.customLink" placeholder="请输入h5链接"></el-input>
+                </el-form-item>
+              </el-col>
               <el-col :span="8" v-if="form.source === '第三方'">
-                <el-form-item label="跳转触发页" required>
-                  <el-select v-model="form.externalJumpMethod" placeholder="请选择跳转触发页">
-                    <el-option label="权益列表页" value="权益列表页"></el-option>
-                    <el-option label="权益详情页" value="权益详情页"></el-option>
-                  </el-select>
+                <el-form-item required label-width="140px">
+                  <span slot="label" style="white-space: nowrap;">是否有详情页<el-tooltip content="没有详情页时直接从首页/列表页跳转" placement="top"><i class="el-icon-question tip-icon"></i></el-tooltip></span>
+                  <el-radio-group v-model="form.externalJumpMethod">
+                    <el-radio-button label="没有"></el-radio-button>
+                    <el-radio-button label="有"></el-radio-button>
+                  </el-radio-group>
                 </el-form-item>
               </el-col>
-              <el-col :span="8" v-if="form.source === '第三方' && form.externalLink === '自定义'" :gutter="20">
-                <el-form-item label="自定义链接" required>
-                  <el-input v-model="form.customLink" placeholder="请输入自定义链接"></el-input>
+            </el-row>
+            <el-row :gutter="20" v-if="form.source === '第三方' && form.externalLink === '小程序链接'">
+              <el-col :span="8">
+                <el-form-item label="小程序appid" required>
+                  <el-input v-model="form.miniAppId" placeholder="请输入小程序appid"></el-input>
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="小程序页面地址" required label-width="140px">
+                  <el-input v-model="form.miniAppPath" placeholder="请输入小程序页面地址"></el-input>
                 </el-form-item>
               </el-col>
             </el-row>
@@ -148,52 +113,149 @@
               </el-row>
               <el-row :gutter="20">
                 <el-col :span="8">
-                  <el-form-item label="权益列表图标" required>
+                  <el-form-item required>
+                    <span slot="label">权益图标
+                      <el-tooltip content="展示在权益首页和权益列表页" placement="top">
+                        <i class="el-icon-question tip-icon"></i>
+                      </el-tooltip>
+                    </span>
                     <el-upload
                       action=""
                       :auto-upload="false"
                       :file-list="form.listIcon"
                       :on-change="file => handleUpload(file, 'listIcon')"
+                      :on-preview="handlePreviewImage"
+                      :on-remove="() => form.listIcon = []"
+                      :class="{ 'upload-hide-trigger': form.listIcon.length >= 1 }"
                       :limit="1"
                       list-type="picture-card"
                     >
                       <i class="el-icon-plus"></i>
                     </el-upload>
+                    <div class="upload-size-tip">建议尺寸:80x80px</div>
+                  </el-form-item>
+                </el-col>
+              </el-row>
+            </template>
+
+            <template v-else-if="form.source === '第三方'">
+              <el-row :gutter="20">
+                <el-col :span="8">
+                  <el-form-item label="权益名称" required>
+                    <el-input v-model="form.equityName" maxlength="40" placeholder="请输入权益名称"></el-input>
                   </el-form-item>
                 </el-col>
                 <el-col :span="8">
-                  <el-form-item label="权益主图" required>
+                  <el-form-item label="权益副标题">
+                    <el-input v-model="form.subTitle" maxlength="40" placeholder="请输入权益副标题"></el-input>
+                  </el-form-item>
+                </el-col>
+              </el-row>
+              <el-row :gutter="20" v-if="form.externalJumpMethod === '没有'">
+                <el-col :span="8">
+                  <el-form-item required>
+                    <span slot="label">权益图标
+                      <el-tooltip content="展示在权益首页和权益列表页" placement="top">
+                        <i class="el-icon-question tip-icon"></i>
+                      </el-tooltip>
+                    </span>
                     <el-upload
                       action=""
                       :auto-upload="false"
-                      :file-list="form.mainImage"
-                      :on-change="file => handleUpload(file, 'mainImage')"
+                      :file-list="form.listIcon"
+                      :on-change="file => handleUpload(file, 'listIcon')"
+                      :on-preview="handlePreviewImage"
+                      :on-remove="() => form.listIcon = []"
+                      :class="{ 'upload-hide-trigger': form.listIcon.length >= 1 }"
                       :limit="1"
                       list-type="picture-card"
                     >
                       <i class="el-icon-plus"></i>
                     </el-upload>
+                    <div class="upload-size-tip">建议尺寸:80x80px</div>
                   </el-form-item>
                 </el-col>
               </el-row>
-              <el-row :gutter="20">
-                <el-col :span="12">
-                  <el-form-item label="权益概述" required>
-                    <quill-editor
-                      v-model="form.overview"
-                      :options="quillOptions"
-                    ></quill-editor>
-                  </el-form-item>
-                </el-col>
-                <el-col :span="12">
-                  <el-form-item label="权益详情" required>
-                    <quill-editor
-                      v-model="form.details"
-                      :options="quillOptions"
-                    ></quill-editor>
+              <el-row v-if="form.externalJumpMethod === '有'" :gutter="20">
+                <el-col :span="8">
+                  <el-form-item required>
+                    <span slot="label">权益图标
+                      <el-tooltip content="展示在权益首页和权益列表页" placement="top">
+                        <i class="el-icon-question tip-icon"></i>
+                      </el-tooltip>
+                    </span>
+                    <el-upload
+                      action=""
+                      :auto-upload="false"
+                      :file-list="form.listIcon"
+                      :on-change="file => handleUpload(file, 'listIcon')"
+                      :on-preview="handlePreviewImage"
+                      :on-remove="() => form.listIcon = []"
+                      :class="{ 'upload-hide-trigger': form.listIcon.length >= 1 }"
+                      :limit="1"
+                      list-type="picture-card"
+                    >
+                      <i class="el-icon-plus"></i>
+                    </el-upload>
+                    <div class="upload-size-tip">建议尺寸:80x80px</div>
                   </el-form-item>
                 </el-col>
               </el-row>
+            </template>
+          </el-form>
+        </el-card>
+
+        <!-- 详情信息 -->
+        <el-card v-if="shouldShowDetailCard" shadow="never" class="section-card">
+          <div slot="header" class="section-title">详情信息</div>
+          <el-form :model="form" label-width="120px" label-position="left">
+            <el-row :gutter="20">
+              <el-col :span="8">
+                <el-form-item label="权益主图" required>
+                  <el-upload
+                    action=""
+                    :auto-upload="false"
+                    :file-list="form.mainImage"
+                    :on-change="file => handleUpload(file, 'mainImage')"
+                    :on-preview="handlePreviewImage"
+                    :on-remove="() => form.mainImage = []"
+                    :class="{ 'upload-hide-trigger': form.mainImage.length >= 1 }"
+                    :limit="1"
+                    list-type="picture-card"
+                  >
+                    <i class="el-icon-plus"></i>
+                  </el-upload>
+                  <div class="upload-size-tip">建议尺寸:716x400px</div>
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="权益介绍" required>
+                  <el-input
+                    v-model="form.introduction"
+                    type="textarea"
+                    :rows="4"
+                    maxlength="500"
+                    show-word-limit
+                    placeholder="请输入权益介绍"
+                  ></el-input>
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="权益内容" required>
+                  <quill-editor v-model="form.overview" :options="quillOptions"></quill-editor>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="权益说明" required>
+                  <quill-editor v-model="form.details" :options="quillOptions"></quill-editor>
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <template v-if="form.source === '银行自有' || form.source === '荣数商品'">
               <el-row :gutter="20">
                 <el-col :span="24">
                   <el-form-item>
@@ -203,7 +265,7 @@
                           此模板为权益支付成功,发送给持卡人的短信模板;<br/>
                           开启后需选择短信模板,关闭则不发送短信。<br/>
                           占位符如下:<br/>
-                          商品名称:${productName}<br/>
+                          权益名称:${productName}<br/>
                           结束日期:${expireDate}<br/>
                           卡号:${exchangeNum}<br/>
                           卡密:${exchangePwd}<br/>
@@ -236,91 +298,12 @@
               </el-row>
               <el-row :gutter="20">
                 <el-col :span="8">
-                  <el-form-item label="领取成功按钮文案" required>
+                  <el-form-item label="领取成功按钮" required>
                     <el-input v-model="form.successButtonText" placeholder="请输入按钮文案"></el-input>
                   </el-form-item>
                 </el-col>
               </el-row>
             </template>
-
-            <template v-else-if="form.source === '第三方'">
-              <el-row :gutter="20">
-                <el-col :span="8">
-                  <el-form-item label="权益名称" required>
-                    <el-input v-model="form.equityName" maxlength="40" placeholder="请输入权益名称"></el-input>
-                  </el-form-item>
-                </el-col>
-                <el-col :span="8">
-                  <el-form-item label="权益副标题">
-                    <el-input v-model="form.subTitle" maxlength="40" placeholder="请输入权益副标题"></el-input>
-                  </el-form-item>
-                </el-col>
-              </el-row>
-              <el-row :gutter="20" v-if="form.externalJumpMethod === '权益列表页'">
-                <el-col :span="8">
-                  <el-form-item label="权益列表图标" required>
-                    <el-upload
-                      action=""
-                      :auto-upload="false"
-                      :file-list="form.listIcon"
-                      :on-change="file => handleUpload(file, 'listIcon')"
-                      :limit="1"
-                      list-type="picture-card"
-                    >
-                      <i class="el-icon-plus"></i>
-                    </el-upload>
-                  </el-form-item>
-                </el-col>
-              </el-row>
-              <el-row v-if="form.externalJumpMethod === '权益详情页'" :gutter="20">
-                <el-col :span="8">
-                  <el-form-item label="权益列表图标" required>
-                    <el-upload
-                      action=""
-                      :auto-upload="false"
-                      :file-list="form.listIcon"
-                      :on-change="file => handleUpload(file, 'listIcon')"
-                      :limit="1"
-                      list-type="picture-card"
-                    >
-                      <i class="el-icon-plus"></i>
-                    </el-upload>
-                  </el-form-item>
-                </el-col>
-                <el-col :span="8">
-                  <el-form-item label="权益主图" required>
-                    <el-upload
-                      action=""
-                      :auto-upload="false"
-                      :file-list="form.mainImage"
-                      :on-change="file => handleUpload(file, 'mainImage')"
-                      :limit="1"
-                      list-type="picture-card"
-                    >
-                      <i class="el-icon-plus"></i>
-                    </el-upload>
-                  </el-form-item>
-                </el-col>
-              </el-row>
-              <el-row v-if="form.externalJumpMethod === '权益详情页'" :gutter="20">
-                <el-col :span="12">
-                  <el-form-item label="权益概述" required>
-                    <quill-editor
-                      v-model="form.overview"
-                      :options="quillOptions"
-                    ></quill-editor>
-                  </el-form-item>
-                </el-col>
-                <el-col :span="12">
-                  <el-form-item label="权益详情" required>
-                    <quill-editor
-                      v-model="form.details"
-                      :options="quillOptions"
-                    ></quill-editor>
-                  </el-form-item>
-                </el-col>
-              </el-row>
-            </template>
           </el-form>
         </el-card>
 
@@ -406,19 +389,13 @@
                 <el-switch v-model="form.claimGroupEnabled"></el-switch>
                 <template v-if="form.claimGroupEnabled">
                   <div style="width: 600px;background: #f1f2f5;padding: 20px;margin-top: 10px;">
-                    <el-form-item label="是否潜客领取" label-width="110px" style="margin-bottom: 12px;">
-                      <el-radio-group v-model="form.claimGroupIsPotential">
-                        <el-radio :label="true">是</el-radio>
-                        <el-radio :label="false">否</el-radio>
-                      </el-radio-group>
-                    </el-form-item>
-                    <el-form-item v-if="!form.claimGroupIsPotential" label="客群奖励活动" label-width="110px" style="margin-bottom: 12px;">
-                      <el-select v-model="form.claimGroupTagId" filterable placeholder="请选择客群奖励活动" style="width: 100%">
-                        <el-option label="客群奖励活动A" value="客群奖励活动A"></el-option>
-                        <el-option label="客群奖励活动B" value="客群奖励活动B"></el-option>
-                        <el-option label="客群奖励活动C" value="客群奖励活动C"></el-option>
-                      </el-select>
-                    </el-form-item>
+                    <el-alert
+                      title="需在【权益管理-权益发放】中选择权益发放,未发放的权益移动端不展示"
+                      type="info"
+                      :closable="false"
+                      show-icon
+                      style="margin-bottom: 12px;"
+                    ></el-alert>
                     <div style="font-size: 14px;color: #606266;margin-bottom: 8px;font-weight: 500;">不满足提示</div>
                     <el-form-item label="提示内容" label-width="80px" style="margin-bottom: 12px;">
                       <el-input v-model="form.claimDenyMessage" maxlength="200" placeholder="请输入提示内容"></el-input>
@@ -442,7 +419,7 @@
         <el-card shadow="never" class="section-card">
           <div slot="header" class="section-title">显示页面</div>
           <el-form :model="form" label-width="120px" label-position="left">
-            <el-form-item label="首页">
+            <el-form-item label="权益页/列表页">
               <el-radio-group v-model="form.showOnHome">
                 <el-radio label="显示">显示</el-radio>
                 <el-radio label="不显示">不显示</el-radio>
@@ -532,15 +509,30 @@
         </div>
       </div>
     </el-card>
+
+    <rongshu-product-dialog
+      :visible.sync="rongshuDialogVisible"
+      @select="onRongshuSelect"
+    />
+
+    <el-dialog :visible.sync="previewVisible" title="图片预览" width="640px" append-to-body>
+      <img v-if="previewImageUrl" :src="previewImageUrl" style="display:block; max-width:100%; margin:0 auto;" />
+    </el-dialog>
   </div>
 </template>
 
 <script>
+import RongshuProductDialog from '@/components/RongshuProductDialog.vue'
+
 export default {
   name: 'AddEquityProduct',
+  components: { RongshuProductDialog },
   data() {
     return {
       activeStep: 0,
+      rongshuDialogVisible: false,
+      previewVisible: false,
+      previewImageUrl: '',
       quillOptions: {
         placeholder: '请输入内容...',
         modules: {
@@ -558,12 +550,6 @@ export default {
         { id: 'SMS_TPL_002', name: '券码类短信模板', content: '【银行】尊敬的客户,您的${productName}购买成功,券码为${exchangeNum},请于${expireDate}前使用。' },
         { id: 'SMS_TPL_003', name: '实物类短信模板', content: '【银行】您购买的${productName}已受理,我们将尽快安排发货,感谢您的支持。' }
       ],
-      rongshuProductMap: {
-        '荣数商品A': { equityName: '荣数机场贵宾厅', mainImage: [{ name: 'rongshu-a.png', url: 'https://placehold.co/200x200/4096ff/ffffff?text=A' }] },
-        '荣数商品B': { equityName: '荣数高尔夫体验', mainImage: [{ name: 'rongshu-b.png', url: 'https://placehold.co/200x200/52c41a/ffffff?text=B' }] },
-        '荣数商品C': { equityName: '荣数洗车服务', mainImage: [{ name: 'rongshu-c.png', url: 'https://placehold.co/200x200/faad14/ffffff?text=C' }] },
-        '荣数商品D': { equityName: '荣数体检套餐', mainImage: [{ name: 'rongshu-d.png', url: 'https://placehold.co/200x200/eb2f96/ffffff?text=D' }] }
-      },
       form: {
         source: '银行自有',
         rongshuProduct: '',
@@ -574,15 +560,18 @@ export default {
         shareEnabled: false,
         shareImage: [],
         shareName: '',
-        externalJumpMethod: '权益列表页',
+        externalJumpMethod: '没有',
         externalLink: '太古地产',
         customLink: '',
+        miniAppId: '',
+        miniAppPath: '',
         equityName: '',
         productId: '',
         merchantName: '',
         subTitle: '',
         listIcon: [],
         mainImage: [],
+        introduction: '',
         overview: '',
         details: '',
         smsEnabled: false,
@@ -609,6 +598,14 @@ export default {
       }
     }
   },
+  computed: {
+    shouldShowDetailCard() {
+      const s = this.form.source
+      if (s === '银行自有' || s === '荣数商品') return true
+      if (s === '第三方' && this.form.externalJumpMethod === '有') return true
+      return false
+    }
+  },
   methods: {
     nextStep() {
       if (this.activeStep < 2) {
@@ -621,7 +618,7 @@ export default {
       }
     },
     handleSubmit() {
-      this.$message.success('权益商品已创建')
+      this.$message.success('权益已创建')
       this.$router.push('/customization/hsbc/equity-products')
     },
     goBack() {
@@ -632,9 +629,6 @@ export default {
         if (!this.form.shareName) {
           this.form.shareName = this.form.equityName || ''
         }
-        if (!this.form.shareImage || !this.form.shareImage.length) {
-          this.form.shareImage = this.form.mainImage ? [...this.form.mainImage] : []
-        }
       }
     },
     handleSmsEnabledChange(val) {
@@ -648,15 +642,23 @@ export default {
       this.form.smsTemplate = tpl ? tpl.content : ''
     },
     handleUpload(file, field) {
-      this.form[field] = [file.file]
-      return false
-    },
-    handleRongshuProductChange(value) {
-      const preset = this.rongshuProductMap[value]
-      if (preset) {
-        this.form.equityName = preset.equityName
-        this.form.mainImage = preset.mainImage ? [...preset.mainImage] : []
+      if (!file || !file.raw) return
+      const reader = new FileReader()
+      reader.onload = (e) => {
+        this.form[field] = [{ name: file.name, url: e.target.result, uid: file.uid }]
       }
+      reader.readAsDataURL(file.raw)
+    },
+    handlePreviewImage(file) {
+      this.previewImageUrl = file.url || ''
+      this.previewVisible = !!this.previewImageUrl
+    },
+    onRongshuSelect(product) {
+      this.form.rongshuProduct = product.name
+      this.form.equityName = product.name
+      this.form.merchantName = product.merchant
+      this.form.stockCount = product.stock
+      this.form.mainImage = product.mainImage ? [...product.mainImage] : []
     }
   }
 }
@@ -674,6 +676,8 @@ export default {
   margin-bottom: 16px;
   border: none;
 }
+.upload-hide-trigger >>> .el-upload--picture-card { display: none; }
+
 .section-card >>> .el-card__header {
   padding: 12px 20px;
   background: #fafafa;
@@ -705,6 +709,12 @@ export default {
   cursor: pointer;
   font-size: 14px;
 }
+.upload-size-tip {
+  margin-top: 4px;
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.5;
+}
 .sms-template-preview {
   margin-top: 12px;
   padding: 12px;

+ 167 - 133
pc/src/views/AuditEquityProduct.vue

@@ -2,12 +2,12 @@
   <div class="audit-equity-product">
     <el-card>
       <div slot="header" class="header-title">
-        <span>审核权益商品</span>
+        <span>审核权益</span>
       </div>
 
       <!-- 步骤条 -->
       <el-steps :active="activeStep" simple style="margin-bottom: 20px;">
-        <el-step title="填写商品信息" icon="el-icon-info"></el-step>
+        <el-step title="填写权益信息" icon="el-icon-info"></el-step>
         <el-step title="设置规则" icon="el-icon-setting"></el-step>
         <el-step title="更多设置" icon="el-icon-monitor"></el-step>
       </el-steps>
@@ -20,7 +20,7 @@
           <el-form :model="form" label-width="120px" label-position="left">
             <el-row :gutter="20">
               <el-col :span="8">
-                <el-form-item label="商品来源">
+                <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>
@@ -29,15 +29,14 @@
                 </el-form-item>
               </el-col>
               <el-col :span="8" v-if="form.source === '荣数商品'">
-                <el-form-item label="选择商品">
+                <el-form-item label="选择权益">
                   <el-input :value="form.rongshuProduct" readonly></el-input>
                 </el-form-item>
               </el-col>
               <el-col :span="8" v-if="form.source === '银行自有'">
-                <el-form-item label="商品类型">
+                <el-form-item label="权益类型">
                   <el-radio-group v-model="form.productType" disabled>
                     <el-radio-button label="券码"></el-radio-button>
-                    <el-radio-button label="微信立减金"></el-radio-button>
                   </el-radio-group>
                 </el-form-item>
               </el-col>
@@ -47,39 +46,41 @@
                 </el-form-item>
               </el-col>
             </el-row>
-            <el-row :gutter="20" v-if="form.source === '银行自有' && form.productType === '微信立减金'">
-              <el-col :span="8">
-                <el-form-item label="面额">
-                  <el-input :value="`满 ${form.faceValueThreshold || 0} 减 ${form.faceValueDiscount || 0}`" readonly></el-input>
-                </el-form-item>
-              </el-col>
-              <el-col :span="8">
-                <el-form-item label="库存数">
-                  <el-input :value="form.stockCount" readonly></el-input>
-                </el-form-item>
-              </el-col>
-            </el-row>
             <el-row :gutter="20">
               <el-col :span="8" v-if="form.source === '第三方'">
-                <el-form-item label="外部商品链接">
-                  <el-select v-model="form.externalLink" placeholder="请选择外部商品链接" disabled>
+                <el-form-item label="外部权益链接">
+                  <el-select v-model="form.externalLink" placeholder="请选择外部权益链接" disabled>
                     <el-option label="太古地产" value="太古地产"></el-option>
                     <el-option label="龙腾" value="龙腾"></el-option>
-                    <el-option label="自定义" value="自定义"></el-option>
+                    <el-option label="h5链接" value="h5链接"></el-option>
+                    <el-option label="小程序链接" value="小程序链接"></el-option>
                   </el-select>
                 </el-form-item>
               </el-col>
+              <el-col :span="8" v-if="form.source === '第三方' && form.externalLink === 'h5链接'" :gutter="20">
+                <el-form-item label="h5链接">
+                  <el-input v-model="form.customLink" placeholder="请输入h5链接" readonly></el-input>
+                </el-form-item>
+              </el-col>
               <el-col :span="8" v-if="form.source === '第三方'">
-                <el-form-item label="跳转触发页">
-                  <el-select v-model="form.externalJumpMethod" placeholder="请选择跳转触发页" disabled>
-                    <el-option label="权益列表页" value="权益列表页"></el-option>
-                    <el-option label="权益详情页" value="权益详情页"></el-option>
-                  </el-select>
+                <el-form-item label-width="140px">
+                  <span slot="label" style="white-space: nowrap;">是否有详情页<el-tooltip content="没有详情页时直接从首页/列表页跳转" placement="top"><i class="el-icon-question tip-icon"></i></el-tooltip></span>
+                  <el-radio-group v-model="form.externalJumpMethod" disabled>
+                    <el-radio-button label="没有"></el-radio-button>
+                    <el-radio-button label="有"></el-radio-button>
+                  </el-radio-group>
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20" v-if="form.source === '第三方' && form.externalLink === '小程序链接'">
+              <el-col :span="8">
+                <el-form-item label="小程序appid">
+                  <el-input v-model="form.miniAppId" readonly></el-input>
                 </el-form-item>
               </el-col>
-              <el-col :span="8" v-if="form.source === '第三方' && form.externalLink === '自定义'" :gutter="20">
-                <el-form-item label="自定义链接">
-                  <el-input v-model="form.customLink" placeholder="请输入自定义链接" readonly></el-input>
+              <el-col :span="8">
+                <el-form-item label="小程序页面地址" label-width="140px">
+                  <el-input v-model="form.miniAppPath" readonly></el-input>
                 </el-form-item>
               </el-col>
             </el-row>
@@ -94,7 +95,7 @@
               </el-row>
               <el-row :gutter="20">
                 <el-col :span="8" v-if="form.source === '银行自有'">
-                  <el-form-item label="SKU编号">
+                  <el-form-item label="SIP商品编号">
                     <el-input v-model="form.skuNo" readonly></el-input>
                   </el-form-item>
                 </el-col>
@@ -111,34 +112,107 @@
               </el-row>
               <el-row :gutter="20">
                 <el-col :span="8">
-                  <el-form-item label="权益列表图标">
+                  <el-form-item>
+                    <span slot="label">权益图标
+                      <el-tooltip content="展示在权益首页和权益列表页" placement="top">
+                        <i class="el-icon-question tip-icon"></i>
+                      </el-tooltip>
+                    </span>
                     <div v-if="form.listIcon && form.listIcon.length" class="image-preview">
                       <img :src="form.listIcon[0]" class="preview-img" />
                     </div>
                     <span v-else class="no-data">暂无图片</span>
                   </el-form-item>
                 </el-col>
+              </el-row>
+            </template>
+
+            <template v-else-if="form.source === '第三方'">
+              <el-row :gutter="20">
+                <el-col :span="8">
+                  <el-form-item label="权益名称">
+                    <el-input v-model="form.equityName" readonly></el-input>
+                  </el-form-item>
+                </el-col>
                 <el-col :span="8">
-                  <el-form-item label="权益主图">
-                    <div v-if="form.mainImage && form.mainImage.length" class="image-preview">
-                      <img :src="form.mainImage[0]" class="preview-img" />
-                    </div>
-                    <span v-else class="no-data">暂无图片</span>
+                  <el-form-item label="权益副标题">
+                    <el-input v-model="form.subTitle" readonly></el-input>
                   </el-form-item>
                 </el-col>
               </el-row>
-              <el-row :gutter="20">
-                <el-col :span="12">
-                  <el-form-item label="权益概述">
-                    <div class="rich-text-view" v-html="form.overview || '暂无内容'"></div>
+              <el-row :gutter="20" v-if="form.externalJumpMethod === '没有'">
+                <el-col :span="8">
+                  <el-form-item>
+                    <span slot="label">权益图标
+                      <el-tooltip content="展示在权益首页和权益列表页" placement="top">
+                        <i class="el-icon-question tip-icon"></i>
+                      </el-tooltip>
+                    </span>
+                    <div v-if="form.listIcon && form.listIcon.length" class="image-preview">
+                      <img :src="form.listIcon[0]" class="preview-img" />
+                    </div>
+                    <span v-else class="no-data">暂无图片</span>
                   </el-form-item>
                 </el-col>
-                <el-col :span="12">
-                  <el-form-item label="权益详情">
-                    <div class="rich-text-view" v-html="form.details || '暂无内容'"></div>
+              </el-row>
+              <el-row v-if="form.externalJumpMethod === '有'" :gutter="20">
+                <el-col :span="8">
+                  <el-form-item>
+                    <span slot="label">权益图标
+                      <el-tooltip content="展示在权益首页和权益列表页" placement="top">
+                        <i class="el-icon-question tip-icon"></i>
+                      </el-tooltip>
+                    </span>
+                    <div v-if="form.listIcon && form.listIcon.length" class="image-preview">
+                      <img :src="form.listIcon[0]" class="preview-img" />
+                    </div>
+                    <span v-else class="no-data">暂无图片</span>
                   </el-form-item>
                 </el-col>
               </el-row>
+            </template>
+          </el-form>
+        </el-card>
+
+        <!-- 详情信息 -->
+        <el-card v-if="shouldShowDetailCard" shadow="never" class="section-card">
+          <div slot="header" class="section-title">详情信息</div>
+          <el-form :model="form" label-width="120px" label-position="left">
+            <el-row :gutter="20">
+              <el-col :span="8">
+                <el-form-item label="权益主图">
+                  <div v-if="form.mainImage && form.mainImage.length" class="image-preview">
+                    <img :src="form.mainImage[0]" class="preview-img" />
+                  </div>
+                  <span v-else class="no-data">暂无图片</span>
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="权益介绍">
+                  <el-input
+                    v-model="form.introduction"
+                    type="textarea"
+                    :rows="4"
+                    readonly
+                  ></el-input>
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="权益内容">
+                  <div class="rich-text-view" v-html="form.overview || '暂无内容'"></div>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="权益说明">
+                  <div class="rich-text-view" v-html="form.details || '暂无内容'"></div>
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <template v-if="form.source === '银行自有' || form.source === '荣数商品'">
               <el-row :gutter="20">
                 <el-col :span="24">
                   <el-form-item label="下单成功短信">
@@ -166,67 +240,12 @@
               </el-row>
               <el-row :gutter="20">
                 <el-col :span="8">
-                  <el-form-item label="领取成功按钮文案">
+                  <el-form-item label="领取成功按钮">
                     <el-input :value="form.successButtonText" readonly></el-input>
                   </el-form-item>
                 </el-col>
               </el-row>
             </template>
-
-            <template v-else-if="form.source === '第三方'">
-              <el-row :gutter="20">
-                <el-col :span="8">
-                  <el-form-item label="权益名称">
-                    <el-input v-model="form.equityName" readonly></el-input>
-                  </el-form-item>
-                </el-col>
-                <el-col :span="8">
-                  <el-form-item label="权益副标题">
-                    <el-input v-model="form.subTitle" readonly></el-input>
-                  </el-form-item>
-                </el-col>
-              </el-row>
-              <el-row :gutter="20" v-if="form.externalJumpMethod === '权益列表页'">
-                <el-col :span="8">
-                  <el-form-item label="权益列表图标">
-                    <div v-if="form.listIcon && form.listIcon.length" class="image-preview">
-                      <img :src="form.listIcon[0]" class="preview-img" />
-                    </div>
-                    <span v-else class="no-data">暂无图片</span>
-                  </el-form-item>
-                </el-col>
-              </el-row>
-              <el-row v-if="form.externalJumpMethod === '权益详情页'" :gutter="20">
-                <el-col :span="8">
-                  <el-form-item label="权益列表图标">
-                    <div v-if="form.listIcon && form.listIcon.length" class="image-preview">
-                      <img :src="form.listIcon[0]" class="preview-img" />
-                    </div>
-                    <span v-else class="no-data">暂无图片</span>
-                  </el-form-item>
-                </el-col>
-                <el-col :span="8">
-                  <el-form-item label="权益主图">
-                    <div v-if="form.mainImage && form.mainImage.length" class="image-preview">
-                      <img :src="form.mainImage[0]" class="preview-img" />
-                    </div>
-                    <span v-else class="no-data">暂无图片</span>
-                  </el-form-item>
-                </el-col>
-              </el-row>
-              <el-row v-if="form.externalJumpMethod === '权益详情页'" :gutter="20">
-                <el-col :span="12">
-                  <el-form-item label="权益概述">
-                    <div class="rich-text-view" v-html="form.overview || '暂无内容'"></div>
-                  </el-form-item>
-                </el-col>
-                <el-col :span="12">
-                  <el-form-item label="权益详情">
-                    <div class="rich-text-view" v-html="form.details || '暂无内容'"></div>
-                  </el-form-item>
-                </el-col>
-              </el-row>
-            </template>
           </el-form>
         </el-card>
 
@@ -280,19 +299,13 @@
                 <el-switch v-model="form.claimGroupEnabled" disabled></el-switch>
                 <template v-if="form.claimGroupEnabled">
                   <div style="width: 600px;background: #f1f2f5;padding: 20px;margin-top: 10px;">
-                    <el-form-item label="是否潜客领取" label-width="110px" style="margin-bottom: 12px;">
-                      <el-radio-group v-model="form.claimGroupIsPotential" disabled>
-                        <el-radio :label="true">是</el-radio>
-                        <el-radio :label="false">否</el-radio>
-                      </el-radio-group>
-                    </el-form-item>
-                    <el-form-item v-if="!form.claimGroupIsPotential" label="客群奖励活动" label-width="110px" style="margin-bottom: 12px;">
-                      <el-select v-model="form.claimGroupTagId" disabled placeholder="请选择客群奖励活动" style="width: 100%">
-                        <el-option label="客群奖励活动A" value="客群奖励活动A"></el-option>
-                        <el-option label="客群奖励活动B" value="客群奖励活动B"></el-option>
-                        <el-option label="客群奖励活动C" value="客群奖励活动C"></el-option>
-                      </el-select>
-                    </el-form-item>
+                    <el-alert
+                      title="需在【权益管理-权益发放】中选择权益发放,未发放的权益移动端不展示"
+                      type="info"
+                      :closable="false"
+                      show-icon
+                      style="margin-bottom: 12px;"
+                    ></el-alert>
                     <div style="font-size: 14px;color: #606266;margin-bottom: 8px;font-weight: 500;">不满足提示</div>
                     <el-form-item label="提示内容" label-width="80px" style="margin-bottom: 12px;">
                       <el-input :value="form.claimDenyMessage" readonly></el-input>
@@ -316,7 +329,7 @@
         <el-card shadow="never" class="section-card">
           <div slot="header" class="section-title">显示页面</div>
           <el-form :model="form" label-width="120px" label-position="left">
-            <el-form-item label="首页">
+            <el-form-item label="权益页/列表页">
               <el-radio-group v-model="form.showOnHome" disabled>
                 <el-radio label="显示">显示</el-radio>
                 <el-radio label="不显示">不显示</el-radio>
@@ -409,9 +422,11 @@ export default {
         shareEnabled: false,
         shareImage: [],
         shareName: '',
-        externalJumpMethod: '权益列表页',
+        externalJumpMethod: '没有',
         externalLink: '太古地产',
         customLink: '',
+        miniAppId: '',
+        miniAppPath: '',
         skuNo: '',
         equityName: '',
         productId: '',
@@ -419,6 +434,7 @@ export default {
         subTitle: '',
         listIcon: [],
         mainImage: [],
+        introduction: '',
         overview: '',
         details: '',
         smsEnabled: false,
@@ -445,7 +461,14 @@ export default {
       }
     }
   },
-  computed: {},
+  computed: {
+    shouldShowDetailCard() {
+      const s = this.form.source
+      if (s === '银行自有' || s === '荣数商品') return true
+      if (s === '第三方' && this.form.externalJumpMethod === '有') return true
+      return false
+    }
+  },
   created() {
     this.loadData()
   },
@@ -454,18 +477,20 @@ export default {
       const id = this.$route.params.id
       // TODO: 根据 id 从接口获取数据,这里用 mock 数据示例
       const mockMap = {
-        P001: {
+        100001: {
           source: '银行自有',
           externalJumpMethod: '',
           externalLink: '',
           customLink: '',
+        miniAppId: '',
+        miniAppPath: '',
           skuNo: 'SKU001',
-          equityName: '权益商品A',
+          equityName: '权益A',
           subTitle: '副标题示例',
           listIcon: [],
           mainImage: [],
-          overview: '<p>权益概述内容</p>',
-          details: '<p>权益详情内容</p>',
+          overview: '<p>权益内容内容</p>',
+          details: '<p>权益说明内容</p>',
           smsEnabled: true,
           smsTemplateId: 'SMS_TPL_001',
           smsTemplate: '【银行】您购买的${productName}已支付成功,卡号:${exchangeNum},卡密:${exchangePwd},有效期至${expireDate},详情:${qrCodeDownPaths}',
@@ -484,13 +509,15 @@ export default {
           claimDenyButtonText: '',
           claimDenyButtonLink: ''
         },
-        P002: {
+        100002: {
           source: '第三方',
-          externalJumpMethod: '权益列表页',
+          externalJumpMethod: '没有',
           externalLink: '太古地产',
           customLink: '',
+        miniAppId: '',
+        miniAppPath: '',
           skuNo: 'SKU002',
-          equityName: '权益商品B',
+          equityName: '权益B',
           subTitle: '外部列表页副标题',
           listIcon: [],
           mainImage: [],
@@ -515,18 +542,20 @@ export default {
           claimDenyButtonText: '了解更多',
           claimDenyButtonLink: 'https://example.com'
         },
-        P003: {
+        100003: {
           source: '第三方',
-          externalJumpMethod: '权益详情页',
+          externalJumpMethod: '',
           externalLink: '龙腾',
           customLink: '',
+        miniAppId: '',
+        miniAppPath: '',
           skuNo: 'SKU003',
-          equityName: '权益商品C',
+          equityName: '权益C',
           subTitle: '外部详情页副标题',
           listIcon: [],
           mainImage: [],
-          overview: '<p>详情页权益概述</p>',
-          details: '<p>详情页权益详情</p>',
+          overview: '<p>详情页权益内容</p>',
+          details: '<p>详情页权益说明</p>',
           smsEnabled: false,
           smsTemplateId: '',
           smsTemplate: '',
@@ -545,18 +574,18 @@ export default {
           claimDenyButtonText: '',
           claimDenyButtonLink: ''
         },
-        P004: {
+        100004: {
           source: '第三方',
-          externalJumpMethod: '权益详情页',
-          externalLink: '自定义',
+          externalJumpMethod: '',
+          externalLink: 'h5链接',
           customLink: 'https://custom-link.com',
           skuNo: 'SKU004',
-          equityName: '权益商品D',
+          equityName: '权益D',
           subTitle: '自定义链接副标题',
           listIcon: [],
           mainImage: [],
-          overview: '<p>自定义链接权益概述</p>',
-          details: '<p>自定义链接权益详情</p>',
+          overview: '<p>自定义链接权益内容</p>',
+          details: '<p>自定义链接权益说明</p>',
           smsEnabled: false,
           smsTemplateId: '',
           smsTemplate: '',
@@ -625,6 +654,11 @@ export default {
 .audit-equity-product {
   padding: 20px;
 }
+.tip-icon {
+  color: #909399;
+  cursor: pointer;
+  font-size: 14px;
+}
 
 .header-title {
   font-size: 18px;

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

@@ -49,6 +49,19 @@
                 <el-button slot="reference" type="text" size="small">调整显示顺序</el-button>
               </el-popover>
               <el-button type="text" size="small" @click="openShowPagesDialog(scope.row)">设置显示页面</el-button>
+              <el-popover
+                placement="bottom"
+                trigger="click"
+                :ref="`morePopover-${scope.$index}`"
+              >
+                <div class="more-actions-menu">
+                  <div
+                    class="more-actions-item"
+                    @click="openLinkDialogFromMore(scope.row, scope.$index)"
+                  >生成链接</div>
+                </div>
+                <el-button slot="reference" type="text" size="small" icon="el-icon-more"></el-button>
+              </el-popover>
             </div>
           </template>
         </el-table-column>
@@ -84,15 +97,23 @@
       :row="showPagesRow"
       @confirm="onShowPagesConfirm"
     ></show-pages-dialog>
+
+    <equity-link-dialog
+      :visible.sync="linkDialogVisible"
+      :id="linkTargetId"
+      path-name="equity/credit"
+      title="生成信用卡权益链接"
+    ></equity-link-dialog>
   </div>
 </template>
 
 <script>
 import ShowPagesDialog from '@/components/ShowPagesDialog.vue'
+import EquityLinkDialog from '@/components/EquityLinkDialog.vue'
 
 export default {
   name: 'CreditCardEquity',
-  components: { ShowPagesDialog },
+  components: { ShowPagesDialog, EquityLinkDialog },
   data() {
     return {
       searchForm: {
@@ -100,6 +121,8 @@ export default {
       },
       showPagesDialogVisible: false,
       showPagesRow: null,
+      linkDialogVisible: false,
+      linkTargetId: '',
       adjustOrderPosition: 1,
       tableData: [
         {
@@ -139,7 +162,7 @@ export default {
     visiblePages() {
       return row => {
         const pages = []
-        if (row.showOnHome === '显示') pages.push('首页')
+        if (row.showOnHome === '显示') pages.push("权益首页/列表页")
         if (row.showOnAssembly === '显示' && Array.isArray(row.assemblyPages)) {
           pages.push(...row.assemblyPages)
         }
@@ -166,6 +189,17 @@ export default {
       this.showPagesRow = row
       this.showPagesDialogVisible = true
     },
+    showLinkDialog(row) {
+      this.linkTargetId = row.productId
+      this.linkDialogVisible = true
+    },
+    openLinkDialogFromMore(row, index) {
+      const moreRef = this.getPopoverInstance(`morePopover-${index}`)
+      if (moreRef) {
+        moreRef.doClose()
+      }
+      this.showLinkDialog(row)
+    },
     onShowPagesConfirm(values) {
       if (this.showPagesRow) {
         this.$set(this.showPagesRow, 'showOnHome', values.showOnHome)

+ 0 - 22
pc/src/views/CrowdManagement.vue

@@ -1,22 +0,0 @@
-<template>
-  <div class="crowd-management">
-    <el-card>
-      <div slot="header">
-        <span>人群管理</span>
-      </div>
-      <el-empty description="人群管理(开发中)"></el-empty>
-    </el-card>
-  </div>
-</template>
-
-<script>
-export default {
-  name: 'CrowdManagement'
-}
-</script>
-
-<style scoped>
-.crowd-management {
-  padding: 0;
-}
-</style>

+ 170 - 0
pc/src/views/CrowdNameList.vue

@@ -0,0 +1,170 @@
+<template>
+  <div class="crowd-name-list">
+    <el-card>
+      <div slot="header" class="search-section">
+        <div class="search-row">
+          <el-form :model="searchForm" label-width="90px" inline>
+            <el-form-item label="所属银行:" required>
+              <el-select v-model="searchForm.bank" placeholder="请选择" style="width: 160px" disabled>
+                <el-option label="汇丰银行" value="hsbc" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="客群编号:">
+              <el-input v-model="searchForm.id" placeholder="请输入" clearable style="width: 180px" />
+            </el-form-item>
+            <el-form-item label="客群名称:">
+              <el-input v-model="searchForm.name" placeholder="请输入" clearable style="width: 180px" />
+            </el-form-item>
+          </el-form>
+        </div>
+        <div class="action-row">
+          <el-button type="primary" @click="handleSearch">查询</el-button>
+          <el-button type="primary" icon="el-icon-plus" @click="handleCreate">新增客群</el-button>
+          <el-button type="primary" icon="el-icon-plus" @click="adjustVisible = true">人员调整</el-button>
+          <el-button type="primary" icon="el-icon-plus" @click="uploadVisible = true">上传调整文件</el-button>
+        </div>
+      </div>
+
+      <el-table :data="pagedData" stripe border style="width: 100%">
+        <el-table-column prop="id" label="客群编号" width="170" />
+        <el-table-column prop="name" label="客群名称" min-width="140" show-overflow-tooltip />
+        <el-table-column label="状态" width="100" align="center">
+          <template slot-scope="scope">
+            <el-tag :type="statusTagType(scope.row.status)" size="small">
+              {{ statusLabel(scope.row.status) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="人数" width="80" align="center">
+          <template slot-scope="scope">{{ (scope.row.members || []).length }}</template>
+        </el-table-column>
+        <el-table-column prop="creator" label="创建人" width="100" />
+        <el-table-column label="审核人" width="100">
+          <template slot-scope="scope">{{ scope.row.reviewer || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="createdAt" label="客群创建时间" width="170" />
+        <el-table-column prop="updatedAt" label="更新时间" width="170" />
+        <el-table-column label="操作" width="160" align="center" fixed="right">
+          <template slot-scope="scope">
+            <el-button
+              v-if="scope.row.status === 'draft'"
+              type="success" circle size="mini" icon="el-icon-check" title="启用"
+              @click="handleActivate(scope.row)"
+            />
+            <el-button
+              v-if="scope.row.status !== 'stopped'"
+              type="primary" circle size="mini" icon="el-icon-edit" title="编辑"
+              @click="handleEdit(scope.row)"
+            />
+            <el-button
+              v-if="scope.row.status !== 'stopped'"
+              type="warning" circle size="mini" icon="el-icon-close" title="停止"
+              @click="handleStop(scope.row)"
+            />
+            <span v-if="scope.row.status === 'stopped'" class="muted">无可执行操作</span>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="pagination-section">
+        <span class="total">共 {{ filteredData.length }} 条</span>
+        <el-pagination
+          layout="prev, pager, next, sizes, jumper"
+          :current-page.sync="page.current"
+          :page-size.sync="page.size"
+          :page-sizes="[10, 20, 50]"
+          :total="filteredData.length"
+        />
+      </div>
+    </el-card>
+
+    <adjust-member-dialog :visible.sync="adjustVisible" />
+    <upload-adjust-dialog :visible.sync="uploadVisible" />
+  </div>
+</template>
+
+<script>
+import crowdNameListStore from '@/store/crowdNameListStore'
+import AdjustMemberDialog from '@/components/crowdNameList/AdjustMemberDialog.vue'
+import UploadAdjustDialog from '@/components/crowdNameList/UploadAdjustDialog.vue'
+
+export default {
+  name: 'CrowdNameList',
+  components: { AdjustMemberDialog, UploadAdjustDialog },
+  data() {
+    return {
+      searchForm: { bank: 'hsbc', id: '', name: '' },
+      page: { current: 1, size: 10 },
+      adjustVisible: false,
+      uploadVisible: false
+    }
+  },
+  computed: {
+    filteredData() {
+      return crowdNameListStore.query({ id: this.searchForm.id, name: this.searchForm.name })
+    },
+    pagedData() {
+      const start = (this.page.current - 1) * this.page.size
+      return this.filteredData.slice(start, start + this.page.size)
+    }
+  },
+  methods: {
+    statusLabel(s) {
+      return { draft: '新建', active: '已生效', stopped: '已停止' }[s] || s
+    },
+    statusTagType(s) {
+      return { draft: 'info', active: 'success', stopped: 'danger' }[s] || ''
+    },
+    handleSearch() {
+      this.page.current = 1
+    },
+    handleCreate() {
+      this.$router.push('/customization/hsbc/crowd-name-lists/add')
+    },
+    handleEdit(row) {
+      this.$router.push(`/customization/hsbc/crowd-name-lists/edit/${row.id}`)
+    },
+    handleActivate(row) {
+      this.$confirm(`启用名单「${row.name}」需通过审核后生效,确认提交?`, '启用', {
+        confirmButtonText: '提交',
+        cancelButtonText: '取消',
+        type: 'info'
+      }).then(() => {
+        crowdNameListStore.activate(row.id)
+        this.$message.success('已生效(demo 默认通过审核)')
+      }).catch(() => {})
+    },
+    handleStop(row) {
+      this.$confirm(`停止后名单不可再调整,确认停止「${row.name}」?`, '停止', {
+        confirmButtonText: '停止',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        crowdNameListStore.stop(row.id)
+        this.$message.success('已停止')
+      }).catch(() => {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.crowd-name-list { padding: 0; }
+
+.search-section { display: flex; flex-direction: column; }
+.search-row .el-form { display: flex; flex-wrap: wrap; gap: 8px 0; }
+.search-row .el-form-item { margin-right: 8px; margin-bottom: 8px; }
+
+.action-row { margin-top: 8px; display: flex; gap: 8px; }
+
+.muted { color: #c0c4cc; font-size: 12px; }
+
+.pagination-section {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  gap: 20px;
+  margin-top: 20px;
+}
+.pagination-section .total { color: #606266; font-size: 13px; }
+</style>

+ 207 - 0
pc/src/views/CrowdNameListEdit.vue

@@ -0,0 +1,207 @@
+<template>
+  <div class="crowd-name-list-edit">
+    <el-card>
+      <div slot="header" class="header">
+        <span class="title">{{ pageTitle }}</span>
+      </div>
+
+      <el-steps v-if="!isEdit" :active="step" finish-status="success" align-center class="steps">
+        <el-step title="基本信息" icon="el-icon-edit-outline" />
+        <el-step title="初始人员(可选)" icon="el-icon-user" />
+      </el-steps>
+
+      <!-- Step 1: 基本信息 -->
+      <div v-show="step === 0" class="section">
+        <div class="section-title">基本信息</div>
+        <el-form
+          ref="baseForm"
+          :model="form"
+          :rules="rules"
+          label-width="120px"
+          style="max-width: 1000px; margin: 0 auto"
+        >
+          <el-row :gutter="32">
+            <el-col :span="12">
+              <el-form-item label="客群名称" prop="name">
+                <el-input v-model="form.name" placeholder="请输入" maxlength="40" show-word-limit />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="客群编号">
+                <el-input v-model="form.id" disabled suffix-icon="el-icon-circle-close" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="24">
+              <el-form-item label="客群描述" prop="description">
+                <el-input
+                  v-model="form.description"
+                  type="textarea"
+                  :rows="4"
+                  placeholder="请输入"
+                  maxlength="200"
+                  show-word-limit
+                />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-form>
+      </div>
+
+      <!-- Step 2: 初始人员(仅新增可见) -->
+      <div v-show="step === 1 && !isEdit" class="section">
+        <div class="section-title">初始人员(可选)</div>
+        <div class="hint">
+          每行一个客户号,留空则创建一个空名单。后续可通过列表页"人员调整"或"上传调整文件"补充人员。
+        </div>
+        <el-input
+          v-model="form.initialMembersText"
+          type="textarea"
+          :rows="8"
+          placeholder="示例:&#10;00001&#10;00002&#10;00003"
+          style="max-width: 480px"
+        />
+        <div class="member-stats" v-if="parsedMembers.length > 0">
+          已识别 <b>{{ parsedMembers.length }}</b> 个有效客户号
+        </div>
+      </div>
+
+      <div class="footer">
+        <el-button v-if="step === 0" @click="handleCancel">取消</el-button>
+        <el-button v-if="step === 1" @click="step = 0">上一步</el-button>
+        <el-button v-if="!isEdit && step === 0" type="primary" @click="handleNext">下一步</el-button>
+        <el-button v-if="isEdit && step === 0" type="primary" @click="handleSave">保存</el-button>
+        <el-button v-if="step === 1" type="primary" @click="handleSave">完成</el-button>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import crowdNameListStore from '@/store/crowdNameListStore'
+
+const emptyForm = () => ({
+  id: '',
+  name: '',
+  description: '',
+  initialMembersText: ''
+})
+
+export default {
+  name: 'CrowdNameListEdit',
+  data() {
+    return {
+      step: 0,
+      form: emptyForm(),
+      current: null,
+      rules: {
+        name: [{ required: true, message: '请输入客群名称', trigger: 'blur' }],
+        description: [{ required: true, message: '请输入客群描述', trigger: 'blur' }]
+      }
+    }
+  },
+  computed: {
+    isEdit() {
+      return !!this.$route.params.id
+    },
+    pageTitle() {
+      return this.isEdit ? '编辑客群' : '新增客群'
+    },
+    parsedMembers() {
+      const text = this.form.initialMembersText || ''
+      const seen = new Set()
+      const out = []
+      text.split(/\r?\n/).forEach((l) => {
+        const v = l.trim()
+        if (v && !seen.has(v)) {
+          seen.add(v)
+          out.push(v)
+        }
+      })
+      return out
+    }
+  },
+  created() {
+    if (this.isEdit) {
+      const target = crowdNameListStore.getById(this.$route.params.id)
+      if (!target) {
+        this.$message.error('客群不存在')
+        this.$router.replace('/customization/hsbc/crowd-name-lists')
+        return
+      }
+      if (target.status === 'stopped') {
+        this.$message.warning('已停止名单不可编辑')
+        this.$router.replace('/customization/hsbc/crowd-name-lists')
+        return
+      }
+      this.current = target
+      this.form = {
+        id: target.id,
+        name: target.name,
+        description: target.description,
+        initialMembersText: ''
+      }
+    } else {
+      this.form.id = '保存后系统自动生成'
+    }
+  },
+  methods: {
+    handleNext() {
+      this.$refs.baseForm.validate((ok) => {
+        if (ok) this.step = 1
+      })
+    },
+    handleSave() {
+      if (this.step === 0 && !this.isEdit) {
+        // 不应直接到保存(按钮控制只在 step 1 出现),兜底
+        return
+      }
+      // 编辑路径直接走基本信息保存
+      if (this.isEdit) {
+        this.$refs.baseForm.validate((ok) => {
+          if (!ok) return
+          crowdNameListStore.update(this.current.id, {
+            name: this.form.name,
+            description: this.form.description
+          })
+          this.$message.success('保存成功')
+          this.$router.push('/customization/hsbc/crowd-name-lists')
+        })
+        return
+      }
+      // 新增路径
+      crowdNameListStore.create({
+        name: this.form.name,
+        description: this.form.description,
+        members: this.parsedMembers
+      })
+      this.$message.success(this.parsedMembers.length > 0
+        ? `创建成功,初始人员 ${this.parsedMembers.length} 人`
+        : '创建成功')
+      this.$router.push('/customization/hsbc/crowd-name-lists')
+    },
+    handleCancel() {
+      this.$router.push('/customization/hsbc/crowd-name-lists')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.crowd-name-list-edit { padding: 0; }
+.header { display: flex; align-items: center; justify-content: center; }
+.title { font-size: 16px; font-weight: 600; color: #409EFF; }
+
+.steps { margin: 16px 0 32px; }
+
+.section { margin-bottom: 24px; max-width: 1000px; margin-left: auto; margin-right: auto; }
+.section-title {
+  font-size: 14px; font-weight: 600; color: #303133;
+  background: #f5f7fa; padding: 10px 14px; border-radius: 4px;
+  margin-bottom: 16px;
+}
+.hint { color: #909399; font-size: 12px; margin-bottom: 8px; }
+.member-stats { margin-top: 8px; color: #67C23A; font-size: 13px; }
+
+.footer { text-align: center; margin-top: 24px; padding-top: 16px; border-top: 1px solid #ebeef5; }
+.footer .el-button { min-width: 100px; }
+</style>

+ 328 - 0
pc/src/views/CrowdTaskEdit.vue

@@ -0,0 +1,328 @@
+<template>
+  <div class="crowd-task-edit">
+    <el-card>
+      <div slot="header" class="header">
+        <span class="title">{{ pageTitle }}</span>
+      </div>
+
+      <el-steps :active="step" finish-status="success" align-center class="steps">
+        <el-step title="基本信息" icon="el-icon-edit-outline" />
+        <el-step title="规则组/奖励信息" icon="el-icon-set-up" />
+      </el-steps>
+
+      <!-- Step 1: 基本信息 -->
+      <div v-show="step === 0" class="section">
+        <el-form
+          ref="baseForm"
+          :model="form"
+          :rules="rules"
+          label-width="140px"
+          style="max-width: 1000px; margin: 0 auto"
+        >
+          <el-row :gutter="32">
+            <el-col :span="12">
+              <el-form-item label="客群名称" prop="name">
+                <el-input v-model="form.name" placeholder="请输入" maxlength="40" show-word-limit />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="客群编号" prop="id">
+                <el-input v-model="form.id" disabled placeholder="系统自动生成" suffix-icon="el-icon-circle-close" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="客群描述" prop="description">
+                <el-input
+                  v-model="form.description"
+                  type="textarea"
+                  :rows="4"
+                  placeholder="请输入"
+                  maxlength="200"
+                  show-word-limit
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="客群类型" prop="type">
+                <el-select v-model="form.type" placeholder="请选择" style="width: 100%">
+                  <el-option label="展示规则" value="display" />
+                  <el-option label="奖励规则" value="reward" />
+                </el-select>
+              </el-form-item>
+              <el-form-item label="圈客周期" prop="cycle">
+                <el-radio-group v-model="form.cycle" @change="onCycleChange">
+                  <el-radio label="once">永久一次</el-radio>
+                  <el-radio label="daily">每日一次</el-radio>
+                  <el-radio label="monthly">每月指定日</el-radio>
+                  <el-radio label="weekly">每周指定日</el-radio>
+                </el-radio-group>
+                <div v-if="form.cycle === 'monthly'" style="margin-top: 8px">
+                  <el-select v-model="form.cycleConfig.day" placeholder="请选择" style="width: 160px">
+                    <el-option
+                      v-for="d in 31"
+                      :key="d"
+                      :label="`每月${d}号`"
+                      :value="d"
+                    />
+                  </el-select>
+                </div>
+                <div v-if="form.cycle === 'weekly'" style="margin-top: 8px">
+                  <el-select v-model="form.cycleConfig.weekday" placeholder="请选择" style="width: 160px">
+                    <el-option label="每周一" :value="1" />
+                    <el-option label="每周二" :value="2" />
+                    <el-option label="每周三" :value="3" />
+                    <el-option label="每周四" :value="4" />
+                    <el-option label="每周五" :value="5" />
+                    <el-option label="每周六" :value="6" />
+                    <el-option label="每周日" :value="7" />
+                  </el-select>
+                </div>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="客群任务开始日期" prop="startDate">
+                <el-date-picker
+                  v-model="form.startDate"
+                  type="date"
+                  placeholder="请选择日期"
+                  value-format="yyyy-MM-dd"
+                  style="width: 100%"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="客群任务结束日期" prop="endDate">
+                <el-date-picker
+                  v-model="form.endDate"
+                  type="date"
+                  placeholder="请选择日期"
+                  value-format="yyyy-MM-dd"
+                  :picker-options="endDateOptions"
+                  style="width: 100%"
+                />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-form>
+      </div>
+
+      <!-- Step 2: 规则组信息 -->
+      <div v-show="step === 1" class="section">
+        <div class="section-title">规则组配置</div>
+        <el-form label-width="140px" style="max-width: 1000px; margin: 0 auto 16px">
+          <el-form-item label="规则组间运算逻辑" required>
+            <el-select v-model="form.ruleGroupLogic" placeholder="请选择" style="width: 320px">
+              <el-option label="and/且" value="AND" />
+              <el-option label="or/或" value="OR" />
+            </el-select>
+          </el-form-item>
+        </el-form>
+
+        <el-table :data="form.ruleGroups" border style="width: 100%" empty-text="暂无规则组,请点击下方加号新增">
+          <el-table-column prop="id" label="规则组编号" width="170" align="center" />
+          <el-table-column prop="name" label="规则组名称" min-width="160" show-overflow-tooltip />
+          <el-table-column prop="description" label="规则组描述" min-width="200" show-overflow-tooltip />
+          <el-table-column label="规则组内逻辑" width="120" align="center">
+            <template slot-scope="scope">{{ scope.row.logic === 'AND' ? 'and/且' : 'or/或' }}</template>
+          </el-table-column>
+          <el-table-column prop="ruleCount" label="规则个数" width="90" align="center" />
+          <el-table-column label="操作" width="90" align="center">
+            <template slot-scope="scope">
+              <el-button
+                type="danger" circle size="mini" icon="el-icon-delete" title="删除"
+                @click="removeRuleGroup(scope.$index)"
+              />
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <div class="add-row">
+          <el-button type="primary" circle icon="el-icon-plus" @click="openAddDialog" />
+        </div>
+      </div>
+
+      <div class="footer">
+        <el-button v-if="step === 0" @click="handleCancel">取消</el-button>
+        <el-button v-if="step === 1" @click="step = 0">上一步</el-button>
+        <el-button v-if="step === 0" type="primary" @click="handleNext">下一步</el-button>
+        <el-button v-if="step === 1" type="primary" @click="handleSave">完成</el-button>
+      </div>
+    </el-card>
+
+    <add-rule-group-dialog
+      :visible.sync="dialogVisible"
+      :exclude-ids="selectedRuleGroupIds"
+      @save="onRuleGroupAdded"
+    />
+  </div>
+</template>
+
+<script>
+import crowdTaskStore from '@/store/crowdTaskStore'
+import AddRuleGroupDialog from '@/components/crowdTask/AddRuleGroupDialog.vue'
+
+const emptyForm = () => ({
+  name: '',
+  id: '',
+  description: '',
+  type: '',
+  cycle: 'once',
+  cycleConfig: { day: 1, weekday: 1 },
+  startDate: '',
+  endDate: '',
+  ruleGroupLogic: '',
+  ruleGroups: []
+})
+
+export default {
+  name: 'CrowdTaskEdit',
+  components: { AddRuleGroupDialog },
+  data() {
+    return {
+      step: 0,
+      form: emptyForm(),
+      current: null,
+      dialogVisible: false,
+      rules: {
+        name: [{ required: true, message: '请输入客群名称', trigger: 'blur' }],
+        description: [{ required: true, message: '请输入客群描述', trigger: 'blur' }],
+        type: [{ required: true, message: '请选择客群类型', trigger: 'change' }],
+        cycle: [{ required: true, message: '请选择圈客周期', trigger: 'change' }],
+        startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }],
+        endDate: [{ required: true, message: '请选择结束日期', trigger: 'change' }]
+      }
+    }
+  },
+  computed: {
+    isEdit() {
+      return this.$route.name === 'CrowdTaskEdit'
+    },
+    isCopy() {
+      return this.$route.name === 'CrowdTaskCopy'
+    },
+    pageTitle() {
+      if (this.isEdit) return '编辑客群规则任务'
+      if (this.isCopy) return '复制客群规则任务'
+      return '新增客群规则任务'
+    },
+    selectedRuleGroupIds() {
+      return this.form.ruleGroups.map((r) => r.id)
+    },
+    endDateOptions() {
+      const start = this.form.startDate
+      return {
+        disabledDate(d) {
+          if (!start) return false
+          return d.getTime() < new Date(start).getTime()
+        }
+      }
+    }
+  },
+  created() {
+    if (this.isEdit || this.isCopy) {
+      const source = crowdTaskStore.getById(this.$route.params.id)
+      if (!source) {
+        this.$message.error('任务不存在')
+        this.$router.replace('/customization/hsbc/crowd-tasks')
+        return
+      }
+      this.current = source
+      this.form = {
+        name: this.isCopy ? source.name + '-副本' : source.name,
+        id: this.isCopy ? '复制保存后生成' : source.id,
+        description: source.description,
+        type: source.type,
+        cycle: source.cycle,
+        cycleConfig: source.cycleConfig ? { ...source.cycleConfig } : { day: 1, weekday: 1 },
+        startDate: source.startDate,
+        endDate: source.endDate,
+        ruleGroupLogic: source.ruleGroupLogic,
+        ruleGroups: JSON.parse(JSON.stringify(source.ruleGroups || []))
+      }
+    } else {
+      this.form.id = '保存后系统自动生成'
+    }
+  },
+  methods: {
+    onCycleChange(v) {
+      if (v === 'monthly' && !this.form.cycleConfig.day) this.form.cycleConfig.day = 1
+      if (v === 'weekly' && !this.form.cycleConfig.weekday) this.form.cycleConfig.weekday = 1
+    },
+    handleNext() {
+      this.$refs.baseForm.validate((ok) => {
+        if (!ok) return
+        if (this.form.startDate && this.form.endDate && this.form.endDate < this.form.startDate) {
+          this.$message.error('结束日期不能早于开始日期')
+          return
+        }
+        this.step = 1
+      })
+    },
+    openAddDialog() {
+      this.dialogVisible = true
+    },
+    onRuleGroupAdded(rg) {
+      this.form.ruleGroups.push(rg)
+      this.$message.success('已添加规则组')
+    },
+    removeRuleGroup(idx) {
+      this.$confirm('确定移除该规则组吗?', '提示', {
+        confirmButtonText: '移除', cancelButtonText: '取消', type: 'warning'
+      }).then(() => {
+        this.form.ruleGroups.splice(idx, 1)
+      }).catch(() => {})
+    },
+    handleSave() {
+      if (!this.form.ruleGroupLogic) {
+        this.$message.error('请选择规则组间运算逻辑')
+        return
+      }
+      if (this.form.ruleGroups.length === 0) {
+        this.$message.error('请至少添加一条规则组')
+        return
+      }
+      const payload = { ...this.form }
+      if (payload.cycle !== 'monthly' && payload.cycle !== 'weekly') {
+        payload.cycleConfig = null
+      } else if (payload.cycle === 'monthly') {
+        payload.cycleConfig = { day: this.form.cycleConfig.day }
+      } else {
+        payload.cycleConfig = { weekday: this.form.cycleConfig.weekday }
+      }
+      if (this.isEdit) {
+        // 校验状态下的编辑需回退到新建并重置运行信息
+        const resetRun = this.current.status === 'verifying'
+        crowdTaskStore.update(this.current.id, payload, { resetRun })
+        this.$message.success(resetRun ? '保存成功,任务回退至新建状态,需重新运行' : '保存成功')
+      } else {
+        crowdTaskStore.create(payload)
+        this.$message.success(this.isCopy ? '复制成功' : '创建成功')
+      }
+      this.$router.push('/customization/hsbc/crowd-tasks')
+    },
+    handleCancel() {
+      this.$router.push('/customization/hsbc/crowd-tasks')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.crowd-task-edit { padding: 0; }
+.header { display: flex; align-items: center; justify-content: center; }
+.title { font-size: 16px; font-weight: 600; color: #409EFF; }
+
+.steps { margin: 16px 0 32px; }
+
+.section { margin-bottom: 24px; }
+.section-title {
+  font-size: 14px; font-weight: 600; color: #303133;
+  background: #f5f7fa; padding: 10px 14px; border-radius: 4px;
+  margin-bottom: 16px;
+}
+
+.add-row { display: flex; padding: 16px 0; }
+
+.footer { text-align: center; margin-top: 24px; padding-top: 16px; border-top: 1px solid #ebeef5; }
+.footer .el-button { min-width: 100px; }
+</style>

+ 218 - 0
pc/src/views/CrowdTaskList.vue

@@ -0,0 +1,218 @@
+<template>
+  <div class="crowd-task-list">
+    <el-card>
+      <div slot="header" class="search-section">
+        <div class="search-row">
+          <el-form :model="searchForm" label-width="90px" inline>
+            <el-form-item label="所属银行:" required>
+              <el-select v-model="searchForm.bank" placeholder="请选择" style="width: 160px" disabled>
+                <el-option label="汇丰银行" value="hsbc" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="客群编号:">
+              <el-input v-model="searchForm.id" placeholder="请输入" clearable style="width: 180px" />
+            </el-form-item>
+            <el-form-item label="客群名称:">
+              <el-input v-model="searchForm.name" placeholder="请输入" clearable style="width: 180px" />
+            </el-form-item>
+          </el-form>
+        </div>
+        <div class="action-row">
+          <el-button type="primary" @click="handleSearch">查询</el-button>
+          <el-button type="primary" icon="el-icon-plus" @click="handleCreate">新增任务</el-button>
+          <el-button type="primary" icon="el-icon-plus" plain @click="handleCopy">复制任务</el-button>
+        </div>
+      </div>
+
+      <el-table
+        :data="pagedData"
+        stripe border highlight-current-row
+        style="width: 100%"
+        @current-change="onRowSelect"
+      >
+        <el-table-column prop="id" label="客群编号" width="160" />
+        <el-table-column prop="name" label="客群名称" min-width="160" show-overflow-tooltip />
+        <el-table-column label="客群类型" width="110" align="center">
+          <template slot-scope="scope">{{ typeLabel(scope.row.type) }}</template>
+        </el-table-column>
+        <el-table-column label="状态" width="100" align="center">
+          <template slot-scope="scope">
+            <el-tag :type="statusTagType(scope.row.status)" size="small">
+              {{ statusLabel(scope.row.status) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="creator" label="创建人" width="100" />
+        <el-table-column label="审核人" width="100">
+          <template slot-scope="scope">{{ scope.row.reviewer || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="createdAt" label="任务创建时间" width="170" />
+        <el-table-column label="任务开始/结束日期" width="190" align="center">
+          <template slot-scope="scope">
+            {{ scope.row.startDate }} / {{ scope.row.endDate }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="updatedAt" label="更新时间" width="170" />
+        <el-table-column label="操作" width="180" fixed="right" align="center">
+          <template slot-scope="scope">
+            <el-button
+              v-if="canDo(scope.row, 'edit')"
+              type="primary" circle size="mini" icon="el-icon-edit" title="编辑"
+              @click.stop="handleEdit(scope.row)"
+            />
+            <el-button
+              v-if="canDo(scope.row, 'run')"
+              type="success" circle size="mini" icon="el-icon-video-play" title="运行"
+              @click.stop="handleRun(scope.row)"
+            />
+            <el-button
+              v-if="canDo(scope.row, 'publish')"
+              type="success" circle size="mini" icon="el-icon-upload2" title="上线"
+              @click.stop="handlePublish(scope.row)"
+            />
+            <el-button
+              v-if="canDo(scope.row, 'stop')"
+              type="warning" circle size="mini" icon="el-icon-close" title="停止"
+              @click.stop="handleStop(scope.row)"
+            />
+            <el-button
+              v-if="canDo(scope.row, 'detail')"
+              type="info" circle size="mini" icon="el-icon-view" title="详情"
+              @click.stop="handleDetail(scope.row)"
+            />
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="pagination-section">
+        <span class="total">共 {{ filteredData.length }} 条</span>
+        <el-pagination
+          layout="prev, pager, next, sizes, jumper"
+          :current-page.sync="page.current"
+          :page-size.sync="page.size"
+          :page-sizes="[10, 20, 50]"
+          :total="filteredData.length"
+        />
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import crowdTaskStore from '@/store/crowdTaskStore'
+
+export default {
+  name: 'CrowdTaskList',
+  data() {
+    return {
+      searchForm: { bank: 'hsbc', id: '', name: '' },
+      page: { current: 1, size: 10 },
+      selectedRow: null
+    }
+  },
+  computed: {
+    filteredData() {
+      return crowdTaskStore.query({ id: this.searchForm.id, name: this.searchForm.name })
+    },
+    pagedData() {
+      const start = (this.page.current - 1) * this.page.size
+      return this.filteredData.slice(start, start + this.page.size)
+    }
+  },
+  methods: {
+    typeLabel(t) {
+      return { display: '展示规则', reward: '奖励规则' }[t] || t
+    },
+    statusLabel(s) {
+      return { draft: '新建', verifying: '校验', online: '上线', stopped: '已停止' }[s] || s
+    },
+    statusTagType(s) {
+      return { draft: 'info', verifying: 'warning', online: 'success', stopped: 'danger' }[s] || ''
+    },
+    canDo(row, action) {
+      return crowdTaskStore.allowedActions(row).includes(action)
+    },
+    onRowSelect(row) {
+      this.selectedRow = row
+    },
+    handleSearch() {
+      this.page.current = 1
+    },
+    handleCreate() {
+      this.$router.push('/customization/hsbc/crowd-tasks/add')
+    },
+    handleCopy() {
+      if (!this.selectedRow) {
+        this.$message.warning('请先在列表中选择一条任务再点击复制')
+        return
+      }
+      this.$confirm(
+        `确定要复制任务「${this.selectedRow.name}」吗?将生成一条状态为「新建」的副本。`,
+        '复制任务',
+        { confirmButtonText: '复制', cancelButtonText: '取消', type: 'info' }
+      ).then(() => {
+        const copy = crowdTaskStore.copyFrom(this.selectedRow.id)
+        if (copy) {
+          this.$message.success(`已复制为 ${copy.id}`)
+          this.selectedRow = null
+        }
+      }).catch(() => {})
+    },
+    handleEdit(row) {
+      this.$router.push(`/customization/hsbc/crowd-tasks/edit/${row.id}`)
+    },
+    handleDetail(row) {
+      this.$router.push(`/customization/hsbc/crowd-tasks/view/${row.id}`)
+    },
+    handleRun(row) {
+      this.$confirm(
+        '点击运行后将向行内提交规则,等待返回 count/sample 结果。',
+        '运行任务',
+        { confirmButtonText: '运行', cancelButtonText: '取消', type: 'info' }
+      ).then(() => {
+        crowdTaskStore.run(row.id)
+        this.$message.success('已触发运行,请稍候查看详情')
+      }).catch(() => {})
+    },
+    handlePublish(row) {
+      this.$confirm(
+        '点击上线后将运行 fullist,审核通过后任务进入上线状态。',
+        '上线任务',
+        { confirmButtonText: '上线', cancelButtonText: '取消', type: 'warning' }
+      ).then(() => {
+        crowdTaskStore.publish(row.id)
+        this.$message.success('已提交审核并触发上线运行')
+      }).catch(() => {})
+    },
+    handleStop(row) {
+      this.$confirm(
+        `停止后任务不可恢复,仅可查看详情。确认停止「${row.name}」?`,
+        '停止任务',
+        { confirmButtonText: '停止', cancelButtonText: '取消', type: 'warning' }
+      ).then(() => {
+        crowdTaskStore.stop(row.id)
+        this.$message.success('已停止')
+      }).catch(() => {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.crowd-task-list { padding: 0; }
+
+.search-section { display: flex; flex-direction: column; }
+.search-row .el-form { display: flex; flex-wrap: wrap; gap: 8px 0; }
+.search-row .el-form-item { margin-right: 8px; margin-bottom: 8px; }
+
+.action-row { margin-top: 8px; display: flex; gap: 8px; }
+
+.pagination-section {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  gap: 20px;
+  margin-top: 20px;
+}
+.pagination-section .total { color: #606266; font-size: 13px; }
+</style>

+ 205 - 0
pc/src/views/CrowdTaskView.vue

@@ -0,0 +1,205 @@
+<template>
+  <div class="crowd-task-view">
+    <el-card v-if="current">
+      <div slot="header" class="header">
+        <span class="title">客群规则任务详情</span>
+      </div>
+
+      <!-- 基本信息 -->
+      <div class="section-title">基本信息</div>
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="客群名称">{{ current.name }}</el-descriptions-item>
+        <el-descriptions-item label="客群编号">{{ current.id }}</el-descriptions-item>
+        <el-descriptions-item label="状态">
+          <el-tag :type="statusTagType(current.status)" size="small">{{ statusLabel(current.status) }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="客群类型">{{ typeLabel(current.type) }}</el-descriptions-item>
+        <el-descriptions-item label="客群描述" :span="2">{{ current.description || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="圈客周期">{{ cycleLabel(current) }}</el-descriptions-item>
+        <el-descriptions-item label="任务开始/结束日期">
+          {{ current.startDate }} ~ {{ current.endDate }}
+        </el-descriptions-item>
+        <el-descriptions-item label="任务创建时间">{{ current.createdAt }}</el-descriptions-item>
+        <el-descriptions-item label="任务创建用户">{{ current.creator }}</el-descriptions-item>
+        <el-descriptions-item label="审核人">{{ current.reviewer || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="最近更新时间">{{ current.updatedAt }}</el-descriptions-item>
+      </el-descriptions>
+
+      <!-- 规则组信息 -->
+      <div class="section-title" style="margin-top: 24px">规则组信息</div>
+      <div class="rule-group-meta">
+        <span class="meta-label">规则组间逻辑:</span>
+        <span>{{ current.ruleGroupLogic === 'AND' ? 'and/且' : 'or/或' }}</span>
+      </div>
+      <div class="rule-group-meta">
+        <span class="meta-label">规则组详情:</span>
+      </div>
+      <el-table :data="current.ruleGroups || []" border style="width: 100%" empty-text="无规则组">
+        <el-table-column prop="id" label="规则组编号" width="170" align="center" />
+        <el-table-column prop="name" label="规则组名称" min-width="160" show-overflow-tooltip />
+        <el-table-column prop="description" label="规则组描述" min-width="200" show-overflow-tooltip />
+        <el-table-column label="规则组内逻辑" width="120" align="center">
+          <template slot-scope="scope">{{ scope.row.logic === 'AND' ? 'and/且' : 'or/或' }}</template>
+        </el-table-column>
+        <el-table-column prop="ruleCount" label="规则个数" width="90" align="center" />
+      </el-table>
+
+      <!-- 运行信息 -->
+      <div class="section-title" style="margin-top: 24px">运行信息</div>
+      <div class="run-info">
+        <div class="run-row">
+          <span class="run-label">总数(count):</span>
+          <span class="run-value">{{ countDisplay }}</span>
+        </div>
+        <div class="run-row">
+          <span class="run-label">校验(sample):</span>
+          <span class="run-value">
+            <el-link
+              v-if="sampleAvailable"
+              type="primary"
+              :underline="false"
+              @click="showSample = true"
+            >详情</el-link>
+            <span v-else-if="current.run.sample === 'running'" class="muted">详情</span>
+            <span v-else class="muted">未运行</span>
+          </span>
+          <el-button
+            size="small"
+            type="primary"
+            :disabled="!canRerun"
+            style="margin-left: 16px"
+            @click="handleRerun"
+          >重新校验</el-button>
+        </div>
+        <div class="run-row">
+          <span class="run-label">全量圈客(fulllist):</span>
+          <span :class="['run-value', fulllistClass]">{{ fulllistDisplay }}</span>
+        </div>
+      </div>
+
+      <div class="footer">
+        <el-button type="primary" @click="$router.push('/customization/hsbc/crowd-tasks')">返回</el-button>
+      </div>
+    </el-card>
+
+    <sample-detail-dialog
+      :visible.sync="showSample"
+      :rows="sampleRows"
+    />
+  </div>
+</template>
+
+<script>
+import crowdTaskStore from '@/store/crowdTaskStore'
+import SampleDetailDialog from '@/components/crowdTask/SampleDetailDialog.vue'
+
+export default {
+  name: 'CrowdTaskView',
+  components: { SampleDetailDialog },
+  data() {
+    return { current: null, showSample: false }
+  },
+  computed: {
+    sampleAvailable() {
+      return this.current && Array.isArray(this.current.run.sample) && this.current.run.sample.length > 0
+    },
+    sampleRows() {
+      return this.sampleAvailable ? this.current.run.sample : []
+    },
+    countDisplay() {
+      const c = this.current && this.current.run.count
+      if (c === null || c === undefined) return '未运行'
+      if (c === 'running') return '正在运行'
+      return c
+    },
+    fulllistDisplay() {
+      const f = this.current && this.current.run.fulllist
+      if (f === null || f === undefined) return '未运行'
+      if (f === 'running') return '正在运行'
+      if (f === 'completed') return '已完成'
+      return f
+    },
+    fulllistClass() {
+      const f = this.current && this.current.run.fulllist
+      if (!f) return 'red'
+      if (f === 'running') return 'muted'
+      return 'green'
+    },
+    canRerun() {
+      if (!this.current) return false
+      if (this.current.run.count === 'running' || this.current.run.sample === 'running') return false
+      if (this.current.status === 'stopped') return false
+      // 任意非"正在运行"且非"已停止"状态都支持运维人员重跑
+      return this.current.run.count !== null
+    }
+  },
+  created() {
+    const target = crowdTaskStore.getById(this.$route.params.id)
+    if (!target) {
+      this.$message.error('任务不存在')
+      this.$router.replace('/customization/hsbc/crowd-tasks')
+      return
+    }
+    this.current = target
+  },
+  methods: {
+    statusLabel(s) {
+      return { draft: '新建', verifying: '校验', online: '上线', stopped: '已停止' }[s] || s
+    },
+    statusTagType(s) {
+      return { draft: 'info', verifying: 'warning', online: 'success', stopped: 'danger' }[s] || ''
+    },
+    typeLabel(t) {
+      return { display: '展示规则', reward: '奖励规则' }[t] || t
+    },
+    cycleLabel(row) {
+      if (!row) return ''
+      if (row.cycle === 'once') return '永久一次'
+      if (row.cycle === 'daily') return '每日一次'
+      if (row.cycle === 'monthly') return `每月${row.cycleConfig && row.cycleConfig.day}号`
+      if (row.cycle === 'weekly') {
+        const w = ['一', '二', '三', '四', '五', '六', '日']
+        const idx = row.cycleConfig && row.cycleConfig.weekday
+        return idx ? `每周${w[idx - 1]}` : '每周'
+      }
+      return row.cycle
+    },
+    handleRerun() {
+      this.$confirm('重新校验将重新运行 count 和 sample,不影响 fullist。', '重新校验', {
+        confirmButtonText: '运行', cancelButtonText: '取消', type: 'info'
+      }).then(() => {
+        crowdTaskStore.rerunCountSample(this.current.id)
+        this.$message.success('已触发重新校验,请稍候查看结果')
+      }).catch(() => {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.crowd-task-view { padding: 0; }
+.header { display: flex; align-items: center; justify-content: center; }
+.title { font-size: 16px; font-weight: 600; color: #409EFF; }
+
+.section-title {
+  font-size: 14px; font-weight: 600; color: #303133;
+  background: #f5f7fa; padding: 10px 14px; border-radius: 4px;
+  margin-bottom: 16px;
+}
+
+.rule-group-meta { margin: 8px 0; font-size: 13px; color: #606266; }
+.rule-group-meta .meta-label { font-weight: 600; color: #303133; }
+
+.run-info {
+  border: 1px solid #fbbfbf; border-radius: 4px;
+  background: #fffaf0; padding: 16px 24px;
+}
+.run-row { display: flex; align-items: center; padding: 6px 0; font-size: 14px; }
+.run-label { width: 160px; color: #303133; font-weight: 600; }
+.run-value { color: #606266; }
+.run-value.red { color: #F56C6C; }
+.run-value.green { color: #67C23A; }
+.muted { color: #c0c4cc; }
+
+.footer { text-align: center; margin-top: 24px; padding-top: 16px; border-top: 1px solid #ebeef5; }
+</style>

+ 237 - 209
pc/src/views/EditEquityProduct.vue

@@ -2,12 +2,12 @@
   <div class="edit-equity-product">
     <el-card>
       <div slot="header" class="header-title">
-        <span>编辑权益商品</span>
+        <span>编辑权益</span>
       </div>
 
       <!-- 步骤条 -->
       <el-steps :active="activeStep" simple style="margin-bottom: 20px;">
-        <el-step title="填写商品信息" icon="el-icon-info"></el-step>
+        <el-step title="填写权益信息" icon="el-icon-info"></el-step>
         <el-step title="设置规则" icon="el-icon-setting"></el-step>
         <el-step title="更多设置" icon="el-icon-monitor"></el-step>
       </el-steps>
@@ -20,8 +20,8 @@
         <el-form :model="form" label-width="120px" label-position="left">
           <el-row :gutter="20">
             <el-col :span="8">
-              <el-form-item label="商品来源" required>
-                <!-- 编辑时不允许修改商品来源 -->
+              <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>
@@ -30,94 +30,59 @@
               </el-form-item>
             </el-col>
             <el-col :span="8" v-if="form.source === '荣数商品'">
-              <el-form-item label="选择商品" required>
-                <el-select
+              <el-form-item label="选择权益" required>
+                <el-input
                   v-model="form.rongshuProduct"
-                  placeholder="请选择商品"
-                  filterable
-                  style="width: 100%"
-                  @change="handleRongshuProductChange"
+                  placeholder="请选择权益"
+                  readonly
                 >
-                  <el-option label="荣数商品A" value="荣数商品A"></el-option>
-                  <el-option label="荣数商品B" value="荣数商品B"></el-option>
-                  <el-option label="荣数商品C" value="荣数商品C"></el-option>
-                  <el-option label="荣数商品D" value="荣数商品D"></el-option>
-                </el-select>
+                  <el-button slot="append" @click="rongshuDialogVisible = true">选择</el-button>
+                </el-input>
               </el-form-item>
             </el-col>
             <el-col :span="8" v-if="form.source === '银行自有'">
-              <el-form-item label="商品类型" required>
+              <el-form-item label="权益类型" required>
                 <el-radio-group v-model="form.productType">
                   <el-radio-button label="券码"></el-radio-button>
-                  <el-radio-button label="微信立减金"></el-radio-button>
                 </el-radio-group>
               </el-form-item>
             </el-col>
-            <el-col :span="8" v-if="form.source === '银行自有'">
-              <el-form-item label="商户名称" required>
-                <el-select v-model="form.merchantName" placeholder="请选择商户名称" filterable style="width: 100%">
-                  <el-option label="商户A" value="商户A"></el-option>
-                  <el-option label="商户B" value="商户B"></el-option>
-                  <el-option label="商户C" value="商户C"></el-option>
-                  <el-option label="商户D" value="商户D"></el-option>
-                  <el-option label="商户E" value="商户E"></el-option>
-                </el-select>
-              </el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20" v-if="form.source === '银行自有' && form.productType === '微信立减金'">
-            <el-col :span="8">
-              <el-form-item label="面额" required>
-                <div style="display: flex;align-items: center;gap: 6px;">
-                  <span>满</span>
-                  <el-input
-                    v-model="form.faceValueThreshold"
-                    placeholder="金额"
-                    style="width: 100px;"
-                    oninput="value=value.replace(/[^\d]/g,'')"
-                  ></el-input>
-                  <span>减</span>
-                  <el-input
-                    v-model="form.faceValueDiscount"
-                    placeholder="金额"
-                    style="width: 100px;"
-                    oninput="value=value.replace(/[^\d]/g,'')"
-                  ></el-input>
-                </div>
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="库存数" required>
-                <el-input-number
-                  v-model="form.stockCount"
-                  :min="0"
-                  :max="100000"
-                  style="width: 100%"
-                ></el-input-number>
-              </el-form-item>
-            </el-col>
           </el-row>
           <el-row :gutter="20">
             <el-col :span="8" v-if="form.source === '第三方'">
-              <el-form-item label="外部商品链接" required>
-                <el-select v-model="form.externalLink" placeholder="请选择外部商品链接">
+              <el-form-item label="外部权益链接" required>
+                <el-select v-model="form.externalLink" placeholder="请选择外部权益链接">
                   <el-option label="太古地产" value="太古地产"></el-option>
                   <el-option label="龙腾" value="龙腾"></el-option>
-                  <el-option label="自定义" value="自定义"></el-option>
+                  <el-option label="h5链接" value="h5链接"></el-option>
+                  <el-option label="小程序链接" value="小程序链接"></el-option>
                 </el-select>
               </el-form-item>
             </el-col>
+            <el-col :span="8" v-if="form.source === '第三方' && form.externalLink === 'h5链接'">
+              <el-form-item label="h5链接" required>
+                <el-input v-model="form.customLink" placeholder="请输入h5链接"></el-input>
+              </el-form-item>
+            </el-col>
             <el-col :span="8" v-if="form.source === '第三方'">
-              <el-form-item label="跳转触发页" required>
-                <el-select v-model="form.externalJumpMethod" placeholder="请选择跳转触发页">
-                  <el-option label="权益列表页" value="权益列表页"></el-option>
-                  <el-option label="权益详情页" value="权益详情页"></el-option>
-                </el-select>
+              <el-form-item required label-width="140px">
+                <span slot="label" style="white-space: nowrap;">是否有详情页<el-tooltip content="没有详情页时直接从首页/列表页跳转" placement="top"><i class="el-icon-question tip-icon"></i></el-tooltip></span>
+                <el-radio-group v-model="form.externalJumpMethod">
+                  <el-radio-button label="没有"></el-radio-button>
+                  <el-radio-button label="有"></el-radio-button>
+                </el-radio-group>
               </el-form-item>
             </el-col>
-            <el-col :span="8" v-if="form.source === '第三方' && form.externalLink === '自定义'">
-              <el-form-item label="自定义链接" required>
-                <el-input v-model="form.customLink" placeholder="请输入自定义链接"></el-input>
+          </el-row>
+          <el-row :gutter="20" v-if="form.source === '第三方' && form.externalLink === '小程序链接'">
+            <el-col :span="8">
+              <el-form-item label="小程序appid" required>
+                <el-input v-model="form.miniAppId" placeholder="请输入小程序appid"></el-input>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="小程序页面地址" required label-width="140px">
+                <el-input v-model="form.miniAppPath" placeholder="请输入小程序页面地址"></el-input>
               </el-form-item>
             </el-col>
           </el-row>
@@ -149,46 +114,149 @@
             </el-row>
             <el-row :gutter="20">
               <el-col :span="8">
-                <el-form-item label="权益列表图标" required>
+                <el-form-item required>
+                  <span slot="label">权益图标
+                    <el-tooltip content="展示在权益首页和权益列表页" placement="top">
+                      <i class="el-icon-question tip-icon"></i>
+                    </el-tooltip>
+                  </span>
                   <el-upload
                     action=""
                     :auto-upload="false"
                     :file-list="form.listIcon"
                     :on-change="file => handleUpload(file, 'listIcon')"
+                    :on-preview="handlePreviewImage"
+                    :on-remove="() => form.listIcon = []"
+                    :class="{ 'upload-hide-trigger': form.listIcon.length >= 1 }"
                     :limit="1"
                     list-type="picture-card"
                   >
                     <i class="el-icon-plus"></i>
                   </el-upload>
+                  <div class="upload-size-tip">建议尺寸:80x80px</div>
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </template>
+
+          <template v-else-if="form.source === '第三方'">
+            <el-row :gutter="20">
+              <el-col :span="8">
+                <el-form-item label="权益名称" required>
+                  <el-input v-model="form.equityName" maxlength="40" placeholder="请输入权益名称"></el-input>
                 </el-form-item>
               </el-col>
               <el-col :span="8">
-                <el-form-item label="权益主图" required>
+                <el-form-item label="权益副标题">
+                  <el-input v-model="form.subTitle" maxlength="40" placeholder="请输入权益副标题"></el-input>
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20" v-if="form.externalJumpMethod === '没有'">
+              <el-col :span="8">
+                <el-form-item required>
+                  <span slot="label">权益图标
+                    <el-tooltip content="展示在权益首页和权益列表页" placement="top">
+                      <i class="el-icon-question tip-icon"></i>
+                    </el-tooltip>
+                  </span>
                   <el-upload
                     action=""
                     :auto-upload="false"
-                    :file-list="form.mainImage"
-                    :on-change="file => handleUpload(file, 'mainImage')"
+                    :file-list="form.listIcon"
+                    :on-change="file => handleUpload(file, 'listIcon')"
+                    :on-preview="handlePreviewImage"
+                    :on-remove="() => form.listIcon = []"
+                    :class="{ 'upload-hide-trigger': form.listIcon.length >= 1 }"
                     :limit="1"
                     list-type="picture-card"
                   >
                     <i class="el-icon-plus"></i>
                   </el-upload>
+                  <div class="upload-size-tip">建议尺寸:80x80px</div>
                 </el-form-item>
               </el-col>
             </el-row>
-            <el-row :gutter="20">
-              <el-col :span="12">
-                <el-form-item label="权益概述" required>
-                  <quill-editor v-model="form.overview" :options="quillOptions"></quill-editor>
-                </el-form-item>
-              </el-col>
-              <el-col :span="12">
-                <el-form-item label="权益详情" required>
-                  <quill-editor v-model="form.details" :options="quillOptions"></quill-editor>
+            <el-row v-if="form.externalJumpMethod === '有'" :gutter="20">
+              <el-col :span="8">
+                <el-form-item required>
+                  <span slot="label">权益图标
+                    <el-tooltip content="展示在权益首页和权益列表页" placement="top">
+                      <i class="el-icon-question tip-icon"></i>
+                    </el-tooltip>
+                  </span>
+                  <el-upload
+                    action=""
+                    :auto-upload="false"
+                    :file-list="form.listIcon"
+                    :on-change="file => handleUpload(file, 'listIcon')"
+                    :on-preview="handlePreviewImage"
+                    :on-remove="() => form.listIcon = []"
+                    :class="{ 'upload-hide-trigger': form.listIcon.length >= 1 }"
+                    :limit="1"
+                    list-type="picture-card"
+                  >
+                    <i class="el-icon-plus"></i>
+                  </el-upload>
+                  <div class="upload-size-tip">建议尺寸:80x80px</div>
                 </el-form-item>
               </el-col>
             </el-row>
+          </template>
+        </el-form>
+      </el-card>
+
+      <!-- 详情信息 -->
+      <el-card v-if="shouldShowDetailCard" shadow="never" class="section-card">
+        <div slot="header" class="section-title">详情信息</div>
+        <el-form :model="form" label-width="120px" label-position="left">
+          <el-row :gutter="20">
+            <el-col :span="8">
+              <el-form-item label="权益主图" required>
+                <el-upload
+                  action=""
+                  :auto-upload="false"
+                  :file-list="form.mainImage"
+                  :on-change="file => handleUpload(file, 'mainImage')"
+                  :on-preview="handlePreviewImage"
+                  :on-remove="() => form.mainImage = []"
+                  :class="{ 'upload-hide-trigger': form.mainImage.length >= 1 }"
+                  :limit="1"
+                  list-type="picture-card"
+                >
+                  <i class="el-icon-plus"></i>
+                </el-upload>
+                <div class="upload-size-tip">建议尺寸:716x400px</div>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="权益介绍" required>
+                <el-input
+                  v-model="form.introduction"
+                  type="textarea"
+                  :rows="4"
+                  maxlength="500"
+                  show-word-limit
+                  placeholder="请输入权益介绍"
+                ></el-input>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="权益内容" required>
+                <quill-editor v-model="form.overview" :options="quillOptions"></quill-editor>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="权益说明" required>
+                <quill-editor v-model="form.details" :options="quillOptions"></quill-editor>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <template v-if="form.source === '银行自有' || form.source === '荣数商品'">
             <el-row :gutter="20">
               <el-col :span="24">
                 <el-form-item>
@@ -198,7 +266,7 @@
                         此模板为权益支付成功,发送给持卡人的短信模板;<br/>
                         开启后需选择短信模板,关闭则不发送短信。<br/>
                         占位符如下:<br/>
-                        商品名称:${productName}<br/>
+                        权益名称:${productName}<br/>
                         结束日期:${expireDate}<br/>
                         卡号:${exchangeNum}<br/>
                         卡密:${exchangePwd}<br/>
@@ -231,85 +299,12 @@
             </el-row>
             <el-row :gutter="20">
               <el-col :span="8">
-                <el-form-item label="领取成功按钮文案" required>
+                <el-form-item label="领取成功按钮" required>
                   <el-input v-model="form.successButtonText" placeholder="请输入按钮文案"></el-input>
                 </el-form-item>
               </el-col>
             </el-row>
           </template>
-
-          <template v-else-if="form.source === '第三方'">
-            <el-row :gutter="20">
-              <el-col :span="8">
-                <el-form-item label="权益名称" required>
-                  <el-input v-model="form.equityName" maxlength="40" placeholder="请输入权益名称"></el-input>
-                </el-form-item>
-              </el-col>
-              <el-col :span="8">
-                <el-form-item label="权益副标题">
-                  <el-input v-model="form.subTitle" maxlength="40" placeholder="请输入权益副标题"></el-input>
-                </el-form-item>
-              </el-col>
-            </el-row>
-            <el-row :gutter="20" v-if="form.externalJumpMethod === '权益列表页'">
-              <el-col :span="8">
-                <el-form-item label="权益列表图标" required>
-                  <el-upload
-                    action=""
-                    :auto-upload="false"
-                    :file-list="form.listIcon"
-                    :on-change="file => handleUpload(file, 'listIcon')"
-                    :limit="1"
-                    list-type="picture-card"
-                  >
-                    <i class="el-icon-plus"></i>
-                  </el-upload>
-                </el-form-item>
-              </el-col>
-            </el-row>
-            <el-row v-if="form.externalJumpMethod === '权益详情页'" :gutter="20">
-              <el-col :span="8">
-                <el-form-item label="权益列表图标" required>
-                  <el-upload
-                    action=""
-                    :auto-upload="false"
-                    :file-list="form.listIcon"
-                    :on-change="file => handleUpload(file, 'listIcon')"
-                    :limit="1"
-                    list-type="picture-card"
-                  >
-                    <i class="el-icon-plus"></i>
-                  </el-upload>
-                </el-form-item>
-              </el-col>
-              <el-col :span="8">
-                <el-form-item label="权益主图" required>
-                  <el-upload
-                    action=""
-                    :auto-upload="false"
-                    :file-list="form.mainImage"
-                    :on-change="file => handleUpload(file, 'mainImage')"
-                    :limit="1"
-                    list-type="picture-card"
-                  >
-                    <i class="el-icon-plus"></i>
-                  </el-upload>
-                </el-form-item>
-              </el-col>
-            </el-row>
-            <el-row v-if="form.externalJumpMethod === '权益详情页'" :gutter="20">
-              <el-col :span="12">
-                <el-form-item label="权益概述" required>
-                  <quill-editor v-model="form.overview" :options="quillOptions"></quill-editor>
-                </el-form-item>
-              </el-col>
-              <el-col :span="12">
-                <el-form-item label="权益详情" required>
-                  <quill-editor v-model="form.details" :options="quillOptions"></quill-editor>
-                </el-form-item>
-              </el-col>
-            </el-row>
-          </template>
         </el-form>
       </el-card>
 
@@ -392,19 +387,13 @@
             <el-switch v-model="form.claimGroupEnabled"></el-switch>
             <template v-if="form.claimGroupEnabled">
               <div style="width: 600px;background: #f1f2f5;padding: 20px;margin-top: 10px;">
-                <el-form-item label="是否潜客领取" label-width="110px" style="margin-bottom: 12px;">
-                  <el-radio-group v-model="form.claimGroupIsPotential">
-                    <el-radio :label="true">是</el-radio>
-                    <el-radio :label="false">否</el-radio>
-                  </el-radio-group>
-                </el-form-item>
-                <el-form-item v-if="!form.claimGroupIsPotential" label="客群奖励活动" label-width="110px" style="margin-bottom: 12px;">
-                  <el-select v-model="form.claimGroupTagId" filterable placeholder="请选择客群奖励活动" style="width: 100%">
-                    <el-option label="客群奖励活动A" value="客群奖励活动A"></el-option>
-                    <el-option label="客群奖励活动B" value="客群奖励活动B"></el-option>
-                    <el-option label="客群奖励活动C" value="客群奖励活动C"></el-option>
-                  </el-select>
-                </el-form-item>
+                <el-alert
+                  title="需在【权益管理-权益发放】中选择权益发放,未发放的权益移动端不展示"
+                  type="info"
+                  :closable="false"
+                  show-icon
+                  style="margin-bottom: 12px;"
+                ></el-alert>
                 <div style="font-size: 14px;color: #606266;margin-bottom: 8px;font-weight: 500;">不满足提示</div>
                 <el-form-item label="提示内容" label-width="80px" style="margin-bottom: 12px;">
                   <el-input v-model="form.claimDenyMessage" maxlength="200" placeholder="请输入提示内容"></el-input>
@@ -427,7 +416,7 @@
         <el-card shadow="never" class="section-card">
           <div slot="header" class="section-title">显示页面</div>
           <el-form :model="form" label-width="120px" label-position="left">
-            <el-form-item label="首页">
+            <el-form-item label="权益页/列表页">
               <el-radio-group v-model="form.showOnHome">
                 <el-radio label="显示">显示</el-radio>
                 <el-radio label="不显示">不显示</el-radio>
@@ -517,15 +506,30 @@
         </div>
       </div>
     </el-card>
+
+    <rongshu-product-dialog
+      :visible.sync="rongshuDialogVisible"
+      @select="onRongshuSelect"
+    />
+
+    <el-dialog :visible.sync="previewVisible" title="图片预览" width="640px" append-to-body>
+      <img v-if="previewImageUrl" :src="previewImageUrl" style="display:block; max-width:100%; margin:0 auto;" />
+    </el-dialog>
   </div>
 </template>
 
 <script>
+import RongshuProductDialog from '@/components/RongshuProductDialog.vue'
+
 export default {
   name: 'EditEquityProduct',
+  components: { RongshuProductDialog },
   data() {
     return {
       activeStep: 0,
+      rongshuDialogVisible: false,
+      previewVisible: false,
+      previewImageUrl: '',
       quillOptions: {
         placeholder: '请输入内容...',
         modules: {
@@ -543,12 +547,6 @@ export default {
         { id: 'SMS_TPL_002', name: '券码类短信模板', content: '【银行】尊敬的客户,您的${productName}购买成功,券码为${exchangeNum},请于${expireDate}前使用。' },
         { id: 'SMS_TPL_003', name: '实物类短信模板', content: '【银行】您购买的${productName}已受理,我们将尽快安排发货,感谢您的支持。' }
       ],
-      rongshuProductMap: {
-        '荣数商品A': { equityName: '荣数机场贵宾厅', mainImage: [{ name: 'rongshu-a.png', url: 'https://placehold.co/200x200/4096ff/ffffff?text=A' }] },
-        '荣数商品B': { equityName: '荣数高尔夫体验', mainImage: [{ name: 'rongshu-b.png', url: 'https://placehold.co/200x200/52c41a/ffffff?text=B' }] },
-        '荣数商品C': { equityName: '荣数洗车服务', mainImage: [{ name: 'rongshu-c.png', url: 'https://placehold.co/200x200/faad14/ffffff?text=C' }] },
-        '荣数商品D': { equityName: '荣数体检套餐', mainImage: [{ name: 'rongshu-d.png', url: 'https://placehold.co/200x200/eb2f96/ffffff?text=D' }] }
-      },
       form: {
         source: '银行自有',
         rongshuProduct: '',
@@ -559,9 +557,11 @@ export default {
         shareEnabled: false,
         shareImage: [],
         shareName: '',
-        externalJumpMethod: '权益列表页',
+        externalJumpMethod: '没有',
         externalLink: '太古地产',
         customLink: '',
+        miniAppId: '',
+        miniAppPath: '',
         skuNo: '',
         equityName: '',
         productId: '',
@@ -569,6 +569,7 @@ export default {
         subTitle: '',
         listIcon: [],
         mainImage: [],
+        introduction: '',
         overview: '',
         details: '',
         smsEnabled: false,
@@ -595,6 +596,14 @@ export default {
       }
     }
   },
+  computed: {
+    shouldShowDetailCard() {
+      const s = this.form.source
+      if (s === '银行自有' || s === '荣数商品') return true
+      if (s === '第三方' && this.form.externalJumpMethod === '有') return true
+      return false
+    }
+  },
   created() {
     this.loadData()
   },
@@ -602,18 +611,20 @@ export default {
     loadData() {
       const id = this.$route.params.id
       const mockMap = {
-        P001: {
+        100001: {
           source: '银行自有',
           externalJumpMethod: '',
           externalLink: '',
           customLink: '',
+        miniAppId: '',
+        miniAppPath: '',
           skuNo: 'SKU001',
-          equityName: '权益商品A',
+          equityName: '权益A',
           subTitle: '副标题示例',
           listIcon: [],
           mainImage: [],
-          overview: '<p>权益概述内容</p>',
-          details: '<p>权益详情内容</p>',
+          overview: '<p>权益内容内容</p>',
+          details: '<p>权益说明内容</p>',
           smsEnabled: true,
           smsTemplateId: 'SMS_TPL_001',
           smsTemplate: '【银行】您购买的${productName}已支付成功,卡号:${exchangeNum},卡密:${exchangePwd},有效期至${expireDate},详情:${qrCodeDownPaths}',
@@ -632,13 +643,15 @@ export default {
           claimDenyButtonText: '',
           claimDenyButtonLink: ''
         },
-        P002: {
+        100002: {
           source: '第三方',
-          externalJumpMethod: '权益列表页',
+          externalJumpMethod: '没有',
           externalLink: '太古地产',
           customLink: '',
+        miniAppId: '',
+        miniAppPath: '',
           skuNo: 'SKU002',
-          equityName: '权益商品B',
+          equityName: '权益B',
           subTitle: '外部列表页副标题',
           listIcon: [],
           mainImage: [],
@@ -663,18 +676,20 @@ export default {
           claimDenyButtonText: '去升级',
           claimDenyButtonLink: 'https://example.com/upgrade'
         },
-        P003: {
+        100003: {
           source: '第三方',
-          externalJumpMethod: '权益详情页',
+          externalJumpMethod: '',
           externalLink: '龙腾',
           customLink: '',
+        miniAppId: '',
+        miniAppPath: '',
           skuNo: 'SKU003',
-          equityName: '权益商品C',
+          equityName: '权益C',
           subTitle: '外部详情页副标题',
           listIcon: [],
           mainImage: [],
-          overview: '<p>外部详情页权益概述</p>',
-          details: '<p>外部详情页权益详情</p>',
+          overview: '<p>外部详情页权益内容</p>',
+          details: '<p>外部详情页权益说明</p>',
           smsEnabled: false,
           smsTemplateId: '',
           smsTemplate: '',
@@ -693,18 +708,18 @@ export default {
           claimDenyButtonText: '',
           claimDenyButtonLink: ''
         },
-        P004: {
+        100004: {
           source: '第三方',
-          externalJumpMethod: '权益详情页',
-          externalLink: '自定义',
+          externalJumpMethod: '',
+          externalLink: 'h5链接',
           customLink: 'https://example.com/custom',
           skuNo: 'SKU004',
-          equityName: '权益商品D',
+          equityName: '权益D',
           subTitle: '外部自定义链接副标题',
           listIcon: [],
           mainImage: [],
-          overview: '<p>自定义链接权益概述</p>',
-          details: '<p>自定义链接权益详情</p>',
+          overview: '<p>自定义链接权益内容</p>',
+          details: '<p>自定义链接权益说明</p>',
           smsEnabled: false,
           smsTemplateId: '',
           smsTemplate: '',
@@ -756,9 +771,6 @@ export default {
         if (!this.form.shareName) {
           this.form.shareName = this.form.equityName || ''
         }
-        if (!this.form.shareImage || !this.form.shareImage.length) {
-          this.form.shareImage = this.form.mainImage ? [...this.form.mainImage] : []
-        }
       }
     },
     handleSmsTemplateChange(id) {
@@ -766,15 +778,23 @@ export default {
       this.form.smsTemplate = tpl ? tpl.content : ''
     },
     handleUpload(file, field) {
-      this.form[field] = [file.file]
-      return false
-    },
-    handleRongshuProductChange(value) {
-      const preset = this.rongshuProductMap[value]
-      if (preset) {
-        this.form.equityName = preset.equityName
-        this.form.mainImage = preset.mainImage ? [...preset.mainImage] : []
+      if (!file || !file.raw) return
+      const reader = new FileReader()
+      reader.onload = (e) => {
+        this.form[field] = [{ name: file.name, url: e.target.result, uid: file.uid }]
       }
+      reader.readAsDataURL(file.raw)
+    },
+    handlePreviewImage(file) {
+      this.previewImageUrl = file.url || ''
+      this.previewVisible = !!this.previewImageUrl
+    },
+    onRongshuSelect(product) {
+      this.form.rongshuProduct = product.name
+      this.form.equityName = product.name
+      this.form.merchantName = product.merchant
+      this.form.stockCount = product.stock
+      this.form.mainImage = product.mainImage ? [...product.mainImage] : []
     }
   }
 }
@@ -792,6 +812,8 @@ export default {
   margin-bottom: 16px;
   border: none;
 }
+.upload-hide-trigger >>> .el-upload--picture-card { display: none; }
+
 .section-card >>> .el-card__header {
   padding: 12px 20px;
   background: #fafafa;
@@ -823,6 +845,12 @@ export default {
   cursor: pointer;
   font-size: 14px;
 }
+.upload-size-tip {
+  margin-top: 4px;
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.5;
+}
 .sms-template-preview {
   margin-top: 12px;
   padding: 12px;

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

@@ -269,11 +269,11 @@ export default {
     },
     equityName() {
       const map = {
-        P001: '权益商品A',
-        P002: '权益商品B',
-        P003: '权益商品C',
-        P004: '权益商品D',
-        P005: '权益商品E'
+        100001: '权益A',
+        100002: '权益B',
+        100003: '权益C',
+        100004: '权益D',
+        100005: '权益E'
       }
       return map[this.productId] || this.productId
     },

+ 89 - 32
pc/src/views/EquityProducts.vue

@@ -8,11 +8,18 @@
             <el-form-item label="权益名称:">
               <el-input v-model="searchForm.equityName" placeholder="请输入" style="width: 150px"></el-input>
             </el-form-item>
-            <el-form-item label="商品ID:">
+            <el-form-item label="权益编号:">
               <el-input v-model="searchForm.productId" placeholder="请输入" style="width: 150px"></el-input>
             </el-form-item>
-            <el-form-item label="SKU编号:">
-              <el-input v-model="searchForm.skuNo" placeholder="请输入" style="width: 150px"></el-input>
+            <el-form-item label="状态:">
+              <el-select v-model="searchForm.reviewStatus" placeholder="全部" clearable style="width: 150px">
+                <el-option
+                  v-for="opt in statusOptions"
+                  :key="opt"
+                  :label="opt"
+                  :value="opt"
+                />
+              </el-select>
             </el-form-item>
             <div class="button-group" style="margin-left: 20px;">
               <el-button type="primary" @click="handleSearch">查询</el-button>
@@ -21,12 +28,12 @@
           </el-form>
         </div>
         <div style="margin-top: 15px;">
-          <el-button type="primary" icon="el-icon-plus" @click="createProduct">添加权益商品</el-button>
+          <el-button type="primary" icon="el-icon-plus" @click="createProduct">添加权益</el-button>
         </div>
       </div>
 
       <!-- 表格区域 -->
-      <el-table :data="tableData" stripe style="width: 100%">
+      <el-table :data="filteredTableData" stripe style="width: 100%">
         <el-table-column label="操作" width="240" fixed="left">
           <template slot-scope="scope">
             <el-button type="text" size="small" @click="handleView(scope.row)">查看</el-button>
@@ -80,8 +87,13 @@
                 </el-popover>
                 <div
                   class="more-actions-item"
-                  @click="openShowPagesDialog(scope.row, scope.$index)"
-                >设置显示页面</div>
+                  @click="openLinkDialogFromMore(scope.row, scope.$index)"
+                >生成链接</div>
+                <div
+                  v-if="scope.row.source === '银行自有' && scope.row.productType === '券码'"
+                  class="more-actions-item"
+                  @click="handleImportCouponsFromMore(scope.row, scope.$index)"
+                >导入券码</div>
                 <div
                   v-if="scope.row.source === '银行自有' && scope.row.productType === '券码'"
                   class="more-actions-item"
@@ -106,12 +118,10 @@
             >{{ scope.row.reviewStatus }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="source" label="商品来源" width="100"></el-table-column>
-        <el-table-column prop="jumpLink" label="外部商品链接" width="120"></el-table-column>
-        <el-table-column prop="jumpMethod" label="跳转触发页" width="120"></el-table-column>
-        <el-table-column prop="productId" label="商品ID" width="100"></el-table-column>
-        <el-table-column prop="skuNo" label="SKU编号" width="120"></el-table-column>
-        <el-table-column prop="merchantName" label="商户名称" width="120"></el-table-column>
+        <el-table-column prop="source" label="权益来源" width="100"></el-table-column>
+        <el-table-column prop="jumpLink" label="外部权益链接" width="120"></el-table-column>
+        <el-table-column prop="jumpMethod" label="是否有详情页" width="120"></el-table-column>
+        <el-table-column prop="productId" label="权益编号" width="100"></el-table-column>
         <el-table-column label="显示页面" min-width="220">
           <template slot-scope="scope">
             <template v-if="visiblePages(scope.row).length">
@@ -125,6 +135,7 @@
             <span v-else class="empty-text">-</span>
           </template>
         </el-table-column>
+        <el-table-column prop="creator" label="创建人" width="100"></el-table-column>
         <el-table-column prop="createTime" label="创建时间" width="180"></el-table-column>
         <el-table-column prop="reviewer" label="审核人" width="100"></el-table-column>
         <el-table-column prop="reviewTime" label="审核时间" width="180"></el-table-column>
@@ -136,6 +147,13 @@
         @confirm="onShowPagesConfirm"
       ></show-pages-dialog>
 
+      <equity-link-dialog
+        :visible.sync="linkDialogVisible"
+        :id="linkTargetId"
+        path-name="equity/debit"
+        title="生成借记卡权益链接"
+      ></equity-link-dialog>
+
       <!-- 分页 -->
       <div class="pagination-section">
         <span>Total 1000</span>
@@ -152,89 +170,98 @@
 
 <script>
 import ShowPagesDialog from '@/components/ShowPagesDialog.vue'
+import EquityLinkDialog from '@/components/EquityLinkDialog.vue'
 
 export default {
   name: 'EquityProducts',
-  components: { ShowPagesDialog },
+  components: { ShowPagesDialog, EquityLinkDialog },
   data() {
     return {
       searchForm: {
         equityName: '',
         productId: '',
-        skuNo: ''
+        reviewStatus: ''
       },
+      statusOptions: ['待审核', '已上架', '审核拒绝', '已下架'],
       adjustOrderPosition: 1,
       showPagesDialogVisible: false,
       showPagesRow: null,
+      linkDialogVisible: false,
+      linkTargetId: '',
       tableData: [
         {
-          equityName: '权益商品A',
+          equityName: '权益A',
           reviewStatus: '待审核',
           source: '银行自有',
           jumpMethod: '-',
           jumpLink: '-',
           exchangeMethod: '权益平台',
-          productId: 'P001',
+          productId: '100001',
           skuNo: 'SKU001',
           merchantName: '商户A',
           productType: '券码',
+          creator: '李运营',
           createTime: '2026-04-10 09:32:15',
           reviewer: '',
           reviewTime: ''
         },
         {
-          equityName: '权益商品B',
+          equityName: '权益B',
           reviewStatus: '待审核',
           source: '第三方',
-          jumpMethod: '权益列表页',
+          jumpMethod: '没有',
           jumpLink: '太古地产',
           exchangeMethod: '第三方(列表跳转)',
-          productId: 'P002',
+          productId: '100002',
           skuNo: 'SKU002',
           merchantName: '商户B',
+          creator: '王运营',
           createTime: '2026-04-12 14:08:42',
           reviewer: '张三',
           reviewTime: '2026-04-15 10:30:00'
         },
         {
-          equityName: '权益商品C',
+          equityName: '权益C',
           reviewStatus: '已上架',
           source: '第三方',
-          jumpMethod: '权益详情页',
+          jumpMethod: '',
           jumpLink: '龙腾',
           exchangeMethod: '第三方(详情页跳转)',
-          productId: 'P003',
+          productId: '100003',
           skuNo: 'SKU003',
           merchantName: '商户C',
+          creator: '李运营',
           createTime: '2026-04-08 11:20:05',
           reviewer: '王五',
           reviewTime: '2026-04-14 16:45:00'
         },
         {
-          equityName: '权益商品D',
+          equityName: '权益D',
           reviewStatus: '审核拒绝',
           source: '第三方',
-          jumpMethod: '权益详情页',
-          jumpLink: '自定义',
+          jumpMethod: '',
+          jumpLink: 'h5链接',
           exchangeMethod: '第三方(详情页跳转)',
-          productId: 'P004',
+          productId: '100004',
           skuNo: 'SKU004',
           merchantName: '商户D',
+          creator: '陈运营',
           createTime: '2026-04-05 15:50:18',
           reviewer: '赵六',
           reviewTime: '2026-04-13 09:15:00'
         },
         {
-          equityName: '权益商品E',
+          equityName: '权益E',
           reviewStatus: '已下架',
           source: '银行自有',
           jumpMethod: '-',
           jumpLink: '-',
           exchangeMethod: '权益平台',
-          productId: 'P005',
+          productId: '100005',
           skuNo: 'SKU005',
           merchantName: '商户E',
-          productType: '微信立减金',
+          productType: '券码',
+          creator: '王运营',
           createTime: '2026-03-28 09:00:11',
           reviewer: '钱七',
           reviewTime: '2026-04-12 11:20:00'
@@ -246,12 +273,21 @@ export default {
     visiblePages() {
       return row => {
         const pages = []
-        if (row.showOnHome === '显示') pages.push('首页')
+        if (row.showOnHome === '显示') pages.push("权益首页/列表页")
         if (row.showOnAssembly === '显示' && Array.isArray(row.assemblyPages)) {
           pages.push(...row.assemblyPages)
         }
         return pages
       }
+    },
+    filteredTableData() {
+      return this.tableData.filter(row => {
+        const f = this.searchForm
+        if (f.equityName && !row.equityName.includes(f.equityName)) return false
+        if (f.productId && !row.productId.includes(f.productId)) return false
+        if (f.reviewStatus && row.reviewStatus !== f.reviewStatus) return false
+        return true
+      })
     }
   },
   methods: {
@@ -262,7 +298,7 @@ export default {
       this.searchForm = {
         equityName: '',
         productId: '',
-        skuNo: ''
+        reviewStatus: ''
       }
     },
     getPopoverInstance(name) {
@@ -313,6 +349,17 @@ export default {
     handleAudit(row) {
       this.$router.push(`/customization/hsbc/equity-products/audit/${row.productId}`)
     },
+    showLinkDialog(row) {
+      this.linkTargetId = row.productId
+      this.linkDialogVisible = true
+    },
+    openLinkDialogFromMore(row, index) {
+      const moreRef = this.getPopoverInstance(`morePopover-${index}`)
+      if (moreRef) {
+        moreRef.doClose()
+      }
+      this.showLinkDialog(row)
+    },
     handleManageCoupons() {
       window.open('https://uat.leagpoint.com/sipAdmin/coupon/inboundCouponList', '_blank')
     },
@@ -323,6 +370,16 @@ export default {
       }
       this.handleManageCoupons(row)
     },
+    handleImportCoupons() {
+      window.open('https://uat.leagpoint.com/sipAdmin/coupon/importCoupon', '_blank')
+    },
+    handleImportCouponsFromMore(row, index) {
+      const moreRef = this.getPopoverInstance(`morePopover-${index}`)
+      if (moreRef) {
+        moreRef.doClose()
+      }
+      this.handleImportCoupons(row)
+    },
     handleOffline(row) {
       this.$confirm(`确定要下架权益"${row.equityName}"吗?`, '提示', {
         confirmButtonText: '确定',

+ 132 - 0
pc/src/views/Login.vue

@@ -0,0 +1,132 @@
+<template>
+  <div class="login-page">
+    <div class="login-box">
+      <div class="login-header">
+        <i class="el-icon-monitor"></i>
+        <span>管理后台</span>
+      </div>
+      <p class="login-tip">请使用账号密码登录</p>
+      <el-form
+        ref="form"
+        :model="form"
+        :rules="rules"
+        size="medium"
+        @submit.native.prevent="handleSubmit"
+      >
+        <el-form-item prop="username">
+          <el-input
+            v-model="form.username"
+            placeholder="请输入账号"
+            prefix-icon="el-icon-user"
+            autocomplete="username"
+          />
+        </el-form-item>
+        <el-form-item prop="password">
+          <el-input
+            v-model="form.password"
+            type="password"
+            placeholder="请输入密码"
+            prefix-icon="el-icon-lock"
+            autocomplete="current-password"
+            show-password
+            @keyup.enter.native="handleSubmit"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button
+            type="primary"
+            :loading="loading"
+            class="login-btn"
+            native-type="submit"
+          >
+            登录
+          </el-button>
+        </el-form-item>
+      </el-form>
+      <p class="login-hint">演示账号:admin / 123456</p>
+    </div>
+  </div>
+</template>
+
+<script>
+import request from '@/utils/request'
+
+export default {
+  name: 'Login',
+  data() {
+    return {
+      loading: false,
+      form: { username: '', password: '' },
+      rules: {
+        username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
+        password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
+      }
+    }
+  },
+  methods: {
+    handleSubmit() {
+      this.$refs.form.validate(async (valid) => {
+        if (!valid) return
+        this.loading = true
+        try {
+          const res = await request.post('/api/login', this.form)
+          localStorage.setItem('token', res.data.token)
+          localStorage.setItem('username', res.data.username)
+          this.$message.success('登录成功')
+          const redirect = this.$route.query.redirect || '/'
+          this.$router.replace(redirect)
+        } finally {
+          this.loading = false
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.login-page {
+  height: 100vh;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: linear-gradient(135deg, #304156 0%, #1f2d3d 100%);
+}
+
+.login-box {
+  width: 380px;
+  padding: 36px 32px 24px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.2);
+}
+
+.login-header {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 10px;
+  color: #304156;
+  font-size: 22px;
+  font-weight: bold;
+}
+.login-header i { font-size: 26px; }
+
+.login-tip {
+  margin: 10px 0 24px;
+  color: #909399;
+  text-align: center;
+  font-size: 13px;
+}
+
+.login-btn {
+  width: 100%;
+}
+
+.login-hint {
+  margin: 8px 0 0;
+  color: #c0c4cc;
+  text-align: center;
+  font-size: 12px;
+}
+</style>

+ 291 - 0
pc/src/views/RuleGroupEdit.vue

@@ -0,0 +1,291 @@
+<template>
+  <div class="rule-group-edit">
+    <el-card>
+      <div slot="header" class="header">
+        <span class="title">{{ isEdit ? '编辑客群规则任务' : '新增客群规则任务' }}</span>
+        <span v-if="isEdit && current" class="rg-id">编号:{{ current.id }}</span>
+      </div>
+
+      <!-- 步骤指示 -->
+      <el-steps :active="step" finish-status="success" align-center class="steps">
+        <el-step title="基本信息" icon="el-icon-edit-outline" />
+        <el-step title="规则配置" icon="el-icon-set-up" />
+      </el-steps>
+
+      <!-- Step 1: 基本信息 -->
+      <div v-show="step === 0" class="section">
+        <div class="section-title">基本信息</div>
+        <el-form
+          ref="baseForm"
+          :model="form"
+          :rules="rules"
+          label-width="120px"
+          style="max-width: 760px; margin: 0 auto"
+        >
+          <el-form-item label="规则组名称" prop="name">
+            <el-input v-model="form.name" placeholder="最长 40 字符" maxlength="40" show-word-limit />
+          </el-form-item>
+          <el-form-item label="规则组类型" prop="type">
+            <el-select v-model="form.type" placeholder="请选择" style="width: 100%">
+              <el-option
+                v-for="opt in groupTypes"
+                :key="opt.value"
+                :label="opt.label"
+                :value="opt.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="规则逻辑" prop="logic">
+            <el-radio-group v-model="form.logic">
+              <el-radio label="AND">且(AND,全部满足)</el-radio>
+              <el-radio label="OR">或(OR,任一满足)</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="备注说明">
+            <el-input
+              v-model="form.description"
+              type="textarea"
+              :rows="2"
+              placeholder="规则组用途、命中场景等,最长 200 字符"
+              maxlength="200"
+              show-word-limit
+            />
+          </el-form-item>
+        </el-form>
+      </div>
+
+      <!-- Step 2: 规则配置 -->
+      <div v-show="step === 1" class="section">
+        <div class="section-title">
+          规则组配置
+          <span class="section-tip">规则之间使用「{{ form.logic === 'AND' ? '且' : '或' }}」逻辑组合</span>
+        </div>
+
+        <el-table :data="form.rules" border style="width: 100%" :empty-text="'暂无规则,请点击下方加号新增'">
+          <el-table-column prop="metaCode" label="元规则编号" width="110" align="center" />
+          <el-table-column label="元规则名称" min-width="180">
+            <template slot-scope="scope">{{ metaName(scope.row.metaCode) }}</template>
+          </el-table-column>
+          <el-table-column prop="ruleName" label="规则名称" min-width="160" show-overflow-tooltip />
+          <el-table-column label="规则描述" min-width="240" show-overflow-tooltip>
+            <template slot-scope="scope">{{ buildDesc(scope.row) }}</template>
+          </el-table-column>
+          <el-table-column label="规则参数" min-width="220" show-overflow-tooltip>
+            <template slot-scope="scope">
+              <span class="params-text">{{ buildParam(scope.row) }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="140" align="center">
+            <template slot-scope="scope">
+              <el-button
+                type="primary"
+                circle
+                size="mini"
+                icon="el-icon-edit"
+                title="编辑"
+                @click="openRuleDialog('edit', scope.row, scope.$index)"
+              />
+              <el-button
+                type="warning"
+                circle
+                size="mini"
+                icon="el-icon-view"
+                title="查看"
+                @click="openRuleDialog('view', scope.row)"
+              />
+              <el-button
+                type="danger"
+                circle
+                size="mini"
+                icon="el-icon-delete"
+                title="删除"
+                @click="removeRule(scope.$index)"
+              />
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <div class="add-rule-row">
+          <el-button
+            type="primary"
+            circle
+            icon="el-icon-plus"
+            @click="openRuleDialog('add')"
+          />
+        </div>
+      </div>
+
+      <!-- 操作按钮 -->
+      <div class="footer">
+        <el-button v-if="step === 0" @click="handleCancel">取消</el-button>
+        <el-button v-if="step === 1" @click="step = 0">上一步</el-button>
+        <el-button v-if="step === 0" type="primary" @click="handleNext">下一步</el-button>
+        <el-button v-if="step === 1" type="primary" @click="handleSave">完成</el-button>
+      </div>
+    </el-card>
+
+    <rule-edit-dialog
+      :visible.sync="dialog.visible"
+      :rule="dialog.rule"
+      :mode="dialog.mode"
+      @save="onRuleSave"
+    />
+  </div>
+</template>
+
+<script>
+import ruleGroupStore from '@/store/ruleGroupStore'
+import {
+  GROUP_TYPES,
+  getMetaRule,
+  buildRuleDescription,
+  buildRuleParams
+} from '@/config/metaRules'
+import RuleEditDialog from '@/components/ruleGroup/RuleEditDialog.vue'
+
+export default {
+  name: 'RuleGroupEdit',
+  components: { RuleEditDialog },
+  data() {
+    return {
+      step: 0,
+      form: {
+        name: '',
+        type: '',
+        logic: 'AND',
+        description: '',
+        rules: []
+      },
+      rules: {
+        name: [
+          { required: true, message: '请输入规则组名称', trigger: 'blur' },
+          { max: 40, message: '最长 40 字符', trigger: 'blur' }
+        ],
+        type: [{ required: true, message: '请选择规则组类型', trigger: 'change' }],
+        logic: [{ required: true, message: '请选择规则逻辑', trigger: 'change' }]
+      },
+      groupTypes: GROUP_TYPES,
+      current: null,
+      dialog: { visible: false, mode: 'add', rule: null, editIndex: -1 }
+    }
+  },
+  computed: {
+    isEdit() {
+      return !!this.$route.params.id
+    }
+  },
+  created() {
+    if (this.isEdit) {
+      const target = ruleGroupStore.getById(this.$route.params.id)
+      if (!target) {
+        this.$message.error('规则组不存在')
+        this.$router.replace('/customization/hsbc/rule-groups')
+        return
+      }
+      if (target.status === 'online') {
+        this.$message.warning('已上线规则组不可编辑')
+        this.$router.replace(`/customization/hsbc/rule-groups/view/${target.id}`)
+        return
+      }
+      this.current = target
+      this.form = {
+        name: target.name,
+        type: target.type,
+        logic: target.logic,
+        description: target.description,
+        rules: JSON.parse(JSON.stringify(target.rules || []))
+      }
+    }
+  },
+  methods: {
+    metaName(code) {
+      const m = getMetaRule(code)
+      return m ? m.name : code
+    },
+    buildDesc(rule) {
+      return buildRuleDescription(rule.metaCode, rule.config) || '-'
+    },
+    buildParam(rule) {
+      return buildRuleParams(rule.metaCode, rule.config) || '-'
+    },
+    handleNext() {
+      this.$refs.baseForm.validate((ok) => {
+        if (ok) this.step = 1
+      })
+    },
+    openRuleDialog(mode, rule = null, index = -1) {
+      this.dialog = {
+        visible: true,
+        mode,
+        rule: rule ? JSON.parse(JSON.stringify(rule)) : null,
+        editIndex: index
+      }
+    },
+    onRuleSave(rule) {
+      if (this.dialog.mode === 'edit' && this.dialog.editIndex >= 0) {
+        this.$set(this.form.rules, this.dialog.editIndex, rule)
+        this.$message.success('已更新规则')
+      } else {
+        this.form.rules.push(rule)
+        this.$message.success('已添加规则')
+      }
+    },
+    removeRule(idx) {
+      this.$confirm('确定要删除该条规则吗?', '提示', {
+        confirmButtonText: '删除',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.form.rules.splice(idx, 1)
+      }).catch(() => {})
+    },
+    handleSave() {
+      if (this.form.rules.length === 0) {
+        this.$message.error('请至少添加一条规则')
+        return
+      }
+      if (this.isEdit) {
+        ruleGroupStore.update(this.current.id, this.form)
+        this.$message.success('保存成功')
+      } else {
+        ruleGroupStore.create(this.form)
+        this.$message.success('创建成功')
+      }
+      this.$router.push('/customization/hsbc/rule-groups')
+    },
+    handleCancel() {
+      this.$router.push('/customization/hsbc/rule-groups')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.rule-group-edit { padding: 0; }
+.header { display: flex; align-items: center; justify-content: space-between; }
+.title { font-size: 16px; font-weight: 600; color: #409EFF; }
+.rg-id { color: #909399; font-size: 13px; }
+
+.steps { margin: 16px 0 32px; }
+
+.section { margin-bottom: 24px; }
+.section-title {
+  font-size: 14px; font-weight: 600; color: #303133;
+  background: #f5f7fa; padding: 10px 14px; border-radius: 4px;
+  margin-bottom: 16px;
+}
+.section-tip { font-weight: normal; color: #909399; margin-left: 12px; font-size: 12px; }
+
+.add-rule-row {
+  display: flex; align-items: center;
+  padding: 16px 0;
+}
+
+.params-text {
+  font-family: 'Menlo', 'Consolas', monospace;
+  color: #606266; font-size: 12px;
+}
+
+.footer { text-align: center; margin-top: 24px; padding-top: 16px; border-top: 1px solid #ebeef5; }
+.footer .el-button { min-width: 100px; }
+</style>

+ 175 - 0
pc/src/views/RuleGroupList.vue

@@ -0,0 +1,175 @@
+<template>
+  <div class="rule-group-list">
+    <el-card>
+      <div slot="header" class="search-section">
+        <div class="search-row">
+          <el-form :model="searchForm" label-width="100px" inline>
+            <el-form-item label="规则组编号:">
+              <el-input v-model="searchForm.id" placeholder="请输入" clearable style="width: 180px" />
+            </el-form-item>
+            <el-form-item label="规则组名称:">
+              <el-input v-model="searchForm.name" placeholder="请输入" clearable style="width: 180px" />
+            </el-form-item>
+            <el-form-item label="规则组类型:">
+              <el-select v-model="searchForm.type" clearable placeholder="全部" style="width: 160px">
+                <el-option
+                  v-for="opt in groupTypes"
+                  :key="opt.value"
+                  :label="opt.label"
+                  :value="opt.value"
+                />
+              </el-select>
+            </el-form-item>
+            <div class="button-group">
+              <el-button type="primary" @click="handleSearch">查询</el-button>
+              <el-button @click="handleReset">重置</el-button>
+            </div>
+          </el-form>
+        </div>
+        <div class="action-row">
+          <el-button type="primary" icon="el-icon-plus" @click="handleCreate">新增规则组</el-button>
+        </div>
+      </div>
+
+      <el-table :data="pagedData" stripe border style="width: 100%">
+        <el-table-column label="操作" width="220" fixed="left">
+          <template slot-scope="scope">
+            <el-button type="text" size="small" @click="handleView(scope.row)">查看详情</el-button>
+            <template v-if="scope.row.status === 'draft'">
+              <el-button type="text" size="small" @click="handleEdit(scope.row)">编辑</el-button>
+              <el-button type="text" size="small" @click="handlePublish(scope.row)">提交审核</el-button>
+              <el-button
+                type="text"
+                size="small"
+                style="color: #F56C6C"
+                @click="handleDelete(scope.row)"
+              >删除</el-button>
+            </template>
+          </template>
+        </el-table-column>
+        <el-table-column prop="id" label="规则组编号" width="170" />
+        <el-table-column prop="name" label="规则组名称" min-width="180" show-overflow-tooltip />
+        <el-table-column label="规则组类型" width="130">
+          <template slot-scope="scope">{{ typeLabel(scope.row.type) }}</template>
+        </el-table-column>
+        <el-table-column label="规则逻辑" width="110" align="center">
+          <template slot-scope="scope">
+            <el-tag size="small" :type="scope.row.logic === 'AND' ? '' : 'warning'">
+              {{ scope.row.logic === 'AND' ? '且(AND)' : '或(OR)' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="规则数量" width="90" align="center">
+          <template slot-scope="scope">{{ (scope.row.rules || []).length }}</template>
+        </el-table-column>
+        <el-table-column label="状态" width="100" align="center">
+          <template slot-scope="scope">
+            <el-tag :type="scope.row.status === 'online' ? 'success' : 'info'" size="small">
+              {{ scope.row.status === 'online' ? '已上线' : '新建' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="creator" label="创建人" width="100" />
+        <el-table-column prop="createdAt" label="创建时间" width="170" />
+      </el-table>
+
+      <div class="pagination-section">
+        <span class="total">共 {{ filteredData.length }} 条</span>
+        <el-pagination
+          layout="prev, pager, next, sizes, jumper"
+          :current-page.sync="page.current"
+          :page-size.sync="page.size"
+          :page-sizes="[10, 20, 50]"
+          :total="filteredData.length"
+        />
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import ruleGroupStore from '@/store/ruleGroupStore'
+import { GROUP_TYPES } from '@/config/metaRules'
+
+export default {
+  name: 'RuleGroupList',
+  data() {
+    return {
+      searchForm: { id: '', name: '', type: '' },
+      page: { current: 1, size: 10 },
+      groupTypes: GROUP_TYPES
+    }
+  },
+  computed: {
+    filteredData() {
+      return ruleGroupStore.query(this.searchForm)
+    },
+    pagedData() {
+      const start = (this.page.current - 1) * this.page.size
+      return this.filteredData.slice(start, start + this.page.size)
+    }
+  },
+  methods: {
+    typeLabel(type) {
+      const item = this.groupTypes.find((g) => g.value === type)
+      return item ? item.label : type
+    },
+    handleSearch() {
+      this.page.current = 1
+    },
+    handleReset() {
+      this.searchForm = { id: '', name: '', type: '' }
+      this.page.current = 1
+    },
+    handleCreate() {
+      this.$router.push('/customization/hsbc/rule-groups/add')
+    },
+    handleView(row) {
+      this.$router.push(`/customization/hsbc/rule-groups/view/${row.id}`)
+    },
+    handleEdit(row) {
+      this.$router.push(`/customization/hsbc/rule-groups/edit/${row.id}`)
+    },
+    handlePublish(row) {
+      this.$confirm(`确定要提交规则组「${row.name}」审核吗?审核通过后将不可编辑/删除。`, '提示', {
+        confirmButtonText: '提交',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        ruleGroupStore.publish(row.id)
+        this.$message.success('已提交审核(demo 默认通过)')
+      }).catch(() => {})
+    },
+    handleDelete(row) {
+      this.$confirm(`确定要删除规则组「${row.name}」吗?删除后无法恢复`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        ruleGroupStore.remove(row.id)
+        this.$message.success('删除成功')
+      }).catch(() => {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.rule-group-list { padding: 0; }
+
+.search-section { display: flex; flex-direction: column; }
+.search-row .el-form { display: flex; flex-wrap: wrap; gap: 8px 0; }
+.search-row .el-form-item { margin-right: 8px; margin-bottom: 8px; }
+
+.button-group { display: flex; gap: 5px; margin-top: 2px; }
+.action-row { margin-top: 12px; }
+
+.pagination-section {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  gap: 20px;
+  margin-top: 20px;
+}
+.pagination-section .total { color: #606266; font-size: 13px; }
+</style>

+ 127 - 0
pc/src/views/RuleGroupView.vue

@@ -0,0 +1,127 @@
+<template>
+  <div class="rule-group-view">
+    <el-card>
+      <div slot="header" class="header">
+        <span class="title">客群规则任务详情</span>
+        <span v-if="current" class="rg-id">编号:{{ current.id }}</span>
+      </div>
+
+      <div v-if="current" class="section">
+        <div class="section-title">基本信息</div>
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="规则组编号">{{ current.id }}</el-descriptions-item>
+          <el-descriptions-item label="规则组名称">{{ current.name }}</el-descriptions-item>
+          <el-descriptions-item label="规则组类型">{{ typeLabel(current.type) }}</el-descriptions-item>
+          <el-descriptions-item label="规则逻辑">
+            <el-tag size="small" :type="current.logic === 'AND' ? '' : 'warning'">
+              {{ current.logic === 'AND' ? '且(AND)' : '或(OR)' }}
+            </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="状态">
+            <el-tag :type="current.status === 'online' ? 'success' : 'info'" size="small">
+              {{ current.status === 'online' ? '已上线' : '新建' }}
+            </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="规则数量">{{ (current.rules || []).length }}</el-descriptions-item>
+          <el-descriptions-item label="创建人">{{ current.creator }}</el-descriptions-item>
+          <el-descriptions-item label="创建时间">{{ current.createdAt }}</el-descriptions-item>
+          <el-descriptions-item label="最后更新人">{{ current.updater }}</el-descriptions-item>
+          <el-descriptions-item label="最后更新时间">{{ current.updatedAt }}</el-descriptions-item>
+          <el-descriptions-item label="备注说明" :span="2">
+            {{ current.description || '-' }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </div>
+
+      <div v-if="current" class="section">
+        <div class="section-title">
+          规则组配置
+          <span class="section-tip">规则之间使用「{{ current.logic === 'AND' ? '且' : '或' }}」逻辑组合</span>
+        </div>
+        <el-table :data="current.rules || []" border style="width: 100%" empty-text="暂无规则">
+          <el-table-column prop="metaCode" label="元规则编号" width="110" align="center" />
+          <el-table-column label="元规则名称" min-width="180">
+            <template slot-scope="scope">{{ metaName(scope.row.metaCode) }}</template>
+          </el-table-column>
+          <el-table-column prop="ruleName" label="规则名称" min-width="160" show-overflow-tooltip />
+          <el-table-column label="规则描述" min-width="240" show-overflow-tooltip>
+            <template slot-scope="scope">{{ buildDesc(scope.row) }}</template>
+          </el-table-column>
+          <el-table-column label="规则参数" min-width="220" show-overflow-tooltip>
+            <template slot-scope="scope">
+              <span class="params-text">{{ buildParam(scope.row) }}</span>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+
+      <div class="footer">
+        <el-button @click="$router.push('/customization/hsbc/rule-groups')">返回</el-button>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import ruleGroupStore from '@/store/ruleGroupStore'
+import {
+  GROUP_TYPES,
+  getMetaRule,
+  buildRuleDescription,
+  buildRuleParams
+} from '@/config/metaRules'
+
+export default {
+  name: 'RuleGroupView',
+  data() {
+    return { current: null }
+  },
+  created() {
+    const target = ruleGroupStore.getById(this.$route.params.id)
+    if (!target) {
+      this.$message.error('规则组不存在')
+      this.$router.replace('/customization/hsbc/rule-groups')
+      return
+    }
+    this.current = target
+  },
+  methods: {
+    typeLabel(type) {
+      const item = GROUP_TYPES.find((g) => g.value === type)
+      return item ? item.label : type
+    },
+    metaName(code) {
+      const m = getMetaRule(code)
+      return m ? m.name : code
+    },
+    buildDesc(rule) {
+      return buildRuleDescription(rule.metaCode, rule.config) || '-'
+    },
+    buildParam(rule) {
+      return buildRuleParams(rule.metaCode, rule.config) || '-'
+    }
+  }
+}
+</script>
+
+<style scoped>
+.rule-group-view { padding: 0; }
+.header { display: flex; align-items: center; justify-content: space-between; }
+.title { font-size: 16px; font-weight: 600; color: #409EFF; }
+.rg-id { color: #909399; font-size: 13px; }
+
+.section { margin-bottom: 24px; }
+.section-title {
+  font-size: 14px; font-weight: 600; color: #303133;
+  background: #f5f7fa; padding: 10px 14px; border-radius: 4px;
+  margin-bottom: 16px;
+}
+.section-tip { font-weight: normal; color: #909399; margin-left: 12px; font-size: 12px; }
+
+.params-text {
+  font-family: 'Menlo', 'Consolas', monospace;
+  color: #606266; font-size: 12px;
+}
+
+.footer { text-align: center; margin-top: 24px; padding-top: 16px; border-top: 1px solid #ebeef5; }
+</style>

+ 180 - 143
pc/src/views/ViewEquityProduct.vue

@@ -2,12 +2,12 @@
   <div class="view-equity-product">
     <el-card>
       <div slot="header" class="header-title">
-        <span>查看权益商品</span>
+        <span>查看权益</span>
       </div>
 
       <!-- 步骤条 -->
       <el-steps :active="activeStep" simple style="margin-bottom: 20px;">
-        <el-step title="填写商品信息" icon="el-icon-info"></el-step>
+        <el-step title="填写权益信息" icon="el-icon-info"></el-step>
         <el-step title="设置规则" icon="el-icon-setting"></el-step>
         <el-step title="更多设置" icon="el-icon-monitor"></el-step>
       </el-steps>
@@ -20,7 +20,7 @@
           <el-form :model="form" label-width="120px" label-position="left">
             <el-row :gutter="20">
               <el-col :span="8">
-                <el-form-item label="商品来源">
+                <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>
@@ -29,15 +29,14 @@
                 </el-form-item>
               </el-col>
               <el-col :span="8" v-if="form.source === '荣数商品'">
-                <el-form-item label="选择商品">
+                <el-form-item label="选择权益">
                   <el-input :value="form.rongshuProduct" readonly></el-input>
                 </el-form-item>
               </el-col>
               <el-col :span="8" v-if="form.source === '银行自有'">
-                <el-form-item label="商品类型">
+                <el-form-item label="权益类型">
                   <el-radio-group v-model="form.productType" disabled>
                     <el-radio-button label="券码"></el-radio-button>
-                    <el-radio-button label="微信立减金"></el-radio-button>
                   </el-radio-group>
                 </el-form-item>
               </el-col>
@@ -47,39 +46,41 @@
                 </el-form-item>
               </el-col>
             </el-row>
-            <el-row :gutter="20" v-if="form.source === '银行自有' && form.productType === '微信立减金'">
-              <el-col :span="8">
-                <el-form-item label="面额">
-                  <el-input :value="`满 ${form.faceValueThreshold || 0} 减 ${form.faceValueDiscount || 0}`" readonly></el-input>
-                </el-form-item>
-              </el-col>
-              <el-col :span="8">
-                <el-form-item label="库存数">
-                  <el-input :value="form.stockCount" readonly></el-input>
-                </el-form-item>
-              </el-col>
-            </el-row>
             <el-row :gutter="20">
               <el-col :span="8" v-if="form.source === '第三方'">
-                <el-form-item label="外部商品链接">
-                  <el-select v-model="form.externalLink" placeholder="请选择外部商品链接" disabled>
+                <el-form-item label="外部权益链接">
+                  <el-select v-model="form.externalLink" placeholder="请选择外部权益链接" disabled>
                     <el-option label="太古地产" value="太古地产"></el-option>
                     <el-option label="龙腾" value="龙腾"></el-option>
-                    <el-option label="自定义" value="自定义"></el-option>
+                    <el-option label="h5链接" value="h5链接"></el-option>
+                    <el-option label="小程序链接" value="小程序链接"></el-option>
                   </el-select>
                 </el-form-item>
               </el-col>
+              <el-col :span="8" v-if="form.source === '第三方' && form.externalLink === 'h5链接'" :gutter="20">
+                <el-form-item label="h5链接">
+                  <el-input v-model="form.customLink" placeholder="请输入h5链接" readonly></el-input>
+                </el-form-item>
+              </el-col>
               <el-col :span="8" v-if="form.source === '第三方'">
-                <el-form-item label="跳转触发页">
-                  <el-select v-model="form.externalJumpMethod" placeholder="请选择跳转触发页" disabled>
-                    <el-option label="权益列表页" value="权益列表页"></el-option>
-                    <el-option label="权益详情页" value="权益详情页"></el-option>
-                  </el-select>
+                <el-form-item label-width="140px">
+                  <span slot="label" style="white-space: nowrap;">是否有详情页<el-tooltip content="没有详情页时直接从首页/列表页跳转" placement="top"><i class="el-icon-question tip-icon"></i></el-tooltip></span>
+                  <el-radio-group v-model="form.externalJumpMethod" disabled>
+                    <el-radio-button label="没有"></el-radio-button>
+                    <el-radio-button label="有"></el-radio-button>
+                  </el-radio-group>
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20" v-if="form.source === '第三方' && form.externalLink === '小程序链接'">
+              <el-col :span="8">
+                <el-form-item label="小程序appid">
+                  <el-input v-model="form.miniAppId" readonly></el-input>
                 </el-form-item>
               </el-col>
-              <el-col :span="8" v-if="form.source === '第三方' && form.externalLink === '自定义'" :gutter="20">
-                <el-form-item label="自定义链接">
-                  <el-input v-model="form.customLink" placeholder="请输入自定义链接" readonly></el-input>
+              <el-col :span="8">
+                <el-form-item label="小程序页面地址" label-width="140px">
+                  <el-input v-model="form.miniAppPath" readonly></el-input>
                 </el-form-item>
               </el-col>
             </el-row>
@@ -93,11 +94,6 @@
                 </el-col>
               </el-row>
               <el-row :gutter="20">
-                <el-col :span="8" v-if="form.source === '银行自有'">
-                  <el-form-item label="SKU编号">
-                    <el-input v-model="form.skuNo" readonly></el-input>
-                  </el-form-item>
-                </el-col>
                 <el-col :span="8">
                   <el-form-item label="权益名称">
                     <el-input v-model="form.equityName" readonly></el-input>
@@ -111,34 +107,115 @@
               </el-row>
               <el-row :gutter="20">
                 <el-col :span="8">
-                  <el-form-item label="权益列表图标">
+                  <el-form-item>
+                    <span slot="label">权益图标
+                      <el-tooltip content="展示在权益首页和权益列表页" placement="top">
+                        <i class="el-icon-question tip-icon"></i>
+                      </el-tooltip>
+                    </span>
                     <div v-if="form.listIcon && form.listIcon.length" class="image-preview">
                       <img :src="form.listIcon[0]" class="preview-img" />
                     </div>
                     <span v-else class="no-data">暂无图片</span>
                   </el-form-item>
                 </el-col>
+              </el-row>
+            </template>
+
+            <template v-else-if="form.source === '第三方'">
+              <el-row :gutter="20">
                 <el-col :span="8">
-                  <el-form-item label="权益主图">
-                    <div v-if="form.mainImage && form.mainImage.length" class="image-preview">
-                      <img :src="form.mainImage[0]" class="preview-img" />
-                    </div>
-                    <span v-else class="no-data">暂无图片</span>
+                  <el-form-item label="权益名称">
+                    <el-input v-model="form.equityName" readonly></el-input>
+                  </el-form-item>
+                </el-col>
+                <el-col :span="8">
+                  <el-form-item label="权益副标题">
+                    <el-input v-model="form.subTitle" readonly></el-input>
                   </el-form-item>
                 </el-col>
               </el-row>
-              <el-row :gutter="20">
-                <el-col :span="12">
-                  <el-form-item label="权益概述">
-                    <div class="rich-text-view" v-html="form.overview || '暂无内容'"></div>
+              <el-row :gutter="20" v-if="form.externalJumpMethod === '没有'">
+                <el-col :span="8">
+                  <el-form-item>
+                    <span slot="label">权益图标
+                      <el-tooltip content="展示在权益首页和权益列表页" placement="top">
+                        <i class="el-icon-question tip-icon"></i>
+                      </el-tooltip>
+                    </span>
+                    <div v-if="form.listIcon && form.listIcon.length" class="image-preview">
+                      <img :src="form.listIcon[0]" class="preview-img" />
+                    </div>
+                    <span v-else class="no-data">暂无图片</span>
                   </el-form-item>
                 </el-col>
-                <el-col :span="12">
-                  <el-form-item label="权益详情">
-                    <div class="rich-text-view" v-html="form.details || '暂无内容'"></div>
+              </el-row>
+              <el-row v-if="form.externalJumpMethod === '有'" :gutter="20">
+                <el-col :span="8">
+                  <el-form-item>
+                    <span slot="label">权益图标
+                      <el-tooltip content="展示在权益首页和权益列表页" placement="top">
+                        <i class="el-icon-question tip-icon"></i>
+                      </el-tooltip>
+                    </span>
+                    <div v-if="form.listIcon && form.listIcon.length" class="image-preview">
+                      <img :src="form.listIcon[0]" class="preview-img" />
+                    </div>
+                    <span v-else class="no-data">暂无图片</span>
                   </el-form-item>
                 </el-col>
               </el-row>
+            </template>
+
+            <el-row :gutter="20">
+              <el-col :span="8">
+                <el-form-item label="SIP商品编号">
+                  <el-input v-model="form.skuNo" readonly></el-input>
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </el-form>
+        </el-card>
+
+        <!-- 详情信息 -->
+        <el-card v-if="shouldShowDetailCard" shadow="never" class="section-card">
+          <div slot="header" class="section-title">详情信息</div>
+          <el-form :model="form" label-width="120px" label-position="left">
+            <el-row :gutter="20">
+              <el-col :span="8">
+                <el-form-item label="权益主图">
+                  <div v-if="form.mainImage && form.mainImage.length" class="image-preview">
+                    <img :src="form.mainImage[0]" class="preview-img" />
+                  </div>
+                  <span v-else class="no-data">暂无图片</span>
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="权益介绍">
+                  <el-input
+                    v-model="form.introduction"
+                    type="textarea"
+                    :rows="4"
+                    readonly
+                  ></el-input>
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="权益内容">
+                  <div class="rich-text-view" v-html="form.overview || '暂无内容'"></div>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="权益说明">
+                  <div class="rich-text-view" v-html="form.details || '暂无内容'"></div>
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <template v-if="form.source === '银行自有' || form.source === '荣数商品'">
               <el-row :gutter="20">
                 <el-col :span="24">
                   <el-form-item label="下单成功短信">
@@ -166,67 +243,12 @@
               </el-row>
               <el-row :gutter="20">
                 <el-col :span="8">
-                  <el-form-item label="领取成功按钮文案">
+                  <el-form-item label="领取成功按钮">
                     <el-input :value="form.successButtonText" readonly></el-input>
                   </el-form-item>
                 </el-col>
               </el-row>
             </template>
-
-            <template v-else-if="form.source === '第三方'">
-              <el-row :gutter="20">
-                <el-col :span="8">
-                  <el-form-item label="权益名称">
-                    <el-input v-model="form.equityName" readonly></el-input>
-                  </el-form-item>
-                </el-col>
-                <el-col :span="8">
-                  <el-form-item label="权益副标题">
-                    <el-input v-model="form.subTitle" readonly></el-input>
-                  </el-form-item>
-                </el-col>
-              </el-row>
-              <el-row :gutter="20" v-if="form.externalJumpMethod === '权益列表页'">
-                <el-col :span="8">
-                  <el-form-item label="权益列表图标">
-                    <div v-if="form.listIcon && form.listIcon.length" class="image-preview">
-                      <img :src="form.listIcon[0]" class="preview-img" />
-                    </div>
-                    <span v-else class="no-data">暂无图片</span>
-                  </el-form-item>
-                </el-col>
-              </el-row>
-              <el-row v-if="form.externalJumpMethod === '权益详情页'" :gutter="20">
-                <el-col :span="8">
-                  <el-form-item label="权益列表图标">
-                    <div v-if="form.listIcon && form.listIcon.length" class="image-preview">
-                      <img :src="form.listIcon[0]" class="preview-img" />
-                    </div>
-                    <span v-else class="no-data">暂无图片</span>
-                  </el-form-item>
-                </el-col>
-                <el-col :span="8">
-                  <el-form-item label="权益主图">
-                    <div v-if="form.mainImage && form.mainImage.length" class="image-preview">
-                      <img :src="form.mainImage[0]" class="preview-img" />
-                    </div>
-                    <span v-else class="no-data">暂无图片</span>
-                  </el-form-item>
-                </el-col>
-              </el-row>
-              <el-row v-if="form.externalJumpMethod === '权益详情页'" :gutter="20">
-                <el-col :span="12">
-                  <el-form-item label="权益概述">
-                    <div class="rich-text-view" v-html="form.overview || '暂无内容'"></div>
-                  </el-form-item>
-                </el-col>
-                <el-col :span="12">
-                  <el-form-item label="权益详情">
-                    <div class="rich-text-view" v-html="form.details || '暂无内容'"></div>
-                  </el-form-item>
-                </el-col>
-              </el-row>
-            </template>
           </el-form>
         </el-card>
 
@@ -280,19 +302,13 @@
                 <el-switch v-model="form.claimGroupEnabled" disabled></el-switch>
                 <template v-if="form.claimGroupEnabled">
                   <div style="width: 600px;background: #f1f2f5;padding: 20px;margin-top: 10px;">
-                    <el-form-item label="是否潜客领取" label-width="110px" style="margin-bottom: 12px;">
-                      <el-radio-group v-model="form.claimGroupIsPotential" disabled>
-                        <el-radio :label="true">是</el-radio>
-                        <el-radio :label="false">否</el-radio>
-                      </el-radio-group>
-                    </el-form-item>
-                    <el-form-item v-if="!form.claimGroupIsPotential" label="客群奖励活动" label-width="110px" style="margin-bottom: 12px;">
-                      <el-select v-model="form.claimGroupTagId" disabled placeholder="请选择客群奖励活动" style="width: 100%">
-                        <el-option label="客群奖励活动A" value="客群奖励活动A"></el-option>
-                        <el-option label="客群奖励活动B" value="客群奖励活动B"></el-option>
-                        <el-option label="客群奖励活动C" value="客群奖励活动C"></el-option>
-                      </el-select>
-                    </el-form-item>
+                    <el-alert
+                      title="需在【权益管理-权益发放】中选择权益发放,未发放的权益移动端不展示"
+                      type="info"
+                      :closable="false"
+                      show-icon
+                      style="margin-bottom: 12px;"
+                    ></el-alert>
                     <div style="font-size: 14px;color: #606266;margin-bottom: 8px;font-weight: 500;">不满足提示</div>
                     <el-form-item label="提示内容" label-width="80px" style="margin-bottom: 12px;">
                       <el-input :value="form.claimDenyMessage" readonly></el-input>
@@ -316,7 +332,7 @@
         <el-card shadow="never" class="section-card">
           <div slot="header" class="section-title">显示页面</div>
           <el-form :model="form" label-width="120px" label-position="left">
-            <el-form-item label="首页">
+            <el-form-item label="权益页/列表页">
               <el-radio-group v-model="form.showOnHome" disabled>
                 <el-radio label="显示">显示</el-radio>
                 <el-radio label="不显示">不显示</el-radio>
@@ -422,9 +438,11 @@ export default {
         shareEnabled: false,
         shareImage: [],
         shareName: '',
-        externalJumpMethod: '权益列表页',
+        externalJumpMethod: '没有',
         externalLink: '太古地产',
         customLink: '',
+        miniAppId: '',
+        miniAppPath: '',
         skuNo: '',
         equityName: '',
         productId: '',
@@ -432,6 +450,7 @@ export default {
         subTitle: '',
         listIcon: [],
         mainImage: [],
+        introduction: '',
         overview: '',
         details: '',
         smsEnabled: false,
@@ -459,7 +478,14 @@ export default {
       operationLogs: []
     }
   },
-  computed: {},
+  computed: {
+    shouldShowDetailCard() {
+      const s = this.form.source
+      if (s === '银行自有' || s === '荣数商品') return true
+      if (s === '第三方' && this.form.externalJumpMethod === '有') return true
+      return false
+    }
+  },
   created() {
     this.loadData()
   },
@@ -468,18 +494,20 @@ export default {
       const id = this.$route.params.id
       // TODO: 根据 id 从接口获取数据,这里用 mock 数据示例
       const mockMap = {
-        P001: {
+        100001: {
           source: '银行自有',
           externalJumpMethod: '',
           externalLink: '',
           customLink: '',
+        miniAppId: '',
+        miniAppPath: '',
           skuNo: 'SKU001',
-          equityName: '权益商品A',
+          equityName: '权益A',
           subTitle: '副标题示例',
           listIcon: [],
           mainImage: [],
-          overview: '<p>权益概述内容</p>',
-          details: '<p>权益详情内容</p>',
+          overview: '<p>权益内容内容</p>',
+          details: '<p>权益说明内容</p>',
           smsEnabled: true,
           smsTemplateId: 'SMS_TPL_001',
           smsTemplate: '【银行】您购买的${productName}已支付成功,卡号:${exchangeNum},卡密:${exchangePwd},有效期至${expireDate},详情:${qrCodeDownPaths}',
@@ -498,13 +526,15 @@ export default {
           claimDenyButtonText: '',
           claimDenyButtonLink: ''
         },
-        P002: {
+        100002: {
           source: '第三方',
-          externalJumpMethod: '权益列表页',
+          externalJumpMethod: '没有',
           externalLink: '太古地产',
           customLink: '',
+        miniAppId: '',
+        miniAppPath: '',
           skuNo: 'SKU002',
-          equityName: '权益商品B',
+          equityName: '权益B',
           subTitle: '外部列表页副标题',
           listIcon: [],
           mainImage: [],
@@ -529,18 +559,20 @@ export default {
           claimDenyButtonText: '去升级',
           claimDenyButtonLink: 'https://example.com/upgrade'
         },
-        P003: {
+        100003: {
           source: '第三方',
-          externalJumpMethod: '权益详情页',
+          externalJumpMethod: '',
           externalLink: '龙腾',
           customLink: '',
+        miniAppId: '',
+        miniAppPath: '',
           skuNo: 'SKU003',
-          equityName: '权益商品C',
+          equityName: '权益C',
           subTitle: '外部详情页副标题',
           listIcon: [],
           mainImage: [],
-          overview: '<p>外部详情页权益概述</p>',
-          details: '<p>外部详情页权益详情</p>',
+          overview: '<p>外部详情页权益内容</p>',
+          details: '<p>外部详情页权益说明</p>',
           smsEnabled: false,
           smsTemplateId: '',
           smsTemplate: '',
@@ -559,18 +591,18 @@ export default {
           claimDenyButtonText: '',
           claimDenyButtonLink: ''
         },
-        P004: {
+        100004: {
           source: '第三方',
-          externalJumpMethod: '权益详情页',
-          externalLink: '自定义',
+          externalJumpMethod: '',
+          externalLink: 'h5链接',
           customLink: 'https://example.com/custom',
           skuNo: 'SKU004',
-          equityName: '权益商品D',
+          equityName: '权益D',
           subTitle: '外部自定义链接副标题',
           listIcon: [],
           mainImage: [],
-          overview: '<p>自定义链接权益概述</p>',
-          details: '<p>自定义链接权益详情</p>',
+          overview: '<p>自定义链接权益内容</p>',
+          details: '<p>自定义链接权益说明</p>',
           smsEnabled: false,
           smsTemplateId: '',
           smsTemplate: '',
@@ -593,30 +625,30 @@ export default {
       if (id && mockMap[id]) {
         Object.assign(this.form, mockMap[id])
       } else if (id) {
-        Object.assign(this.form, mockMap['P001'])
+        Object.assign(this.form, mockMap['100001'])
       }
 
       const logsMap = {
-        P001: [
+        100001: [
           { operator: '张三', operateTime: '2026-01-10 09:32:15', action: '创建' },
           { operator: '李四', operateTime: '2026-01-10 14:08:42', action: '审核' },
           { operator: '赵六', operateTime: '2026-03-01 16:45:30', action: '下架' }
         ],
-        P002: [
+        100002: [
           { operator: '张三', operateTime: '2026-01-12 10:15:00', action: '创建' },
           { operator: '李四', operateTime: '2026-01-12 15:22:18', action: '审核' }
         ],
-        P003: [
+        100003: [
           { operator: '钱七', operateTime: '2026-01-15 11:03:27', action: '创建' },
           { operator: '李四', operateTime: '2026-01-15 17:30:09', action: '审核' }
         ],
-        P004: [
+        100004: [
           { operator: '钱七', operateTime: '2026-02-01 09:48:12', action: '创建' },
           { operator: '李四', operateTime: '2026-02-01 14:11:36', action: '审核' },
           { operator: '赵六', operateTime: '2026-04-05 18:22:47', action: '下架' }
         ]
       }
-      this.operationLogs = (id && logsMap[id]) ? logsMap[id] : (id ? logsMap['P001'] : [])
+      this.operationLogs = (id && logsMap[id]) ? logsMap[id] : (id ? logsMap['100001'] : [])
     },
     actionTagType(action) {
       const map = {
@@ -647,6 +679,11 @@ export default {
 .view-equity-product {
   padding: 20px;
 }
+.tip-icon {
+  color: #909399;
+  cursor: pointer;
+  font-size: 14px;
+}
 .header-title {
   font-size: 18px;
   font-weight: 500;

+ 1114 - 0
页面管理.html

@@ -0,0 +1,1114 @@
+<!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>