Quellcode durchsuchen

券码管理:待导入券码编辑弹窗支持上传券码文件

- 编辑弹窗去掉权益商品,改为重新上传券码文件(必填)并加提交校验
- 补充券码管理优化需求文档的编辑弹窗说明
- 一并提交借记卡权益相关页面与 Mock/路由调整、文档整理(新增移动端权益等文档,移除旧 skill 与过时需求文档)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
csk vor 2 Wochen
Ursprung
Commit
c70887a266
21 geänderte Dateien mit 1437 neuen und 975 gelöschten Zeilen
  1. BIN
      .resources/image-1782119828893.png
  2. BIN
      .resources/image-1782119847775.png
  3. BIN
      .resources/image-1782119877480.png
  4. 133 0
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/components/CouponBatchPicker.vue
  5. 3 0
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/components/Layout.vue
  6. 285 4
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/mock/index.js
  7. 6 0
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/router/index.js
  8. 21 3
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/views/AddEquityProduct.vue
  9. 15 4
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/views/CouponAuditList.vue
  10. 566 0
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/views/CouponBatchList.vue
  11. 21 3
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/views/EditEquityProduct.vue
  12. 0 436
      工作空间/产研/权益/汇丰全行/需求文档/prd-generator-from-demo/SKILL.md
  13. 0 4
      工作空间/产研/权益/汇丰全行/需求文档/prd-generator-from-demo/agents/openai.yaml
  14. 34 0
      工作空间/产研/权益/汇丰全行/需求文档/券码管理优化-20260622.md
  15. 85 1
      工作空间/产研/权益/汇丰全行/需求文档/权益介绍页移动端.md
  16. 88 1
      工作空间/产研/权益/汇丰全行/需求文档/权益首页移动端.md
  17. 142 5
      工作空间/产研/权益/汇丰全行/需求文档/汇丰借记卡移动端——权益.md
  18. 8 0
      工作空间/产研/权益/汇丰全行/需求文档/汇丰银行权益中台-FSD.md
  19. BIN
      工作空间/产研/权益/汇丰全行/需求文档/汇丰需求文档 (1).pdf
  20. 0 514
      工作空间/产研/权益/汇丰全行/需求文档/组装页.md
  21. 30 0
      工作空间/产研/权益/汇丰全行/需求文档/组装页移动端.md

BIN
.resources/image-1782119828893.png


BIN
.resources/image-1782119847775.png


BIN
.resources/image-1782119877480.png


+ 133 - 0
工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/components/CouponBatchPicker.vue

@@ -0,0 +1,133 @@
+<template>
+  <div class="coupon-batch-picker">
+    <el-button @click="dialogVisible = true">选择批次</el-button>
+    <div v-if="modelValue" class="picked">
+      已选批次:<span class="batch-id">{{ modelValue }}</span>
+      <el-button type="text" size="small" @click="clearPick">清除</el-button>
+    </div>
+
+    <el-dialog title="选择券码批次" v-model="dialogVisible" width="760px" @open="loadData">
+      <div class="picker-tip">仅展示未关联权益商品且审核通过的待导入券码批次</div>
+      <div class="search-section">
+        <el-form :model="search" inline label-width="70px">
+          <el-form-item label="批次ID:">
+            <el-input v-model="search.batchId" placeholder="精准匹配" style="width: 220px" />
+          </el-form-item>
+          <div class="button-group" style="margin-left: 12px;">
+            <el-button type="primary" @click="loadData">查询</el-button>
+            <el-button @click="resetSearch">重置</el-button>
+          </div>
+        </el-form>
+      </div>
+      <el-table :data="pagedData" stripe style="width: 100%">
+        <el-table-column prop="batchId" label="批次ID" width="180" />
+        <el-table-column prop="importCount" label="券码数量" width="120" align="center" />
+        <el-table-column prop="creator" label="创建人" width="120" align="center" />
+        <el-table-column prop="createTime" label="创建时间" min-width="180" align="center" />
+        <el-table-column label="操作" width="100" fixed="right">
+          <template #default="scope">
+            <el-button type="text" size="small" @click="choose(scope.row)">选择</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="pagination-section">
+        <span>Total {{ list.length }}</span>
+        <el-pagination
+          v-model:current-page="currentPage"
+          :page-size="pageSize"
+          layout="prev, pager, next, jumper"
+          :total="list.length"
+        />
+      </div>
+      <template #footer>
+        <el-button @click="dialogVisible = false">关闭</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import request from '@/utils/request'
+
+export default {
+  name: 'CouponBatchPicker',
+  props: {
+    modelValue: { type: String, default: '' }
+  },
+  emits: ['update:modelValue'],
+  data() {
+    return {
+      dialogVisible: false,
+      search: { batchId: '' },
+      list: [],
+      currentPage: 1,
+      pageSize: 10
+    }
+  },
+  computed: {
+    pagedData() {
+      const start = (this.currentPage - 1) * this.pageSize
+      return this.list.slice(start, start + this.pageSize)
+    }
+  },
+  methods: {
+    loadData() {
+      const params = new URLSearchParams()
+      params.append('status', 'approved')
+      params.append('unlinked', '1')
+      if (this.search.batchId) params.append('batchId', this.search.batchId)
+      request.get('/api/coupon/pending-list?' + params.toString()).then(res => {
+        this.list = res.data || []
+        this.currentPage = 1
+      }).catch(() => {})
+    },
+    resetSearch() {
+      this.search = { batchId: '' }
+      this.loadData()
+    },
+    choose(row) {
+      this.$emit('update:modelValue', row.batchId)
+      this.dialogVisible = false
+    },
+    clearPick() {
+      this.$emit('update:modelValue', '')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.picked {
+  margin-top: 10px;
+  font-size: 14px;
+  color: #606266;
+}
+
+.picked .batch-id {
+  color: #303133;
+  font-weight: 500;
+}
+
+.picker-tip {
+  font-size: 13px;
+  color: #909399;
+  margin-bottom: 12px;
+}
+
+.search-section {
+  margin-bottom: 8px;
+}
+
+.button-group {
+  display: flex;
+  gap: 5px;
+}
+
+.pagination-section {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  gap: 20px;
+  margin-top: 16px;
+}
+</style>

+ 3 - 0
工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/components/Layout.vue

@@ -44,6 +44,9 @@
               <el-menu-item index="/customization/hsbc/audit">
                 权益审核
               </el-menu-item>
+              <el-menu-item index="/customization/hsbc/coupon-batches">
+                券码管理
+              </el-menu-item>
               <el-menu-item index="/customization/hsbc/coupon-audit">
                 券码审核
               </el-menu-item>

+ 285 - 4
工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/mock/index.js

@@ -670,7 +670,7 @@ function nextAuditId() {
   return 'AR' + ymd + String(auditIdCounter).padStart(4, '0')
 }
 
-function snapshotOf(equity) {
+function snapshotOf(equity, couponBatchId) {
   return {
     equityName: equity.equityName,
     source: equity.source,
@@ -680,7 +680,8 @@ function snapshotOf(equity) {
     jumpLink: equity.jumpLink,
     skuNo: equity.skuNo,
     merchantName: equity.merchantName,
-    subTitle: equity.subTitle || ''
+    subTitle: equity.subTitle || '',
+    couponBatchId: couponBatchId !== undefined ? (couponBatchId || '') : (equity.couponBatchId || '')
   }
 }
 
@@ -748,7 +749,8 @@ Mock.mock(/\/api\/equity\/save-draft/, 'post', (options) => {
     showOnHome: body.showOnHome || '不显示',
     showOnAssembly: body.showOnAssembly || '不显示',
     assemblyPages: body.assemblyPages || [],
-    subTitle: body.subTitle || ''
+    subTitle: body.subTitle || '',
+    couponBatchId: body.couponBatchId || ''
   }
   equityList.unshift(newRow)
   return { code: 200, data: { productId: newId }, msg: '草稿已保存' }
@@ -794,7 +796,8 @@ Mock.mock(/\/api\/equity\/submit/, 'post', (options) => {
       showOnHome: body.showOnHome || '不显示',
       showOnAssembly: body.showOnAssembly || '不显示',
       assemblyPages: body.assemblyPages || [],
-      subTitle: body.subTitle || ''
+      subTitle: body.subTitle || '',
+      couponBatchId: body.couponBatchId || ''
     }
     equityList.unshift(newRow)
     auditList.unshift({
@@ -954,6 +957,10 @@ Mock.mock(/\/api\/audit\/approve/, 'post', (options) => {
 
   if (audit.auditType === 'new') {
     equityList[eqIdx] = { ...eq, shelfStatus: '已上架', auditStatus: '审核通过', reviewer: auditor, reviewTime: now }
+    // 券码商品且关联了待导入批次:首次上架通过时模拟 SIP 导入、移入已导入列表
+    if (audit.snapshot && audit.snapshot.couponBatchId) {
+      importPendingBatch(audit.snapshot.couponBatchId, eq.productId, eq.equityName)
+    }
   } else if (audit.auditType === 'offshelf') {
     equityList[eqIdx] = { ...eq, shelfStatus: '未上架', auditStatus: '审核通过', reviewer: auditor, reviewTime: now }
   }
@@ -1734,6 +1741,14 @@ Mock.mock(/\/api\/coupon-audit\/approve/, 'post', (options) => {
   const audit = couponAuditList[idx]
   if (audit.auditStatus !== 'pending') return { code: 400, data: null, msg: '该审核单已完成' }
   couponAuditList[idx] = { ...audit, auditStatus: 'approved', auditorName: body.auditorName || '王五', auditTime: nowStr() }
+  // 联动待导入记录:已关联权益商品 → 模拟 SIP 导入、移入已导入列表;未关联 → 仅置为审核通过,留在待导入列表
+  if (audit.batchId) {
+    const pending = pendingCouponList.find(p => p.batchId === audit.batchId)
+    if (pending) {
+      pending.auditStatus = 'approved'
+      if (pending.productId) importPendingBatch(pending.batchId)
+    }
+  }
   return { code: 200, data: null, msg: '审核通过' }
 })
 
@@ -1744,7 +1759,273 @@ Mock.mock(/\/api\/coupon-audit\/reject/, 'post', (options) => {
   const audit = couponAuditList[idx]
   if (audit.auditStatus !== 'pending') return { code: 400, data: null, msg: '该审核单已完成' }
   couponAuditList[idx] = { ...audit, auditStatus: 'rejected', auditorName: body.auditorName || '王五', auditTime: nowStr() }
+  if (audit.batchId) {
+    const pending = pendingCouponList.find(p => p.batchId === audit.batchId)
+    if (pending) pending.auditStatus = 'rejected'
+  }
   return { code: 200, data: null, msg: '已驳回' }
 })
 
+// ==================== 已导入券码批次(全部借记卡权益商品) ====================
+const couponBatchList = [
+  {
+    batchId: 'B100001', productId: 'Q100001', equityName: '权益A',
+    importCount: 1000, actualImportCount: 998, failCount: 2, remainingCount: 720,
+    effectiveTime: '2026-04-16 00:00:00', expireTime: '2026-12-31 23:59:59',
+    fileStatus: '处理成功', importTime: '2026-04-15 10:32:18', creator: '张三',
+    recycleRecords: [
+      { recycleTime: '2026-05-10 14:22:08', type: '未发放券码回收', operator: '张三', amount: 120 },
+      { recycleTime: '2026-04-28 09:15:33', type: '已过期券码回收', operator: '李四', amount: 80 }
+    ]
+  },
+  {
+    batchId: 'B100002', productId: 'Q100001', equityName: '权益A',
+    importCount: 500, actualImportCount: 500, failCount: 0, remainingCount: 320,
+    effectiveTime: '2026-04-21 00:00:00', expireTime: '2026-09-30 23:59:59',
+    fileStatus: '处理成功', importTime: '2026-04-20 14:15:42', creator: '李四',
+    recycleRecords: [
+      { recycleTime: '2026-04-12 16:40:51', type: '未发放券码回收', operator: '王五', amount: 200 }
+    ]
+  },
+  {
+    batchId: 'B100003', productId: 'Q100002', equityName: '权益B',
+    importCount: 800, actualImportCount: 0, failCount: 0, remainingCount: 0,
+    effectiveTime: '2026-05-08 00:00:00', expireTime: '2026-12-31 23:59:59',
+    fileStatus: '处理中', importTime: '2026-05-07 09:08:11', creator: '王五',
+    recycleRecords: []
+  },
+  {
+    batchId: 'B100004', productId: 'Q100002', equityName: '权益B',
+    importCount: 200, actualImportCount: 0, failCount: 200, remainingCount: 0,
+    effectiveTime: '2026-05-02 00:00:00', expireTime: '2026-10-31 23:59:59',
+    fileStatus: '处理失败', importTime: '2026-05-01 16:45:30', creator: '赵六',
+    recycleRecords: []
+  },
+  {
+    batchId: 'B100005', productId: 'Q100003', equityName: '权益C',
+    importCount: 1500, actualImportCount: 1500, failCount: 0, remainingCount: 1280,
+    effectiveTime: '2026-04-10 00:00:00', expireTime: '2026-12-31 23:59:59',
+    fileStatus: '处理成功', importTime: '2026-04-09 09:21:05', creator: '张三',
+    recycleRecords: []
+  },
+  {
+    batchId: 'B100006', productId: 'Q100003', equityName: '权益C',
+    importCount: 600, actualImportCount: 598, failCount: 2, remainingCount: 420,
+    effectiveTime: '2026-04-05 00:00:00', expireTime: '2026-11-30 23:59:59',
+    fileStatus: '处理成功', importTime: '2026-04-04 14:02:33', creator: '李四',
+    recycleRecords: []
+  },
+  {
+    batchId: 'B100007', productId: 'Q100004', equityName: '权益D',
+    importCount: 300, actualImportCount: 300, failCount: 0, remainingCount: 0,
+    effectiveTime: '2026-03-20 00:00:00', expireTime: '2026-09-30 23:59:59',
+    fileStatus: '处理成功', importTime: '2026-03-19 10:45:12', creator: '王五',
+    recycleRecords: []
+  },
+  {
+    batchId: 'B100008', productId: 'Q100004', equityName: '权益D',
+    importCount: 2000, actualImportCount: 1995, failCount: 5, remainingCount: 1620,
+    effectiveTime: '2026-03-15 00:00:00', expireTime: '2026-12-31 23:59:59',
+    fileStatus: '处理成功', importTime: '2026-03-14 16:30:48', creator: '张三',
+    recycleRecords: []
+  },
+  {
+    batchId: 'B100009', productId: 'Q100005', equityName: '权益E',
+    importCount: 450, actualImportCount: 450, failCount: 0, remainingCount: 130,
+    effectiveTime: '2026-03-01 00:00:00', expireTime: '2026-08-31 23:59:59',
+    fileStatus: '处理成功', importTime: '2026-02-28 11:18:27', creator: '李四',
+    recycleRecords: []
+  },
+  {
+    batchId: 'B100010', productId: 'Q100005', equityName: '权益E',
+    importCount: 800, actualImportCount: 796, failCount: 4, remainingCount: 510,
+    effectiveTime: '2026-02-20 00:00:00', expireTime: '2026-10-31 23:59:59',
+    fileStatus: '处理成功', importTime: '2026-02-19 15:55:09', creator: '王五',
+    recycleRecords: []
+  },
+  {
+    batchId: 'B100011', productId: 'Q100007', equityName: '微信立减金 5元(满10减5)',
+    importCount: 1200, actualImportCount: 1200, failCount: 0, remainingCount: 60,
+    effectiveTime: '2026-02-10 00:00:00', expireTime: '2026-07-31 23:59:59',
+    fileStatus: '处理成功', importTime: '2026-02-09 09:40:51', creator: '赵六',
+    recycleRecords: []
+  },
+  {
+    batchId: 'B100012', productId: 'Q100001', equityName: '权益A',
+    importCount: 350, actualImportCount: 348, failCount: 2, remainingCount: 0,
+    effectiveTime: '2026-01-15 00:00:00', expireTime: '2026-06-30 23:59:59',
+    fileStatus: '处理成功', importTime: '2026-01-14 13:27:36', creator: '张三',
+    recycleRecords: []
+  }
+]
+
+// 已导入券码列表:权益编号、批次ID 均为精准匹配
+Mock.mock(/\/api\/coupon\/batch-list/, 'get', (options) => {
+  const url = new URL(options.url, 'http://localhost')
+  const productId = url.searchParams.get('productId') || ''
+  const batchId = url.searchParams.get('batchId') || ''
+  let filtered = couponBatchList
+  if (productId) filtered = filtered.filter(b => b.productId === productId)
+  if (batchId) filtered = filtered.filter(b => b.batchId === batchId)
+  return { code: 200, data: filtered, msg: 'ok' }
+})
+
+// 导入券码:生成「待导入券码记录」+「券码审核单」,券码审核通过后才真正导入
+Mock.mock(/\/api\/coupon\/import/, 'post', (options) => {
+  const body = JSON.parse(options.body)
+  let productId = ''
+  let equityName = ''
+  if (body.productId) {
+    const equity = equityList.find(e => e.productId === body.productId)
+    if (!equity) return { code: 404, data: null, msg: '权益商品不存在' }
+    productId = equity.productId
+    equityName = equity.equityName
+  }
+  const batchId = nextPendingBatchId()
+  // 原型读不了上传文件,导入数量用 mock 随机数模拟
+  const importCount = 100 + Math.floor(Math.random() * 4900)
+  const createTime = nowStr()
+  const pending = { batchId, productId, equityName, importCount, creator: '当前用户', createTime, auditStatus: 'pending' }
+  pendingCouponList.unshift(pending)
+  couponAuditList.unshift(makeImportAudit(pending))
+  return { code: 200, data: { batchId }, msg: '已提交,等待券码审核' }
+})
+
+Mock.mock(/\/api\/coupon\/recycle/, 'post', (options) => {
+  const body = JSON.parse(options.body)
+  const batch = couponBatchList.find(b => b.batchId === body.batchId)
+  if (!batch) return { code: 404, data: null, msg: '批次不存在' }
+  const amount = Number(body.amount) || 0
+  if (amount < 1) return { code: 400, data: null, msg: '回收数量无效' }
+  // 未发放券码回收会扣减剩余数量;已过期券码回收仅记录,不扣减剩余库存
+  if (body.type === 'unissued') {
+    batch.remainingCount = Math.max(0, Number(batch.remainingCount) - amount)
+  }
+  const typeLabel = body.type === 'expired' ? '已过期券码回收' : '未发放券码回收'
+  if (!batch.recycleRecords) batch.recycleRecords = []
+  batch.recycleRecords.unshift({ recycleTime: nowStr(), type: typeLabel, operator: '当前用户', amount })
+  return { code: 200, data: null, msg: '回收成功' }
+})
+
+// ==================== 待导入券码记录(券码先导入、后关联权益) ====================
+function nextPendingBatchId() {
+  const d = new Date()
+  const p = n => String(n).padStart(2, '0')
+  return 'I' + d.getFullYear() + p(d.getMonth() + 1) + p(d.getDate()) + p(d.getHours()) + p(d.getMinutes()) + p(d.getSeconds())
+}
+
+const pendingCouponList = [
+  { batchId: 'I20260620090301', productId: '', equityName: '', importCount: 1000, creator: '张三', createTime: '2026-06-20 09:03:01', auditStatus: 'pending' },
+  { batchId: 'I20260620143512', productId: 'Q100001', equityName: '权益A', importCount: 500, creator: '李四', createTime: '2026-06-20 14:35:12', auditStatus: 'pending' },
+  { batchId: 'I20260618101055', productId: '', equityName: '', importCount: 1200, creator: '张三', createTime: '2026-06-18 10:10:55', auditStatus: 'approved' },
+  { batchId: 'I20260617161820', productId: '', equityName: '', importCount: 800, creator: '王五', createTime: '2026-06-17 16:18:20', auditStatus: 'approved' },
+  { batchId: 'I20260616113045', productId: 'Q100003', equityName: '权益C', importCount: 300, creator: '赵六', createTime: '2026-06-16 11:30:45', auditStatus: 'rejected' },
+  { batchId: 'I20260615153000', productId: '', equityName: '', importCount: 600, creator: '李四', createTime: '2026-06-15 15:30:00', auditStatus: 'rejected' }
+]
+
+// 待导入记录 → 已导入批次(模拟 SIP 导入成功)
+function importPendingBatch(batchId, productId, equityName) {
+  const idx = pendingCouponList.findIndex(p => p.batchId === batchId)
+  if (idx < 0) return false
+  const p = pendingCouponList[idx]
+  pendingCouponList.splice(idx, 1)
+  couponBatchList.unshift({
+    batchId: p.batchId,
+    productId: productId || p.productId,
+    equityName: equityName || p.equityName,
+    importCount: p.importCount,
+    actualImportCount: p.importCount,
+    failCount: 0,
+    remainingCount: p.importCount,
+    effectiveTime: '',
+    expireTime: '',
+    fileStatus: '处理成功',
+    importTime: nowStr(),
+    creator: p.creator,
+    recycleRecords: []
+  })
+  return true
+}
+
+// 由待导入记录派生对应的券码审核单
+let couponAuditSeq = 0
+function makeImportAudit(p) {
+  couponAuditSeq += 1
+  const reviewed = p.auditStatus !== 'pending'
+  return {
+    id: 'CPA' + p.batchId.slice(1) + String(couponAuditSeq).padStart(2, '0'),
+    type: 'import',
+    batchId: p.batchId,
+    productName: p.equityName || '',
+    productId: p.productId || '',
+    importCount: p.importCount,
+    effectiveTime: '',
+    expireTime: '',
+    creator: p.creator,
+    createTime: p.createTime,
+    auditorName: reviewed ? '王五' : '',
+    auditTime: reviewed ? p.createTime : '',
+    auditStatus: p.auditStatus
+  }
+}
+
+// 用待导入记录派生的审核单替换旧的占位 import 审核单(recycle 记录保留)
+for (let i = couponAuditList.length - 1; i >= 0; i--) {
+  if (couponAuditList[i].type === 'import') couponAuditList.splice(i, 1)
+}
+pendingCouponList.forEach(p => couponAuditList.push(makeImportAudit(p)))
+
+// 待导入券码列表(券码管理-待导入 Tab;选择批次弹窗传 status=approved&unlinked=1)
+Mock.mock(/\/api\/coupon\/pending-list/, 'get', (options) => {
+  const url = new URL(options.url, 'http://localhost')
+  const batchId = url.searchParams.get('batchId') || ''
+  const status = url.searchParams.get('status') || ''
+  const unlinked = url.searchParams.get('unlinked') || ''
+  let filtered = pendingCouponList
+  if (batchId) filtered = filtered.filter(p => p.batchId === batchId)
+  if (status) filtered = filtered.filter(p => p.auditStatus === status)
+  if (unlinked === '1') filtered = filtered.filter(p => !p.productId)
+  return { code: 200, data: filtered, msg: 'ok' }
+})
+
+// 编辑待导入记录(仅审核拒绝可编辑:可改关联权益商品,提交后重新进入待审核)
+Mock.mock(/\/api\/coupon\/pending-edit/, 'post', (options) => {
+  const body = JSON.parse(options.body)
+  const p = pendingCouponList.find(x => x.batchId === body.batchId)
+  if (!p) return { code: 404, data: null, msg: '记录不存在' }
+  if (p.auditStatus !== 'rejected') return { code: 400, data: null, msg: '仅审核拒绝的记录可编辑' }
+  let productId = ''
+  let equityName = ''
+  if (body.productId) {
+    const equity = equityList.find(e => e.productId === body.productId)
+    if (!equity) return { code: 404, data: null, msg: '权益商品不存在' }
+    productId = equity.productId
+    equityName = equity.equityName
+  }
+  p.productId = productId
+  p.equityName = equityName
+  p.auditStatus = 'pending'
+  const audit = couponAuditList.find(a => a.type === 'import' && a.batchId === p.batchId)
+  if (audit) {
+    audit.productId = productId
+    audit.productName = equityName
+    audit.auditStatus = 'pending'
+    audit.auditorName = ''
+    audit.auditTime = ''
+  }
+  return { code: 200, data: null, msg: '已重新提交审核' }
+})
+
+// 删除待导入记录(仅审核拒绝可删除)
+Mock.mock(/\/api\/coupon\/pending-delete/, 'post', (options) => {
+  const body = JSON.parse(options.body)
+  const idx = pendingCouponList.findIndex(p => p.batchId === body.batchId)
+  if (idx < 0) return { code: 404, data: null, msg: '记录不存在' }
+  if (pendingCouponList[idx].auditStatus !== 'rejected') return { code: 400, data: null, msg: '仅审核拒绝的记录可删除' }
+  pendingCouponList.splice(idx, 1)
+  const aIdx = couponAuditList.findIndex(a => a.type === 'import' && a.batchId === body.batchId)
+  if (aIdx >= 0) couponAuditList.splice(aIdx, 1)
+  return { code: 200, data: null, msg: '已删除' }
+})
+
 export default Mock

+ 6 - 0
工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/router/index.js

@@ -282,6 +282,12 @@ const routes = [
         component: () => import('@/views/GrantTaskDetail.vue'),
         meta: { title: '执行发放任务' }
       },
+      {
+        path: 'customization/hsbc/coupon-batches',
+        name: 'CouponBatchList',
+        component: () => import('@/views/CouponBatchList.vue'),
+        meta: { title: '券码管理' }
+      },
       {
         path: 'customization/hsbc/coupon-audit',
         name: 'CouponAuditList',

+ 21 - 3
工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/views/AddEquityProduct.vue

@@ -502,6 +502,21 @@
       </div>
 
       <div v-show="activeStep === 2">
+        <!-- 券码导入(仅券码商品) -->
+        <el-card
+          v-if="form.isPlatformExchange === '是' && form.productType === '券码'"
+          shadow="never"
+          class="section-card"
+        >
+          <template #header>
+            <div class="section-title">券码导入</div>
+          </template>
+          <el-form :model="form" label-width="120px" label-position="left">
+            <el-form-item label="券码批次">
+              <CouponBatchPicker v-model="form.couponBatchId" />
+            </el-form-item>
+          </el-form>
+        </el-card>
         <!-- 显示页面 -->
         <el-card shadow="never" class="section-card">
           <template #header>
@@ -618,13 +633,14 @@
 
 <script>
 import RongshuProductDialog from '@/components/RongshuProductDialog.vue'
+import CouponBatchPicker from '@/components/CouponBatchPicker.vue'
 import request from '@/utils/request'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import crowdTaskStore from '@/store/crowdTaskStore'
 
 export default {
   name: 'AddEquityProduct',
-  components: { RongshuProductDialog },
+  components: { RongshuProductDialog, CouponBatchPicker },
   data() {
     return {
       activeStep: 0,
@@ -704,7 +720,8 @@ export default {
         claimDenyButtonLink: '权益首页',
         showOnHome: '显示',
         showOnAssembly: '不显示',
-        assemblyPages: []
+        assemblyPages: [],
+        couponBatchId: ''
       },
       assemblyPageOptions: []
     }
@@ -987,7 +1004,8 @@ export default {
         validityDays: f.validityDays || 30,
         showOnHome: f.showOnHome,
         showOnAssembly: f.showOnAssembly,
-        assemblyPages: [...(f.assemblyPages || [])]
+        assemblyPages: [...(f.assemblyPages || [])],
+        couponBatchId: (f.isPlatformExchange === '是' && f.productType === '券码') ? (f.couponBatchId || '') : ''
       }
     },
     handleSaveDraft() {

+ 15 - 4
工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/views/CouponAuditList.vue

@@ -36,8 +36,12 @@
             >查看</el-button>
           </template>
         </el-table-column>
-        <el-table-column prop="productName" label="权益商品名称" min-width="160" show-overflow-tooltip />
-        <el-table-column prop="productId" label="权益编号" width="140" align="center" />
+        <el-table-column label="权益商品名称" min-width="160" show-overflow-tooltip>
+          <template #default="scope">{{ scope.row.productName || '未关联' }}</template>
+        </el-table-column>
+        <el-table-column label="权益编号" width="140" align="center">
+          <template #default="scope">{{ scope.row.productId || '-' }}</template>
+        </el-table-column>
         <el-table-column label="类型" width="150" align="center">
           <template #default="scope">
             <el-tag :type="typeTag(scope.row.type)" size="small">{{ typeLabel(scope.row.type) }}</el-tag>
@@ -82,8 +86,15 @@
       @closed="resetAuditDialog"
     >
       <el-form v-if="currentRow" label-width="150px" label-position="left" class="audit-detail-form">
-        <el-form-item label="权益商品名称">{{ currentRow.productName }}</el-form-item>
-        <el-form-item label="权益编号">{{ currentRow.productId }}</el-form-item>
+        <el-form-item label="权益商品名称">{{ currentRow.productName || '未关联' }}</el-form-item>
+        <el-form-item label="权益编号">{{ currentRow.productId || '-' }}</el-form-item>
+        <el-alert
+          v-if="!currentRow.productId"
+          type="info"
+          :closable="false"
+          style="margin: 0 0 12px;"
+          title="该批次未关联权益商品,审核通过后暂不导入,将在关联的权益商品首次上架通过后再导入。"
+        />
         <el-form-item label="类型">
           <el-tag :type="typeTag(currentRow.type)" size="small">{{ typeLabel(currentRow.type) }}</el-tag>
         </el-form-item>

+ 566 - 0
工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/views/CouponBatchList.vue

@@ -0,0 +1,566 @@
+<template>
+  <div class="coupon-batch-list">
+    <el-card>
+      <div class="toolbar">
+        <el-button type="primary" @click="openImportDialog">
+          <el-icon><Upload /></el-icon>导入券码
+        </el-button>
+      </div>
+
+      <el-tabs v-model="activeTab" @tab-change="handleTabChange">
+        <el-tab-pane label="待导入券码列表" name="pending"></el-tab-pane>
+        <el-tab-pane label="已导入券码列表" name="imported"></el-tab-pane>
+      </el-tabs>
+
+      <!-- 待导入券码列表 -->
+      <template v-if="activeTab === 'pending'">
+        <div class="search-section">
+          <el-form :model="pendingSearch" label-width="80px" inline>
+            <el-form-item label="批次ID:">
+              <el-input v-model="pendingSearch.batchId" placeholder="精准匹配" style="width: 220px" />
+            </el-form-item>
+            <div class="button-group" style="margin-left: 20px;">
+              <el-button type="primary" @click="loadData">查询</el-button>
+              <el-button @click="resetPending">重置</el-button>
+            </div>
+          </el-form>
+        </div>
+
+        <el-table :data="pagedData" stripe style="width: 100%">
+          <el-table-column prop="batchId" label="批次ID" width="170"></el-table-column>
+          <el-table-column label="权益编号" width="120">
+            <template #default="scope">{{ scope.row.productId || '-' }}</template>
+          </el-table-column>
+          <el-table-column label="权益名称" min-width="160" show-overflow-tooltip>
+            <template #default="scope">{{ scope.row.equityName || '-' }}</template>
+          </el-table-column>
+          <el-table-column prop="importCount" label="导入数量" width="110"></el-table-column>
+          <el-table-column prop="creator" label="创建人" width="110"></el-table-column>
+          <el-table-column prop="createTime" label="创建时间" width="180"></el-table-column>
+          <el-table-column label="审核状态" width="110">
+            <template #default="scope">
+              <el-tag :type="auditStatusTag(scope.row.auditStatus)" size="small">{{ auditStatusLabel(scope.row.auditStatus) }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="140" fixed="right">
+            <template #default="scope">
+              <template v-if="scope.row.auditStatus === 'rejected'">
+                <el-button type="text" size="small" @click="openEdit(scope.row)">编辑</el-button>
+                <el-button type="text" size="small" @click="handleDelete(scope.row)">删除</el-button>
+              </template>
+              <span v-else>-</span>
+            </template>
+          </el-table-column>
+        </el-table>
+      </template>
+
+      <!-- 已导入券码列表 -->
+      <template v-else>
+        <div class="search-section">
+          <el-form :model="importedSearch" label-width="80px" inline>
+            <el-form-item label="权益编号:">
+              <el-input v-model="importedSearch.productId" placeholder="精准匹配" style="width: 200px" />
+            </el-form-item>
+            <el-form-item label="批次ID:">
+              <el-input v-model="importedSearch.batchId" placeholder="精准匹配" style="width: 200px" />
+            </el-form-item>
+            <div class="button-group" style="margin-left: 20px;">
+              <el-button type="primary" @click="loadData">查询</el-button>
+              <el-button @click="resetImported">重置</el-button>
+            </div>
+          </el-form>
+        </div>
+
+        <el-table :data="pagedData" stripe style="width: 100%">
+          <el-table-column prop="batchId" label="批次ID" width="170"></el-table-column>
+          <el-table-column prop="productId" label="权益编号" width="120"></el-table-column>
+          <el-table-column prop="equityName" label="权益商品名称" min-width="160" show-overflow-tooltip></el-table-column>
+          <el-table-column prop="importCount" label="导入数量" width="100"></el-table-column>
+          <el-table-column prop="actualImportCount" label="实际导入数量" width="120"></el-table-column>
+          <el-table-column prop="failCount" label="导入失败数量" width="120"></el-table-column>
+          <el-table-column prop="remainingCount" label="券码剩余数量" width="120"></el-table-column>
+          <el-table-column prop="effectiveTime" label="使用有效期开始时间" width="180"></el-table-column>
+          <el-table-column prop="expireTime" label="使用有效期结束时间" width="180"></el-table-column>
+          <el-table-column label="文件处理状态" width="120">
+            <template #default="scope">
+              <el-tag :type="statusTagType(scope.row.fileStatus)" size="small">{{ scope.row.fileStatus }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="importTime" label="导入时间" width="180"></el-table-column>
+          <el-table-column prop="creator" label="创建人" width="100"></el-table-column>
+          <el-table-column label="操作" width="280" fixed="right">
+            <template #default="scope">
+              <el-button
+                v-if="Number(scope.row.remainingCount) > 0"
+                type="text"
+                size="small"
+                @click="handleRecycle(scope.row)"
+              >回收</el-button>
+              <el-button
+                type="text"
+                size="small"
+                @click="openRecycleRecordDialog(scope.row)"
+              >回收记录</el-button>
+              <el-button
+                v-if="Number(scope.row.failCount) > 0"
+                type="text"
+                size="small"
+                @click="handleExportFail(scope.row)"
+              >导出失败券码</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </template>
+
+      <div class="pagination-section">
+        <span>Total {{ total }}</span>
+        <el-pagination
+          v-model:current-page="currentPage"
+          v-model:page-size="pageSize"
+          layout="prev, pager, next, sizes, jumper"
+          :page-sizes="[10, 20, 50]"
+          :total="total"
+        />
+      </div>
+    </el-card>
+
+    <el-dialog
+      title="券码批次回收"
+      v-model="recycleDialogVisible"
+      width="640px"
+      @closed="resetRecycleForm"
+    >
+      <div class="recycle-types">
+        <div
+          v-for="opt in recycleTypeOptions"
+          :key="opt.value"
+          :class="['recycle-type-item', { active: recycleForm.type === opt.value }]"
+          @click="recycleForm.type = opt.value"
+        >
+          <el-radio v-model="recycleForm.type" :label="opt.value">{{ opt.label }}</el-radio>
+        </div>
+      </div>
+
+      <el-form label-width="100px" label-position="left" style="margin-top: 24px;">
+        <el-form-item label="回收数量" required>
+          <span v-if="recycleForm.type === 'expired'">{{ recycleMaxAmount }}</span>
+          <template v-else>
+            <el-input-number
+              v-model="recycleForm.amount"
+              :min="1"
+              :max="recycleMaxAmount"
+              :placeholder="`输入范围1-${recycleMaxAmount}`"
+              :controls-position="''"
+              style="width: 100%"
+            ></el-input-number>
+            <div class="recycle-tip">本批次{{ currentRecycleTypeLabel }}最多可回收数量:{{ recycleMaxAmount }}</div>
+          </template>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <span class="dialog-footer-center">
+          <el-button @click="recycleDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="confirmRecycle">提交</el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <el-dialog title="导入券码" v-model="importDialogVisible" width="520px" @closed="resetImportForm">
+      <el-form :model="importForm" label-width="120px" label-position="left">
+        <el-form-item label="权益商品">
+          <el-select
+            v-model="importForm.productId"
+            filterable
+            clearable
+            placeholder="可输入权益名称/编号搜索(非必填)"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="item in productOptions"
+              :key="item.productId"
+              :label="`${item.equityName}(${item.productId})`"
+              :value="item.productId"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="券码文件" required>
+          <el-upload
+            action=""
+            :auto-upload="false"
+            :file-list="importForm.files"
+            :on-change="handleFileChange"
+            :on-remove="handleFileRemove"
+            :limit="1"
+          >
+            <el-button>
+              <el-icon><Upload /></el-icon>选择文件
+            </el-button>
+            <template #tip>
+              <div class="el-upload__tip">
+                <div>*只支持模板格式,请先下载模板再导入</div>
+                <div>*不能导入相同的券码</div>
+                <div>*最多支持50000条,若超过请分次导入</div>
+              </div>
+              <el-button type="text" @click="downloadTemplate">
+                <el-icon><Download /></el-icon>模板下载
+              </el-button>
+            </template>
+          </el-upload>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <span>
+          <el-button @click="importDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="confirmImport">提交</el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <el-dialog title="编辑待导入券码" v-model="editDialogVisible" width="520px" @closed="resetEditForm">
+      <el-form :model="editForm" label-width="120px" label-position="left">
+        <el-form-item label="批次ID">{{ editForm.batchId }}</el-form-item>
+        <el-form-item label="权益商品">
+          <el-select
+            v-model="editForm.productId"
+            filterable
+            clearable
+            placeholder="可输入权益名称/编号搜索(非必填)"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="item in productOptions"
+              :key="item.productId"
+              :label="`${item.equityName}(${item.productId})`"
+              :value="item.productId"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <span>
+          <el-button @click="editDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="confirmEdit">重新提交审核</el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <el-dialog title="回收记录" v-model="recycleRecordDialogVisible" width="900px">
+      <el-table :data="recycleRecords" stripe style="width: 100%">
+        <el-table-column prop="recycleTime" label="回收时间" width="180"></el-table-column>
+        <el-table-column prop="type" label="类型" width="150"></el-table-column>
+        <el-table-column prop="operator" label="操作人" width="120"></el-table-column>
+        <el-table-column prop="amount" label="回收数量" width="120"></el-table-column>
+        <el-table-column label="操作" min-width="140">
+          <template #default="scope">
+            <el-button type="text" size="small" @click="downloadRecycleCoupons(scope.row)">下载回收券码</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <template #footer>
+        <el-button @click="recycleRecordDialogVisible = false">关闭</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { ElMessage, ElMessageBox } from 'element-plus'
+import request from '@/utils/request'
+
+export default {
+  name: 'CouponBatchList',
+  data() {
+    return {
+      activeTab: 'pending',
+      pendingSearch: { batchId: '' },
+      importedSearch: { productId: '', batchId: '' },
+      pendingData: [],
+      importedData: [],
+      currentPage: 1,
+      pageSize: 10,
+      importDialogVisible: false,
+      editDialogVisible: false,
+      recycleDialogVisible: false,
+      recycleRecordDialogVisible: false,
+      recycleRow: null,
+      recordRow: null,
+      recycleForm: {
+        type: 'unissued',
+        amount: undefined
+      },
+      recycleTypeOptions: [
+        { label: '未发放券码回收', value: 'unissued' },
+        { label: '已过期券码回收', value: 'expired' }
+      ],
+      importForm: {
+        productId: '',
+        files: []
+      },
+      editForm: {
+        batchId: '',
+        productId: ''
+      },
+      productOptions: []
+    }
+  },
+  created() {
+    this.loadData()
+    this.loadProductOptions()
+  },
+  activated() {
+    this.loadData()
+  },
+  computed: {
+    currentData() {
+      return this.activeTab === 'pending' ? this.pendingData : this.importedData
+    },
+    total() {
+      return this.currentData.length
+    },
+    pagedData() {
+      const start = (this.currentPage - 1) * this.pageSize
+      return this.currentData.slice(start, start + this.pageSize)
+    },
+    currentRecycleTypeLabel() {
+      const opt = this.recycleTypeOptions.find(o => o.value === this.recycleForm.type)
+      if (!opt) return ''
+      return opt.label.replace('回收', '')
+    },
+    recycleRecords() {
+      return (this.recordRow && this.recordRow.recycleRecords) || []
+    },
+    recycleMaxAmount() {
+      const row = this.recycleRow
+      if (!row) return 0
+      switch (this.recycleForm.type) {
+        case 'unissued': return Number(row.remainingCount) || 0
+        case 'expired': {
+          const ts = row.expireTime ? new Date(row.expireTime).getTime() : 0
+          return ts && ts < Date.now() ? Number(row.remainingCount) || 0 : 0
+        }
+        default: return 0
+      }
+    }
+  },
+  methods: {
+    loadData() {
+      this.currentPage = 1
+      if (this.activeTab === 'pending') {
+        this.loadPending()
+      } else {
+        this.loadImported()
+      }
+    },
+    loadPending() {
+      const params = new URLSearchParams()
+      if (this.pendingSearch.batchId) params.append('batchId', this.pendingSearch.batchId)
+      request.get('/api/coupon/pending-list?' + params.toString()).then(res => {
+        this.pendingData = res.data || []
+      }).catch(() => {})
+    },
+    loadImported() {
+      const params = new URLSearchParams()
+      if (this.importedSearch.productId) params.append('productId', this.importedSearch.productId)
+      if (this.importedSearch.batchId) params.append('batchId', this.importedSearch.batchId)
+      request.get('/api/coupon/batch-list?' + params.toString()).then(res => {
+        this.importedData = res.data || []
+      }).catch(() => {})
+    },
+    handleTabChange() {
+      this.loadData()
+    },
+    loadProductOptions() {
+      request.get('/api/equity/list').then(res => {
+        this.productOptions = res.data || []
+      }).catch(() => {})
+    },
+    resetPending() {
+      this.pendingSearch = { batchId: '' }
+      this.loadData()
+    },
+    resetImported() {
+      this.importedSearch = { productId: '', batchId: '' }
+      this.loadData()
+    },
+    auditStatusLabel(s) {
+      return { pending: '待审核', approved: '审核通过', rejected: '审核拒绝' }[s] || s
+    },
+    auditStatusTag(s) {
+      return { pending: 'warning', approved: 'success', rejected: 'danger' }[s] || ''
+    },
+    statusTagType(status) {
+      switch (status) {
+        case '处理成功': return 'success'
+        case '处理中': return 'warning'
+        case '处理失败': return 'danger'
+        default: return 'info'
+      }
+    },
+    openImportDialog() {
+      this.importDialogVisible = true
+    },
+    downloadTemplate() {
+      ElMessage.success('模板已开始下载')
+    },
+    handleFileChange(file) {
+      this.importForm.files = [file]
+    },
+    handleFileRemove() {
+      this.importForm.files = []
+    },
+    resetImportForm() {
+      this.importForm = { productId: '', files: [] }
+    },
+    confirmImport() {
+      if (!this.importForm.files.length) {
+        ElMessage.warning('请选择券码文件')
+        return
+      }
+      request.post('/api/coupon/import', { productId: this.importForm.productId }).then(() => {
+        ElMessage.success('已提交,等待券码审核')
+        this.importDialogVisible = false
+        this.activeTab = 'pending'
+        this.loadData()
+      }).catch(() => {})
+    },
+    openEdit(row) {
+      this.editForm = { batchId: row.batchId, productId: row.productId || '' }
+      this.editDialogVisible = true
+    },
+    resetEditForm() {
+      this.editForm = { batchId: '', productId: '' }
+    },
+    confirmEdit() {
+      request.post('/api/coupon/pending-edit', {
+        batchId: this.editForm.batchId,
+        productId: this.editForm.productId
+      }).then(() => {
+        ElMessage.success('已重新提交审核')
+        this.editDialogVisible = false
+        this.loadData()
+      }).catch(() => {})
+    },
+    handleDelete(row) {
+      ElMessageBox.confirm(`确定删除批次 ${row.batchId} 吗?`, '提示', {
+        confirmButtonText: '删除',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        request.post('/api/coupon/pending-delete', { batchId: row.batchId }).then(() => {
+          ElMessage.success('已删除')
+          this.loadData()
+        }).catch(() => {})
+      }).catch(() => {})
+    },
+    handleRecycle(row) {
+      this.recycleRow = row
+      this.recycleForm = { type: 'unissued', amount: undefined }
+      this.recycleDialogVisible = true
+    },
+    handleExportFail(row) {
+      ElMessage.success(`已开始导出失败券码(${row.failCount} 条)`)
+    },
+    openRecycleRecordDialog(row) {
+      this.recordRow = row
+      this.recycleRecordDialogVisible = true
+    },
+    downloadRecycleCoupons() {
+      ElMessage.success('已开始下载回收券码')
+    },
+    resetRecycleForm() {
+      this.recycleRow = null
+      this.recycleForm = { type: 'unissued', amount: undefined }
+    },
+    confirmRecycle() {
+      const max = this.recycleMaxAmount
+      // 已过期券码回收:数量为文本展示,直接按可回收数量全部回收
+      const amount = this.recycleForm.type === 'expired' ? max : Number(this.recycleForm.amount)
+      if (!amount || amount < 1) {
+        ElMessage.warning(this.recycleForm.type === 'expired' ? '当前批次没有可回收的已过期券码' : '请输入回收数量')
+        return
+      }
+      if (amount > max) {
+        ElMessage.error(`回收数量不能超过 ${max}`)
+        return
+      }
+      request.post('/api/coupon/recycle', {
+        batchId: this.recycleRow.batchId,
+        type: this.recycleForm.type,
+        amount
+      }).then(() => {
+        ElMessage.success('回收成功')
+        this.recycleDialogVisible = false
+        this.loadData()
+      }).catch(() => {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.coupon-batch-list {
+  padding: 0;
+}
+
+.toolbar {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  margin-bottom: 16px;
+}
+
+.search-section {
+  margin-bottom: 8px;
+}
+
+.button-group {
+  display: flex;
+  gap: 5px;
+}
+
+.recycle-types {
+  display: flex;
+  gap: 16px;
+  justify-content: space-between;
+}
+
+.recycle-type-item {
+  flex: 1;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  padding: 12px 16px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.recycle-type-item.active {
+  border-color: #409eff;
+  background: #ecf5ff;
+}
+
+.recycle-type-item >>> .el-radio__label {
+  font-size: 14px;
+  color: #303133;
+}
+
+.recycle-tip {
+  margin-top: 8px;
+  font-size: 13px;
+  color: #909399;
+}
+
+.dialog-footer-center {
+  display: flex;
+  justify-content: center;
+  gap: 24px;
+}
+
+.pagination-section {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  gap: 20px;
+  margin-top: 20px;
+}
+</style>

+ 21 - 3
工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/views/EditEquityProduct.vue

@@ -500,6 +500,21 @@
       </div>
 
       <div v-show="activeStep === 2">
+        <!-- 券码导入(仅券码商品) -->
+        <el-card
+          v-if="form.isPlatformExchange === '是' && form.productType === '券码'"
+          shadow="never"
+          class="section-card"
+        >
+          <template #header>
+            <div class="section-title">券码导入</div>
+          </template>
+          <el-form :model="form" label-width="120px" label-position="left">
+            <el-form-item label="券码批次">
+              <CouponBatchPicker v-model="form.couponBatchId" />
+            </el-form-item>
+          </el-form>
+        </el-card>
         <!-- 显示页面 -->
         <el-card shadow="never" class="section-card">
           <template #header>
@@ -624,12 +639,13 @@
 <script>
 import { ElMessage, ElMessageBox } from 'element-plus'
 import RongshuProductDialog from '@/components/RongshuProductDialog.vue'
+import CouponBatchPicker from '@/components/CouponBatchPicker.vue'
 import request from '@/utils/request'
 import crowdTaskStore from '@/store/crowdTaskStore'
 
 export default {
   name: 'EditEquityProduct',
-  components: { RongshuProductDialog },
+  components: { RongshuProductDialog, CouponBatchPicker },
   data() {
     return {
       activeStep: 0,
@@ -712,7 +728,8 @@ export default {
         claimDenyButtonLink: '权益首页',
         showOnHome: '显示',
         showOnAssembly: '不显示',
-        assemblyPages: []
+        assemblyPages: [],
+        couponBatchId: ''
       },
       assemblyPageOptions: []
     }
@@ -802,7 +819,8 @@ export default {
         validityDays: f.validityDays || 30,
         showOnHome: f.showOnHome,
         showOnAssembly: f.showOnAssembly,
-        assemblyPages: [...(f.assemblyPages || [])]
+        assemblyPages: [...(f.assemblyPages || [])],
+        couponBatchId: (f.isPlatformExchange === '是' && f.productType === '券码') ? (f.couponBatchId || '') : ''
       }
     },
     handleSaveDraft() {

+ 0 - 436
工作空间/产研/权益/汇丰全行/需求文档/prd-generator-from-demo/SKILL.md

@@ -1,436 +0,0 @@
----
-name: prd-generator-from-demo
-description: Generate and incrementally update detailed Chinese PRD documents for any business domain from incomplete requirement notes, frontend or mobile prototypes, admin or backend demos, screenshots, existing API docs, config tables, dictionary tables, mock data, or static JSON. Use when Codex must reverse-engineer and complete a development-ready and testable PRD from prototypes and fragmented materials, or revise an existing PRD after prototypes change, with page display rules, backend/admin configuration rules, API fields, button interactions, validation, edge cases, flow diagrams, and open questions.
----
-# PRD Generator From Demo
-
-## Objective
-
-Generate a detailed Chinese product requirements document from fragmented inputs for any business domain. The output must be concrete enough for frontend, backend, QA, and product stakeholders to split work, implement, and test.
-
-Do not merely polish an existing PRD. Infer and complete missing page structure, fields, states, interactions, admin rules, API contracts, field mappings, edge cases, and unresolved questions from the provided materials.
-
-This skill is domain-neutral. Do not assume a specific industry, product type, tenant, channel, or business vocabulary unless it appears in the user's materials. Preserve the user's domain terms and infer only from visible prototypes, supplied requirements, code, screenshots, APIs, and data files.
-
-## Input Materials
-
-Accept any combination of the following materials from any project:
-
--   Incomplete requirement notes, partial PRDs, business remarks, or unclear field and rule descriptions.
--   Frontend or mobile prototype materials, including HTML demos, app page code, screenshots, prototypes, or runnable projects.
--   Admin, backend, or management-console demo materials, including HTML files, generated admin projects, React/Vue/HTML/CSS/JS code, screenshots, prototypes, or runnable pages.
--   Existing API documentation.
--   Screenshots, prototype images, old requirement documents, test cases, configuration tables, dictionary tables, mock data, static JSON, or page notes.
-
-When reading demos, identify page structure, modules, fields, buttons, states, display rules, interaction behavior, validation, permissions, persistence rules, mobile impact, and edge cases.
-
-If the product is not mobile-first, replace "移动端" in the output with the actual user-facing channel, such as "前台", "Web端", "小程序端", "App端", "H5端", or another channel found in the materials.
-
-## Source Priority
-
-When materials conflict, apply this priority:
-
-1.  Incomplete requirement description
-2.  Existing API documentation
-3.  Admin/backend demo
-4.  Frontend/mobile prototype
-5.  Mock data or static data
-6.  Reasonable inference
-
-Follow these conflict rules:
-
--   If requirement notes and demos conflict, keep both signals and mark the issue as a "疑似冲突点".
--   If API docs and page fields differ, prefer the API docs and list the difference under "待确认问题".
--   If demos contain fields, buttons, or modules that are missing from requirement notes, add them to the PRD.
--   If information cannot be determined, do not pretend it is certain. Put it under "待确认问题".
-
-## Writing Rules
-
--   Write the final PRD in Chinese.
--   Use product manager requirement-document language.
--   Do not write generic background, goals, project value, or market analysis.
--   Avoid vague wording such as "根据实际情况", "按需展示", or "相关数据".
--   Explain every field's source, purpose, display rule, interaction rule, and exception rule.
--   Explain every button's click behavior, permission control, secondary confirmation, success message, and failure message.
--   Make rules specific, developable, and testable.
--   Clearly mark inferred content and explain the basis for inference.
--   Mark virtual APIs as "【虚拟接口,待技术确认】".
--   When designing virtual APIs, use Chinese interface names and Chinese field names. Do not use English, pinyin, camelCase, snake_case, or technical placeholder names for virtual API fields.
--   If an existing API document uses English field names, preserve the original technical field names, but add Chinese field meanings so product, business, and testing readers can understand them.
--   Do not make the PRD read like a stack of tables. For each module, page, API, rule set, and diagram, first write a short natural-language explanation of what it does, when it applies, and what the key rule is. Then use tables only for dense field-level details, mappings, validation lists, and button inventories.
--   Write in a human product-document style: use complete sentences, explicit cause-and-effect rules, and short paragraphs before tables. Avoid table-only sections unless the content is purely an inventory.
-
-## Generation Workflow
-
-Use this workflow when creating a PRD for the first time:
-
-1.  Inventory all input materials and identify their type.
-2.  Extract user-facing page modules from top to bottom, using the actual channel name from the materials.
-3.  Extract admin pages, filters, lists, dialogs, fields, buttons, validation, permissions, and persistence rules.
-4.  Extract existing API names and fields. Reuse them exactly when available.
-5.  If dynamic data is required but no API exists, design virtual APIs with Chinese interface names and Chinese field names, and mark them as pending technical confirmation.
-6.  Build frontend-admin field mappings.
-7.  Add business flow, swimlane, state-transition, or sequence diagrams when the requirement contains frontend-admin linkage, API call chains, state transitions, publish/enable/disable flows, multiple roles, or complex user paths.
-8.  Collect suspected conflicts, unclear rules, missing fields, and technical assumptions into "待确认问题".
-
-## Incremental Update Workflow
-
-Use this workflow when the user provides an existing PRD plus changed frontend/mobile, backend/admin, or management-console prototypes.
-
-1.  Read the existing PRD first and preserve its section structure, numbering style, terminology, confirmed decisions, and table formats.
-2.  Inspect the current frontend/mobile prototype and backend/admin prototype. Compare them against the existing PRD and identify only changed, added, deleted, or behaviorally affected items.
-3.  Update only impacted sections. Do not rewrite unrelated modules, do not rename stable fields, and do not reorder stable content unless the prototype structure itself changed.
-4.  For every change, update all directly affected places:
-    -   Frontend/mobile module description, display rules, field table, interaction rules, state rules, and edge cases.
-    -   Backend/admin page description, filter/list/form fields, button rules, validation rules, popup prompts, permissions, and edge cases.
-    -   API field descriptions or virtual APIs.
-    -   Frontend-admin field mapping table.
-    -   Mermaid flow diagrams.
-    -   Pending questions.
-5.  If a prototype removes a field, button, or module, mark the removal clearly and delete or update the corresponding requirement only when the removal is evident from the new prototype. If uncertain, keep the existing requirement and add a pending question.
-6.  If a prototype adds a field but the source is unclear, add the field with the most likely source marked as inferred and create a pending question.
-7.  If behavior changes are visible but the persistence/API impact is unclear, update visible behavior and add a pending technical question.
-8.  Add a short "本次更新说明" section at the top of the output when performing an incremental update. Include changed prototype source, changed modules, updated sections, and pending confirmation items.
-
-Use this table for the incremental update summary:
-
-| 更新项 | 变更来源 | 影响页面/模块 | 已更新章节 | 是否存在待确认 |
-| --- | --- | --- | --- | --- |
-
-When returning an updated PRD, include either the full updated document or a patch-style section replacement, depending on the user's request. If the user does not specify, provide the full updated document when the document is short; provide section-level replacements when the document is long.
-
-## Output Structure
-
-Generate the PRD with the following sections.
-
-### 一、需求范围说明
-
-State the covered pages, modules, admin configuration scope, and API scope. Do not write background or goals.
-
-### 二、功能范围与模块结构
-
-This section must only include the structural information needed before detailed requirements. Do not add a separate feature inventory, summary table, background, goals, or value analysis.
-
-2.1 前台/移动端模块结构
-
-Use the actual user-facing channel name from the materials. Describe the user-facing modules in their actual display or operation order, and explain what each module is responsible for. Do not only list visual areas; include the business meaning of each module.
-
-Examples:
-
--   顶部展示区
--   用户信息区
--   权益列表区
--   活动轮播区
--   底部按钮区
-
-2.2 后台/管理端配置结构
-
-List backend/admin/management-console modules by configuration responsibility. Explain which frontend/mobile modules they control, not only where they appear in the admin page.
-
-Examples:
-
--   菜单入口
--   查询筛选区
--   列表区
--   新增/编辑弹窗
--   配置项区域
--   上传区域
--   排序区域
--   操作按钮区
-
-2.3 前后台关系概览
-
-Briefly explain only the necessary relationship between frontend/mobile display, backend/admin configuration, API data, and user actions. Keep this section concise because the detailed field mapping table appears later in the PRD.
-
-Use this table when applicable:
-
-| 前台/移动端模块 | 后台/管理端配置 | 依赖接口 | 用户操作影响 | 说明 |
-| --- | --- | --- | --- | --- |
-
-### 三、前台/移动端功能详细说明
-
-Use the actual user-facing channel name from the materials. Write one module at a time. Use this structure for each module:
-
-3.x 模块名称
-
-3.x.1 模块说明
-
-Describe the module position, purpose, and display conditions.
-
-3.x.2 展示规则
-
-Explain when the module is shown or hidden, whether it is controlled by admin configuration, and whether it is controlled by API state.
-
-3.x.3 字段说明
-
-Use this table:
-
-| 字段/元素 | 类型  | 展示位置 | 数据来源 | 来源说明 | 展示规则 | 交互规则 | 异常规则 |
-| --- | --- | --- | --- | --- | --- | --- | --- |
-
-Only use one of these data source values:
-
--   写死
--   后台配置
--   接口获取
--   前端计算
--   用户操作产生
--   系统默认
-
-For data source explanations:
-
--   写死: describe the fixed copy or fixed rule.
--   后台配置: name the corresponding admin configuration field.
--   接口获取: name the API, field name, and Chinese field meaning.
--   前端计算: describe the formula and dependent fields.
--   用户操作产生: describe the user action source.
--   系统默认: describe the default value and effective scenario.
-
-3.x.4 交互规则
-
-Explain clicks, jumps, expand/collapse, popups, copy actions, submit actions, return behavior, refresh behavior, and any other interactions.
-
-3.x.5 状态规则
-
-If the module has state branches, use this table:
-
-| 状态值 | 状态含义 | 展示文案 | 展示样式 | 可操作按钮 | 规则说明 |
-| --- | --- | --- | --- | --- | --- |
-
-3.x.6 异常、空数据和边界规则
-
-Describe empty fields, API errors, API timeouts, image load failures, unknown states, user ineligibility, and other boundaries.
-
-### 四、后台/管理端功能详细说明
-
-Write backend/admin/management-console requirements by page, area, field, button, validation, and boundary.
-
-4.x 后台页面名称
-
-4.x.1 页面入口
-
-Describe menu path, page name, and page purpose.
-
-4.x.2 查询筛选区
-
-Describe filter fields, default values, reset support, fuzzy search support, and relationships between filters.
-
-Use this table:
-
-| 字段名称 | 字段类型 | 是否必填 | 默认值 | 数据来源 | 查询规则 | 校验规则 | 说明  |
-| --- | --- | --- | --- | --- | --- | --- | --- |
-
-4.x.3 列表区
-
-Describe list fields, sorting rules, pagination, empty display, and operation column.
-
-Use this table:
-
-| 列表字段 | 字段类型 | 数据来源 | 展示规则 | 排序规则 | 空值规则 | 说明  |
-| --- | --- | --- | --- | --- | --- | --- |
-
-4.x.4 新增/编辑弹窗或配置表单
-
-Describe field differences for create, edit, and view modes, echo rules, and save rules.
-
-Use this table:
-
-| 字段名称 | 字段类型 | 是否必填 | 默认值 | 数据来源 | 配置用途 | 校验规则 | 前台/移动端影响 |
-| --- | --- | --- | --- | --- | --- | --- | --- |
-
-Common field types include:
-
--   文本输入框
--   数字输入框
--   文本域
--   单选框
--   多选框
--   下拉选择
--   开关
--   日期选择
--   时间选择
--   图片上传
--   文件上传
--   富文本编辑器
--   颜色选择器
--   排序输入框
--   表格配置
--   字典选择
--   枚举选择
-
-4.x.5 按钮说明
-
-Explain every admin button. Repeated buttons may be described once under common button rules and referenced in specific modules.
-
-Use this table:
-
-| 按钮名称 | 出现场景 | 操作对象 | 点击后行为 | 权限控制 | 二次确认 | 成功提示 | 失败提示 |
-| --- | --- | --- | --- | --- | --- | --- | --- |
-
-Common buttons include:
-
--   查询
--   重置
--   新增
--   编辑
--   删除
--   查看
--   保存
--   取消
--   上传
--   更换图片
--   排序
--   上移
--   下移
--   发布
--   下线
--   启用
--   停用
--   复制
--   预览
-
-4.x.6 校验规则
-
-Describe required validation, format validation, length validation, image size, image format, duplicate sorting, duplicate enum values, and time ranges.
-
-Use this table:
-
-| 字段/操作 | 校验规则 | 触发时机 | 不通过提示 | 是否阻断提交 |
-| --- | --- | --- | --- | --- |
-
-4.x.7 后台边界规则
-
-Describe admin exceptions and boundaries, such as:
-
--   必填项为空
--   字数超限
--   图片格式错误
--   图片大小超限
--   排序值重复
--   删除已上线配置
--   编辑已生效配置
--   多人同时编辑
--   保存失败
--   查询无结果
--   筛选条件重置
--   权限不足
--   枚举值被引用后不可删除
--   配置正在被移动端使用
-
-### 五、接口与字段说明
-
-If existing APIs are provided, reuse original API names and fields. If no API is provided but the page clearly needs dynamic data, design virtual APIs.
-
-Virtual API rules:
-
--   API names must be Chinese.
--   Field names must be Chinese.
--   Do not use English field names, pinyin field names, camelCase, snake_case, abbreviations, or technical placeholders for virtual APIs.
--   Do not invent names such as `id`, `type`, `status`, `title`, `imageUrl`, `createdTime`, `isEnabled`, or similar English fields for virtual APIs. Use Chinese names such as "记录编号", "类型", "状态", "标题", "图片地址", "创建时间", and "是否启用".
--   If the user later provides a real API, replace or map the virtual Chinese interface and Chinese fields to the real technical interface and fields.
--   Mark as "【虚拟接口,待技术确认】".
--   Explain purpose, input parameters, output parameters, field meanings, field sources, usage locations, and exception rules.
--   Do not write only "调用接口获取"; specify which API and which field is used by which module.
-
-Before the API tables, write a short paragraph explaining why the page needs this API, which module calls it, and what information it returns or saves.
-
-Use this structure for each API:
-
-接口名称:
-
-接口类型:
-
-接口用途:
-
-是否虚拟接口:
-
-调用时机:
-
-使用页面/模块:
-
-入参:
-
-| 字段名称 | 是否必填 | 字段类型 | 字段说明 | 示例值 |
-| --- | --- | --- | --- | --- |
-
-出参:
-
-| 字段名称 | 字段类型 | 字段说明 | 使用位置 | 异常规则 |
-| --- | --- | --- | --- | --- |
-
-接口异常规则:
-
-| 异常场景 | 页面表现 | 是否允许重试 | 提示文案 |
-| --- | --- | --- | --- |
-
-### 六、前台与后台字段映射关系
-
-Explain how backend/admin configuration fields affect frontend/mobile display.
-
-Use this table:
-
-| 后台配置字段 | 后台位置 | 前台/移动端展示字段或模块 | 影响规则 | 是否必填 | 空值处理 |
-| --- | --- | --- | --- | --- | --- |
-
-### 七、业务流程图 / 泳道图 / 状态流转图 / 接口调用时序图
-
-Add Mermaid diagrams when any of these are present:
-
--   Frontend-admin linkage
--   API call chains
--   State transitions
--   Publish, enable, disable, approve, online, or offline flows
--   Multi-role collaboration
--   Complex user operation paths
-
-Choose the diagram type based on the requirement:
-
--   普通业务流程图
--   泳道图
--   状态流转图
--   接口调用时序图
-
-Use Mermaid syntax. Keep node text in Chinese and make flow branches explicit.
-
-### 八、权限规则
-
-Describe permission control for admin entry, buttons, data visibility, editable status, deletable status, and mobile eligibility if present.
-
-Use this table when applicable:
-
-| 权限点 | 适用对象 | 控制范围 | 无权限表现 | 说明  |
-| --- | --- | --- | --- | --- |
-
-### 九、埋点与日志需求
-
-If user behavior, operation audit, publish history, or troubleshooting is relevant, define tracking and logs.
-
-Use this table:
-
-| 事件/日志名称 | 触发时机 | 记录字段 | 使用目的 | 备注  |
-| --- | --- | --- | --- | --- |
-
-### 十、待确认问题
-
-List unresolved items clearly. Include suspected conflicts, missing API fields, missing admin rules, unclear button behavior, unclear permission rules, unclear image requirements, unclear state definitions, and inferred assumptions.
-
-Use this table:
-
-| 序号  | 问题类型 | 问题描述 | 影响范围 | 建议确认对象 | 当前处理方式 |
-| --- | --- | --- | --- | --- | --- |
-
-## Quality Checklist
-
-Before finalizing the PRD, verify:
-
--   Every visible frontend/mobile field has a data source and display rule.
--   Every backend/admin field has validation and frontend/mobile impact when applicable.
--   Every button has click behavior, permission, confirmation, success, and failure rules.
--   Every dynamic module maps to an API field or admin configuration field.
--   Every virtual API is marked as pending technical confirmation.
--   Every virtual API uses Chinese interface names and Chinese field names.
--   Existing English API fields, if any, have Chinese meanings and usage explanations.
--   Each major section includes natural-language explanation before tables.
--   Every conflict or uncertainty appears in "待确认问题".
--   Mermaid diagrams are included when workflows or state transitions are non-trivial.
--   The document avoids generic background and vague wording.

+ 0 - 4
工作空间/产研/权益/汇丰全行/需求文档/prd-generator-from-demo/agents/openai.yaml

@@ -1,4 +0,0 @@
-interface:
-  display_name: "PRD Generator From Demo"
-  short_description: "通用原型反推中文PRD并按原型变更增量更新需求文档"
-  default_prompt: "根据我提供的需求描述、前台/移动端原型、后台/管理端Demo、接口说明或截图,生成或增量更新可交付研发和测试的详细需求文档。"

Datei-Diff unterdrückt, da er zu groß ist
+ 34 - 0
工作空间/产研/权益/汇丰全行/需求文档/券码管理优化-20260622.md


+ 85 - 1
工作空间/产研/权益/汇丰全行/需求文档/权益介绍页.md → 工作空间/产研/权益/汇丰全行/需求文档/权益介绍页移动端.md

@@ -88,10 +88,15 @@ flowchart TD
 编辑页由五部分组成:
 
 1.  顶部操作栏:页面状态、获取链接、保存草稿、发布上线。
+    
 2.  客户层级配置 Tab:卓越理财、卓越理财·尊尚、环球私人银行。
+    
 3.  Tab 名称配置。
+    
 4.  头部配置。
+    
 5.  按钮配置与权益列表配置。
+    
 
 客户层级 Tab:
 
@@ -113,8 +118,11 @@ flowchart TD
 保存范围:
 
 1.  后台保存或发布时提交 `premier`、`elite`、`global` 三档完整配置。
+    
 2.  不允许仅保存当前后台 Tab,避免其他等级配置被覆盖或丢失。
+    
 3.  任意字段修改后,如当前状态为已发布,需切换为“有未发布的修改”。
+    
 
 ### 2.3 获取页面链接
 
@@ -137,10 +145,15 @@ flowchart TD
 边界规则:
 
 1.  Campaign ID、Channel ID 清空时,不拼接对应参数。
+    
 2.  同时选择 Campaign ID、Channel ID 时,按 `CampaignId=xxx&Channelid=yyy` 拼接。
+    
 3.  草稿预览链接如同时存在投放参数,需保留 `draft=1`。
+    
 4.  复制链接成功后提示“链接已复制到剪贴板”。
+    
 5.  浏览器不支持剪贴板时,允许用户手动复制链接。
+    
 
 ### 2.4 Tab 名称配置
 
@@ -151,9 +164,13 @@ flowchart TD
 规则:
 
 1.  后台当前层级 Tab 的标题优先展示已配置的 Tab 名称。
+    
 2.  Tab 名称为空时,后台和移动端均使用默认名称兜底。
+    
 3.  Tab 名称过长时,移动端需单行展示并省略或自适应压缩,不得挤压其他 Tab。
+    
 4.  后台不支持新增、删除或调整三档 Tab 顺序。
+    
 
 ### 2.5 头部配置
 
@@ -168,16 +185,24 @@ flowchart TD
 移动端展示:
 
 1.  头图按移动端内容宽度铺满,保持视觉裁切,不得变形拉伸。
+    
 2.  头图为空或加载失败时,展示默认占位背景,不显示破图。
+    
 3.  主标题为空时不展示标题节点,副标题上移。
+    
 4.  副标题为空时不展示副标题节点。
+    
 5.  主标题、副标题过长时允许自然换行,但不得遮挡按钮或权益列表。
+    
 
 图片边界:
 
 1.  上传新图片后覆盖旧图片。
+    
 2.  图片超过后端限制大小时,上传失败并提示“图片大小超出限制,请重新上传”。
+    
 3.  图片格式限制以统一上传组件为准,建议支持 JPG、JPEG、PNG。
+    
 
 ### 2.6 按钮配置
 
@@ -200,10 +225,15 @@ flowchart TD
 交互规则:
 
 1.  已解锁按钮点击后,进入客户真实 MSC 对应的权益首页。
+    
 2.  已解锁按钮跳转目标不取当前查看 Tab;例如尊尚客户查看卓越 Tab,点击后仍进入尊尚客户权益首页。
+    
 3.  锁定按钮点击后,前端拦截跳转。
+    
 4.  如本期接入达标提示弹窗,点击锁定按钮后展示半屏达标提示弹窗;弹窗内容不由权益介绍页后台配置。
+    
 5.  连续点击按钮时需防重复跳转。
+    
 
 ### 2.7 权益列表配置
 
@@ -237,17 +267,26 @@ flowchart TD
 校验规则:
 
 1.  当某档“展示权益图片”开启时,该档所有权益项必须上传图片。
+    
 2.  任一权益缺图时,保存草稿和发布上线均阻断。
+    
 3.  缺图提示格式:“{Tab名称} 第 {序号} 个权益未上传图片,请补充后再保存。”
+    
 4.  缺图时后台自动切换到对应客户层级 Tab。
+    
 
 移动端展示边界:
 
 1.  标题为空且描述有值时,仅展示描述。
+    
 2.  描述为空且标题有值时,仅展示标题。
+    
 3.  标题和描述均为空时,该权益项不展示,避免出现空行。
+    
 4.  当前 Tab 无权益项时,移动端隐藏权益列表区域,保留头图、标题、副标题和按钮。
+    
 5.  权益图片加载失败时展示默认图标或圆点兜底,不显示破图。
+    
 
 ### 2.8 配置数据结构
 
@@ -283,9 +322,13 @@ flowchart TD
 数据规则:
 
 1.  `premier`、`elite`、`global` 为固定键,不允许运营新增或删除。
+    
 2.  `privilegeImageEnabled` 为当前层级列表级开关,不是单条权益字段。
+    
 3.  前台不应依赖后台返回客户等级权重,解锁关系需由前端或服务端统一维护。
+    
 4.  接口返回二期轮播字段时,本期前端忽略。
+    
 
 ## 三、移动端展示需求
 
@@ -315,12 +358,19 @@ flowchart TD
 页面结构:
 
 1.  页面导航栏:标题“权益介绍页”,左侧返回按钮。
+    
 2.  顶部固定入口:仅无三档等级客户展示。
+    
 3.  等级 Tab:卓越理财、卓越理财·尊尚、环球私人银行。
+    
 4.  当前 Tab 的头图。
+    
 5.  当前 Tab 的主标题、副标题。
+    
 6.  进入权益首页按钮。
+    
 7.  当前 Tab 的权益列表。
+    
 
 ### 3.3 客户等级与默认展示
 
@@ -338,9 +388,13 @@ flowchart TD
 边界规则:
 
 1.  客户等级读取失败或无法识别时,按无等级处理。
+    
 2.  无等级客户默认选中第一个 Tab,即卓越理财。
+    
 3.  切换 Tab 只改变当前查看内容,不改变客户真实等级。
+    
 4.  后台配置顺序不影响移动端展示顺序。
+    
 
 ### 3.4 顶部固定入口
 
@@ -362,24 +416,35 @@ flowchart TD
 交互规则:
 
 1.  点击后进入客户真实 MSC 对应的权益首页。
+    
 2.  对无三档等级客户,进入对应权益首页兜底版本。
+    
 3.  该入口不支持后台隐藏、改名或换图。
+    
 4.  跳转失败时提示“暂时无法查看,请稍后再试”。
+    
 
 ### 3.5 等级 Tab 与内容切换
 
 展示规则:
 
 1.  固定展示三档 Tab:卓越理财、卓越理财·尊尚、环球私人银行。
+    
 2.  Tab 名称读取后台 `tabName`,为空时使用默认名称。
+    
 3.  当前 Tab 需展示高亮文字和下划线。
+    
 
 交互规则:
 
 1.  支持点击 Tab 切换内容。
+    
 2.  如后续支持左右滑动切换,滑动结果需与点击 Tab 完全一致。
+    
 3.  切换后需刷新头图、主标题、副标题、按钮状态、权益列表。
+    
 4.  Tab 名称过长时需单行省略或自适应压缩。
+    
 
 ### 3.6 头部视觉区
 
@@ -394,9 +459,13 @@ flowchart TD
 边界规则:
 
 1.  头图为空或加载失败时展示默认占位。
+    
 2.  主标题为空时隐藏标题节点。
+    
 3.  副标题为空时隐藏副标题节点。
+    
 4.  文案过长时自然换行,不遮挡按钮和权益列表。
+    
 
 ### 3.7 按钮展示与跳转
 
@@ -412,15 +481,22 @@ flowchart TD
 展示规则:
 
 1.  已解锁状态展示后台 `buttons.unlocked`,为空时默认“进入权益专区”。
+    
 2.  锁定状态展示后台 `buttons.locked`,为空时默认“待解锁”。
+    
 3.  已解锁和锁定状态需有明显视觉区分。
+    
 
 点击规则:
 
 1.  已解锁按钮点击后,跳转至客户真实 MSC 对应的权益首页。
+    
 2.  当前正在查看的 Tab 不作为跳转目标。
+    
 3.  锁定按钮点击后不跳转,可展示达标提示弹窗。
+    
 4.  连续点击需防重复跳转。
+    
 
 ### 3.8 权益列表展示
 
@@ -436,11 +512,17 @@ flowchart TD
 展示规则:
 
 1.  权益列表按后台配置顺序展示。
+    
 2.  `privilegeImageEnabled=false` 时,展示圆点列表样式。
+    
 3.  `privilegeImageEnabled=true` 时,展示每条权益图片。
+    
 4.  图片加载失败时展示默认图标或圆点兜底。
+    
 5.  当前 Tab 无权益项时,隐藏权益列表区域。
+    
 6.  标题和描述均为空的权益项不展示。
+    
 
 ### 3.9 异常与兜底
 
@@ -493,4 +575,6 @@ flowchart TD
 | M12 | 空权益项 | 标题和描述均为空的权益项不展示 |
 | M13 | 空权益列表 | 当前 Tab 无权益项时隐藏列表区域 |
 | M14 | 二期轮播排除 | 页面不展示精选活动/活动轮播图 |
-| M15 | 异常提示 | 配置不存在、未发布、接口失败时展示错误或重试提示 |
+| M15 | 异常提示 | 配置不存在、未发布、接口失败时展示错误或重试提示 |
+
+<br>

+ 88 - 1
工作空间/产研/权益/汇丰全行/需求文档/权益首页.md → 工作空间/产研/权益/汇丰全行/需求文档/权益首页移动端.md

@@ -19,12 +19,19 @@
 本期权益首页包含以下模块:
 
 1.  客户身份模块。
+    
 2.  专属礼遇模块。
+    
 3.  信用卡权益模块。
+    
 4.  活动广告轮播图模块。
+    
 5.  积分商城与常用兑换模块。
+    
 6.  积分规则弹窗。
+    
 7.  后台保存草稿、发布上线、获取链接、Campaign ID 和 Channel ID 参数拼接。
+    
 
 ### 1.2 本期明确不包含
 
@@ -142,9 +149,13 @@ flowchart TD
 边界规则:
 
 1.  Campaign ID、Channel ID 为空时,不拼接对应参数。
+    
 2.  参数拼接需避免重复问号;已有查询参数时使用 `&` 追加。
+    
 3.  点击复制后提示“链接已复制到剪贴板”。
+    
 4.  移动端需透传 Campaign ID、Channel ID 到埋点和后续跳转链路,便于归因。
+    
 
 ### 3.3 客户身份模块配置
 
@@ -180,11 +191,17 @@ flowchart TD
 身份配置边界:
 
 1.  私人银行身份不展示账户月内日均余额,也不展示余额说明配置。
+    
 2.  运筹理财不配置等级说明,移动端不展示等级名称旁边的“?”说明入口。
+    
 3.  卓越理财和卓越理财尊尚的等级说明只解释客户身份等级来源,不包含本期排除的“合格(上月日均余额)”。
+    
 4.  若“显示账户月内日均余额”关闭,则移动端隐藏余额行、截止日期和余额说明“?”入口。
+    
 5.  若余额说明为空但余额开关开启,移动端仍可展示余额行,但点击“?”时展示默认提示“暂无说明”或不展示“?”,以产品最终口径为准。推荐后台在发布时校验余额说明必填。
+    
 6.  后台不提供“身份标识文字”“显示合格状态”“显示 PB 权益等级及编号”等旧字段配置。
+    
 
 ### 3.4 活动广告轮播图配置
 
@@ -233,9 +250,13 @@ flowchart TD
 发布校验:
 
 1.  存在广告项时,每条广告必须填写完整跳转配置。
+    
 2.  跳转类型为小程序时,AppID 和页面路径必须同时填写。
+    
 3.  跳转类型切换后,后台应清空非当前类型的旧字段,避免移动端误读。
+    
 4.  不配置任何广告时,移动端隐藏活动广告模块。
+    
 
 ### 3.5 常用兑换配置
 
@@ -260,9 +281,13 @@ flowchart TD
 边界规则:
 
 1.  展示文字建议不超过 8 个中文字符,超长时移动端最多展示 1 行,超出省略。
+    
 2.  兑换图标加载失败时,该兑换项可隐藏或展示默认图标。推荐展示默认图标并记录前端日志。
+    
 3.  常用兑换项为空时,移动端隐藏“常用兑换”区域;积分账户区域仍按积分账户状态展示。
+    
 4.  跳转链接为空时阻断保存或发布。
+    
 
 ### 3.6 积分规则弹窗配置
 
@@ -276,9 +301,13 @@ flowchart TD
 富文本规则:
 
 1.  支持文字、基础样式、链接、图片。
+    
 2.  移动端渲染前需做安全过滤,禁止执行脚本。
+    
 3.  富文本内链接按外部链接识别规则处理。
+    
 4.  内容为空、仅空格或仅空标签时视为空内容。
+    
 
 ### 3.7 专属礼遇与信用卡权益后台联动
 
@@ -287,16 +316,24 @@ flowchart TD
 借记卡权益进入权益首页需同时满足:
 
 1.  商品状态为已上架。
+    
 2.  审核状态为审核通过。
+    
 3.  “显示页面 / 权益首页”配置为显示。
+    
 4.  当前客户命中商品的展示客群或标签条件。
+    
 5.  当前时间处于商品展示有效期内。
+    
 
 信用卡权益进入权益首页需同时满足:
 
 1.  信用卡权益配置“显示页面 / 权益首页”为显示。
+    
 2.  当前客户满足信用卡模块展示前提。
+    
 3.  如权益配置了卡种、客户等级、资格、次数等条件,移动端按客户实际数据判断可用状态。
+    
 
 排序规则:
 
@@ -339,10 +376,15 @@ flowchart TD
 模块顺序:
 
 1.  客户身份模块。
+    
 2.  专属礼遇模块。
+    
 3.  信用卡权益模块。
+    
 4.  活动广告轮播图模块。
+    
 5.  积分商城与常用兑换模块。
+    
 
 任一模块因配置为空、客户不命中、接口无数据而隐藏时,不占位,不留白,后续模块自动上移。
 
@@ -381,16 +423,24 @@ flowchart TD
 说明弹窗:
 
 1.  点击等级名称旁边的“?”,展示后台配置的等级说明富文本。
+    
 2.  点击余额名称旁边的“?”,展示后台配置的余额说明富文本。
+    
 3.  弹窗样式为底部弹窗或系统统一弹窗,支持点击关闭按钮和遮罩关闭。
+    
 4.  弹窗内容为空时不应展示空弹窗。
+    
 
 编号复制:
 
 1.  卓越理财尊尚客户如返回尊尚编号,则展示复制图标。
+    
 2.  环球私人银行客户如返回私行业务 ID,则展示复制图标。
+    
 3.  点击复制后调用系统剪贴板,成功提示“编号已复制”。
+    
 4.  编号为空时隐藏编号和复制图标。
+    
 
 ### 4.3 专属礼遇模块
 
@@ -399,10 +449,15 @@ flowchart TD
 展示规则:
 
 1.  仅展示“权益首页=显示”、已上架、审核通过且客户命中的权益。
+    
 2.  无权限或不命中的权益不渲染,不留空位。
+    
 3.  无任何可展示权益时,隐藏整个“专属礼遇”模块,包括标题和“我的权益”入口。
+    
 4.  点击“我的权益”进入我的权益列表页。
+    
 5.  点击具体权益 Icon 进入对应权益详情页或承接页。
+    
 
 排版规则:
 
@@ -439,9 +494,13 @@ flowchart TD
 排版规则:
 
 1.  一屏最多展示 4 个权益 Icon。
+    
 2.  超过 1 屏时支持横向滑动并展示 dots。
+    
 3.  “境外消费”等固定权益点击后进入信用卡权益平台对应 Tab 或锚点。
+    
 4.  “更多权益”如存在,由信用卡权益平台统一承接。
+    
 
 ### 4.5 活动广告轮播图模块
 
@@ -459,16 +518,24 @@ flowchart TD
 文案规则:
 
 1.  主标题为空时隐藏主标题。
+    
 2.  副标题为空时隐藏副标题。
+    
 3.  可见人群标签为空代表全量可见,移动端不展示人群标签文案。
+    
 4.  配置了特定人群标签时,可按产品要求展示标签名称;如无明确要求,移动端仅用于过滤,不外显。
+    
 
 点击规则:
 
 1.  H5 链接按链接安全规则跳转。
+    
 2.  固定链接按后台字典承接。
+    
 3.  小程序跳转需同时具备 AppID 和页面路径。
+    
 4.  跳转失败时提示“暂时无法打开,请稍后重试”。
+    
 
 ### 4.6 积分商城与常用兑换模块
 
@@ -485,10 +552,15 @@ flowchart TD
 积分账户展示:
 
 1.  默认展示客户总积分。
+    
 2.  点击总积分区域展开积分明细。
+    
 3.  再次点击收起积分明细。
+    
 4.  明细包含汇友荟积分、信用卡积分、最近到期积分等接口返回项。
+    
 5.  到期积分日期格式为 `YYYY年M月D日到期积分`。
+    
 
 常用兑换展示:
 
@@ -503,10 +575,15 @@ flowchart TD
 积分规则:
 
 1.  “积分规则”入口位于常用兑换区域右下方。
+    
 2.  点击后拉起底部弹窗。
+    
 3.  弹窗标题读取后台“弹窗顶部名称”。
+    
 4.  弹窗正文读取后台“规则内容”富文本。
+    
 5.  内容较长时弹窗内部滚动,页面底部安全区不遮挡正文。
+    
 
 ### 4.7 链接安全与外部跳转
 
@@ -534,9 +611,13 @@ flowchart TD
 加载规则:
 
 1.  首次进入权益首页时展示骨架屏。
+    
 2.  身份信息、权益列表、信用卡权益、积分账户可并行请求。
+    
 3.  单个模块失败时优先做模块级降级,不阻断其他模块展示。
+    
 4.  页面关键配置读取失败时展示页面级错误。
+    
 
 异常规则:
 
@@ -567,9 +648,13 @@ flowchart TD
 隐私与安全口径:
 
 1.  环球私人银行客户不展示账户月内日均余额。
+    
 2.  余额、编号、积分等客户敏感数据不得写入前端日志明文。
+    
 3.  外部链接跳转必须经过安全白名单判断。
+    
 4.  富文本渲染必须过滤脚本和危险属性。
+    
 
 ## 六、验收标准
 
@@ -615,4 +700,6 @@ flowchart TD
 | M17 | 积分规则 | 点击积分规则拉起底部弹窗,内容来自后台富文本 |
 | M18 | 外部链接 | 第三方外链跳转前展示离开汇丰中国确认弹窗 |
 | M19 | 图片异常 | 图片加载失败不出现破图,使用默认图或隐藏对应项 |
-| M20 | 页面异常 | 配置不存在、链接无效、接口异常时不暴露错误代码 |
+| M20 | 页面异常 | 配置不存在、链接无效、接口异常时不暴露错误代码 |
+
+<br>

+ 142 - 5
工作空间/产研/权益/汇丰全行/需求文档/汇丰借记卡移动端——权益.md

@@ -1,13 +1,21 @@
 **与后台的关联说明:**
 
 > -   移动端根据后台"是否权益平台兑换"、"商品类型"和"是否有详情页"组合判断跳转逻辑
+>     
 > -   后台"是否权益平台兑换"分为两类:**是**(商品类型为券码或微信立减金)、**否**(第三方权益:太古地产/龙腾/h5链接/小程序链接)
+>     
 > -   后台"外部权益链接"字段(太古地产/龙腾/h5链接/小程序链接)决定外部跳转时的交互流程
+>     
 > -   后台"展示规则"(展示时间限制 / 展示客群限制)控制移动端权益列表的可见性
+>     
 > -   后台"领取规则"(领取时间限制 / 领取客群限制)控制移动端权益详情页的领取校验;**领取客群统一由后台【发放计划】维护,未发放的权益移动端不展示**
+>     
 > -   后台"显示页面"(权益首页/列表页、组装页)决定权益在移动端哪些页面展示
+>     
 > -   后台"调整显示顺序"决定权益在移动端权益首页/列表中的展示顺序
+>     
 > -   后台"分享设置"决定权益是否支持分享及分享内容(分享名称、分享图片)
+>     
 
 ## 1\. 我的权益
 
@@ -20,34 +28,64 @@
 **(1)借记卡权益(新增)**
 
 -   **数据来源:** 后台权益商品列表中需同时满足:
+    
     -   商品状态:已上架
+        
     -   商品类型:不为微信立减金(微信立减金统一在"我的立减金"入口跳转展示)
+        
     -   展示时间限制:未开启 或 当前时间在后台配置的展示开始时间~展示结束时间范围内
+        
     -   展示客群限制:未开启 或 用户命中后台配置的客群标签ID
+        
     -   显示页面:后台配置了"权益首页/列表页 = 显示"
+        
     -   奖励点:如果权益商品开启了领取客群限制,需要判断用户的的该科目是否奖励点>0,如果等于0不展示
+        
 -   **列表字段**
+    
     -   权益列表图标(取后台"权益列表图标"字段)
+        
     -   权益名称(取后台"权益名称"字段)
+        
 -   **列表顺序:** 按照后台"调整显示顺序"排列(后台可通过列表【更多 → 调整显示顺序】调整)。
+    
 -   **列表跳转** 根据后台【是否权益平台兑换】+【商品类型】+【是否有详情页】判断:
+    
     -   **是否权益平台兑换=是(券码):** 跳转到权益详情页
+        
     -   **是否权益平台兑换=否,是否有详情页=没有:** 点击直接从列表跳转第三方链接
+        
         -   根据后台配置的【外部权益链接】类型判断跳转交互:
+            
         -   **h5链接:** 弹窗提示
+            
             -   **标题:** 您正在离开汇丰中国
+                
             -   **描述:** 请注意,外部网站的政策与我们网站的条款和条件以及隐私政策不同。将在新浏览器窗口或选项卡中打开下一个网站。
+                
             -   **操作**
+                
                 -   **取消:** 关闭弹窗
-                -   **继续前往:** 跳转后台配置的h5链接![|42%](../../../../../.resources/image-1776678703451.png)
+                    
+                -   **继续前往:** 跳转后台配置的h5链接
+                    
+                    ![|42%](../../../../../.resources/image-1776678703451.png)
         -   **小程序链接:** 弹窗提示
+            
             -   **标题:** 您正在离开汇丰中国
+                
             -   **描述:** 请注意,外部网站的政策与我们网站的条款和条件以及隐私政策不同。将在新浏览器窗口或选项卡中打开下一个网站。
+                
             -   **操作**
+                
                 -   **取消:** 关闭弹窗
+                    
                 -   **继续前往:** 跳转后台配置的微信小程序(使用小程序appid + 页面地址)
+                    
         -   **太古地产:** 点击后弹出授权提醒
+            
             -   勾选同意并**确认**后,显示离开提示,点击确认时调用太古地产接口,然后跳转至后端返回的太古地产免登地址。
+                
             -   **点击取消:** 关闭授权提醒。
                 
                 <br>
@@ -58,8 +96,12 @@
                 
                 ![|45%](../../../../../.resources/image-1776678755981.png)
 -   **龙腾:** 点击后弹出授权提醒
+    
     -   勾选同意并**确认**后,显示离开提示,点击确认时调用龙腾接口,然后跳转至后端返回的龙腾免登地址。
-    -   **点击取消:** 关闭授权提醒。![|46%](../../../../../.resources/image-1776678775726.png)
+        
+    -   **点击取消:** 关闭授权提醒。
+        
+        ![|46%](../../../../../.resources/image-1776678775726.png)
 
 **(2)信用卡权益(现有)**
 
@@ -74,48 +116,77 @@
 展示顺序:
 
 1.  是否权益平台兑换=是的券码权益(未过期)。
+    
 2.  信用卡权益。
+    
 3.  是否权益平台兑换=是的券码权益(已失效)。
+    
 
 筛选项:
 
 -   全部
+    
 -   可使用:未过期
+    
 -   已失效
+    
 
 ![](../../../../../.resources/image-1776678841917.png)
 
 **(1)券码权益(未过期)**
 
 -   **列表字段**
+    
     -   权益列表图标(取"权益图片"字段)
+        
     -   权益名称(取"权益名称"字段)
+        
     -   可用次数
+        
 -   **列表顺序**
+    
     -   领取时间倒序展示
+        
 -   **跳转链接**
+    
     -   跳转订单详情页
+        
 
 **(2)信用卡权益**
 
 -   **列表字段**
+    
     -   权益列表图标(取后台"权益列表图标"字段)
+        
     -   权益名称(取后台"权益名称"字段)
+        
     -   权益副标题(取后台"权益副标题"字段,为空则不展示)
+        
 -   **列表顺序**
+    
     -   领取时间倒序展示
+        
 -   **跳转链接**
+    
     -   跳转订单详情页
+        
 
 **(3)券码权益(已失效)**
 
 -   **列表字段**
+    
     -   权益列表图标(取后台"权益列表图标"字段)
+        
     -   权益名称(取后台"权益名称"字段)
+        
 -   **列表顺序**
+    
     -   领取时间倒序展示
+        
 -   **跳转链接**
+    
     -   跳转订单详情页
+        
 
 ## 2\. 权益详情
 
@@ -124,19 +195,33 @@
 **(1)详情页内容**
 
 -   **权益主图:** 取后台"权益主图"字段
+    
 -   **权益名称:** 取后台"权益名称"字段,超出换行展示
+    
 -   **权益介绍:** 取后台"权益介绍"字段,非富文本,样式固定。图中卡片中权益名称下方内容
+    
 -   **权益有效期:** 取后台"权益有效期"字段
+    
     -   取发放计划字段:
+        
         -   固定日期:【开始日期】-【结束日期】
+            
         -   相对日期:领取后 x 天内有效
+            
     -   自定义:显示自定义输入的内容
+        
 -   **单客户权益数量(无领取限制则不显示):**x 次(整个周期)、月度 x 次、季度 x 次、年度 x 次
+    
 -   **权益内容:** 取后台"权益内容"字段,富文本展示,图中卡片下方的内容。PDF链接需支持点击打开预览查看,不需要支持下载。
+    
 -   **权益说明:** 取后台"权益说明"字段,富文本展示。PDF链接需支持点击打开预览查看,不需要支持下载。
+    
 -   **按钮:** 根据后台"领取规则"中的领取时间限制判断
+    
     -   **开启了领取时间限制且未到领取开始时间:** 显示【未开始】,按钮无法点击
+        
     -   **开启了领取时间限制且超过领取结束时间:** 显示【已失效】,按钮无法点击
+        
 -   **其他:** 显示【领取权益】
     
     <br>
@@ -169,6 +254,8 @@
     
     <br>
     
+    <br>
+    
     ![|50%](../../../../../.resources/image-1776678923546.png)![|50%](../../../../../.resources/image-1776678942001.png)
 
 **(2)领取权益**
@@ -182,9 +269,13 @@
 **规则校验失败提示:**
 
 -   **领取客群不满足时:** 展示后台配置的不满足提示
+    
     -   内容:取后台"领取客群限制-不满足提示-提示内容"
+        
     -   按钮文案:取后台"领取客群限制-不满足提示-跳转按钮文案"
+        
     -   按钮跳转:取后台"领取客群限制-不满足提示-跳转按钮链接"
+        
 
 **规则校验成功,处理逻辑分两种情况:**
 
@@ -193,42 +284,72 @@
 领取成功提示:
 
 -   **内容:** 尊敬的客户,您已成功领取权益
+    
 -   **按钮文案:** 取后台配置的"领取成功按钮文案"(默认值:"马上去使用")
+    
 -   **按钮跳转:** 跳转权益订单详情页
+    
 
 **第二种:是否权益平台兑换=否,是否有详情页=有**
 
 根据后台配置的【外部权益链接】类型判断跳转交互:
 
 -   **h5链接:** 弹窗提示
+    
     -   **标题:** 您正在离开汇丰中国
+        
     -   **描述:** 请注意,外部网站的政策与我们网站的条款和条件以及隐私政策不同。将在新浏览器窗口或选项卡中打开下一个网站。
+        
     -   **操作**
+        
         -   **取消:** 关闭弹窗
-        -   **继续前往:** 跳转后台配置的h5链接![|48%](../../../../../.resources/image-1776679010497.png)
+            
+        -   **继续前往:** 跳转后台配置的h5链接
+            
+            ![|48%](../../../../../.resources/image-1776679010497.png)
 -   **小程序链接:** 弹窗提示
+    
     -   **标题:** 您正在离开汇丰中国
+        
     -   **描述:** 请注意,外部网站的政策与我们网站的条款和条件以及隐私政策不同。将在新浏览器窗口或选项卡中打开下一个网站。
+        
     -   **操作**
+        
         -   **取消:** 关闭弹窗
+            
         -   **继续前往:** 跳转后台配置的微信小程序(使用小程序appid + 页面地址)
+            
 -   **太古地产:** 点击后弹出授权提醒
+    
     -   勾选同意并**确认**后,显示离开提示,点击确认时调用太古地产接口,然后跳转至后端返回的太古地产免登地址。
-    -   **点击取消:** 关闭授权提醒。![](../../../../../.resources/image-1776679026345.png)
+        
+    -   **点击取消:** 关闭授权提醒。
+        
+        ![](../../../../../.resources/image-1776679026345.png)
 -   **龙腾:** 点击后弹出授权提醒
+    
     -   勾选同意并**确认**后,显示离开提示,点击确认时调用龙腾接口,然后跳转至后端返回的龙腾免登地址。
-    -   **点击取消:** 关闭授权提醒。![|45%](../../../../../.resources/image-1776679039096.png)
+        
+    -   **点击取消:** 关闭授权提醒。
+        
+        ![|45%](../../../../../.resources/image-1776679039096.png)
 
 ## 3\. 权益订单详情
 
 > 仅【券码】类型(是否权益平台兑换=是,商品类型=券码)领取成功后可进入此页面。
 
 -   兑换卡号:默认为掩码状态,点击右侧复制按钮可复制
+    
 -   兑换密钥:默认为掩码状态,点击右侧复制按钮可复制
+    
 -   操作:
+    
     -   隐藏:点击后兑换卡号和兑换密钥为掩码状态,卡号和密钥默认为掩码
+        
     -   显示:点击后变成明文显示
+        
 -   使用按钮:按钮文案取后台配置的"领取成功按钮文案"(默认值:"马上去使用"),点击跳转使用链接
+    
 
 ![](../../../../../.resources/image-1776679076106.png)
 
@@ -241,32 +362,48 @@
 逻辑:
 
 -   查询到openid→调接口→领取成功
+    
 -   查询不到openid、接口失败异常情况:系统异常
+    
 
 (1)商品展示信息
 
 -   权益图片:取后台"权益列表图标"字段
+    
 -   商品名称:取后台"权益名称"字段
+    
 -   副标题描述:取后台"权益副标题"字段
+    
 -   有效期:取SIP固定日期模式的有效期结束时间
+    
 -   按钮名称:根据3种状态分别展示
+    
     -   未领取:立即领取,点击领取该微信立减金
+        
     -   已领取:已领取,点击进入权益商品详情
+        
     -   已过期:已过期,不可点击
+        
 
 今日到期:已领取状态下,有效期结束日 = 当前日。
 
 (2)进入页面,默认展示客户名下所有 未领取、已领取、已过期的不同面值立减金。
 
 -   未领取:客户有权益点 且 有效期结束时间 < 当前时间;
+    
 -   已领取:实质是客户的立减金订单,有效期结束时间 < 当前时间;
+    
 -   已过期:有效期结束时间 >= 当前时间。
+    
 
 (3)展示顺序按照:未领取->已领取->已过期
 
 -   未领取:按照权益点有效期结束时间,先到期先展示;
+    
 -   已领取:按照权益下单时间,先下单先展示;
+    
 -   已过期:按照权益点有效期结束时间,先到期先展示。
+    
 
 (4)页面支持筛选不同状态的商品;不同筛选状态下,没有商品信息,则展示"暂无立减金"。
 

+ 8 - 0
工作空间/产研/权益/汇丰全行/需求文档/汇丰银行权益中台-FSD.md

@@ -74,6 +74,10 @@
 
 <br>
 
+<br>
+
+<br>
+
 ![BlockNote image](../../../../../.resources/image-1779243010146.png)![BlockNote image](../../../../../.resources/image-1779243019476.png)
 
 ## 2\. 用户登录
@@ -279,6 +283,10 @@
 
 <br>
 
+<br>
+
+<br>
+
 ![BlockNote image](./screenshots/2-2-equity-add-source-rongshu.png)![BlockNote image](./screenshots/dialog-rongshu-product.png)
 
 **C. 是否权益平台兑换 = 否**

BIN
工作空间/产研/权益/汇丰全行/需求文档/汇丰需求文档 (1).pdf


+ 0 - 514
工作空间/产研/权益/汇丰全行/需求文档/组装页.md

@@ -1,514 +0,0 @@
-# 组装页前后台需求文档
-
-版本日期:2026-05-31
-
-## 一、业务流程图
-
-### 1.1 后台配置与发布流程
-
-```mermaid
-flowchart TD
-    A["运营进入页面管理 / 组装页"] --> B["查询或新建组装页"]
-    B --> C{"是否已有页面 ID"}
-    C -- "否,新建" --> D["填写组装页名称和基础配置"]
-    C -- "是,编辑" --> E["加载已有草稿或线上配置"]
-    D --> F["添加并配置页面模块"]
-    E --> F
-    F --> G{"点击保存草稿或发布上线"}
-    G -- "保存草稿" --> H{"基础字段与强校验是否通过"}
-    G -- "发布上线" --> I{"发布校验是否通过"}
-    H -- "否" --> J["提示错误并停留当前页面"]
-    I -- "否" --> J
-    H -- "是" --> K["保存草稿版本"]
-    I -- "是" --> L["生成或更新线上发布版本"]
-    K --> M["可生成草稿预览链接"]
-    L --> N["可生成正式 H5 / 小程序链接"]
-```
-
-### 1.2 移动端访问与渲染流程
-
-```mermaid
-flowchart TD
-    A["客户打开组装页链接"] --> B{"链接是否包含 draft=1"}
-    B -- "是" --> C["读取草稿预览配置"]
-    B -- "否" --> D["读取已发布配置"]
-    C --> E{"页面配置是否存在"}
-    D --> E
-    E -- "否" --> F["展示页面不存在或已下线提示"]
-    E -- "是" --> G["读取组装页名称作为导航标题"]
-    G --> H["按模块 sort 顺序渲染"]
-    H --> I{"存在权益商品模块"}
-    I -- "否" --> J["渲染静态模块和按钮"]
-    I -- "是" --> K["获取客户标签 / MSC group/ 权益资格"]
-    K --> L["过滤并展示可见权益商品"]
-    L --> J
-```
-
-### 1.3 按钮点击与外部跳转流程
-
-```mermaid
-flowchart TD
-    A["用户点击按钮"] --> B{"点击动作"}
-    B -- "跳转链接" --> C["读取按钮跳转链接"]
-    B -- "弹窗提示" --> D["展示底部提示弹窗"]
-    D --> E{"用户点击确认"}
-    E -- "否,取消或关闭" --> F["关闭弹窗,留在当前页"]
-    E -- "是" --> G{"确认按钮是否配置跳转链接"}
-    G -- "否" --> F
-    G -- "是" --> C
-    C --> H{"是否第三方外部域名"}
-    H -- "否" --> I["直接跳转"]
-    H -- "是" --> J["展示离开汇丰中国确认弹窗"]
-    J --> K{"用户点击继续前往"}
-    K -- "否" --> F
-    K -- "是" --> I
-```
-
-## 二、后台配置需求
-
-### 2.1 后台入口与页面列表
-
-后台入口:`定制功能 / 汇丰银行 / 页面管理 / 组装页`
-
-| 功能  | 需求说明 | 边界规则 |
-| --- | --- | --- |
-| 查询  | 支持按页面 ID、页面名称查询 | 页面名称支持模糊查询;页面 ID 支持精确或包含查询,以后端实现为准 |
-| 重置  | 清空查询条件并刷新列表 | 重置后展示全部组装页 |
-| 新建组装页 | 进入新建编辑态 | 新建时页面 ID 显示“保存后生成” |
-| 编辑  | 进入组装页编辑态 | 编辑已发布页面时,未发布修改不影响当前线上版本 |
-| 链接  | 打开“获取页面链接”弹窗 | 未保存页面无页面 ID,禁止获取链接并提示“请先保存组装页再获取链接” |
-| 状态展示 | 展示草稿、已上架 | 已上架代表存在正式发布版本;草稿代表仅后台可编辑或预览 |
-| 时间展示 | 展示创建时间、更新时间 | 新建保存后写入创建时间和更新时间;编辑保存后只更新更新时间 |
-
-列表字段:
-
-| 字段  | 说明  |
-| --- | --- |
-| 操作  | 编辑、链接 |
-| 页面 ID | 系统生成的唯一页面标识 |
-| 组装页名称 | 后台配置名称,也作为移动端导航标题 |
-| 发布状态 | 草稿、已上架 |
-| 创建时间 | 页面首次保存时间 |
-| 更新时间 | 最近一次保存草稿或发布上线时间 |
-
-### 2.2 编辑页整体结构与操作
-
-编辑页由四部分组成:
-
-1.  顶部操作栏:返回、页面状态、获取链接、保存草稿、发布上线。
-2.  页面整体基础信息:页面 ID、组装页名称。
-3.  微信分享卡片配置。
-4.  动态模块配置区:图片模块、权益商品模块、图文模块、按钮模块。
-
-页面状态规则:
-
-| 状态  | 触发条件 | 后台展示 | 移动端影响 |
-| --- | --- | --- | --- |
-| 新建未保存 | 点击“新建组装页”后未保存 | 页面 ID 为“保存后生成” | 不可生成链接,移动端不可访问 |
-| 草稿  | 保存草稿且未发布 | 草稿  | 仅草稿预览链接可访问 |
-| 已上架 | 发布上线成功且无未发布修改 | 正式线上环境 | 正式链接读取该版本 |
-| 有未发布的修改 | 已发布页面编辑后未发布 | 有未发布的修改 | 正式链接仍读取上一次发布版本;草稿预览读取最新草稿 |
-
-操作规则:
-
-| 操作  | 需求说明 | 校验与边界 |
-| --- | --- | --- |
-| 返回  | 返回组装页列表 | 如存在未保存修改,应弹出确认:“当前修改未保存,确认返回?” |
-| 保存草稿 | 保存当前配置为草稿版本 | 必须校验基础字段和已添加模块的必填项;保存成功后生成页面 ID |
-| 发布上线 | 将当前配置发布为正式版本 | 在草稿校验基础上,要求至少存在 1 个可展示模块 |
-| 获取链接 | 生成正式链接和草稿预览链接 | 未保存页面禁止生成;未发布页面只展示草稿预览链接 |
-
-组装页名称规则:
-
-| 字段  | 规则  |
-| --- | --- |
-| 是否必填 | 保存草稿、发布上线均必填 |
-| 长度  | 1 至 40 个中文字符,英文、数字、空格按 1 个字符计 |
-| 用途  | 后台列表展示、移动端导航标题、小程序页面标题 |
-| 空值处理 | 为空时阻断保存并在字段下方提示“请输入组装页名称” |
-
-> 当前 Demo 中空名称会自动兜底为“未命名组装页”。正式需求不保留该行为,以必填校验为准。
-
-### 2.3 获取页面链接
-
-点击“获取链接”后展示链接弹窗,支持选择 Campaign ID、Channel ID,生成带投放参数的链接。
-
-| 链接类型 | 展示条件 | 链接格式 | 用途  |
-| --- | --- | --- | --- |
-| 正式 H5 链接 | 页面状态为已上架 | `https://hsbc.com/assembly?id={pageId}` | 正式投放 |
-| 正式小程序链接 | 页面状态为已上架 | `hsbc-app://assembly?id={pageId}` | 小程序或 App 内打开 |
-| 草稿预览链接 | 页面已保存 | `https://test.hsbc.com/assembly?id={pageId}&draft=1` | 内部测试预览 |
-
-参数拼接规则:
-
-| 参数  | 来源  | 规则  |
-| --- | --- | --- |
-| `id` | 页面 ID | 必填  |
-| `draft` | 是否草稿预览 | 草稿预览链接固定为 `draft=1` |
-| `CampaignId` | Campaign 下拉选择 | 选填;选择后拼接到链接 |
-| `Channelid` | Channel 下拉选择 | 选填;选择后拼接到链接 |
-
-边界规则:
-
-1.  未发布页面不展示正式 H5 链接和正式小程序链接。
-2.  Campaign ID、Channel ID 清空时,不拼接对应参数。
-3.  复制链接成功后提示“链接已复制到剪贴板”。
-4.  移动端需透传 Campaign ID、Channel ID 到埋点和后续跳转链路,便于归因。
-
-### 2.4 微信分享卡片配置
-
-微信分享卡片配置位于页面基础信息下方,用于控制小程序环境内的分享能力。
-
-| 字段  | 类型  | 必填规则 | 移动端消费规则 |
-| --- | --- | --- | --- |
-| 自定义微信分享 | 复选框 / 开关 | 非必填 | 开启后允许小程序分享;关闭后禁用小程序原生分享入口 |
-| 分享缩略图 | 图片上传 | 开启自定义分享后必填 | 作为微信聊天卡片缩略图 |
-| 分享标题 | 单行文本 | 开启自定义分享后必填 | 作为微信聊天卡片标题 |
-
-图片规则:
-
-| 项目  | 规则  |
-| --- | --- |
-| 文件格式 | JPG、JPEG、PNG |
-| 建议尺寸 | 100px x 100px |
-| 上传数量 | 仅 1 张 |
-| 替换  | 再次上传覆盖原图 |
-| 发布校验 | 开启自定义分享且未上传缩略图时,阻断保存或发布 |
-
-移动端规则:
-
-1.  非小程序环境打开组装页时,不展示微信原生分享控制逻辑。
-2.  小程序环境且自定义分享关闭时,前端调用小程序能力禁用分享菜单。
-3.  小程序环境且自定义分享开启时,前端向小程序分享 SDK 注入分享标题、分享缩略图和当前页面链接。
-4.  分享出去的链接必须携带当前页面 ID;如原链接含 Campaign ID、Channel ID,分享链接需保留参数。
-
-### 2.5 动态模块通用规则
-
-组装页支持以下模块:
-
-| 模块  | 类型  | 是否可重复 | 是否可复制 | 是否可删除 | 是否可排序 |
-| --- | --- | --- | --- | --- | --- |
-| 图片模块 | `image` | 是   | 是   | 是   | 是   |
-| 权益商品模块 | `privilege` | 否,每页最多 1 个 | 否   | 是   | 是   |
-| 图文模块 | `richtext` | 是   | 是   | 是   | 是   |
-| 按钮模块 | `buttonGroup` | 是   | 是   | 是   | 是   |
-
-添加规则:
-
-1.  新建组装页时默认不含任何业务模块。
-2.  页面底部固定展示添加模块按钮:图片模块、权益商品模块、图文模块、按钮模块。
-3.  页面已存在权益商品模块时,添加区隐藏“权益商品模块”按钮。
-4.  点击添加模块后,新模块追加到当前模块列表末尾。
-
-排序规则:
-
-| 场景  | 规则  |
-| --- | --- |
-| 模块排序 | 点击模块右上角“排序”,输入目标序号,确认后移动到目标位置 |
-| 序号范围 | 最小为 1,最大为当前模块数量 |
-| 无效输入 | 输入为空、非数字、超出范围时不执行移动,并提示“请输入有效的顺序位置” |
-| 同位置移动 | 目标位置等于当前位置时关闭气泡,不改变顺序 |
-
-复制规则:
-
-1.  图片、图文、按钮模块点击复制后,复制当前模块的全部配置内容。
-2.  复制出的模块生成新的模块 ID,默认插入到原模块下方。
-3.  权益商品模块不可复制,点击复制时提示“权益商品模块每页仅限配置一个,无法复制”。
-
-删除规则:
-
-1.  点击模块右上角“删除”后弹出二次确认:“确定删除该模块吗?”
-2.  确认后删除整个模块及其内部配置。
-3.  删除后不立即保存到后端,需点击保存草稿或发布上线后生效。
-
-发布校验规则:
-
-| 校验项 | 保存草稿 | 发布上线 |
-| --- | --- | --- |
-| 组装页名称 | 必填  | 必填  |
-| 至少 1 个展示模块 | 不强制 | 必填  |
-| 已添加图片模块上传图片 | 必填  | 必填  |
-| 已添加图文模块内容 | 必填  | 必填  |
-| 已添加按钮模块按钮文案 | 必填  | 必填  |
-| 按钮动作为跳转链接时填写链接 | 必填  | 必填  |
-| 按钮动作为弹窗提示时填写弹窗标题、描述、确认按钮文案 | 必填  | 必填  |
-| 微信分享开启时填写缩略图和标题 | 必填  | 必填  |
-
-### 2.6 图片模块
-
-后台配置:
-
-| 字段  | 类型  | 规则  |
-| --- | --- | --- |
-| 图片  | 图片上传 | 必填;仅支持 JPG、JPEG、PNG;建议宽度 750px;高度不限 |
-
-移动端展示:
-
-1.  图片按模块顺序展示。
-2.  图片宽度为页面通栏 `100%`,左右无边距。
-3.  高度不固定,按图片原始宽高比等比缩放。
-4.  图片不支持点击跳转。
-5.  图片加载失败时,该图片模块不展示,并记录前端错误日志。
-
-边界:
-
-1.  添加图片模块但未上传图片时,保存草稿和发布上线均阻断。
-2.  上传新图片后覆盖旧图片。
-3.  图片超过后端限制大小时,上传失败并提示“图片大小超出限制,请重新上传”。
-
-### 2.7 图文模块
-
-后台配置:
-
-| 字段  | 类型  | 规则  |
-| --- | --- | --- |
-| 图文内容 | 富文本编辑器 | 必填;支持文字、图片、链接、基础标题、列表、PDF 附件、表格 |
-
-富文本能力:
-
-| 能力  | 后台配置 | 移动端展示 |
-| --- | --- | --- |
-| 文字样式 | 支持加粗、斜体、下划线、标题、列表 | 按富文本样式渲染 |
-| 图片  | 支持正文内插入图片 | 宽度自适应,不撑破屏幕 |
-| 超链接 | 支持给文字配置 URL | 点击后走链接跳转规则 |
-| PDF 附件 | 支持上传 PDF 并插入附件标识 | 点击后拉起浏览器或系统 PDF 预览 |
-| 表格  | 支持基础表格 | 表格宽度超过屏幕时,外层横向滚动 |
-
-安全与边界:
-
-1.  移动端渲染前需对富文本进行安全过滤,禁止执行脚本。
-2.  富文本为空、仅空格、仅空标签时视为空内容。
-3.  富文本内链接也需执行外部域名识别;第三方外链必须展示“离开汇丰中国”确认弹窗。
-4.  富文本内图片加载失败时展示破图兜底或隐藏失败图片,不影响其他内容展示。
-
-### 2.8 权益商品模块
-
-权益商品模块用于展示与当前组装页关联的权益商品。
-
-后台配置:
-
-| 字段  | 类型  | 规则  |
-| --- | --- | --- |
-| 可配置大标题 | 单行文本 | 选填;为空时移动端隐藏模块标题 |
-| 权益商品列表 | 自动回显列表 | 来源于权益商品管理中选择“组装页=显示”且包含当前组装页的商品 |
-| 商品排序 | 行级排序 | 支持调整当前组装页内权益商品展示顺序 |
-
-商品排序仅作用于当前组装页,不影响权益首页/列表页排序,也不影响同一权益商品在其他组装页中的展示顺序。
-
-唯一性规则:
-
-1.  单个组装页最多添加 1 个权益商品模块。
-2.  已添加权益商品模块后,添加模块按钮区不再展示“权益商品模块”。
-3.  权益商品模块不支持复制。
-
-与权益商品管理的关系:
-
-在“借记卡权益 / 添加权益 / 编辑权益 / 更多设置 / 显示页面”中,支持配置:
-
-| 字段  | 规则  |
-| --- | --- |
-| 权益首页/列表页 | 选择显示或不显示 |
-| 组装页 | 选择显示或不显示 |
-| 组装页列表 | 当“组装页=显示”时,必须选择一个或多个组装页 |
-
-权益商品进入组装页展示列表需同时满足:
-
-1.  权益商品状态为已上架。
-2.  权益商品审核状态为审核通过。
-3.  权益商品配置了“组装页=显示”。
-4.  权益商品选择的组装页包含当前页面 ID。
-5.  如配置了展示时间限制,当前时间在展示时间范围内。
-6.  如配置了展示客群限制,当前客户命中配置客群。
-7.  如配置了领取客群或发放计划限制,移动端按领取资格决定点击后的领取结果;不满足领取资格的商品是否展示,按权益商品的展示规则决定。
-
-客户数据来源:
-
-| 数据  | 字段建议 | 说明  |
-| --- | --- | --- |
-| 客户 MSC Group List | `mscCodeList` | 用于判断客户所属 MSC 类型 |
-| 客户标签列表 | `tagIds` | 用于判断展示客群限制 |
-| 权益资格 | `eligibleProductIds` 或权益资格接口返回 | 用于判断客户是否可领取或可跳转 |
-
-移动端展示:
-
-1.  模块大标题为空时,只展示权益商品列表。
-2.  无可展示商品时,整个权益商品模块隐藏,包括大标题。
-3.  商品项展示圆形图标、权益名称、权益简介和右箭头。
-4.  商品图标为空时展示默认权益图标。
-5.  商品名称超长时最多展示 1 行,超出省略。
-6.  商品简介超长时最多展示 2 行,超出省略。
-
-点击交互:
-
-1.  点击商品项后,移动端先校验登录态。
-2.  未登录时进入 SSO 登录,登录完成后回到当前商品跳转链路。
-3.  银行自有或荣数商品走权益平台详情或领取页。
-4.  第三方权益如果配置“没有详情页”,从列表直接跳转外部权益链接。
-5.  第三方权益如果配置“有详情页”,先进入权益详情页,再由详情页承接领取或外部跳转。
-6.  跳转第三方 H5 前必须展示“离开汇丰中国”确认弹窗。
-
-> 当前 Demo 的权益商品模块内置了模拟 `items`。正式需求以“权益商品管理”联动回显为准,模块内不手工新增商品内容,只维护标题和当前页商品排序。
-
-### 2.9 按钮模块
-
-按钮模块用于配置页面操作入口。一个按钮模块内可添加多个按钮,移动端按后台按钮顺序纵向展示。
-
-按钮字段:
-
-| 字段  | 类型  | 必填  | 规则  |
-| --- | --- | --- | --- |
-| 按钮文案 | 单行文本 | 是   | 1 至 10 个中文字符 |
-| 点击动作 | 下拉选择 | 是   | 跳转链接、弹窗提示 |
-| 按钮样式 | 下拉选择 | 是   | 突出显示(红色)、次要操作(白色) |
-| 跳转链接类型 | 单选  | 条件必填 | 点击动作=跳转链接时展示并必填,支持 H5 链接跳转、固定链接跳转、小程序跳转 |
-| 目标链接 | 下拉选择 | 条件必填 | 跳转链接类型=固定链接跳转时展示并必填,选项由后台维护,当前 demo 包含“太古地产”“龙腾” |
-| 配置链接 | URL 输入框 | 条件必填 | 跳转链接类型=H5 链接跳转时展示并必填 |
-| 小程序 AppID | 单行文本 | 条件必填 | 跳转链接类型=小程序跳转时展示并必填 |
-| 小程序页面路径 | 单行文本 | 条件必填 | 跳转链接类型=小程序跳转时展示并必填 |
-| 弹窗标题 | 单行文本 | 条件必填 | 点击动作=弹窗提示时必填,最多 20 个中文字符 |
-| 弹窗描述 | 富文本编辑器 | 条件必填 | 点击动作=弹窗提示时必填 |
-| 左侧取消按钮文案 | 单行文本 | 条件必填 | 点击动作=弹窗提示时展示,默认“取消” |
-| 右侧确认按钮展示 | 勾选框 | 条件必填 | 点击动作=弹窗提示时展示,默认展示 |
-| 右侧确认按钮文案 | 单行文本 | 条件必填 | 右侧确认按钮展示=是时必填,最多 6 个中文字符,默认“确认” |
-| 确认按钮跳转链接类型 | 单选  | 条件必填 | 右侧确认按钮展示=是时展示并必填,支持 H5 链接跳转、固定链接跳转、小程序跳转 |
-| 确认按钮目标链接 | 下拉选择 | 条件必填 | 确认按钮跳转链接类型=固定链接跳转时展示并必填,选项由后台维护 |
-| 确认按钮配置链接 | URL 输入框 | 条件必填 | 确认按钮跳转链接类型=H5 链接跳转时展示并必填 |
-| 确认按钮小程序 AppID | 单行文本 | 条件必填 | 确认按钮跳转链接类型=小程序跳转时展示并必填 |
-| 确认按钮小程序页面路径 | 单行文本 | 条件必填 | 确认按钮跳转链接类型=小程序跳转时展示并必填 |
-
-按钮组内部操作:
-
-| 操作  | 规则  |
-| --- | --- |
-| 添加按钮 | 点击“添加按钮”后在按钮列表末尾新增按钮,默认文案为“新按钮”,默认样式为次要操作,默认动作为跳转链接 |
-| 内部排序 | 输入目标序号后移动按钮 |
-| 内部删除 | 点击删除图标后删除当前按钮;删除后如按钮列表为空,移动端不展示该按钮模块 |
-
-移动端展示:
-
-1.  按钮在页面内容流中展示,不固定在屏幕底部。
-2.  按钮宽度为内容区域 `100%`,按钮之间保持固定间距。
-3.  突出显示按钮为汇丰红底白字。
-4.  次要操作按钮为白底深色字,带浅灰描边。
-5.  同一按钮模块配置多个按钮时,按后台顺序纵向排列,后台不限制按钮数量。
-
-跳转链接规则:
-
-1.  点击动作=跳转链接时,必须完整填写当前跳转链接类型对应字段,否则阻断保存并提示“保存失败:按钮模块中存在未填写完整的跳转链接配置!”。
-2.  H5 链接跳转保存 `linkType=h5`、`linkH5`,并同步兼容字段 `link=linkH5`。
-3.  固定链接跳转保存 `linkType=fixed`、`fixedLink`,并同步兼容字段 `link=fixedLink`;切换到固定链接时如未选择目标链接,默认回填“太古地产”。
-4.  小程序跳转保存 `linkType=miniProgram`、`miniAppId`、`miniAppPath`,兼容字段 `link` 置空。
-5.  切换跳转链接类型时,清空未选中类型对应字段,避免保存无效冗余配置。
-6.  内部汇丰域名、App 内部路径或固定内部目标直接跳转。
-7.  第三方外部域名跳转前展示“离开汇丰中国”确认弹窗。
-8.  URL 格式非法时阻断保存并提示“请输入正确的跳转链接”。
-
-弹窗提示规则:
-
-1.  点击按钮后展示底部弹窗。
-2.  弹窗标题、描述、左侧取消按钮文案、右侧确认按钮展示状态和右侧确认按钮文案读取后台配置。
-3.  弹窗右上角固定展示关闭按钮。
-4.  弹窗底部固定展示左侧取消按钮;右侧确认按钮根据“展示”开关决定是否展示。
-5.  右侧确认按钮展示=是时,必须完整填写确认按钮跳转链接类型对应字段,否则阻断保存并提示“保存失败:按钮模块中存在未填写完整的跳转链接配置!”。
-6.  确认按钮跳转链接配置同样支持 H5 链接跳转、固定链接跳转、小程序跳转;字段保存规则与按钮跳转一致,对应字段为 `confirmLinkType`、`confirmFixedLink`、`confirmLinkH5`、`confirmMiniAppId`、`confirmMiniAppPath`,并同步兼容字段 `confirmLink`。
-7.  右侧确认按钮展示=否时,移动端不展示确认按钮,也不校验确认按钮跳转配置。
-8.  点击取消、关闭、遮罩层时关闭弹窗,不执行跳转。
-9.  点击确认后,如确认为第三方外部 H5 域名,先关闭底部弹窗,再展示“离开汇丰中国”确认弹窗。
-
-外部跳转确认弹窗固定文案:
-
-| 字段  | 文案  |
-| --- | --- |
-| 标题  | 您正在离开汇丰中国 |
-| 描述  | 请注意,外部网站的政策与我们网站的条款和条件以及隐私政策不同。将在新浏览器窗口或选项卡中打开下一个网站。 |
-| 取消按钮 | 取消  |
-| 确认按钮 | 继续前往 |
-
-## 三、移动端展示需求
-
-### 3.1 页面访问
-
-移动端通过页面 ID 访问组装页。
-
-| 场景  | 处理规则 |
-| --- | --- |
-| 页面 ID 为空 | 展示错误页:“页面参数错误” |
-| 页面不存在 | 展示错误页:“页面不存在或已下线” |
-| 页面未发布且非草稿预览 | 展示错误页:“页面不存在或已下线” |
-| 草稿预览链接 | 仅内部测试环境可访问,读取草稿配置 |
-| 正式链接 | 读取最近一次发布配置 |
-
-### 3.2 页面骨架
-
-| 区域  | 规则  |
-| --- | --- |
-| 状态栏 | 使用小程序或 App 容器原生状态栏 |
-| 导航栏 | 标题读取组装页名称 |
-| 返回按钮 | 点击返回上一页;无上一页时返回权益首页或关闭当前 WebView |
-| 内容区 | 白底,自上而下按模块顺序渲染 |
-| 安全区 | iOS 底部安全区需留白,避免按钮或内容被遮挡 |
-
-### 3.3 模块渲染顺序
-
-1.  移动端严格按后台保存的模块顺序渲染。
-2.  后台未配置任何模块时,正式页面不可发布;草稿预览展示空白内容区。
-3.  某个模块因数据为空或客户不命中而隐藏时,不占位,不影响后续模块展示。
-4.  同类型模块可以出现多次,权益商品模块除外。
-
-### 3.4 微信小程序分享
-
-| 条件  | 结果  |
-| --- | --- |
-| 非小程序环境 | 不处理微信小程序原生分享 |
-| 小程序环境且自定义分享关闭 | 禁用右上角分享能力 |
-| 小程序环境且自定义分享开启 | 分享标题、缩略图、链接使用后台配置 |
-
-分享链接规则:
-
-1.  分享链接使用正式页面链接,不使用草稿预览链接。
-2.  原访问链接带 Campaign ID、Channel ID 时,分享链接保留参数。
-3.  分享缩略图为空或分享标题为空时,后台不允许发布,因此移动端不做二次兜底。
-
-### 3.5 外部链接识别
-
-后端根据汇丰内部域名白名单识别链接是否为外部链接。白名单内链接直接跳转,白名单外链接展示离开汇丰中国确认弹窗。
-
-白名单由后端维护,范围包含:
-
-| 类型  | 示例  |
-| --- | --- |
-| 汇丰 H5 域名 | `hsbc.com`、`hsbc.com.cn` 及项目实际域名 |
-| 小程序内部路径 | `/pages/`、`hsbc-app://` |
-| 测试域名 | `test.hsbc.com`,仅测试环境生效 |
-
-## 四、验收标准
-
-### 4.1 后台验收
-
-| 编号  | 验收项 | 通过标准 |
-| --- | --- | --- |
-| A1  | 新建组装页 | 输入组装页名称并保存草稿后生成页面 ID,列表出现新记录 |
-| A2  | 组装页名称必填 | 名称为空时保存草稿和发布上线均失败 |
-| A3  | 添加模块 | 图片、图文、按钮模块可多次添加;权益商品模块最多添加 1 次 |
-| A4  | 模块排序 | 输入合法序号后模块移动到目标位置 |
-| A5  | 模块复制 | 图片、图文、按钮模块可复制,权益商品模块不可复制 |
-| A6  | 模块删除 | 删除前二次确认,确认后模块从编辑区移除 |
-| A7  | 分享配置校验 | 开启自定义分享但缺少缩略图或标题时保存失败 |
-| A8  | 按钮链接校验 | 按钮动作为跳转链接,或弹窗右侧确认按钮开启但对应跳转配置未填写完整时保存失败 |
-| A9  | 草稿和正式链接 | 草稿页面只展示预览链接,已上架页面展示正式链接和预览链接 |
-| A10 | 权益商品联动 | 商品配置组装页显示后,在对应组装页权益商品模块中回显 |
-
-### 4.2 移动端验收
-
-| 编号  | 验收项 | 通过标准 |
-| --- | --- | --- |
-| M1  | 标题回显 | 导航标题等于后台组装页名称 |
-| M2  | 模块顺序 | 移动端模块顺序与后台配置一致 |
-| M3  | 图片展示 | 图片通栏展示,按原始比例等比缩放 |
-| M4  | 富文本展示 | 图片、链接、PDF、表格均可正常展示或预览 |
-| M5  | 权益商品过滤 | 未上架、未审核通过、不属于当前页面、不命中客户条件的商品不展示 |
-| M6  | 权益商品空列表 | 无可见商品时隐藏整个权益商品模块 |
-| M7  | 按钮跳转 | H5、固定链接、小程序跳转按后台配置执行;内部链接直接跳转,外部链接先展示离开汇丰确认弹窗 |
-| M8  | 弹窗按钮 | 弹窗取消关闭不跳转;确认按钮按后台展示开关控制,展示时按配置的跳转规则执行 |
-| M9  | 微信分享 | 开关关闭时小程序禁用分享;开启时分享卡片使用后台标题和缩略图 |
-| M10 | 异常页面 | 页面不存在、未发布、ID 为空时展示错误提示 |

+ 30 - 0
工作空间/产研/权益/汇丰全行/需求文档/组装页2.md → 工作空间/产研/权益/汇丰全行/需求文档/组装页移动端.md

@@ -7,9 +7,13 @@
 需求范围包括:
 
 -   移动端组装页:页面标题、微信分享、图片模块、图文模块、权益商品模块、按钮模块、底部弹窗、第三方跳转拦截提示。移动端运行环境包括 App 内 H5 和微信小程序内页面。
+    
 -   后台管理端:组装页列表、查询、分页、新建、编辑、保存草稿、发布上线、获取链接、模块新增/复制/删除/排序、微信分享卡片配置、各模块字段配置。
+    
 -   关联配置:借记卡/信用卡权益商品的“显示页面-组装页”选择,决定权益商品是否可在指定组装页中被动态展示。
+    
 -   接口范围:当前未提供正式接口说明,本文根据 PDF、PC Demo 与 H5 Demo 设计虚拟接口,均标记为【虚拟接口,待技术确认】。
+    
 
 ## 二、功能范围与模块结构
 
@@ -18,26 +22,42 @@
 组装页是可灵活配置的营销活动或线下沙龙引导落地页。页面内容由后台按模块顺序拼装,移动端在 App 内 H5 或微信小程序内按配置顺序自上而下展示。后台的链接配置项用于描述目标类型,选择 H5 时配置 H5 地址,选择小程序时配置 AppID 和页面路径;移动端再根据当前运行环境和端能力执行打开或跳转。
 
 -   顶部系统状态栏与导航栏:展示返回入口和页面标题。页面标题读取后台“组装页名称”,用于 Page Title 与导航栏标题回显。
+    
 -   微信分享能力:仅小程序环境生效。后台开启自定义微信分享后,前端注入分享标题、缩略图与当前页面链接;关闭时禁用分享菜单。
+    
 -   图片模块:作为页面头图或通栏广告图展示,移动端不支持点击。
+    
 -   图文模块:展示活动说明、正文、文字链接与外链图标。正文由后台富文本配置生成。后台可添加多个图文模块,移动端按排序多次出现时仍属于同一个功能模块,本文只描述一次完整规则。
+    
 -   权益商品模块:展示可配置大标题和当前客户有权限看到的权益商品列表。具体商品来自全局权益商品配置,并结合客户标签、发放资格和组装页展示配置动态筛选。
+    
 -   按钮模块:展示一个或多个操作按钮,支持红色主按钮和白色次按钮;点击动作支持直接跳转或先弹出底部提示弹窗。
+    
 -   底部提示弹窗:由按钮模块配置触发,展示弹窗标题、描述、取消按钮、确认按钮及确认按钮跳转配置。
+    
 -   第三方跳转拦截弹窗:当跳转目标识别为第三方页面时,在真正跳转前提示“您正在离开汇丰中国”,客户确认后才跳转。
+    
 
 ### 2.2 后台/管理端配置结构
 
 后台入口为“汇丰银行 / 页面管理 / 组装页”。后台通过列表和编辑页管理多个组装页。
 
 -   列表查询区:支持按页面 ID、页面名称筛选组装页。
+    
 -   列表区:展示操作、页面 ID、组装页名称、发布状态、创建时间、更新时间,并提供分页。发布状态包括“草稿”“待发布”“正式”。
+    
 -   新建/编辑页基础信息区:配置组装页名称,页面 ID 由系统保存后自动生成。
+    
 -   微信分享卡片配置区:控制是否启用自定义微信分享,并配置分享缩略图和分享标题。
+    
 -   模块配置区:支持图片模块、权益商品模块、图文模块、按钮模块的组合配置。
+    
 -   模块操作区:支持模块排序、复制、删除;权益商品模块每页仅允许一个,不支持复制。
+    
 -   按钮配置区:支持配置按钮文案、点击动作、按钮样式、跳转目标、弹窗内容和弹窗确认按钮跳转。后台配置的跳转目标可以是 H5 地址、小程序 AppID/页面路径,或固定目标“太古地产”“龙腾”。
+    
 -   获取链接弹窗:支持生成正式环境 H5 链接、小程序链接和草稿预览链接,并可选择 Campaign ID、Channel ID 拼接到链接参数。草稿状态仅生成草稿预览链接;待发布和正式状态可生成正式环境链接与草稿预览链接。
+    
 
 ### 2.3 前后台关系概览
 
@@ -384,13 +404,21 @@
 4.1.7 后台边界规则
 
 -   新建页面未保存时点击“获取链接”,提示“请先保存组装页再获取链接!”。
+    
 -   编辑页有未保存修改时点击返回或离开路由,弹出“当前有未保存修改,离开后修改内容将丢失,确定离开吗?”。
+    
 -   编辑页有未保存修改时点击“获取链接”,弹窗仍可打开,但提示“当前链接基于上一次保存内容,最新修改需保存后生效”。
+    
 -   草稿状态仅展示草稿预览链接;正式或已修改状态展示正式环境链接和草稿预览链接。
+    
 -   权益商品模块每页仅限一个;添加后底部“权益商品模块”新增按钮隐藏。
+    
 -   权益商品模块不支持复制。
+    
 -   模块排序、按钮排序、权益商品排序输入目标序号范围为 1 至当前列表长度,越界不移动。
+    
 -   删除模块需二次确认;删除按钮 Demo 未展示二次确认,建议补充确认规则。
+    
 
 ## 五、接口与字段说明
 
@@ -699,3 +727,5 @@ stateDiagram-v2
 | 10  | 发布确认 | PC Demo 发布上线未展示二次确认,但发布属于线上生效动作,是否需要确认弹窗未明确。 | 后台发布流程 | 产品  | 当前记录 Demo 行为,并建议补充确认 |
 | 11  | 删除能力 | PC Demo 中存在删除接口和方法,但列表操作列未展示删除按钮。是否需要组装页删除能力未明确。 | 后台列表操作、权限 | 产品  | 文档未作为列表显性按钮要求,仅在边界规则中保留 |
 | 12  | 移动端空页面 | 若组装页仅有权益商品模块且客户无可见权益,页面是否展示空白、失效页或其他兜底文案未明确。 | H5/小程序端体验 | 产品、设计 | 当前按“隐藏权益模块,其他模块正常展示”处理 |
+
+<br>

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.