Browse Source

完善活动模块原型与文档

csk 17 hours ago
parent
commit
de9c408f22
21 changed files with 3373 additions and 1146 deletions
  1. 1 0
      启动项目.txt
  2. BIN
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/public/demo-assets/privileges/品质生活.png
  3. BIN
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/public/demo-assets/privileges/国际教育.png
  4. BIN
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/public/demo-assets/privileges/礼宾服务.png
  5. BIN
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/public/demo-assets/resource-banners/轮播图1.png
  6. BIN
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/public/demo-assets/resource-banners/轮播图2.jpg
  7. BIN
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/public/demo-assets/resource-banners/轮播图3.png
  8. 49 18
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/components/activity/ActivityAchievementConfig.vue
  9. 15 3
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/components/activity/ActivityRewardEditor.vue
  10. 15 4
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/components/activity/ActivityRuleEditor.vue
  11. 6 0
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/mock/index.js
  12. 7 1
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/router/index.js
  13. 141 13
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/store/activityStore.js
  14. 528 137
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/views/ActivityEdit.vue
  15. 69 21
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/views/ActivityManagement.vue
  16. 79 21
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/views/ActivityMutexGroup.vue
  17. 72 5
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/views/ActivityReport.vue
  18. 634 40
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/views/ActivityView.vue
  19. 16 0
      工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/views/PageAssembly.vue
  20. 0 883
      工作空间/产研/权益/汇丰全行/需求文档/权益平台活动模块_产品文档_后台菜单进度模板版.md
  21. 1741 0
      工作空间/产研/权益/汇丰全行/需求文档/活动模块需求文档.md

+ 1 - 0
启动项目.txt

@@ -0,0 +1 @@
+Start-Process -FilePath "cmd.exe" -ArgumentList '/c cd /d "E:\工作文件\汇丰项目\rongshu3\工作空间\产研\权益\汇丰全行\产品原型\pc-demo" && npm run dev' -WindowStyle Hidden

BIN
工作空间/产研/权益/汇丰全行/产品原型/pc-demo/public/demo-assets/privileges/品质生活.png


BIN
工作空间/产研/权益/汇丰全行/产品原型/pc-demo/public/demo-assets/privileges/国际教育.png


BIN
工作空间/产研/权益/汇丰全行/产品原型/pc-demo/public/demo-assets/privileges/礼宾服务.png


BIN
工作空间/产研/权益/汇丰全行/产品原型/pc-demo/public/demo-assets/resource-banners/轮播图1.png


BIN
工作空间/产研/权益/汇丰全行/产品原型/pc-demo/public/demo-assets/resource-banners/轮播图2.jpg


BIN
工作空间/产研/权益/汇丰全行/产品原型/pc-demo/public/demo-assets/resource-banners/轮播图3.png


+ 49 - 18
工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/components/activity/ActivityAchievementConfig.vue

@@ -31,7 +31,7 @@
 
       <section class="config-block">
         <h3>达标规则标签配置</h3>
-        <el-button :disabled="form.ntbTimeRules.length >= 1" :icon="Plus" @click="addTimeRule">添加时间标签</el-button>
+        <el-button v-if="!readonly" :disabled="form.ntbTimeRules.length >= 1" :icon="Plus" @click="addTimeRule">添加时间标签</el-button>
         <div v-for="(rule, index) in form.ntbTimeRules" :key="rule.key" class="time-rule">
           <el-form-item label="规则标签" label-width="120px" required>
             <div class="time-rule-controls">
@@ -51,10 +51,10 @@
               />
             </div>
           </el-form-item>
-          <el-button class="time-rule-delete" type="danger" link :icon="Delete" title="删除时间标签" @click="form.ntbTimeRules.splice(index, 1)" />
+          <el-button v-if="!readonly" class="time-rule-delete" type="danger" link :icon="Delete" title="删除时间标签" @click="form.ntbTimeRules.splice(index, 1)" />
         </div>
-        <el-button type="primary" :icon="Plus" @click="addRule(form.ntbRules)">添加进度标签</el-button>
-        <ActivityRuleEditor v-for="(rule, index) in form.ntbRules" :key="rule.key" :rule="rule" :index="index" mode="ntb" removable @remove="form.ntbRules.splice(index, 1)" />
+        <el-button v-if="!readonly" type="primary" :icon="Plus" @click="addRule(form.ntbRules)">添加进度标签</el-button>
+        <ActivityRuleEditor v-for="(rule, index) in form.ntbRules" :key="rule.key" :rule="rule" :index="index" mode="ntb" :removable="!readonly" @remove="form.ntbRules.splice(index, 1)" />
       </section>
 
       <section class="config-block reward-section">
@@ -71,8 +71,8 @@
         <div class="field-panel">
           <el-form-item label="已达标展示标签" required><el-select v-model="form.mgmAchievedTag" style="width:260px"><el-option label="客户MSC Group" value="客户MSC Group" /></el-select></el-form-item>
         </div>
-        <el-button type="primary" :icon="Plus" @click="addMgmRule">添加进度标签</el-button>
-        <ActivityRuleEditor v-for="(rule, index) in form.mgmRules" :key="rule.key" :rule="rule" :index="index" mode="mgm" :removable="form.mgmRules.length > 1" @remove="form.mgmRules.splice(index, 1)" />
+        <el-button v-if="!readonly" type="primary" :icon="Plus" @click="addMgmRule">添加进度标签</el-button>
+        <ActivityRuleEditor v-for="(rule, index) in form.mgmRules" :key="rule.key" :rule="rule" :index="index" mode="mgm" :removable="!readonly && form.mgmRules.length > 1" @remove="form.mgmRules.splice(index, 1)" />
       </section>
       <section class="config-block">
         <h3>被推荐进度配置</h3>
@@ -124,12 +124,12 @@
                 <el-input v-model="level.title" class="compact-copy-input" />
               </el-form-item>
             </div>
-            <el-button type="primary" :icon="Plus" @click="addActivity(level)">添加活动</el-button>
+            <el-button v-if="!readonly" type="primary" :icon="Plus" @click="addActivity(level)">添加活动</el-button>
 
             <div v-for="(activity, activityIndex) in level.activities" :key="activity.key" class="layer-activity">
               <div class="activity-head">
                 <div><strong>活动{{ activityIndex + 1 }}</strong><span v-if="activityIndex > 0" class="notice">*注意:活动{{ activityIndex + 1 }}达标前提为活动{{ activityIndex }}</span></div>
-                <el-button v-if="level.activities.length > 1" type="danger" link :icon="Delete" @click="level.activities.splice(activityIndex, 1)">删除活动</el-button>
+                <el-button v-if="!readonly && level.activities.length > 1" type="danger" link :icon="Delete" @click="level.activities.splice(activityIndex, 1)">删除活动</el-button>
               </div>
               <div class="layer-activity-fields">
                 <el-form-item label="活动标题" label-width="120px">
@@ -139,8 +139,8 @@
                   <el-input v-model="activity.description" class="compact-copy-input" />
                 </el-form-item>
               </div>
-              <el-button type="primary" :icon="Plus" @click="addRule(activity.rules)">添加进度标签</el-button>
-              <ActivityRuleEditor v-for="(rule, ruleIndex) in activity.rules" :key="rule.key" :rule="rule" :index="ruleIndex" mode="layer" :dependent="activityIndex > 0" :removable="activity.rules.length > 1" @remove="activity.rules.splice(ruleIndex, 1)" />
+              <el-button v-if="!readonly" type="primary" :icon="Plus" @click="addRule(activity.rules)">添加进度标签</el-button>
+              <ActivityRuleEditor v-for="(rule, ruleIndex) in activity.rules" :key="rule.key" :rule="rule" :index="ruleIndex" mode="layer" :dependent="activityIndex > 0" :removable="!readonly && activity.rules.length > 1" @remove="activity.rules.splice(ruleIndex, 1)" />
               <div class="activity-reward"><ActivityRewardEditor :reward="activity.reward" :grant-plans="grantPlans" /></div>
             </div>
           </el-tab-pane>
@@ -163,7 +163,8 @@ export default {
   props: {
     form: { type: Object, required: true },
     crowds: { type: Array, default: () => [] },
-    grantPlans: { type: Array, default: () => [] }
+    grantPlans: { type: Array, default: () => [] },
+    readonly: { type: Boolean, default: false }
   },
   data() { return { Delete, Plus, activeLayer: '0' } },
   created() { this.normalize() },
@@ -262,20 +263,27 @@ export default {
       this.form.layerCount = this.form.layerCount || this.form.achievementLayers.length || 2
       this.syncLayerCount(this.form.layerCount)
     },
-    handleTemplateChange() { this.normalize() },
+    handleTemplateChange() {
+      if (this.readonly) return
+      this.normalize()
+    },
     addRule(list) {
+      if (this.readonly) return
       if (list.length >= 5) return this.$message.warning('最多添加 5 个进度标签')
       list.push(this.rule())
     },
     addTimeRule() {
+      if (this.readonly) return
       if (this.form.ntbTimeRules.length >= 1) return
       this.form.ntbTimeRules.push({ key: key('time'), field: '开户时间', operator: '介于', dateRange: [] })
     },
     addMgmRule() {
+      if (this.readonly) return
       if (this.form.mgmRules.length >= 5) return this.$message.warning('最多添加 5 个进度标签')
       this.form.mgmRules.push(this.rule('开户时间'))
     },
     addActivity(level) {
+      if (this.readonly) return
       if (level.activities.length >= 5) return this.$message.warning('每个层级最多添加 5 个活动')
       level.activities.push(this.activity(level.activities.length))
     },
@@ -290,7 +298,7 @@ export default {
 </script>
 
 <style scoped>
-.achievement-config { display:flex; flex-direction:column; gap:20px; padding:4px 0 12px; }
+.achievement-config { display:flex; flex-direction:column; gap:20px; padding:0 0 12px; }
 .template-row {
   display:flex;
   align-items:center;
@@ -298,14 +306,14 @@ export default {
   padding:20px 24px 2px;
   background:#fff;
   border:1px solid #f0f2f5;
-  border-radius:8px;
+  border-radius:4px;
   box-shadow:0 1px 3px rgba(0,0,0,.05);
 }
 .config-block {
   padding:0 24px 24px;
   background:#fff;
   border:1px solid #f0f2f5;
-  border-radius:8px;
+  border-radius:4px;
   box-shadow:0 1px 3px rgba(0,0,0,.05);
   overflow:hidden;
 }
@@ -320,9 +328,30 @@ export default {
 }
 .field-panel { margin-bottom:24px; padding:20px 20px 2px; background:#f7f8fa; border:1px solid #f0f2f5; border-radius:4px; }
 .copy-panel :deep(.el-form-item__content) { max-width:640px; }
-.compact-copy-panel :deep(.el-form-item__label),
+.template-row :deep(.el-form-item),
+.field-panel :deep(.el-form-item),
+.layer-meta-fields :deep(.el-form-item),
+.layer-activity-fields :deep(.el-form-item),
+.reward-section :deep(.reward-type-row .el-form-item) { display:block; margin-bottom:18px; }
+.template-row :deep(.el-form-item__label),
+.field-panel :deep(.el-form-item__label),
 .layer-meta-fields :deep(.el-form-item__label),
-.layer-activity-fields :deep(.el-form-item__label) { white-space:nowrap; }
+.layer-activity-fields :deep(.el-form-item__label),
+.reward-section :deep(.reward-type-row .el-form-item__label) {
+  width:auto !important;
+  height:auto;
+  margin-bottom:8px;
+  justify-content:flex-start;
+  color:#303133;
+  font-weight:600;
+  line-height:22px;
+  white-space:nowrap;
+}
+.template-row :deep(.el-form-item__content),
+.field-panel :deep(.el-form-item__content),
+.layer-meta-fields :deep(.el-form-item__content),
+.layer-activity-fields :deep(.el-form-item__content),
+.reward-section :deep(.reward-type-row .el-form-item__content) { margin-left:0 !important; display:block; line-height:normal; }
 .compact-copy-input { width:480px; max-width:100%; }
 .boolean-select { width:100px; }
 .mgm-referred-panel :deep(.el-form-item__label) { white-space:nowrap; }
@@ -339,7 +368,9 @@ export default {
   border:1px solid #ebeef5;
   border-radius:4px;
 }
-.time-rule :deep(.el-form-item__label) { white-space:nowrap; }
+.time-rule :deep(.el-form-item) { display:block; }
+.time-rule :deep(.el-form-item__label) { width:auto !important; height:auto; margin-bottom:8px; justify-content:flex-start; color:#303133; font-weight:600; line-height:22px; white-space:nowrap; }
+.time-rule :deep(.el-form-item__content) { margin-left:0 !important; display:block; line-height:normal; }
 .time-rule-controls { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
 .time-rule-delete { position:absolute; top:16px; right:18px; }
 .compact-field-select { width:280px; }

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

@@ -28,7 +28,7 @@
           </el-select>
         </el-form-item>
         <el-form-item label="权益有效期" label-width="130px" required>
-          <el-date-picker v-model="reward.validity" class="reward-validity-picker" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" />
+          <el-date-picker v-model="reward.validity" class="reward-validity-picker" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" style="width:420px;max-width:100%;flex:0 1 420px" />
         </el-form-item>
         <el-form-item label="权益赠送次数" label-width="130px" required>
           <div class="reward-times-controls">
@@ -74,11 +74,23 @@ export default {
 .reward-config.titled .reward-type-row { padding:0 24px; }
 .field-panel { padding:18px 18px 0; background:#f7f8fa; border:1px solid #f0f2f5; border-radius:4px; }
 .reward-config.titled .field-panel { margin:0 24px 24px; }
+.reward-type-row :deep(.el-form-item),
+.reward-field-panel :deep(.el-form-item) { display:block; margin-bottom:18px; }
 .reward-type-row :deep(.el-form-item__label),
-.reward-field-panel :deep(.el-form-item__label) { white-space:nowrap; }
+.reward-field-panel :deep(.el-form-item__label) {
+  width:auto !important;
+  height:auto;
+  margin-bottom:8px;
+  justify-content:flex-start;
+  color:#303133;
+  font-weight:600;
+  line-height:22px;
+  white-space:nowrap;
+}
+.reward-type-row :deep(.el-form-item__content),
+.reward-field-panel :deep(.el-form-item__content) { margin-left:0 !important; display:block; line-height:normal; }
 .unit { margin-left:10px; color:#606266; }
 .reward-plan-select { width:480px; max-width:100%; }
-.reward-validity-picker { width:320px; max-width:100%; flex:0 1 320px; }
 .reward-times-controls { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
 .grant-period-select { width:150px; }
 .times-input { width:110px; }

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

@@ -323,12 +323,23 @@ export default {
   border-radius:4px;
 }
 .rule-panel.dependent { background:#fff1f1; border-color:#f7c6c7; }
-.rule-panel :deep(.el-form-item__label) { white-space:nowrap; }
+.rule-panel :deep(.el-form-item) { display:block; margin-bottom:18px; }
+.rule-panel :deep(.el-form-item__label) {
+  width:auto !important;
+  height:auto;
+  margin-bottom:8px;
+  justify-content:flex-start;
+  color:#303133;
+  font-weight:600;
+  line-height:22px;
+  white-space:nowrap;
+}
+.rule-panel :deep(.el-form-item__content) { margin-left:0 !important; display:block; line-height:normal; }
 .rule-head { margin-bottom:14px; display:flex; justify-content:space-between; align-items:center; }
 .unit { margin-left:8px; color:#606266; }
 .ntb-delete { position:absolute; top:12px; right:14px; z-index:1; font-size:18px; }
 .ntb-main-rule { margin-right:48px; }
-.ntb-details { margin-left:100px; }
+.ntb-details { margin-left:0; }
 .ntb-inline { display:flex; align-items:center; gap:14px; flex-wrap:wrap; color:#303133; }
 .ntb-field-select { width:240px; }
 .ntb-operator-select { width:160px; }
@@ -362,7 +373,7 @@ export default {
   align-items:flex-start;
   gap:18px;
   flex-wrap:wrap;
-  margin-left:70px;
+  margin-left:0;
 }
 .layer-month-input { width:128px; }
 .layer-copy-fields { max-width:620px; }
@@ -380,7 +391,7 @@ export default {
 .mgm-trb-status :deep(.el-form-item__label),
 .mgm-copy-fields :deep(.el-form-item__label) { white-space:nowrap; }
 .mgm-month-input { width:128px; }
-.mgm-date-range { width:300px; }
+.mgm-date-range { width:320px; }
 
 @media (max-width:1200px) {
   .ntb-details { margin-left:0; }

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

@@ -161,6 +161,9 @@ const assemblyList = [
       title: '',
       desc: ''
     },
+    returnHomeConfig: {
+      enabled: false
+    },
     blocks: [
       { type: 'image', id: 'b_init1', data: { imageUrl: 'https://images.unsplash.com/photo-1557683316-973673baf926?w=800' } },
       { type: 'privilege', id: 'b_init2', data: { title: '专属权益', items: [{ id: '1', title: '机场贵宾厅服务', desc: '全年免费使用6次指定机场贵宾厅' }, { id: '2', title: '境内礼宾车', desc: '免费2次接送' }] } },
@@ -177,6 +180,9 @@ const assemblyList = [
       title: '',
       desc: ''
     },
+    returnHomeConfig: {
+      enabled: false
+    },
     blocks: [
       { type: 'image', id: 'b_init5', data: { imageUrl: 'https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?w=800' } },
       { type: 'privilege', id: 'b_init6', data: { title: '尊享体验', items: [{ id: '1', title: '高尔夫权益', desc: 'VIP高尔夫俱乐部免会籍预约' }] } }

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

@@ -69,9 +69,15 @@ const routes = [
       {
         path: 'customization/hsbc/activities/view/:id',
         name: 'ActivityView',
-        component: () => import('@/views/ActivityView.vue'),
+        component: () => import('@/views/ActivityEdit.vue'),
         meta: { title: '查看活动' }
       },
+      {
+        path: 'customization/hsbc/activities/audit/:id',
+        name: 'ActivityAudit',
+        component: () => import('@/views/ActivityEdit.vue'),
+        meta: { title: '活动审核' }
+      },
       {
         path: 'customization/hsbc/activity-forms',
         name: 'ActivityFormList',

+ 141 - 13
工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/store/activityStore.js

@@ -22,14 +22,15 @@ function emptyActivity() {
     auditor: '',
     target: '借记卡新客',
     shareTitle: '',
-    shareImage: '',
+    shareImage: [],
     cities: [],
+    cityText: '',
     description: '',
     scrmActivityCode: '',
     scrmChannelCode: '',
     labels: [],
-    thumbnail: '',
-    detailImage: '',
+    thumbnail: [],
+    detailImage: [],
     displayTitle: '',
     displaySubtitle: '',
     buttonText: '立即报名',
@@ -38,7 +39,11 @@ function emptyActivity() {
     activityTime: [],
     displayTime: [],
     location: '',
+    offlineEnabled: false,
     content: '',
+    layerContents: [
+      { key: 'layer-copy-1', title: '层级一', content: '' }
+    ],
     showOnHome: true,
     showOnAll: true,
     showOnAssembly: false,
@@ -152,6 +157,7 @@ function emptyActivity() {
     signupNoticeTemplateId: '',
     beforeStartTemplateId: '',
     rewardTemplateId: '',
+    rewardNoClaimTemplateId: '',
     ownerStaffId: '',
     beforeStartSmsEnabled: false,
     rewardSmsEnabled: false,
@@ -170,13 +176,53 @@ const state = reactive({
       ...emptyActivity(),
       id: 'ACT202606001',
       internalTitle: '2026借记卡新客开户达标活动',
-      name: '新客开户达标礼',
+      name: '汇丰卓越理财新户达标礼',
+      category: '普通活动',
       status: 'published',
       auditStatus: '已发布',
+      target: '2026年新开户并完成卓越理财资产达标的借记卡客户',
+      shareTitle: '报名汇丰卓越理财新户达标礼,解锁积分奖励',
+      shareImage: [{ name: '轮播图1.png', url: '/demo-assets/resource-banners/轮播图1.png' }],
+      cities: ['全国'],
+      cityText: '全国',
+      description: '面向新户的开户与资产达标活动,报名后完成开户、资产留存和指定月度达标即可获得积分奖励。',
+      scrmActivityCode: 'SCRM-ACT-NTB-202607',
+      scrmChannelCode: 'HSBC-RS-HOME',
+      labels: ['新客', '达标奖励', '积分'],
+      thumbnail: [{ name: '国际教育.png', url: '/demo-assets/privileges/国际教育.png' }],
+      detailImage: [{ name: '轮播图1.png', url: '/demo-assets/resource-banners/轮播图1.png' }],
+      displayTitle: '汇丰卓越理财新户达标礼',
+      displaySubtitle: '开户并完成资产达标,最高可享320万积分奖励',
+      buttonText: '立即报名',
+      detailButtonText: '查看达标进度',
       activityTime: ['2026-07-01', '2026-09-30'],
       signupTime: ['2026-06-25', '2026-09-15'],
+      displayTime: ['2026-07-01', '2026-09-30'],
+      location: '线上参与',
+      content: '<p>活动期间,客户完成汇丰借记卡开户,并在活动期内保持指定资产达标,即可参与积分奖励。</p><p>活动流程:报名活动 → 完成开户 → 维持月末 TRB 达标 → 系统自动发放积分。</p><p>奖励说明:基础达标可获得积分奖励,连续达标客户可获得进阶奖励。</p><p>温馨提示:达标结果以汇丰系统记录为准,数据通常于 T+1 更新。</p>',
+      layerContents: [
+        { key: 'ntb-layer-copy-1', title: '基础达标', content: '<p>完成借记卡开户,且活动期内任一月末 TRB 达到人民币50万元,可获得基础积分奖励。</p>' },
+        { key: 'ntb-layer-copy-2', title: '进阶达标', content: '<p>在基础达标基础上,连续3个月保持 TRB 达标,可获得进阶积分奖励。</p>' }
+      ],
       signedCount: 628,
       quota: 3000,
+      signupSuccessTitle: '报名成功',
+      signupSuccessDescription: '您已成功报名,后续可在“我的活动”中查看达标进度。',
+      extraForms: [
+        { name: '航空会员号', placeholder: '请输入航空会员号(如无可填“暂无”)', type: 'text', required: true, maxLength: 30 },
+        { name: '常用手机号', placeholder: '请输入用于接收活动通知的手机号', type: 'text', required: true, maxLength: 11 }
+      ],
+      progressCopy: {
+        unreached: '您已报名,请在活动期内完成开户及资产达标。',
+        reached: '恭喜您已完成达标,奖励将在规则约定时间内发放。',
+        help: '开户、资产和奖励状态通常于 T+1 更新。'
+      },
+      ntbTimeRules: [
+        { key: 'ntb-open-date', field: '开户时间', operator: '介于', value: '2026-07-01 至 2026-09-30', dateRange: ['2026-07-01', '2026-09-30'], reachedText: '已完成开户', unreachedText: '未完成开户' }
+      ],
+      ntbRules: [
+        { key: 'ntb-trb-50', field: 'TRB值(含家庭账户)', operator: '大于等于', value: 500000, dateRange: ['2026-07-01', '2026-09-30'], openingMonthRequired: true, continuous: true, months: 3, allowBreak: true, reachedText: '已达50万', unreachedText: '未达50万', progressText: '月末 TRB 达标进度', failureReason: '资产未达到活动要求' }
+      ],
       visibleCrowdId: 'ACD001',
       signupCrowdId: 'ACD001',
       signupFormId: 'FORM001',
@@ -184,28 +230,68 @@ const state = reactive({
       progressRuleId: 'PROG001',
       grantPlanId: 'AGP001',
       auditor: '王审核',
+      ownerStaffId: 'U10086',
+      beforeStartSmsEnabled: true,
+      rewardSmsEnabled: true,
+      signupNoticeTemplateId: 'NTF001',
+      rewardTemplateId: 'NTF002',
+      launchUrl: 'https://demo.hsbc.com.cn/activity/ACT202606001',
       launchChannels: ['权益首页', '全部活动', '渠道二维码']
     },
     {
       ...emptyActivity(),
       id: 'ACT202606002',
       internalTitle: '酒店合作方新客礼遇活动',
-      name: '酒店新客礼遇',
-      status: 'draft',
-      auditStatus: '未提交',
+      name: '汇丰高端酒店新客礼遇',
+      category: '普通活动',
+      status: 'pending',
+      auditStatus: '待审核',
       target: '潜客/新客',
+      shareTitle: '报名汇丰高端酒店新客礼遇',
+      shareImage: [{ name: '轮播图2.jpg', url: '/demo-assets/resource-banners/轮播图2.jpg' }],
+      cities: ['北京', '上海', '广州', '深圳'],
+      cityText: '北京、上海、广州、深圳',
+      description: '与高端酒店合作的新客礼遇活动,客户完成报名与开户达标后可领取酒店权益券。',
+      scrmActivityCode: 'SCRM-ACT-HOTEL-202607',
+      scrmChannelCode: 'HSBC-RS-PARTNER',
+      labels: ['酒店礼遇', '新客', '第三方权益'],
+      thumbnail: [{ name: '品质生活.png', url: '/demo-assets/privileges/品质生活.png' }],
+      detailImage: [{ name: '轮播图2.jpg', url: '/demo-assets/resource-banners/轮播图2.jpg' }],
+      displayTitle: '汇丰高端酒店新客礼遇',
+      displaySubtitle: '完成开户及资产达标,领取高端酒店权益券',
+      buttonText: '立即报名',
+      detailButtonText: '查看礼遇详情',
       signupTime: ['2026-07-05', '2026-08-31'],
       activityTime: ['2026-07-05', '2026-10-31'],
+      displayTime: ['2026-07-05', '2026-10-31'],
+      location: '线上报名,指定城市酒店权益适用',
+      content: '<p>活动期间,符合条件的新客户报名后完成开户及资产达标,可领取合作酒店礼遇。</p><p>参与方式:填写酒店会员号 → 完成汇丰开户 → 达成资产要求 → 跳转领取酒店权益券。</p><p>权益数量有限,先到先得。若报名名额已满,页面将提示无法继续报名。</p>',
+      layerContents: [
+        { key: 'hotel-layer-copy-1', title: '报名要求', content: '<p>客户需在报名期内提交酒店会员号,并确认授权用于活动资格核验。</p>' },
+        { key: 'hotel-layer-copy-2', title: '达标奖励', content: '<p>完成开户及资产达标后,可领取合作酒店权益券,具体权益以领取页展示为准。</p>' }
+      ],
       signedCount: 1200,
       quota: 1200,
+      signupSuccessTitle: '报名成功,待完成达标',
+      signupSuccessDescription: '请在活动期内完成开户与资产达标,达标后可查看酒店权益券。',
       visibleCrowdId: 'ACD001',
       signupCrowdId: 'ACD001',
       signupFormId: 'FORM002',
       progressTemplateId: 'TPL002',
       progressRuleId: 'PROG001',
       grantPlanId: 'AGP001',
-      extraFields: [
-        { name: '酒店会员号', placeholder: '请输入酒店会员号', type: 'text', required: true, maxLength: 30, link: '' }
+      extraForms: [
+        { name: '酒店会员号', placeholder: '请输入酒店会员号', type: 'text', required: true, maxLength: 30 },
+        { name: '入住偏好城市', placeholder: '请输入偏好城市,如上海', type: 'text', required: false, maxLength: 20 }
+      ],
+      progressCopy: {
+        unreached: '您已报名,请继续完成开户、资产和酒店会员信息核验。',
+        reached: '您已完成活动达标,可前往领取酒店权益券。',
+        help: '酒店会员号用于合作方权益发放,请确保填写准确。'
+      },
+      ntbRules: [
+        { key: 'hotel-open', field: '开户时间', operator: '介于', value: '2026-07-05 至 2026-10-31', dateRange: ['2026-07-05', '2026-10-31'], reachedText: '已完成开户', unreachedText: '未完成开户' },
+        { key: 'hotel-trb', field: 'TRB值(含家庭账户)', operator: '大于等于', value: 500000, dateRange: ['2026-07-05', '2026-10-31'], openingMonthRequired: true, continuous: false, months: 1, allowBreak: false, reachedText: '已达资产要求', unreachedText: '未达资产要求', progressText: '资产达标进度', failureReason: '资产未达到活动要求' }
       ],
       reward: {
         name: '酒店权益券',
@@ -213,17 +299,36 @@ const state = reactive({
         grantMode: '达标后跳转领取',
         buttonText: '查看奖励',
         linkTarget: '第三方权益页'
-      }
+      },
+      ownerStaffId: 'U10087',
+      signupNoticeTemplateId: 'NTF001',
+      rewardTemplateId: 'NTF002',
+      launchUrl: 'https://demo.hsbc.com.cn/activity/ACT202606002',
+      launchChannels: ['全部活动', '渠道二维码']
     },
     {
       ...emptyActivity(),
       id: 'ACT202606003',
       internalTitle: 'MGM亲友推荐互斥演示活动',
-      name: 'MGM 亲友推荐活动',
+      name: '汇丰亲友推荐双享礼',
       category: 'MGM活动',
       status: 'offline',
       auditStatus: '已下线',
       target: '推荐人与被推荐人',
+      shareTitle: '邀请亲友加入汇丰,推荐人与被推荐人均可享奖励',
+      shareImage: [{ name: '轮播图3.png', url: '/demo-assets/resource-banners/轮播图3.png' }],
+      cities: ['全国'],
+      cityText: '全国',
+      description: '用于演示 MGM 推荐活动与其他 NTB 活动的互斥关系、T+1 标签修正和推荐人侧进度展示。',
+      scrmActivityCode: 'SCRM-ACT-MGM-202605',
+      scrmChannelCode: 'HSBC-RS-MGM',
+      labels: ['MGM', '亲友推荐', '互斥演示'],
+      thumbnail: [{ name: '礼宾服务.png', url: '/demo-assets/privileges/礼宾服务.png' }],
+      detailImage: [{ name: '轮播图3.png', url: '/demo-assets/resource-banners/轮播图3.png' }],
+      displayTitle: '汇丰亲友推荐双享礼',
+      displaySubtitle: '推荐亲友开户达标,双方均有机会获得奖励',
+      buttonText: '查看推荐进度',
+      detailButtonText: '查看活动规则',
       needExtraInfo: false,
       visibleCrowdId: 'ACD002',
       signupCrowdId: 'ACD002',
@@ -236,14 +341,37 @@ const state = reactive({
       mutexGroup: 'NTB新客互斥组',
       activityTime: ['2026-05-01', '2026-06-15'],
       signupTime: ['2026-05-01', '2026-06-10'],
+      displayTime: ['2026-05-01', '2026-06-15'],
+      location: '线上推荐',
+      content: '<p>推荐人通过活动链接邀请亲友,被推荐人在活动期内完成开户及资产达标后,推荐人与被推荐人可分别获得活动奖励。</p><p>本活动与同组 NTB 新客活动互斥。若客户已报名航司、酒店等同组新客活动,将无法继续参与本活动。</p><p>系统会在次日根据 MGM 标签、开户时间和报名记录进行修正,演示 T+1 标签延迟场景。</p>',
+      layerContents: [
+        { key: 'mgm-layer-copy-1', title: '推荐关系', content: '<p>推荐人与被推荐人需建立有效推荐关系,被推荐人需符合新客条件。</p>' },
+        { key: 'mgm-layer-copy-2', title: '开户达标', content: '<p>被推荐人在活动期内完成开户并达到基础资产要求后,双方奖励进入发放流程。</p>' }
+      ],
       progressStyle: 'MGM推荐进度',
+      progressCopy: {
+        unreached: '被推荐人尚未完成开户或资产达标,请继续关注进度。',
+        reached: '被推荐人已完成达标,推荐奖励将按规则发放。',
+        help: '推荐关系、开户和资产状态通常于 T+1 更新。'
+      },
+      mgmRules: [
+        { key: 'mgm-relation', field: '客户MSC Group', operator: '大于等于', value: 1, reachedText: '推荐关系有效', unreachedText: '推荐关系待确认', progressText: '推荐关系核验', failureReason: '未建立有效推荐关系' },
+        { key: 'mgm-trb', field: 'TRB值(含家庭账户)', operator: '大于等于', value: 500000, dateRange: ['2026-05-01', '2026-06-15'], reachedText: '被推荐人已达标', unreachedText: '被推荐人未达标', progressText: '被推荐人资产进度', failureReason: '被推荐人资产未达标' }
+      ],
+      mgmAchievedTag: 'MGM_REACHED_202606',
       reward: {
         name: '推荐立减金',
         type: '立减金',
         grantMode: '达标后自动发放',
         buttonText: '前往领取',
         linkTarget: '立减金列表'
-      }
+      },
+      blockText: '您已参与同组新客活动,暂无法继续参与亲友推荐活动。',
+      ownerStaffId: 'U10088',
+      signupNoticeTemplateId: 'NTF001',
+      rewardTemplateId: 'NTF002',
+      launchUrl: 'https://demo.hsbc.com.cn/activity/ACT202606003',
+      launchChannels: ['全部活动', 'MGM推荐链接']
     }
   ],
   forms: [
@@ -502,7 +630,7 @@ export default {
       Object.assign(target, clone(payload), { updatedAt: nowText() })
       return target
     }
-    const record = { ...clone(payload), id: genActivityId(), status: 'draft', auditStatus: '未提交', creator: '当前用户', createdAt: nowText(), updatedAt: nowText() }
+    const record = { ...clone(payload), id: genActivityId(), status: payload.status || 'draft', auditStatus: payload.auditStatus || '未提交', creator: '当前用户', createdAt: nowText(), updatedAt: nowText() }
     state.activities.unshift(record)
     return record
   },

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

@@ -1,52 +1,176 @@
 <template>
-  <div class="activity-edit">
-    <el-card>
-      <template #header><div class="page-header"><span>{{ isEdit ? '编辑活动' : '新建活动' }}</span><el-button @click="goBack">返回</el-button></div></template>
-      <el-form ref="formRef" :model="form" :rules="rules" label-width="150px">
-        <el-steps :active="activeStep" simple class="activity-steps">
-          <el-step v-for="item in stepItems" :key="item.title" :title="item.title">
-            <template #icon><el-icon><component :is="item.icon" /></el-icon></template>
-          </el-step>
-        </el-steps>
+  <div class="activity-edit" :class="{ 'readonly-mode': isReadonly }">
+    <el-card :class="['activity-page-card', { 'basic-modular-mode': activeStep === 0 }]">
+      <template #header><div class="page-header"><span>{{ pageTitle }}</span><el-button @click="goBack">返回</el-button></div></template>
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="150px" :disabled="isReadonly">
+        <div class="steps-card">
+          <el-steps :active="activeStep" simple class="activity-steps">
+            <el-step v-for="item in stepItems" :key="item.title" :title="item.title">
+              <template #icon><el-icon><component :is="item.icon" /></el-icon></template>
+            </el-step>
+          </el-steps>
+        </div>
 
         <div v-show="activeStep === 0" class="step-pane">
           <el-card shadow="never" class="section-card">
             <template #header><div class="section-title">基础信息</div></template>
-            <el-row :gutter="20">
-              <el-col :span="12"><el-form-item label="活动标题(对内)" prop="internalTitle"><el-input v-model="form.internalTitle" maxlength="50" show-word-limit /></el-form-item></el-col>
-              <el-col :span="12"><el-form-item label="活动名称(对客)" prop="name"><el-input v-model="form.name" maxlength="50" show-word-limit /></el-form-item></el-col>
-              <el-col :span="12"><el-form-item label="微信分享名称"><el-input v-model="form.shareTitle" placeholder="为空默认取活动名称" /></el-form-item></el-col>
-              <el-col :span="12"><el-form-item label="活动类型" prop="category"><el-select v-model="form.category" style="width:100%"><el-option label="普通活动" value="普通活动" /><el-option label="卓越迎新礼" value="卓越迎新礼" /><el-option label="层层礼" value="层层礼" /><el-option label="MGM" value="MGM" /><el-option label="信用卡活动" value="信用卡活动" /></el-select></el-form-item></el-col>
-              <el-col :span="12"><el-form-item label="活动城市"><el-select v-model="form.cities" multiple allow-create filterable default-first-option style="width:100%"><el-option label="全国" value="全国" /><el-option label="上海" value="上海" /><el-option label="北京" value="北京" /><el-option label="广州" value="广州" /></el-select></el-form-item></el-col>
-              <el-col :span="12"><el-form-item label="活动地点"><el-input v-model="form.location" placeholder="线下活动填写" /></el-form-item></el-col>
-              <el-col :span="12"><el-form-item label="活动小图" prop="thumbnail"><el-input v-model="form.thumbnail" placeholder="列表卡片图片 URL" /></el-form-item></el-col>
-              <el-col :span="12"><el-form-item label="活动头图" prop="detailImage"><el-input v-model="form.detailImage" placeholder="详情页头图 URL" /></el-form-item></el-col>
-              <el-col :span="12"><el-form-item label="分享图片"><el-input v-model="form.shareImage" placeholder="为空默认取小图或头图" /></el-form-item></el-col>
-              <el-col :span="12"><el-form-item label="是否可转发"><el-switch v-model="form.sharable" /></el-form-item></el-col>
-              <el-col :span="12"><el-form-item label="SCRM activity code"><el-input v-model="form.scrmActivityCode" /></el-form-item></el-col>
-              <el-col :span="12"><el-form-item label="SCRM channel code"><el-input v-model="form.scrmChannelCode" /></el-form-item></el-col>
-              <el-col :span="24"><el-form-item label="Label"><el-select v-model="form.labels" multiple allow-create filterable default-first-option style="width:100%"><el-option label="interest-新客" value="interest-新客" /><el-option label="interest-MGM" value="interest-MGM" /></el-select></el-form-item></el-col>
-              <el-col :span="24"><el-form-item label="活动描述"><el-input v-model="form.description" type="textarea" :rows="2" /></el-form-item></el-col>
-              <el-col :span="24"><el-form-item label="活动详情" prop="content"><el-input v-model="form.content" type="textarea" :rows="5" /></el-form-item></el-col>
+            <el-row :gutter="30" class="basic-info-form">
+              <el-col :span="12"><el-form-item label="活动标题(对内)" prop="internalTitle"><el-input v-model="form.internalTitle" maxlength="50" show-word-limit placeholder="请输入对内活动标题" /></el-form-item></el-col>
+              <el-col :span="12"><el-form-item label="活动名称(对客)" prop="name"><el-input v-model="form.name" maxlength="50" show-word-limit placeholder="请输入面向客户的活动名称" /></el-form-item></el-col>
+              <el-col :span="12"><el-form-item label="活动类型" prop="category"><el-select v-model="form.category" placeholder="请选择活动类型" style="width:100%"><el-option label="普通活动" value="普通活动" /><el-option label="层层礼" value="层层礼" /><el-option label="MGM活动" value="MGM" /><el-option label="NTB活动" value="NTB活动" /></el-select></el-form-item></el-col>
+              <el-col :span="12"><el-form-item label="活动对象"><el-input v-model="form.target" maxlength="50" show-word-limit placeholder="请输入活动对象" /></el-form-item></el-col>
+              <el-col :span="12">
+                <el-form-item label="活动小图" prop="thumbnail">
+                  <div class="image-upload-field">
+                    <el-upload
+                      action="#"
+                      list-type="picture-card"
+                      :auto-upload="false"
+                      :file-list="uploadList('thumbnail')"
+                      :on-change="file => handleUpload(file, 'thumbnail')"
+                      :on-preview="handlePreviewImage"
+                      :on-remove="() => form.thumbnail = []"
+                      :disabled="isReadonly"
+                      :class="{ 'upload-hide-trigger': uploadList('thumbnail').length >= 1 }"
+                    >
+                      <el-icon><Plus /></el-icon>
+                    </el-upload>
+                    <div class="upload-size-tip">建议尺寸:780x438px<br>支持 JPG、PNG 格式</div>
+                  </div>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="活动头图" prop="detailImage">
+                  <div class="image-upload-field">
+                    <el-upload
+                      action="#"
+                      list-type="picture-card"
+                      :auto-upload="false"
+                      :file-list="uploadList('detailImage')"
+                      :on-change="file => handleUpload(file, 'detailImage')"
+                      :on-preview="handlePreviewImage"
+                      :on-remove="() => form.detailImage = []"
+                      :disabled="isReadonly"
+                      :class="{ 'upload-hide-trigger': uploadList('detailImage').length >= 1 }"
+                    >
+                      <el-icon><Plus /></el-icon>
+                    </el-upload>
+                    <div class="upload-size-tip">建议尺寸:780x438px<br>支持 JPG、PNG 格式</div>
+                  </div>
+                </el-form-item>
+              </el-col>
+              <el-col :span="24">
+                <div class="offline-switch-line">
+                  <div>
+                    <div class="setting-title">线下活动</div>
+                    <div class="setting-desc">开启后,需配置线下活动所在的城市及具体地点</div>
+                  </div>
+                  <el-switch v-model="form.offlineEnabled" />
+                </div>
+                <div v-if="form.offlineEnabled" class="offline-config-panel">
+                  <el-row :gutter="30">
+                    <el-col :span="12"><el-form-item label="活动城市"><el-input v-model="form.cityText" placeholder="如:上海市" /></el-form-item></el-col>
+                    <el-col :span="12"><el-form-item label="活动地点"><el-input v-model="form.location" placeholder="请输入详细活动地址" /></el-form-item></el-col>
+                  </el-row>
+                </div>
+              </el-col>
+              <el-col :span="24">
+                <el-form-item label="活动内容" prop="content">
+                  <quill-editor v-model:content="form.content" content-type="html" :options="quillOptions" :read-only="isReadonly"></quill-editor>
+                </el-form-item>
+              </el-col>
+              <el-col v-if="form.category === '层层礼'" :span="24">
+                <div class="layer-copy-config">
+                  <div class="layer-copy-header">
+                    <div class="layer-copy-title"><span class="required-mark">*</span> 活动达标层级内容</div>
+                    <div class="layer-copy-actions">
+                      <span>已配置 <strong>{{ form.layerContents.length }}</strong> / 5 个层级</span>
+                      <el-button v-if="!isReadonly" type="danger" link :icon="Plus" :disabled="form.layerContents.length >= 5" @click="addLayerCopy">添加层级</el-button>
+                    </div>
+                  </div>
+                  <div class="layer-copy-tabs">
+                    <el-tabs v-model="layerCopyActiveTab" type="card">
+                      <el-tab-pane v-for="(level, index) in form.layerContents" :key="level.key" :label="level.title || layerTitle(index)" :name="String(index)">
+                        <div class="layer-copy-panel">
+                          <div class="layer-copy-meta">
+                            <el-form-item label="Tab 标题" required>
+                              <el-input v-model="level.title" class="layer-title-input" />
+                            </el-form-item>
+                            <el-button v-if="!isReadonly && form.layerContents.length > 1" type="info" link :icon="Delete" @click="removeLayerCopy(index)">删除此层级</el-button>
+                          </div>
+                          <quill-editor v-model:content="level.content" content-type="html" :options="layerQuillOptions" :read-only="isReadonly"></quill-editor>
+                        </div>
+                      </el-tab-pane>
+                    </el-tabs>
+                  </div>
+                </div>
+              </el-col>
+            </el-row>
+          </el-card>
+
+          <el-card shadow="never" class="section-card toggle-section-card" :class="{ 'is-collapsed': !form.sharable }" :body-style="form.sharable ? undefined : { display: 'none', padding: '0' }">
+            <template #header>
+              <div class="section-title section-title-with-action">
+                <span>分享设置</span>
+                <el-checkbox v-model="form.sharable" class="share-toggle-checkbox">自定义微信分享</el-checkbox>
+              </div>
+            </template>
+            <el-row v-if="form.sharable" :gutter="30" class="basic-info-form">
+              <el-col :span="12"><el-form-item label="分享名称"><el-input v-model="form.shareTitle" maxlength="50" show-word-limit placeholder="输入分享到微信的卡片标题" /></el-form-item></el-col>
+              <el-col :span="12">
+                <el-form-item label="分享图片">
+                  <div class="image-upload-field share-upload-field">
+                    <el-upload
+                      action="#"
+                      list-type="picture-card"
+                      :auto-upload="false"
+                      :file-list="uploadList('shareImage')"
+                      :on-change="file => handleUpload(file, 'shareImage')"
+                      :on-preview="handlePreviewImage"
+                      :on-remove="() => form.shareImage = []"
+                      :disabled="isReadonly"
+                      :class="{ 'upload-hide-trigger': uploadList('shareImage').length >= 1 }"
+                    >
+                      <el-icon><Plus /></el-icon>
+                    </el-upload>
+                    <div class="upload-size-tip">建议尺寸:100x100px<br>支持 JPG、PNG 格式</div>
+                  </div>
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </el-card>
+
+          <el-card shadow="never" class="section-card">
+            <template #header><div class="section-title">其他设置</div></template>
+            <el-row :gutter="30" class="basic-info-form">
+              <el-col :span="12"><el-form-item label="CD staff ID"><el-input v-model="form.ownerStaffId" placeholder="输入相关 Staff ID" /></el-form-item></el-col>
+              <el-col :span="12"><el-form-item label="SCRM activity code"><el-input v-model="form.scrmActivityCode" placeholder="输入 SCRM 对应的活动编号" /></el-form-item></el-col>
+              <el-col :span="12"><el-form-item label="SCRM channel code"><el-input v-model="form.scrmChannelCode" placeholder="输入渠道编号" /></el-form-item></el-col>
+              <el-col :span="12"><el-form-item label="Label"><el-select v-model="form.labels" multiple allow-create filterable default-first-option placeholder="活动相关标签" style="width:100%"><el-option label="interest-新客" value="interest-新客" /><el-option label="interest-MGM" value="interest-MGM" /></el-select></el-form-item></el-col>
             </el-row>
           </el-card>
         </div>
 
         <div v-show="activeStep === 1" class="step-pane">
           <el-card shadow="never" class="section-card">
-            <template #header><div class="section-title">展示规则设置</div></template>
-            <el-row :gutter="20">
-              <el-col :span="24">
+            <template #header><div class="section-title">时间配置</div></template>
+            <el-row :gutter="30" class="module-form">
+              <el-col :span="12">
                 <el-form-item label="活动时间" prop="activityTime">
                   <el-date-picker v-model="form.activityTime" type="daterange" value-format="YYYY-MM-DD" class="date-range-field" />
                 </el-form-item>
               </el-col>
-              <el-col :span="24">
+              <el-col :span="12">
                 <el-form-item label="展示时间">
                   <el-date-picker v-model="form.displayTime" type="daterange" value-format="YYYY-MM-DD" class="date-range-field" />
                 </el-form-item>
               </el-col>
-              <el-col :span="24">
+            </el-row>
+          </el-card>
+          <el-card shadow="never" class="section-card">
+            <template #header><div class="section-title">展示客群</div></template>
+            <el-row :gutter="30" class="module-form">
+              <el-col :span="12">
                 <el-form-item label="展示客群">
                   <el-select
                     v-model="form.visibleCrowdIds"
@@ -62,6 +186,11 @@
                   <div class="form-tip">不选代表全部用户展示(全部展示不包括潜客)</div>
                 </el-form-item>
               </el-col>
+            </el-row>
+          </el-card>
+          <el-card shadow="never" class="section-card">
+            <template #header><div class="section-title">入口展示</div></template>
+            <el-row :gutter="30" class="module-form">
               <el-col :span="24">
                 <el-form-item label="权益首页">
                   <el-radio-group v-model="form.showOnHome">
@@ -104,14 +233,19 @@
         </div>
 
         <div v-show="activeStep === 2" class="step-pane">
-          <el-card shadow="never" class="section-card">
-            <template #header><div class="section-title">客群与报名</div></template>
-            <el-row :gutter="16" class="signup-row">
-              <el-col :span="24">
-                <el-form-item label="是否需要报名"><el-switch v-model="form.needSignup" /></el-form-item>
-              </el-col>
-              <template v-if="form.needSignup">
-                <el-col :span="24">
+          <el-card shadow="never" class="section-card toggle-section-card" :body-style="{ display: 'none', padding: '0' }">
+            <template #header>
+              <div class="section-title section-title-with-action">
+                <span>报名设置</span>
+                <el-checkbox v-model="form.needSignup" class="section-toggle-checkbox">需要报名</el-checkbox>
+              </div>
+            </template>
+          </el-card>
+          <template v-if="form.needSignup">
+            <el-card shadow="never" class="section-card">
+              <template #header><div class="section-title">报名客群与时间</div></template>
+              <el-row :gutter="30" class="module-form signup-row">
+                <el-col :span="12">
                   <el-form-item label="报名客群">
                     <el-select
                       v-model="form.signupCrowdIds"
@@ -126,34 +260,48 @@
                     </el-select>
                   </el-form-item>
                 </el-col>
-                <el-col :span="24">
+                <el-col :span="12">
                   <el-form-item label="报名时间" required>
                     <el-date-picker v-model="form.signupTime" type="daterange" value-format="YYYY-MM-DD" class="date-range-field" />
                   </el-form-item>
                 </el-col>
-                <el-col :span="24">
-                  <el-form-item label="报名确认"><el-switch v-model="form.needConfirm" /></el-form-item>
-                </el-col>
-                <el-col v-if="form.needConfirm" :span="24">
+              </el-row>
+            </el-card>
+            <el-card shadow="never" class="section-card toggle-section-card" :class="{ 'is-collapsed': !form.needConfirm }">
+              <template #header>
+                <div class="section-title section-title-with-action">
+                  <span>报名确认</span>
+                  <el-checkbox v-model="form.needConfirm" class="section-toggle-checkbox">报名确认</el-checkbox>
+                </div>
+              </template>
+              <el-row v-if="form.needConfirm" :gutter="30" class="module-form signup-row">
+                <el-col :span="12">
                   <el-form-item label="报名确认文案">
                     <el-input v-model="form.confirmText" placeholder="请输入报名确认文案" class="field-lg" />
                   </el-form-item>
                 </el-col>
+              </el-row>
+            </el-card>
+            <el-card shadow="never" class="section-card toggle-section-card" :class="{ 'is-collapsed': !form.needExtraInfo }">
+              <template #header>
+                <div class="section-title section-title-with-action">
+                  <span>补充信息表单</span>
+                  <el-checkbox v-model="form.needExtraInfo" class="section-toggle-checkbox" @change="handleNeedExtraInfoChange">补充信息</el-checkbox>
+                </div>
+              </template>
+              <el-row v-if="form.needExtraInfo" :gutter="30" class="module-form signup-row">
                 <el-col :span="24">
-                  <el-form-item label="补充信息"><el-switch v-model="form.needExtraInfo" @change="handleNeedExtraInfoChange" /></el-form-item>
-                </el-col>
-                <el-col v-if="form.needExtraInfo" :span="24">
                   <div class="extra-forms-header">
                     <div>
                       <span class="extra-forms-title">补充信息配置</span>
                       <span class="extra-forms-limit">最多配置 2 项</span>
                     </div>
-                    <el-button v-if="form.extraForms.length < 2" type="primary" link :icon="Plus" @click="addExtraForm">添加补充信息</el-button>
+                    <el-button v-if="!isReadonly && form.extraForms.length < 2" type="primary" link :icon="Plus" @click="addExtraForm">添加补充信息</el-button>
                   </div>
                   <div v-for="(extra, extraIndex) in form.extraForms" :key="extraIndex" class="extra-form-panel">
                     <div class="extra-form-head">
                       <span>补充信息{{ extraIndex + 1 }}</span>
-                      <el-button v-if="form.extraForms.length > 1" type="danger" link :icon="Delete" @click="removeExtraForm(extraIndex)">删除补充信息</el-button>
+                      <el-button v-if="!isReadonly && form.extraForms.length > 1" type="danger" link :icon="Delete" @click="removeExtraForm(extraIndex)">删除补充信息</el-button>
                     </div>
                     <el-row :gutter="16">
                       <el-col :span="12"><el-form-item label="字段名称" :prop="`extraForms.${extraIndex}.name`" required><el-input v-model="extra.name" class="field-md" /></el-form-item></el-col>
@@ -171,50 +319,47 @@
                     </el-form-item>
                   </div>
                 </el-col>
-                <el-col :span="24">
-                  <el-form-item label="名额控制"><el-switch v-model="form.quotaEnabled" /></el-form-item>
-                </el-col>
-                <template v-if="form.quotaEnabled">
-                  <el-col :span="12"><el-form-item label="报名人数上限"><el-input-number v-model="form.quota" :min="0" class="field-md" /></el-form-item></el-col>
-                  <el-col :span="12"><el-form-item label="展示报名人数"><el-switch v-model="form.showSignupCount" /></el-form-item></el-col>
-                  <el-col v-if="isEdit" :span="24"><el-form-item label="已报名人数"><el-input-number v-model="form.signedCount" :min="0" disabled class="field-md" /></el-form-item></el-col>
-                </template>
-                <el-col :span="24">
-                  <el-form-item label="报名成功引导语"><el-switch v-model="form.signupGuideEnabled" /></el-form-item>
-                </el-col>
-                <el-col v-if="form.signupGuideEnabled" :span="24">
-                  <div class="signup-success-panel">
-                    <div class="signup-success-title">报名成功页引导语配置</div>
-                    <el-form-item label="标题" label-width="140px" required>
-                      <el-input
-                        v-model="form.signupSuccessTitle"
-                        maxlength="30"
-                        show-word-limit
-                        placeholder="请输入移动端报名成功页标题"
-                        class="field-md"
-                      />
-                    </el-form-item>
-                    <el-form-item label="描述" label-width="140px">
-                      <el-input
-                        v-model="form.signupSuccessDescription"
-                        type="textarea"
-                        :rows="3"
-                        maxlength="200"
-                      show-word-limit
-                      placeholder="请输入移动端报名成功页描述"
-                    />
-                  </el-form-item>
+              </el-row>
+            </el-card>
+            <el-card shadow="never" class="section-card toggle-section-card" :class="{ 'is-collapsed': !form.quotaEnabled }">
+              <template #header>
+                <div class="section-title section-title-with-action">
+                  <span>名额控制</span>
+                  <el-checkbox v-model="form.quotaEnabled" class="section-toggle-checkbox">名额控制</el-checkbox>
                 </div>
-              </el-col>
               </template>
-            </el-row>
-          </el-card>
+              <el-row v-if="form.quotaEnabled" :gutter="30" class="module-form signup-row">
+                <el-col :span="12"><el-form-item label="报名人数上限"><el-input-number v-model="form.quota" :min="0" class="field-md" /></el-form-item></el-col>
+                <el-col :span="12"><el-form-item label="展示报名人数"><el-switch v-model="form.showSignupCount" /></el-form-item></el-col>
+                <el-col v-if="isEdit" :span="24"><el-form-item label="已报名人数"><el-input-number v-model="form.signedCount" :min="0" disabled class="field-md" /></el-form-item></el-col>
+              </el-row>
+            </el-card>
+            <el-card shadow="never" class="section-card toggle-section-card" :class="{ 'is-collapsed': !form.signupGuideEnabled }">
+              <template #header>
+                <div class="section-title section-title-with-action">
+                  <span>报名成功页配置</span>
+                  <el-checkbox v-model="form.signupGuideEnabled" class="section-toggle-checkbox">报名成功页配置</el-checkbox>
+                </div>
+              </template>
+              <el-row v-if="form.signupGuideEnabled" :gutter="30" class="module-form signup-row">
+                <el-col :span="12">
+                  <el-form-item label="标题" required>
+                    <el-input v-model="form.signupSuccessTitle" maxlength="30" show-word-limit placeholder="请输入移动端报名成功页标题" />
+                  </el-form-item>
+                </el-col>
+                <el-col :span="12">
+                  <el-form-item label="描述">
+                    <el-input v-model="form.signupSuccessDescription" type="textarea" :rows="3" maxlength="200" show-word-limit placeholder="请输入移动端报名成功页描述" />
+                  </el-form-item>
+                </el-col>
+              </el-row>
+            </el-card>
+          </template>
         </div>
 
         <div v-show="activeStep === 3" class="step-pane">
-          <el-card shadow="never" class="section-card">
-            <template #header><div class="section-title">进度与奖励</div></template>
-            <ActivityAchievementConfig :form="form" :crowds="crowds" :grant-plans="grantPlans" />
+          <ActivityAchievementConfig :form="form" :crowds="crowds" :grant-plans="grantPlans" :readonly="isReadonly" />
+          <el-card v-if="false" shadow="never" class="section-card">
             <div v-if="false" class="progress-config">
               <div class="config-section template-section">
                 <div class="section-body">
@@ -257,7 +402,7 @@
                 <div class="config-section">
                   <div class="section-header">
                     <span>{{ form.achievementTemplate === 'LAYER' ? '达标层级配置' : '达标规则标签配置' }}</span>
-                    <el-button v-if="form.achievementTemplate !== 'LAYER'" type="primary" :icon="Plus" @click="addAchievementRule">添加进度标签</el-button>
+                    <el-button v-if="!isReadonly && form.achievementTemplate !== 'LAYER'" type="primary" :icon="Plus" @click="addAchievementRule">添加进度标签</el-button>
                   </div>
                   <div class="section-body">
                     <template v-if="form.achievementTemplate === 'MGM'">
@@ -272,7 +417,7 @@
                       <div v-for="(rule, index) in form.achievementRules" :key="rule.key" class="rule-block">
                         <div class="rule-title">
                           <strong>规则标签{{ index + 1 }}</strong>
-                          <el-button v-if="form.achievementRules.length > 1" type="danger" link :icon="Delete" @click="removeAchievementRule(index)">删除</el-button>
+                          <el-button v-if="!isReadonly && form.achievementRules.length > 1" type="danger" link :icon="Delete" @click="removeAchievementRule(index)">删除</el-button>
                         </div>
                         <el-row :gutter="16">
                           <el-col :span="8">
@@ -320,7 +465,7 @@
                               <el-col :span="12"><el-form-item label="层级标题"><el-input v-model="level.title" /></el-form-item></el-col>
                             </el-row>
                             <div class="subsection-actions">
-                              <el-button type="primary" :icon="Plus" @click="addLayerActivity(level)">添加活动</el-button>
+                              <el-button v-if="!isReadonly" type="primary" :icon="Plus" @click="addLayerActivity(level)">添加活动</el-button>
                             </div>
 
                             <div v-for="(activity, activityIndex) in level.activities" :key="activity.key" class="layer-activity">
@@ -329,15 +474,15 @@
                                   <strong>活动 {{ activityIndex + 1 }}</strong>
                                   <span v-if="activityIndex > 0" class="prerequisite-note">*注意:活动{{ activityIndex + 1 }}达标前提为活动{{ activityIndex }}</span>
                                 </div>
-                                <el-button v-if="level.activities.length > 1" type="danger" link :icon="Delete" @click="removeLayerActivity(level, activityIndex)">删除活动</el-button>
+                                <el-button v-if="!isReadonly && level.activities.length > 1" type="danger" link :icon="Delete" @click="removeLayerActivity(level, activityIndex)">删除活动</el-button>
                               </div>
                               <el-row :gutter="16">
                                 <el-col :span="12"><el-form-item label="活动标题"><el-input v-model="activity.title" /></el-form-item></el-col>
                                 <el-col :span="12"><el-form-item label="活动描述"><el-input v-model="activity.description" /></el-form-item></el-col>
                               </el-row>
-                              <div class="subsection-actions rule-actions"><el-button type="primary" plain :icon="Plus" @click="addLayerRule(activity)">添加进度标签</el-button></div>
+                              <div v-if="!isReadonly" class="subsection-actions rule-actions"><el-button type="primary" plain :icon="Plus" @click="addLayerRule(activity)">添加进度标签</el-button></div>
                               <div v-for="(rule, ruleIndex) in activity.rules" :key="rule.key" class="rule-block inner-rule" :class="{ 'dependent-rule': activityIndex > 0 }">
-                                <div class="rule-title"><strong>规则标签{{ ruleIndex + 1 }}</strong><el-button v-if="activity.rules.length > 1" type="danger" link :icon="Delete" @click="activity.rules.splice(ruleIndex, 1)">删除</el-button></div>
+                                <div class="rule-title"><strong>规则标签{{ ruleIndex + 1 }}</strong><el-button v-if="!isReadonly && activity.rules.length > 1" type="danger" link :icon="Delete" @click="activity.rules.splice(ruleIndex, 1)">删除</el-button></div>
                                 <el-row :gutter="16">
                                   <el-col :span="8"><el-form-item label="规则标签" required><el-select v-model="rule.field" style="width:100%"><el-option v-for="item in ruleFieldOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item></el-col>
                                   <template v-if="rule.field === '境外消费'">
@@ -408,16 +553,24 @@
         </div>
 
         <div v-show="activeStep === 4" class="step-pane">
+          <el-card shadow="never" class="section-card toggle-section-card" :class="{ 'is-collapsed': !form.mutexEnabled }">
+            <template #header>
+              <div class="section-title section-title-with-action">
+                <span>互斥设置</span>
+                <el-checkbox v-model="form.mutexEnabled" class="section-toggle-checkbox">参与互斥</el-checkbox>
+              </div>
+            </template>
+            <el-row v-if="form.mutexEnabled" :gutter="30" class="module-form">
+              <el-col :span="16"><el-form-item label="互斥组" required><el-select v-model="form.mutexGroup" clearable style="width:100%"><el-option v-for="item in mutexGroups" :key="item.id" :label="item.name" :value="item.name" /></el-select></el-form-item></el-col>
+            </el-row>
+          </el-card>
           <el-card shadow="never" class="section-card">
-            <template #header><div class="section-title">互斥与通知</div></template>
-            <el-row :gutter="20">
-              <el-col :span="8"><el-form-item label="是否参与互斥"><el-switch v-model="form.mutexEnabled" /></el-form-item></el-col>
-              <el-col :span="16"><el-form-item label="互斥组" :required="form.mutexEnabled"><el-select v-model="form.mutexGroup" clearable style="width:100%"><el-option v-for="item in mutexGroups" :key="item.id" :label="item.name" :value="item.name" /></el-select></el-form-item></el-col>
-              <el-col :span="8"><el-form-item label="报名成功通知"><el-switch v-model="form.smsEnabled" /></el-form-item></el-col>
-              <el-col :span="16"><el-form-item label="报名成功模板"><el-select v-model="form.signupNoticeTemplateId" clearable style="width:100%"><el-option v-for="item in notifications" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
+            <template #header><div class="section-title">通知设置</div></template>
+            <el-row :gutter="30" class="module-form">
+              <el-col :span="12"><el-form-item label="报名成功模板"><el-select v-model="form.signupNoticeTemplateId" clearable style="width:100%"><el-option v-for="item in notifications" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
               <el-col :span="12"><el-form-item label="开始前提醒模板"><el-select v-model="form.beforeStartTemplateId" clearable style="width:100%"><el-option v-for="item in notifications" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
-              <el-col :span="12"><el-form-item label="达标发奖通知模板"><el-select v-model="form.rewardTemplateId" clearable style="width:100%"><el-option v-for="item in notifications" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
-              <el-col :span="12"><el-form-item label="活动 owner staff ID"><el-input v-model="form.ownerStaffId" /></el-form-item></el-col>
+              <el-col :span="12"><el-form-item label="达标发奖通知模板(需领取)"><el-select v-model="form.rewardTemplateId" clearable style="width:100%"><el-option v-for="item in notifications" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
+              <el-col :span="12"><el-form-item label="达标发奖通知模板(无需领取)"><el-select v-model="form.rewardNoClaimTemplateId" clearable style="width:100%"><el-option v-for="item in notifications" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
             </el-row>
           </el-card>
         </div>
@@ -425,23 +578,28 @@
       </el-form>
       <div class="action-bar">
         <div class="action-left">
-          <el-button v-if="activeStep > 0 && activeStep < stepItems.length - 1" @click="prevStep"><el-icon><ArrowLeft /></el-icon>上一步</el-button>
+          <el-button v-if="activeStep > 0" @click="prevStep"><el-icon><ArrowLeft /></el-icon>上一步</el-button>
         </div>
         <div class="action-center">
-          <el-button @click="goBack">取消</el-button>
-          <el-button type="primary" plain @click="saveDraft">保存草稿</el-button>
-          <el-button v-if="activeStep === stepItems.length - 1" type="primary" @click="markPending">提交审核</el-button>
+          <el-button @click="goBack">{{ isReadonly ? '返回' : '取消' }}</el-button>
+          <el-button v-if="isAudit" type="danger" @click="rejectActivity">驳回</el-button>
+          <el-button v-if="isAudit" type="primary" @click="approveActivity">审核通过</el-button>
+          <el-button v-if="!isReadonly" type="primary" plain @click="saveDraft">保存草稿</el-button>
+          <el-button v-if="!isReadonly && activeStep === stepItems.length - 1" type="primary" @click="markPending">提交审核</el-button>
         </div>
         <div class="action-right">
           <el-button v-if="activeStep < stepItems.length - 1" type="primary" @click="nextStep">下一步<el-icon class="el-icon--right"><ArrowRight /></el-icon></el-button>
         </div>
       </div>
     </el-card>
+    <el-dialog v-model="previewVisible" title="图片预览" width="640px" append-to-body>
+      <img v-if="previewImageUrl" :src="previewImageUrl" style="display:block; max-width:100%; margin:0 auto;" />
+    </el-dialog>
   </div>
 </template>
 
 <script>
-import { ElMessage } from 'element-plus'
+import { ElMessage, ElMessageBox } from 'element-plus'
 import { ArrowLeft, ArrowRight, Bell, InfoFilled, Medal, Plus, Setting, User, Delete } from '@element-plus/icons-vue'
 import request from '@/utils/request'
 import activityStore from '@/store/activityStore'
@@ -457,6 +615,9 @@ export default {
       ArrowLeft,
       ArrowRight,
       activeStep: 0,
+      previewVisible: false,
+      previewImageUrl: '',
+      layerCopyActiveTab: '0',
       stepItems: [
         { title: '基础信息', icon: InfoFilled },
         { title: '展示规则', icon: Setting },
@@ -468,6 +629,27 @@ export default {
       assemblyPages: [],
       ruleFieldOptions: ['开户时间', 'TRB值(含家庭账户)', '客户MSC Group', '境外消费'],
       operatorOptions: ['介于', '大于等于', '小于等于'],
+      quillOptions: {
+        placeholder: '请输入活动详情介绍、流程、注意事项等...',
+        modules: {
+          toolbar: [
+            ['bold', 'italic', 'underline', 'strike'],
+            [{ list: 'ordered' }, { list: 'bullet' }],
+            ['link', 'image'],
+            ['clean']
+          ]
+        }
+      },
+      layerQuillOptions: {
+        placeholder: '请输入该层级的达标要求、奖励内容等...',
+        modules: {
+          toolbar: [
+            ['bold', 'italic', 'underline', 'strike'],
+            [{ list: 'ordered' }, { list: 'bullet' }],
+            ['link', 'clean']
+          ]
+        }
+      },
       form: activityStore.emptyActivity(),
       rules: {
         internalTitle: [{ required: true, message: '请输入活动标题(对内)', trigger: 'blur' }],
@@ -482,6 +664,13 @@ export default {
   },
   computed: {
     isEdit() { return this.$route.name === 'ActivityEdit' },
+    isAudit() { return this.$route.name === 'ActivityAudit' },
+    isReadonly() { return this.$route.name === 'ActivityView' || this.isAudit },
+    pageTitle() {
+      if (this.isAudit) return '活动审核'
+      if (this.isReadonly) return '查看活动'
+      return this.isEdit ? '编辑活动' : '新建活动'
+    },
     crowds() { return activityStore.activityCrowds },
     forms() { return activityStore.forms },
     progressTemplates() { return activityStore.progressTemplates },
@@ -496,7 +685,7 @@ export default {
     notifications() { return activityStore.notifications }
   },
   created() {
-    if (this.isEdit) {
+    if (this.isEdit || this.isReadonly) {
       const row = activityStore.getActivity(this.$route.params.id)
       if (!row) {
         ElMessage.error('活动不存在')
@@ -506,6 +695,8 @@ export default {
     }
     this.normalizeProgressConfig()
     this.normalizeExtraForms()
+    this.normalizeActivityImages()
+    this.normalizeLayerContents()
     this.fetchAssemblyPages()
   },
   methods: {
@@ -556,15 +747,41 @@ export default {
         key: `${Date.now()}-${index}-${Math.random()}`,
         crowdId: '',
         title: `层级${index + 1}`,
+        content: '',
         activities: [this.newLayerActivity(0), this.newLayerActivity(1)]
       }
     },
+    layerTitle(index) {
+      const names = ['一', '二', '三', '四', '五']
+      return `层级${names[index] || index + 1}`
+    },
+    newLayerContent(index) {
+      return {
+        key: `${Date.now()}-layer-copy-${index}-${Math.random()}`,
+        title: this.layerTitle(index),
+        content: ''
+      }
+    },
+    normalizeLayerContents() {
+      if (!Array.isArray(this.form.layerContents) || !this.form.layerContents.length) {
+        this.form.layerContents = [this.newLayerContent(0)]
+      }
+      this.form.layerContents = this.form.layerContents.slice(0, 5).map((item, index) => ({
+        key: item.key || `${Date.now()}-layer-copy-${index}-${Math.random()}`,
+        title: item.title || this.layerTitle(index),
+        content: item.content || ''
+      }))
+      if (Number(this.layerCopyActiveTab) >= this.form.layerContents.length) this.layerCopyActiveTab = String(this.form.layerContents.length - 1)
+    },
     normalizeProgressConfig() {
       if (!this.form.progressCopy) this.form.progressCopy = { unreached: '', reached: '', help: '' }
       if (!Array.isArray(this.form.achievementRules) || !this.form.achievementRules.length) this.form.achievementRules = [this.newAchievementRule()]
       if (!this.form.mgmReferredProgress) this.form.mgmReferredProgress = { labelOne: '', labelTwo: '', advancedReward: false, advancedRuleField: '', advancedRuleText: '' }
       if (!Array.isArray(this.form.achievementLayers) || !this.form.achievementLayers.length) this.form.achievementLayers = [this.newAchievementLayer(0)]
-      this.form.achievementLayers.forEach((level) => {
+      this.form.achievementLayers.forEach((level, index) => {
+        if (!level.key) level.key = `${Date.now()}-${index}-${Math.random()}`
+        if (!level.title) level.title = `层级${index + 1}`
+        if (level.content === undefined) level.content = ''
         if (!Array.isArray(level.activities) || !level.activities.length) {
           const firstActivity = this.newLayerActivity(0)
           firstActivity.title = level.activityTitle || ''
@@ -600,31 +817,85 @@ export default {
       this.form.extraForms = normalized.length ? normalized : [this.newExtraForm()]
     },
     addExtraForm() {
+      if (this.isReadonly) return
       if (this.form.extraForms.length >= 2) return ElMessage.warning('最多配置 2 项补充信息')
       this.form.extraForms.push(this.newExtraForm())
     },
     removeExtraForm(index) {
+      if (this.isReadonly) return
       if (this.form.extraForms.length <= 1) return
       this.form.extraForms.splice(index, 1)
     },
     handleNeedExtraInfoChange(value) {
+      if (this.isReadonly) return
       if (value && (!Array.isArray(this.form.extraForms) || !this.form.extraForms.length)) this.normalizeExtraForms()
     },
+    normalizeUploadField(field) {
+      const value = this.form[field]
+      if (Array.isArray(value)) return value
+      if (!value) return []
+      this.form[field] = [{ name: `${field}.png`, url: value }]
+      return this.form[field]
+    },
+    normalizeActivityImages() {
+      this.normalizeUploadField('thumbnail')
+      this.normalizeUploadField('detailImage')
+      this.normalizeUploadField('shareImage')
+      if (this.form.offlineEnabled === undefined) this.form.offlineEnabled = false
+      if (this.form.cityText === undefined) this.form.cityText = Array.isArray(this.form.cities) ? this.form.cities.join('、') : ''
+    },
+    uploadList(field) {
+      return this.normalizeUploadField(field)
+    },
+    handleUpload(file, field) {
+      if (this.isReadonly) return
+      if (!file || !file.raw) return
+      const reader = new FileReader()
+      reader.onload = (e) => {
+        this.form[field] = [{ name: file.name, url: e.target.result, uid: file.uid }]
+      }
+      reader.readAsDataURL(file.raw)
+    },
+    handlePreviewImage(file) {
+      this.previewImageUrl = file.url || ''
+      this.previewVisible = !!this.previewImageUrl
+    },
+    addLayerCopy() {
+      if (this.isReadonly) return
+      if (!Array.isArray(this.form.layerContents)) this.form.layerContents = []
+      if (this.form.layerContents.length >= 5) return ElMessage.warning('最多配置 5 个层级')
+      this.form.layerContents.push(this.newLayerContent(this.form.layerContents.length))
+      this.layerCopyActiveTab = String(this.form.layerContents.length - 1)
+    },
+    removeLayerCopy(index) {
+      if (this.isReadonly) return
+      if (this.form.layerContents.length <= 1) return
+      this.form.layerContents.splice(index, 1)
+      if (Number(this.layerCopyActiveTab) >= this.form.layerContents.length) this.layerCopyActiveTab = String(this.form.layerContents.length - 1)
+      this.form.layerContents.forEach((level, levelIndex) => {
+        if (!level.title) level.title = this.layerTitle(levelIndex)
+      })
+    },
     addAchievementRule() {
+      if (this.isReadonly) return
       if (this.form.achievementRules.length >= 5) return ElMessage.warning('最多添加 5 个进度标签')
       this.form.achievementRules.push(this.newAchievementRule())
     },
     removeAchievementRule(index) {
+      if (this.isReadonly) return
       this.form.achievementRules.splice(index, 1)
     },
     addLayerActivity(level) {
+      if (this.isReadonly) return
       if (level.activities.length >= 5) return ElMessage.warning('每个层级最多添加 5 个活动')
       level.activities.push(this.newLayerActivity(level.activities.length))
     },
     removeLayerActivity(level, index) {
+      if (this.isReadonly) return
       level.activities.splice(index, 1)
     },
     addLayerRule(activity) {
+      if (this.isReadonly) return
       if (activity.rules.length >= 5) return ElMessage.warning('每个活动最多添加 5 个进度标签')
       activity.rules.push(this.newAchievementRule())
     },
@@ -641,21 +912,61 @@ export default {
     prevStep() {
       if (this.activeStep > 0) this.activeStep -= 1
     },
-    saveDraft() { this.submit(false) },
-    markPending() { this.submit(true) },
-    submit(toAudit) {
-      this.$refs.formRef.validate((valid) => {
-        if (!valid) return
-        const error = this.validateBusiness()
-        if (error) return ElMessage.warning(error)
-        if (toAudit) {
-          this.form.status = 'pending'
-          this.form.auditStatus = '待审核'
-        }
-        activityStore.saveActivity(this.form)
-        ElMessage.success(toAudit ? '已保存并提交审核' : '保存成功')
-        this.goBack()
+    async saveDraft() { await this.submit(false) },
+    async markPending() {
+      await ElMessageBox.confirm('提交后将进入审核流程,期间不可再编辑。确定提交审核吗?', '提交审核', { type: 'warning' })
+      await this.submit(true)
+    },
+    async approveActivity() {
+      if (!this.isAudit) return
+      await ElMessageBox.confirm(`确定审核通过活动「${this.form.name}」吗?`, '审核通过', { type: 'warning' })
+      const target = activityStore.getActivity(this.form.id)
+      if (target) {
+        target.status = 'approved'
+        target.auditStatus = '审核通过'
+        target.auditor = '当前审核人'
+        target.updatedAt = this.nowText()
+      }
+      ElMessage.success('审核通过')
+      this.goBack()
+    },
+    async rejectActivity() {
+      if (!this.isAudit) return
+      await ElMessageBox.confirm(`确定驳回活动「${this.form.name}」吗?`, '驳回活动', {
+        type: 'warning',
+        confirmButtonText: '驳回',
+        confirmButtonClass: 'el-button--danger'
       })
+      const target = activityStore.getActivity(this.form.id)
+      if (target) {
+        target.status = 'rejected'
+        target.auditStatus = '审核驳回'
+        target.auditor = '当前审核人'
+        target.updatedAt = this.nowText()
+      }
+      ElMessage.success('已驳回')
+      this.goBack()
+    },
+    async submit(toAudit) {
+      if (this.isReadonly) return false
+      const valid = await this.$refs.formRef.validate().catch(() => false)
+      if (!valid) {
+        ElMessage.warning('请先补全必填信息')
+        return false
+      }
+      const error = this.validateBusiness()
+      if (error) {
+        ElMessage.warning(error)
+        return false
+      }
+      if (toAudit) {
+        this.form.status = 'pending'
+        this.form.auditStatus = '待审核'
+      }
+      activityStore.saveActivity(this.form)
+      ElMessage.success(toAudit ? '已保存并提交审核' : '保存成功')
+      this.$router.replace('/customization/hsbc/activities')
+      return true
     },
     validateBusiness() {
       if (this.form.activityTime?.length === 2 && this.form.activityTime[0] > this.form.activityTime[1]) return '活动结束时间必须晚于开始时间'
@@ -687,11 +998,16 @@ export default {
         }))
         if (invalidLayerConfig) return '请完整配置每个层级活动的规则和达标奖励'
       }
-      if (this.form.progressEnabled && this.form.achievementTemplate !== 'LAYER' && !this.form.templateRewards?.[this.form.achievementTemplate]?.planId) return '请选择达标奖励'
+      if (this.form.progressEnabled && this.form.achievementTemplate !== 'LAYER' && this.form.rewardEnabled && !this.form.grantPlanId) return '请选择达标奖励'
       if (this.form.mutexEnabled && !this.form.mutexGroup) return '参与互斥时必须选择互斥组'
       return ''
     },
     preview() { ElMessage.success('已生成移动端预览效果') },
+    nowText() {
+      const pad = (n) => String(n).padStart(2, '0')
+      const d = new Date()
+      return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
+    },
     statusText(status) { return { draft: '草稿', pending: '待审核', approved: '审核通过', published: '已发布', offline: '已下线' }[status] || status },
     goBack() { this.$router.push('/customization/hsbc/activities') }
   }
@@ -700,15 +1016,100 @@ export default {
 
 <style scoped>
 .activity-edit { padding:20px; }
+.readonly-mode :deep(.ql-toolbar),
+.readonly-mode :deep(.ql-container) { background:#f5f7fa; }
+.readonly-mode :deep(.ql-toolbar) { pointer-events:none; opacity:.7; }
+.readonly-mode :deep(.el-input.is-disabled .el-input__wrapper),
+.readonly-mode :deep(.el-textarea.is-disabled .el-textarea__inner),
+.readonly-mode :deep(.el-select .el-input.is-disabled .el-input__wrapper),
+.readonly-mode :deep(.el-date-editor.is-disabled),
+.readonly-mode :deep(.el-input-number.is-disabled .el-input__wrapper) {
+  background:#f7f8fa;
+  box-shadow:0 0 0 1px #dcdfe6 inset;
+}
+.readonly-mode :deep(.el-input.is-disabled .el-input__inner),
+.readonly-mode :deep(.el-textarea.is-disabled .el-textarea__inner),
+.readonly-mode :deep(.el-select .el-input.is-disabled .el-input__inner),
+.readonly-mode :deep(.el-select.is-disabled .el-select__selected-item),
+.readonly-mode :deep(.el-select.is-disabled .el-select__placeholder),
+.readonly-mode :deep(.el-select.is-disabled .el-select__tags-text),
+.readonly-mode :deep(.el-select.is-disabled .el-tag__content),
+.readonly-mode :deep(.el-date-editor.is-disabled .el-range-input),
+.readonly-mode :deep(.el-date-editor.is-disabled .el-range-separator),
+.readonly-mode :deep(.el-input-number.is-disabled .el-input__inner),
+.readonly-mode :deep(.el-tag),
+.readonly-mode :deep(.ql-editor) {
+  color:#303133;
+  -webkit-text-fill-color:#303133;
+}
+.readonly-mode :deep(.el-select.is-disabled .el-tag) {
+  background:#eef0f3;
+  border-color:#d4d7de;
+}
+.readonly-mode :deep(.el-input.is-disabled .el-input__inner::placeholder),
+.readonly-mode :deep(.el-textarea.is-disabled .el-textarea__inner::placeholder),
+.readonly-mode :deep(.el-date-editor.is-disabled .el-range-input::placeholder) {
+  color:#a8abb2;
+  -webkit-text-fill-color:#a8abb2;
+}
 .page-header { display:flex; justify-content:space-between; align-items:center; font-weight:600; }
-.activity-steps { margin-bottom:20px; }
+.activity-page-card { background:transparent; border:none; box-shadow:none; }
+.activity-page-card :deep(> .el-card__header) { margin-bottom:18px; background:#fff; border:1px solid #ebeef5; border-radius:4px; box-shadow:0 1px 4px rgba(0,0,0,.04); }
+.activity-page-card :deep(> .el-card__body) { padding:0; }
+.steps-card { margin-bottom:20px; padding:12px 16px; background:#fff; border:1px solid #ebeef5; border-radius:4px; box-shadow:0 1px 4px rgba(0,0,0,.04); }
+.activity-steps { margin-bottom:0; }
 .step-pane { min-height:360px; }
-.section-card { margin-bottom:16px; border:none; }
+.section-card { margin-bottom:16px; border:none; border-radius:4px; }
+.section-card { margin-bottom:20px; border:1px solid #ebeef5; box-shadow:0 1px 4px rgba(0,0,0,.04); }
 .section-card :deep(.el-card__header) { padding:12px 20px; background:#fafafa; border-bottom:1px solid #ebeef5; }
+.toggle-section-card.is-collapsed :deep(.el-card__header) { border-bottom:none; }
+.toggle-section-card.is-collapsed :deep(.el-card__body) { display:none; }
 .section-title { color:#303133; font-size:16px; font-weight:600; }
+.section-title-with-action { display:flex; align-items:center; justify-content:space-between; gap:16px; }
+.share-toggle-checkbox,
+.section-toggle-checkbox { font-weight:400; }
+.basic-info-form :deep(.el-form-item),
+.module-form :deep(.el-form-item) { display:block; margin-bottom:24px; }
+.basic-info-form :deep(.el-form-item__label),
+.module-form :deep(.el-form-item__label) { width:auto !important; height:auto; margin-bottom:8px; justify-content:flex-start; color:#303133; font-weight:600; line-height:22px; white-space:nowrap; }
+.basic-info-form :deep(.el-form-item__content),
+.module-form :deep(.el-form-item__content) { margin-left:0 !important; display:block; line-height:normal; }
+.basic-info-form :deep(.el-input),
+.basic-info-form :deep(.el-select),
+.module-form :deep(.el-input),
+.module-form :deep(.el-select) { width:100%; }
+.image-upload-field { display:flex; align-items:center; gap:18px; min-height:104px; }
+.share-upload-field { min-height:92px; }
+.upload-hide-trigger :deep(.el-upload--picture-card) { display:none; }
+.upload-size-tip { color:#909399; font-size:13px; line-height:1.7; }
+.offline-switch-line { display:flex; align-items:center; justify-content:space-between; gap:24px; margin:4px 0 24px; padding:20px 0; border-top:1px solid #ebeef5; border-bottom:1px solid #ebeef5; }
+.offline-switch-line:has(+ .offline-config-panel) { margin-bottom:0; }
+.setting-title { margin-bottom:8px; color:#303133; font-size:15px; font-weight:600; }
+.setting-desc { color:#606266; font-size:13px; line-height:1.5; }
+.offline-config-panel { margin-bottom:24px; padding:18px 18px 0; border:1px solid #ebeef5; border-radius:4px; background:#fafafa; }
+.basic-modular-mode :deep(.ql-container) { min-height:220px; }
+.basic-modular-mode :deep(.ql-editor) { min-height:220px; }
+.layer-copy-config { margin-top:8px; padding-top:24px; border-top:1px solid #ebeef5; }
+.layer-copy-header { display:flex; align-items:center; justify-content:space-between; gap:16px; margin-bottom:16px; }
+.layer-copy-title { color:#303133; font-size:15px; font-weight:600; }
+.required-mark { color:#f56c6c; }
+.layer-copy-actions { display:flex; align-items:center; gap:14px; color:#606266; font-size:14px; }
+.layer-copy-actions strong { color:#f56c6c; }
+.layer-copy-tabs { border:1px solid #dcdfe6; border-radius:4px; overflow:hidden; background:#fff; }
+.layer-copy-tabs :deep(.el-tabs__header) { margin:0; background:#f5f7fa; }
+.layer-copy-tabs :deep(.el-tabs__nav) { border-top:none; border-left:none; border-radius:0; }
+.layer-copy-tabs :deep(.el-tabs__item) { min-width:128px; text-align:center; font-weight:500; }
+.layer-copy-panel { padding:20px; }
+.layer-copy-meta { display:flex; align-items:flex-start; justify-content:space-between; gap:24px; margin-bottom:18px; padding:16px 16px 0; background:#fafafa; border:1px solid #ebeef5; border-radius:4px; }
+.layer-copy-meta :deep(.el-form-item) { display:block; margin-bottom:16px; }
+.layer-copy-meta :deep(.el-form-item__label) { width:auto !important; height:auto; margin-bottom:8px; justify-content:flex-start; color:#303133; font-weight:600; line-height:22px; white-space:nowrap; }
+.layer-copy-meta :deep(.el-form-item__content) { margin-left:0 !important; display:block; line-height:normal; }
+.layer-title-input { width:360px; max-width:100%; }
+.layer-copy-panel :deep(.ql-container) { min-height:180px; }
+.layer-copy-panel :deep(.ql-editor) { min-height:180px; }
 .form-tip { margin-top:4px; color:#909399; font-size:12px; line-height:1.4; }
 .signup-row :deep(.el-form-item) { margin-bottom:16px; }
-.signup-row :deep(.el-form-item__content) { display:flex; align-items:center; }
+.signup-row :deep(.el-form-item__content) { display:block; }
 .field-md { width:240px; max-width:100%; }
 .field-lg { width:320px; max-width:100%; }
 .field-xs { width:140px; max-width:100%; }
@@ -717,7 +1118,7 @@ export default {
 .activity-edit :deep(.mgm-date-range) { width:300px; max-width:100%; }
 .activity-edit :deep(.reward-validity-picker) { width:320px; max-width:100%; flex:0 1 320px; }
 .assembly-select-panel { padding:12px 14px; margin-bottom:16px; background:#fafafa; border:1px solid #ebeef5; border-radius:4px; }
-.action-bar { display:flex; align-items:center; justify-content:space-between; margin-top:20px; padding-top:20px; border-top:1px solid #ebeef5; }
+.action-bar { display:flex; align-items:center; justify-content:space-between; margin-top:20px; padding:16px 20px; background:#fff; border:1px solid #ebeef5; border-radius:4px; box-shadow:0 1px 4px rgba(0,0,0,.04); }
 .action-left, .action-right { width:120px; }
 .action-center { display:flex; justify-content:center; gap:12px; }
 .action-center .el-button { min-width:100px; }
@@ -751,16 +1152,6 @@ export default {
 .extra-form-panel { margin-bottom:16px; padding:16px 18px; border:1px solid #dcdfe6; border-radius:4px; background:#fafafa; }
 .extra-form-head { margin-bottom:14px; display:flex; align-items:center; justify-content:space-between; color:#303133; font-size:14px; font-weight:600; }
 .extra-link-panel { padding:16px 18px 0; border:1px solid #dcdfe6; border-radius:4px; background:#fafafa; }
-.signup-success-panel { margin-bottom:18px; padding:0 18px 2px; border:1px solid #ebeef5; border-radius:4px; background:#fafafa; }
-.signup-success-panel :deep(.el-form-item) { margin-bottom:18px; }
-.signup-success-panel :deep(.el-form-item:last-child) { margin-bottom:6px; }
-.signup-success-title { margin:0 -18px 18px; padding:14px 18px; border-bottom:1px solid #ebeef5; background:#f5f7fa; color:#303133; font-size:15px; font-weight:600; }
-.signup-success-panel :deep(.el-form-item__content) { max-width:640px; }
-@media (max-width: 768px) {
-  .signup-success-panel :deep(.el-form-item) { display:block; }
-  .signup-success-panel :deep(.el-form-item__label) { width:auto !important; height:auto; margin-bottom:8px; justify-content:flex-start; line-height:22px; }
-  .signup-success-panel :deep(.el-form-item__content) { margin-left:0 !important; }
-}
 @media (max-width: 1200px) {
   .section-body :deep(.el-col) { max-width:100%; flex:0 0 100%; }
 }

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

@@ -2,7 +2,8 @@
   <div class="activity-management">
     <el-card>
       <template #header>
-        <div class="toolbar">
+        <div class="search-section">
+          <div class="search-row">
           <el-form :model="searchForm" inline label-width="86px">
             <el-form-item label="活动 ID">
               <el-input v-model="searchForm.id" clearable placeholder="精确查询活动编号" style="width: 180px" />
@@ -37,20 +38,37 @@
               <el-button @click="handleReset">重置</el-button>
             </el-form-item>
           </el-form>
-          <el-button type="primary" @click="handleCreate"><el-icon><Plus /></el-icon>新建活动</el-button>
+          </div>
+          <div class="operation-row">
+            <el-button type="primary" @click="handleCreate"><el-icon><Plus /></el-icon>新建活动</el-button>
+          </div>
         </div>
       </template>
 
-      <el-table :data="tableData" border stripe>
-        <el-table-column label="操作" width="310" fixed="left">
-          <template #default="{ row }">
-            <el-button type="primary" link @click="handleView(row)">查看</el-button>
-            <el-button v-if="['draft', 'rejected', 'approved'].includes(row.status)" type="primary" link @click="handleEdit(row)">编辑</el-button>
-            <el-button type="primary" link @click="preview(row)">预览</el-button>
-            <el-button v-if="row.status === 'draft'" type="primary" link @click="submitAudit(row)">提交审核</el-button>
-            <el-button v-if="row.status === 'pending'" type="success" link @click="approve(row)">审核</el-button>
-            <el-button v-if="row.status === 'approved'" type="success" link @click="publish(row)">发布</el-button>
-            <el-button v-if="row.status === 'published'" type="warning" link @click="offline(row)">下线</el-button>
+      <el-table :data="pagedTableData" stripe style="width: 100%">
+        <el-table-column label="操作" width="240" fixed="left">
+          <template #default="{ row, $index }">
+            <el-button type="text" size="small" @click="handleView(row)">查看</el-button>
+            <el-button v-if="['draft', 'rejected', 'approved'].includes(row.status)" type="text" size="small" @click="handleEdit(row)">编辑</el-button>
+            <el-button type="text" size="small" @click="preview(row)">预览</el-button>
+            <el-popover placement="bottom" trigger="click" :ref="`morePopover-${$index}`">
+              <div class="more-actions-menu">
+                <div v-if="row.status === 'draft'" class="more-actions-item" @click="submitAudit(row)">提交审核</div>
+                <div v-if="row.status === 'pending'" class="more-actions-item" @click="handleAudit(row)">审核</div>
+                <div v-if="row.status === 'approved'" class="more-actions-item" @click="publish(row)">发布</div>
+                <div v-if="row.status === 'published'" class="more-actions-item more-actions-item-danger" @click="offline(row)">下线</div>
+              </div>
+              <template #reference>
+                <el-button
+                  v-if="['draft', 'pending', 'approved', 'published'].includes(row.status)"
+                  type="text"
+                  size="small"
+                  class="more-button"
+                >
+                  <el-icon><More /></el-icon>
+                </el-button>
+              </template>
+            </el-popover>
           </template>
         </el-table-column>
         <el-table-column prop="id" label="活动 ID" width="130" />
@@ -71,6 +89,16 @@
         <el-table-column prop="createdAt" label="创建时间" width="170" />
         <el-table-column prop="updatedAt" label="更新时间" width="170" />
       </el-table>
+      <div class="pagination-section">
+        <span>Total {{ tableData.length }}</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="tableData.length"
+        />
+      </div>
     </el-card>
   </div>
 </template>
@@ -82,7 +110,18 @@ import activityStore from '@/store/activityStore'
 export default {
   name: 'ActivityManagement',
   data() {
-    return { searchForm: { id: '', name: '', category: '', status: '', creator: '' }, tableData: [] }
+    return {
+      searchForm: { id: '', name: '', category: '', status: '', creator: '' },
+      tableData: [],
+      currentPage: 1,
+      pageSize: 10
+    }
+  },
+  computed: {
+    pagedTableData() {
+      const start = (this.currentPage - 1) * this.pageSize
+      return this.tableData.slice(start, start + this.pageSize)
+    }
   },
   created() { this.handleSearch() },
   activated() { this.handleSearch() },
@@ -92,6 +131,7 @@ export default {
         if (this.searchForm.creator && !String(item.creator || '').includes(this.searchForm.creator)) return false
         return true
       })
+      this.currentPage = 1
     },
     handleReset() {
       this.searchForm = { id: '', name: '', category: '', status: '', creator: '' }
@@ -100,6 +140,7 @@ export default {
     handleCreate() { this.$router.push('/customization/hsbc/activities/add') },
     handleEdit(row) { this.$router.push(`/customization/hsbc/activities/edit/${row.id}`) },
     handleView(row) { this.$router.push(`/customization/hsbc/activities/view/${row.id}`) },
+    handleAudit(row) { this.$router.push(`/customization/hsbc/activities/audit/${row.id}`) },
     preview(row) { ElMessage.success(`已生成移动端预览:${row.name}`) },
     submitAudit(row) {
       const error = this.validateRequired(row)
@@ -109,13 +150,6 @@ export default {
       ElMessage.success('已提交审核')
       this.handleSearch()
     },
-    approve(row) {
-      row.status = 'approved'
-      row.auditStatus = '审核通过'
-      row.auditor = '当前审核人'
-      ElMessage.success('审核通过')
-      this.handleSearch()
-    },
     async publish(row) {
       await ElMessageBox.confirm(`确定发布活动「${row.name}」吗?`, '发布活动', { type: 'warning' })
       activityStore.publishActivity(row.id)
@@ -149,5 +183,19 @@ export default {
 </script>
 
 <style scoped>
-.toolbar { display:flex; justify-content:space-between; align-items:flex-start; gap:16px; }
+.activity-management { padding:0; }
+.search-section { display:flex; flex-direction:column; }
+.search-row { margin-bottom:15px; }
+.search-row .el-form { display:flex; flex-wrap:wrap; }
+.search-row .el-form-item { margin-right:0; margin-bottom:0; }
+.operation-row { display:flex; align-items:center; }
+.pagination-section { display:flex; justify-content:flex-end; align-items:center; gap:20px; margin-top:20px; }
+.more-button { margin-left:8px; }
+</style>
+
+<style>
+.more-actions-menu { display:flex; flex-direction:column; margin:-12px; }
+.more-actions-item { padding:8px 16px; cursor:pointer; color:#606266; font-size:14px; }
+.more-actions-item:hover { background:#f5f7fa; color:#409eff; }
+.more-actions-item-danger:hover { color:#f56c6c; }
 </style>

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

@@ -2,18 +2,39 @@
   <div class="activity-mutex-group">
     <el-card>
       <template #header>
-        <div class="header">
-          <div>
-            <div class="title">活动互斥组管理</div>
-            <div class="desc">配置 NTB/MGM 等活动之间的实时拦截、T+1 修正、释放规则和对客状态文案。</div>
+        <div class="search-section">
+          <div class="search-row">
+            <el-form :model="searchForm" inline label-width="90px">
+              <el-form-item label="互斥组名称">
+                <el-input v-model="searchForm.name" clearable placeholder="请输入" class="search-field" />
+              </el-form-item>
+              <el-form-item label="状态">
+                <el-select v-model="searchForm.status" clearable placeholder="全部" class="status-field">
+                  <el-option label="启用" value="启用" />
+                  <el-option label="停用" value="停用" />
+                </el-select>
+              </el-form-item>
+              <el-form-item>
+                <el-button type="primary" @click="handleSearch">查询</el-button>
+                <el-button @click="handleReset">重置</el-button>
+              </el-form-item>
+            </el-form>
+          </div>
+          <div class="operation-row">
+            <el-button type="primary" @click="openDialog()">
+              <el-icon><Plus /></el-icon>新增互斥组
+            </el-button>
           </div>
-          <el-button type="primary" @click="openDialog()">
-            <el-icon><Plus /></el-icon>新增互斥组
-          </el-button>
         </div>
       </template>
 
-      <el-table :data="rows" border stripe>
+      <el-table :data="pagedRows" stripe style="width:100%">
+        <el-table-column label="操作" width="150" fixed="left">
+          <template #default="{ row }">
+            <el-button type="text" size="small" @click="openDialog(row)">编辑</el-button>
+            <el-button type="text" size="small" @click="toggleStatus(row)">{{ row.status === '启用' ? '停用' : '启用' }}</el-button>
+          </template>
+        </el-table-column>
         <el-table-column prop="id" label="互斥组编号" width="120" />
         <el-table-column prop="name" label="互斥组名称" min-width="160" show-overflow-tooltip />
         <el-table-column prop="mutexType" label="互斥类型" width="120" />
@@ -41,16 +62,20 @@
           </template>
         </el-table-column>
         <el-table-column prop="updatedAt" label="更新时间" width="170" />
-        <el-table-column label="操作" width="170" fixed="right">
-          <template #default="{ row }">
-            <el-button type="primary" link @click="openDialog(row)">编辑</el-button>
-            <el-button type="primary" link @click="toggleStatus(row)">{{ row.status === '启用' ? '停用' : '启用' }}</el-button>
-          </template>
-        </el-table-column>
       </el-table>
+      <div class="pagination-section">
+        <span>Total {{ filteredRows.length }}</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="filteredRows.length"
+        />
+      </div>
     </el-card>
 
-    <el-dialog v-model="dialogVisible" :title="editing.id ? '编辑互斥组' : '新增互斥组'" width="860px">
+    <el-dialog v-model="dialogVisible" :title="editing.id ? '编辑互斥组' : '新增互斥组'" width="860px" :close-on-click-modal="false">
       <el-form :model="editing" label-width="130px">
         <el-divider content-position="left">基础规则</el-divider>
         <el-row :gutter="16">
@@ -174,7 +199,7 @@
 </template>
 
 <script>
-import { ElMessage } from 'element-plus'
+import { ElMessage, ElMessageBox } from 'element-plus'
 import activityStore from '@/store/activityStore'
 
 function emptyMutex() {
@@ -203,15 +228,38 @@ export default {
   data() {
     return {
       dialogVisible: false,
-      editing: emptyMutex()
+      editing: emptyMutex(),
+      searchForm: { name: '', status: '' },
+      appliedSearch: { name: '', status: '' },
+      currentPage: 1,
+      pageSize: 10
     }
   },
   computed: {
     rows() {
       return activityStore.mutexGroups
+    },
+    filteredRows() {
+      return this.rows.filter((row) => {
+        if (this.appliedSearch.name && !String(row.name || '').includes(this.appliedSearch.name)) return false
+        if (this.appliedSearch.status && row.status !== this.appliedSearch.status) return false
+        return true
+      })
+    },
+    pagedRows() {
+      const start = (this.currentPage - 1) * this.pageSize
+      return this.filteredRows.slice(start, start + this.pageSize)
     }
   },
   methods: {
+    handleSearch() {
+      this.appliedSearch = { ...this.searchForm }
+      this.currentPage = 1
+    },
+    handleReset() {
+      this.searchForm = { name: '', status: '' }
+      this.handleSearch()
+    },
     basisText(value) {
       return Array.isArray(value) ? value.join('、') : value || '-'
     },
@@ -229,7 +277,9 @@ export default {
       ElMessage.success('保存成功')
       this.dialogVisible = false
     },
-    toggleStatus(row) {
+    async toggleStatus(row) {
+      const action = row.status === '启用' ? '停用' : '启用'
+      await ElMessageBox.confirm(`确定${action}互斥组「${row.name}」吗?`, `${action}互斥组`, { type: 'warning' })
       row.status = row.status === '启用' ? '停用' : '启用'
       activityStore.saveSimple('mutexGroups', row)
       ElMessage.success('状态已更新')
@@ -239,7 +289,15 @@ export default {
 </script>
 
 <style scoped>
-.header { display: flex; justify-content: space-between; align-items: center; gap: 16px; }
-.title { font-size: 16px; font-weight: 600; color: #303133; }
-.desc { margin-top: 4px; font-size: 13px; color: #909399; }
+.activity-mutex-group { padding:0; }
+.search-section { display:flex; flex-direction:column; }
+.search-row { margin-bottom:15px; }
+.search-row .el-form { display:flex; flex-wrap:wrap; }
+.search-row .el-form-item { margin-right:0; margin-bottom:0; }
+.search-field { width:200px; }
+.status-field { width:140px; }
+.operation-row { display:flex; align-items:center; }
+.pagination-section { display:flex; justify-content:flex-end; align-items:center; gap:20px; margin-top:20px; }
+.activity-mutex-group :deep(.el-dialog__body) { padding-top:8px; }
+.activity-mutex-group :deep(.el-divider__text) { color:#303133; font-size:15px; font-weight:600; }
 </style>

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

@@ -9,9 +9,29 @@
 
     <el-card>
       <template #header>
-        <div class="title">活动报表/进度查询</div>
+        <div class="search-section">
+          <el-form :model="searchForm" inline label-width="86px">
+            <el-form-item label="活动名称">
+              <el-input v-model="searchForm.name" clearable placeholder="请输入" class="name-field" />
+            </el-form-item>
+            <el-form-item label="活动状态">
+              <el-select v-model="searchForm.status" clearable placeholder="全部" class="status-field">
+                <el-option label="草稿" value="draft" />
+                <el-option label="待审核" value="pending" />
+                <el-option label="审核通过" value="approved" />
+                <el-option label="已发布" value="published" />
+                <el-option label="已下线" value="offline" />
+                <el-option label="已结束" value="ended" />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" @click="handleSearch">查询</el-button>
+              <el-button @click="handleReset">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </div>
       </template>
-      <el-table :data="activities" border stripe>
+      <el-table :data="pagedActivities" stripe style="width:100%">
         <el-table-column prop="id" label="活动编号" width="130" />
         <el-table-column prop="name" label="活动名称" min-width="180" />
         <el-table-column label="报名进度" width="220">
@@ -33,6 +53,16 @@
         </el-table-column>
         <el-table-column prop="updatedAt" label="更新时间" width="170" />
       </el-table>
+      <div class="pagination-section">
+        <span>Total {{ filteredActivities.length }}</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="filteredActivities.length"
+        />
+      </div>
     </el-card>
   </div>
 </template>
@@ -42,6 +72,14 @@ import activityStore from '@/store/activityStore'
 
 export default {
   name: 'ActivityReport',
+  data() {
+    return {
+      searchForm: { name: '', status: '' },
+      appliedSearch: { name: '', status: '' },
+      currentPage: 1,
+      pageSize: 10
+    }
+  },
   computed: {
     activities() {
       return activityStore.activities
@@ -54,9 +92,29 @@ export default {
     },
     mutexCount() {
       return this.activities.filter((item) => item.mutexEnabled).length
+    },
+    filteredActivities() {
+      return this.activities.filter((item) => {
+        const name = `${item.internalTitle || ''}${item.name || ''}`
+        if (this.appliedSearch.name && !name.includes(this.appliedSearch.name)) return false
+        if (this.appliedSearch.status && item.status !== this.appliedSearch.status) return false
+        return true
+      })
+    },
+    pagedActivities() {
+      const start = (this.currentPage - 1) * this.pageSize
+      return this.filteredActivities.slice(start, start + this.pageSize)
     }
   },
   methods: {
+    handleSearch() {
+      this.appliedSearch = { ...this.searchForm }
+      this.currentPage = 1
+    },
+    handleReset() {
+      this.searchForm = { name: '', status: '' }
+      this.handleSearch()
+    },
     signupPercent(row) {
       if (!row.quotaEnabled || !row.quota) return 100
       return Math.min(100, Math.round((row.signedCount / row.quota) * 100))
@@ -69,8 +127,17 @@ export default {
 </script>
 
 <style scoped>
+.activity-report { padding:0; }
 .summary-row { margin-bottom: 16px; }
-.metric { display: flex; flex-direction: column; gap: 8px; color: #606266; }
-.metric strong { font-size: 28px; color: #303133; }
-.title { font-weight: 600; }
+.metric { display:flex; flex-direction:column; gap:8px; color:#606266; }
+.metric strong { font-size:24px; color:#303133; }
+.search-section .el-form { display:flex; flex-wrap:wrap; }
+.search-section .el-form-item { margin-right:0; margin-bottom:0; }
+.name-field { width:220px; }
+.status-field { width:150px; }
+.pagination-section { display:flex; justify-content:flex-end; align-items:center; gap:20px; margin-top:20px; }
+@media (max-width:1200px) {
+  .summary-row :deep(.el-col) { max-width:50%; flex:0 0 50%; margin-bottom:16px; }
+  .summary-row { margin-bottom:0; }
+}
 </style>

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

@@ -1,70 +1,664 @@
 <template>
   <div class="activity-view">
-    <el-card v-if="activity">
+    <el-card v-if="form" class="activity-page-card">
       <template #header>
         <div class="page-header">
           <span>查看活动</span>
-          <el-button @click="$router.push('/customization/hsbc/activities')">返回</el-button>
+          <el-button @click="goBack">返回</el-button>
         </div>
       </template>
-      <el-descriptions :column="2" border>
-        <el-descriptions-item label="活动编号">{{ activity.id }}</el-descriptions-item>
-        <el-descriptions-item label="活动名称">{{ activity.name }}</el-descriptions-item>
-        <el-descriptions-item label="活动分类">{{ activity.category }}</el-descriptions-item>
-        <el-descriptions-item label="状态">{{ statusText(activity.status) }}</el-descriptions-item>
-        <el-descriptions-item label="活动对象">{{ activity.target }}</el-descriptions-item>
-        <el-descriptions-item label="报名人数">{{ activity.signedCount }} / {{ activity.quotaEnabled ? activity.quota : '不限' }}</el-descriptions-item>
-        <el-descriptions-item label="报名时间">{{ rangeText(activity.signupTime) }}</el-descriptions-item>
-        <el-descriptions-item label="活动时间">{{ rangeText(activity.activityTime) }}</el-descriptions-item>
-        <el-descriptions-item label="展示规则" :span="2">
-          <el-tag v-if="activity.showOnHome">权益首页</el-tag>
-          <el-tag v-if="activity.showOnAll" type="success">全部活动</el-tag>
-          <el-tag v-for="item in activity.launchChannels" :key="item" type="info">{{ item }}</el-tag>
-        </el-descriptions-item>
-        <el-descriptions-item label="报名规则" :span="2">
-          {{ activity.needSignup ? '需要报名' : '无需报名' }},
-          {{ activity.needConfirm ? '需要二次确认' : '无需二次确认' }},
-          {{ activity.needExtraInfo ? `补充信息 ${activity.extraForms?.length || 0} 项` : '无需补充信息' }}
-        </el-descriptions-item>
-        <el-descriptions-item label="报名成功标题">{{ activity.signupSuccessTitle || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="报名成功描述">{{ activity.signupSuccessDescription || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="达标规则" :span="2">
-          {{ activity.progressEnabled ? `${activity.progressStyle},${activity.levels.length} 个层级` : '不跟踪进度' }}
-        </el-descriptions-item>
-        <el-descriptions-item label="奖品配置" :span="2">
-          {{ activity.reward.name }} / {{ activity.reward.type }} / {{ activity.reward.buttonText }} -> {{ activity.reward.linkTarget }}
-        </el-descriptions-item>
-        <el-descriptions-item label="互斥规则" :span="2">
-          {{ activity.mutexEnabled ? `${activity.mutexGroup},${activity.blockText}` : '不参与互斥' }}
-        </el-descriptions-item>
-        <el-descriptions-item label="活动内容" :span="2">{{ activity.content || '-' }}</el-descriptions-item>
-      </el-descriptions>
+
+      <div class="steps-card">
+        <el-steps :active="activeStep" simple class="activity-steps">
+          <el-step v-for="item in stepItems" :key="item.title" :title="item.title">
+            <template #icon><el-icon><component :is="item.icon" /></el-icon></template>
+          </el-step>
+        </el-steps>
+      </div>
+
+      <div v-show="activeStep === 0" class="step-pane">
+        <el-card shadow="never" class="section-card">
+          <template #header><div class="section-title">基础信息</div></template>
+          <el-row :gutter="30" class="readonly-grid">
+            <el-col :span="12"><ReadonlyField label="活动编号" :value="form.id" /></el-col>
+            <el-col :span="12"><ReadonlyField label="状态" :value="statusText(form.status)" /></el-col>
+            <el-col :span="12"><ReadonlyField label="活动标题(对内)" :value="form.internalTitle" /></el-col>
+            <el-col :span="12"><ReadonlyField label="活动名称(对客)" :value="form.name" /></el-col>
+            <el-col :span="12"><ReadonlyField label="活动类型" :value="form.category" /></el-col>
+            <el-col :span="12"><ReadonlyField label="活动对象" :value="form.target" /></el-col>
+            <el-col :span="12">
+              <ReadonlyField label="活动小图">
+                <ImagePreview :src="imageUrl(form.thumbnail)" />
+              </ReadonlyField>
+            </el-col>
+            <el-col :span="12">
+              <ReadonlyField label="活动头图">
+                <ImagePreview :src="imageUrl(form.detailImage)" />
+              </ReadonlyField>
+            </el-col>
+            <el-col :span="24">
+              <div class="readonly-switch-line">
+                <div>
+                  <div class="setting-title">线下活动</div>
+                  <div class="setting-desc">开启后,需配置线下活动所在的城市及具体地点</div>
+                </div>
+                <el-switch :model-value="!!form.offlineEnabled" disabled />
+              </div>
+              <div v-if="form.offlineEnabled" class="readonly-panel">
+                <el-row :gutter="30">
+                  <el-col :span="12"><ReadonlyField label="活动城市" :value="form.cityText || joinText(form.cities)" /></el-col>
+                  <el-col :span="12"><ReadonlyField label="活动地点" :value="form.location" /></el-col>
+                </el-row>
+              </div>
+            </el-col>
+            <el-col :span="24">
+              <ReadonlyField label="活动内容">
+                <div class="rich-text-view" v-html="form.content || emptyHtml"></div>
+              </ReadonlyField>
+            </el-col>
+            <el-col v-if="isLayerActivity" :span="24">
+              <div class="layer-copy-config">
+                <div class="layer-copy-header">
+                  <div class="layer-copy-title"><span class="required-mark">*</span> 活动达标层级内容</div>
+                  <span class="muted">已配置 <strong>{{ layerContents.length }}</strong> / 5 个层级</span>
+                </div>
+                <div class="layer-copy-tabs">
+                  <el-tabs v-model="layerCopyActiveTab" type="card">
+                    <el-tab-pane v-for="(level, index) in layerContents" :key="level.key || index" :label="level.title || layerTitle(index)" :name="String(index)">
+                      <div class="layer-copy-panel">
+                        <ReadonlyField label="Tab 标题" :value="level.title || layerTitle(index)" />
+                        <ReadonlyField label="层级内容">
+                          <div class="rich-text-view compact-rich-text" v-html="level.content || emptyHtml"></div>
+                        </ReadonlyField>
+                      </div>
+                    </el-tab-pane>
+                  </el-tabs>
+                </div>
+              </div>
+            </el-col>
+          </el-row>
+        </el-card>
+
+        <el-card shadow="never" class="section-card toggle-section-card" :class="{ 'is-collapsed': !form.sharable }" :body-style="form.sharable ? undefined : { display: 'none', padding: '0' }">
+          <template #header>
+            <div class="section-title section-title-with-action">
+              <span>分享设置</span>
+              <el-checkbox :model-value="!!form.sharable" disabled>自定义微信分享</el-checkbox>
+            </div>
+          </template>
+          <el-row v-if="form.sharable" :gutter="30" class="readonly-grid">
+            <el-col :span="12"><ReadonlyField label="分享名称" :value="form.shareTitle" /></el-col>
+            <el-col :span="12">
+              <ReadonlyField label="分享图片">
+                <ImagePreview :src="imageUrl(form.shareImage)" small />
+              </ReadonlyField>
+            </el-col>
+          </el-row>
+        </el-card>
+
+        <el-card shadow="never" class="section-card">
+          <template #header><div class="section-title">其他设置</div></template>
+          <el-row :gutter="30" class="readonly-grid">
+            <el-col :span="12"><ReadonlyField label="CD staff ID" :value="form.ownerStaffId" /></el-col>
+            <el-col :span="12"><ReadonlyField label="SCRM activity code" :value="form.scrmActivityCode" /></el-col>
+            <el-col :span="12"><ReadonlyField label="SCRM channel code" :value="form.scrmChannelCode" /></el-col>
+            <el-col :span="12"><ReadonlyField label="Label" :value="joinText(form.labels)" /></el-col>
+          </el-row>
+        </el-card>
+      </div>
+
+      <div v-show="activeStep === 1" class="step-pane">
+        <el-card shadow="never" class="section-card">
+          <template #header><div class="section-title">时间配置</div></template>
+          <el-row :gutter="30" class="readonly-grid">
+            <el-col :span="12"><ReadonlyField label="活动时间" :value="rangeText(form.activityTime)" /></el-col>
+            <el-col :span="12"><ReadonlyField label="展示时间" :value="rangeText(form.displayTime)" /></el-col>
+          </el-row>
+        </el-card>
+        <el-card shadow="never" class="section-card">
+          <template #header><div class="section-title">展示客群</div></template>
+          <el-row :gutter="30" class="readonly-grid">
+            <el-col :span="12"><ReadonlyField label="展示客群" :value="crowdNames(form.visibleCrowdIds, form.visibleCrowdId, form.visibleCrowds)" /></el-col>
+          </el-row>
+        </el-card>
+        <el-card shadow="never" class="section-card">
+          <template #header><div class="section-title">入口展示</div></template>
+          <el-row :gutter="30" class="readonly-grid">
+            <el-col :span="24"><ReadonlyField label="权益首页" :value="displayText(form.showOnHome)" /></el-col>
+            <el-col :span="24">
+              <ReadonlyField label="组装页">
+                <div class="display-value">{{ displayText(form.showOnAssembly) }}</div>
+                <div v-if="form.showOnAssembly" class="sub-value">{{ assemblyNames(form.assemblyPageIds) }}</div>
+              </ReadonlyField>
+            </el-col>
+            <el-col :span="24"><ReadonlyField label="活动轮播图" :value="displayText(form.showOnCarousel)" /></el-col>
+          </el-row>
+        </el-card>
+      </div>
+
+      <div v-show="activeStep === 2" class="step-pane">
+        <el-card shadow="never" class="section-card toggle-section-card" :body-style="{ display: 'none', padding: '0' }">
+          <template #header>
+            <div class="section-title section-title-with-action">
+              <span>报名设置</span>
+              <el-checkbox :model-value="!!form.needSignup" disabled>需要报名</el-checkbox>
+            </div>
+          </template>
+        </el-card>
+
+        <template v-if="form.needSignup">
+          <el-card shadow="never" class="section-card">
+            <template #header><div class="section-title">报名客群与时间</div></template>
+            <el-row :gutter="30" class="readonly-grid">
+              <el-col :span="12"><ReadonlyField label="报名客群" :value="crowdNames(form.signupCrowdIds, form.signupCrowdId, form.signupCrowds)" /></el-col>
+              <el-col :span="12"><ReadonlyField label="报名时间" :value="rangeText(form.signupTime)" /></el-col>
+            </el-row>
+          </el-card>
+
+          <el-card shadow="never" class="section-card toggle-section-card" :class="{ 'is-collapsed': !form.needConfirm }" :body-style="form.needConfirm ? undefined : { display: 'none', padding: '0' }">
+            <template #header>
+              <div class="section-title section-title-with-action">
+                <span>报名确认</span>
+                <el-checkbox :model-value="!!form.needConfirm" disabled>报名确认</el-checkbox>
+              </div>
+            </template>
+            <el-row v-if="form.needConfirm" :gutter="30" class="readonly-grid">
+              <el-col :span="12"><ReadonlyField label="报名确认文案" :value="form.confirmText" /></el-col>
+            </el-row>
+          </el-card>
+
+          <el-card shadow="never" class="section-card toggle-section-card" :class="{ 'is-collapsed': !form.needExtraInfo }" :body-style="form.needExtraInfo ? undefined : { display: 'none', padding: '0' }">
+            <template #header>
+              <div class="section-title section-title-with-action">
+                <span>补充信息表单</span>
+                <el-checkbox :model-value="!!form.needExtraInfo" disabled>补充信息</el-checkbox>
+              </div>
+            </template>
+            <div v-if="form.needExtraInfo" class="extra-form-list">
+              <div v-for="(extra, index) in normalizedExtraForms" :key="index" class="readonly-panel extra-form-panel">
+                <div class="panel-title">补充信息{{ index + 1 }}</div>
+                <el-row :gutter="30">
+                  <el-col :span="12"><ReadonlyField label="字段名称" :value="extra.name" /></el-col>
+                  <el-col :span="12"><ReadonlyField label="提示文案" :value="extra.placeholder" /></el-col>
+                  <el-col :span="12"><ReadonlyField label="字段类型" :value="fieldTypeText(extra.type)" /></el-col>
+                  <el-col :span="12"><ReadonlyField label="是否必填" :value="yesNo(extra.required)" /></el-col>
+                  <el-col :span="12"><ReadonlyField label="最大长度" :value="extra.maxLength" /></el-col>
+                </el-row>
+              </div>
+              <ReadonlyField label="第三方链接" :value="form.extraInfoLink" />
+            </div>
+          </el-card>
+
+          <el-card shadow="never" class="section-card toggle-section-card" :class="{ 'is-collapsed': !form.quotaEnabled }" :body-style="form.quotaEnabled ? undefined : { display: 'none', padding: '0' }">
+            <template #header>
+              <div class="section-title section-title-with-action">
+                <span>名额控制</span>
+                <el-checkbox :model-value="!!form.quotaEnabled" disabled>名额控制</el-checkbox>
+              </div>
+            </template>
+            <el-row v-if="form.quotaEnabled" :gutter="30" class="readonly-grid">
+              <el-col :span="12"><ReadonlyField label="报名名额" :value="form.quota" /></el-col>
+              <el-col :span="12"><ReadonlyField label="已报名人数" :value="form.signedCount" /></el-col>
+            </el-row>
+          </el-card>
+
+          <el-card shadow="never" class="section-card toggle-section-card" :class="{ 'is-collapsed': !form.signupGuideEnabled }" :body-style="form.signupGuideEnabled ? undefined : { display: 'none', padding: '0' }">
+            <template #header>
+              <div class="section-title section-title-with-action">
+                <span>报名成功页配置</span>
+                <el-checkbox :model-value="!!form.signupGuideEnabled" disabled>报名成功页配置</el-checkbox>
+              </div>
+            </template>
+            <el-row v-if="form.signupGuideEnabled" :gutter="30" class="readonly-grid">
+              <el-col :span="12"><ReadonlyField label="标题" :value="form.signupSuccessTitle" /></el-col>
+              <el-col :span="12"><ReadonlyField label="描述" :value="form.signupSuccessDescription" /></el-col>
+            </el-row>
+          </el-card>
+        </template>
+      </div>
+
+      <div v-show="activeStep === 3" class="step-pane">
+        <el-card shadow="never" class="section-card">
+          <template #header><div class="section-title">活动模板</div></template>
+          <el-row :gutter="30" class="readonly-grid">
+            <el-col :span="12"><ReadonlyField label="活动模板" :value="templateText(form.achievementTemplate)" /></el-col>
+            <el-col v-if="form.achievementTemplate === 'LAYER'" :span="12"><ReadonlyField label="层级数量" :value="form.layerCount || achievementLayers.length" /></el-col>
+          </el-row>
+        </el-card>
+
+        <template v-if="form.achievementTemplate === 'NTB'">
+          <el-card shadow="never" class="section-card">
+            <template #header><div class="section-title">达标进度提示话术配置</div></template>
+            <el-row :gutter="30" class="readonly-grid">
+              <el-col :span="12"><ReadonlyField label="未达标提示" :value="form.progressCopy?.unreached" /></el-col>
+              <el-col :span="12"><ReadonlyField label="已达标提示" :value="form.progressCopy?.reached" /></el-col>
+              <el-col :span="12"><ReadonlyField label="达标进度提示" :value="form.progressCopy?.help" /></el-col>
+            </el-row>
+          </el-card>
+          <el-card shadow="never" class="section-card">
+            <template #header><div class="section-title">达标规则标签配置</div></template>
+            <RuleSummary v-for="(rule, index) in ntbRules" :key="rule.key || index" :rule="rule" :index="index" />
+          </el-card>
+          <RewardSummary :reward="form.templateRewards?.NTB" :grant-plans="grantPlans" />
+        </template>
+
+        <template v-else-if="form.achievementTemplate === 'MGM'">
+          <RewardSummary :reward="form.templateRewards?.MGM" :grant-plans="grantPlans" />
+          <el-card shadow="never" class="section-card">
+            <template #header><div class="section-title">达标规则标签配置</div></template>
+            <el-row :gutter="30" class="readonly-grid">
+              <el-col :span="12"><ReadonlyField label="已达标展示标签" :value="form.mgmAchievedTag" /></el-col>
+            </el-row>
+            <RuleSummary v-for="(rule, index) in mgmRules" :key="rule.key || index" :rule="rule" :index="index" />
+          </el-card>
+          <el-card shadow="never" class="section-card">
+            <template #header><div class="section-title">被推荐进度配置</div></template>
+            <el-row :gutter="30" class="readonly-grid">
+              <el-col :span="12"><ReadonlyField label="规则标签1描述" :value="form.mgmReferredProgress?.labelOne" /></el-col>
+              <el-col :span="12"><ReadonlyField label="规则标签2描述" :value="form.mgmReferredProgress?.labelTwo" /></el-col>
+              <el-col :span="12"><ReadonlyField label="是否有进阶奖励" :value="yesNo(form.mgmReferredProgress?.advancedReward)" /></el-col>
+              <el-col v-if="form.mgmReferredProgress?.advancedReward" :span="12"><ReadonlyField label="进阶奖励规则标签" :value="advancedRuleText" /></el-col>
+              <el-col v-if="form.mgmReferredProgress?.advancedReward" :span="12"><ReadonlyField label="进阶奖励规则标签描述" :value="form.mgmReferredProgress?.advancedRuleText" /></el-col>
+            </el-row>
+          </el-card>
+        </template>
+
+        <template v-else>
+          <el-card shadow="never" class="section-card">
+            <template #header><div class="section-title">达标层级配置</div></template>
+            <el-tabs v-model="achievementLayerActiveTab" type="card" class="view-tabs">
+              <el-tab-pane v-for="(level, levelIndex) in achievementLayers" :key="level.key || levelIndex" :label="`层级${levelIndex + 1}`" :name="String(levelIndex)">
+                <el-row :gutter="30" class="readonly-grid">
+                  <el-col :span="12"><ReadonlyField label="可参与人群" :value="crowdName(level.crowdId)" /></el-col>
+                  <el-col :span="12"><ReadonlyField label="层级标题" :value="level.title" /></el-col>
+                </el-row>
+                <div v-for="(activity, activityIndex) in level.activities || []" :key="activity.key || activityIndex" class="layer-activity">
+                  <div class="panel-title">活动{{ activityIndex + 1 }}</div>
+                  <el-row :gutter="30" class="readonly-grid">
+                    <el-col :span="12"><ReadonlyField label="活动标题" :value="activity.title" /></el-col>
+                    <el-col :span="12"><ReadonlyField label="活动描述" :value="activity.description" /></el-col>
+                  </el-row>
+                  <RuleSummary v-for="(rule, ruleIndex) in activity.rules || []" :key="rule.key || ruleIndex" :rule="rule" :index="ruleIndex" />
+                  <RewardSummary inner :reward="activity.reward" :grant-plans="grantPlans" />
+                </div>
+              </el-tab-pane>
+            </el-tabs>
+          </el-card>
+        </template>
+      </div>
+
+      <div v-show="activeStep === 4" class="step-pane">
+        <el-card shadow="never" class="section-card toggle-section-card" :class="{ 'is-collapsed': !form.mutexEnabled }" :body-style="form.mutexEnabled ? undefined : { display: 'none', padding: '0' }">
+          <template #header>
+            <div class="section-title section-title-with-action">
+              <span>互斥设置</span>
+              <el-checkbox :model-value="!!form.mutexEnabled" disabled>参与互斥</el-checkbox>
+            </div>
+          </template>
+          <el-row v-if="form.mutexEnabled" :gutter="30" class="readonly-grid">
+            <el-col :span="12"><ReadonlyField label="互斥组" :value="form.mutexGroup" /></el-col>
+          </el-row>
+        </el-card>
+        <el-card shadow="never" class="section-card">
+          <template #header><div class="section-title">通知设置</div></template>
+          <el-row :gutter="30" class="readonly-grid">
+            <el-col :span="12"><ReadonlyField label="报名成功模板" :value="notificationName(form.signupNoticeTemplateId)" /></el-col>
+            <el-col :span="12"><ReadonlyField label="开始前提醒模板" :value="notificationName(form.beforeStartTemplateId)" /></el-col>
+            <el-col :span="12"><ReadonlyField label="达标发奖通知模板(需领取)" :value="notificationName(form.rewardTemplateId)" /></el-col>
+            <el-col :span="12"><ReadonlyField label="达标发奖通知模板(无需领取)" :value="notificationName(form.rewardNoClaimTemplateId)" /></el-col>
+          </el-row>
+        </el-card>
+      </div>
+
+      <div class="action-bar">
+        <div class="action-left">
+          <el-button v-if="activeStep > 0" @click="prevStep"><el-icon><ArrowLeft /></el-icon>上一步</el-button>
+        </div>
+        <div class="action-center">
+          <el-button @click="goBack">返回</el-button>
+        </div>
+        <div class="action-right">
+          <el-button v-if="activeStep < stepItems.length - 1" type="primary" @click="nextStep">下一步<el-icon class="el-icon--right"><ArrowRight /></el-icon></el-button>
+        </div>
+      </div>
     </el-card>
+
+    <el-empty v-else description="活动不存在">
+      <el-button type="primary" @click="goBack">返回活动管理</el-button>
+    </el-empty>
   </div>
 </template>
 
 <script>
+import { h } from 'vue'
+import { ArrowLeft, ArrowRight, Bell, InfoFilled, Medal, Setting, User } from '@element-plus/icons-vue'
+import request from '@/utils/request'
 import activityStore from '@/store/activityStore'
 
+const emptyText = '-'
+
+const ReadonlyField = {
+  name: 'ReadonlyField',
+  props: {
+    label: { type: String, required: true },
+    value: { type: [String, Number, Boolean], default: '' }
+  },
+  render() {
+    const content = this.$slots.default
+      ? this.$slots.default()
+      : h('div', { class: 'display-value' }, this.value === '' || this.value === undefined || this.value === null ? emptyText : String(this.value))
+    return h('div', { class: 'readonly-field' }, [
+      h('div', { class: 'readonly-label' }, this.label),
+      content
+    ])
+  }
+}
+
+const ImagePreview = {
+  name: 'ImagePreview',
+  props: {
+    src: { type: String, default: '' },
+    small: { type: Boolean, default: false }
+  },
+  render() {
+    if (!this.src) return h('div', { class: ['image-empty', { small: this.small }] }, '暂无图片')
+    return h('div', { class: ['image-preview', { small: this.small }] }, [
+      h('img', { src: this.src, alt: '图片预览' })
+    ])
+  }
+}
+
+const RuleSummary = {
+  name: 'RuleSummary',
+  props: {
+    rule: { type: Object, required: true },
+    index: { type: Number, required: true }
+  },
+  methods: {
+    valueText(value) {
+      return value === '' || value === undefined || value === null ? emptyText : String(value)
+    },
+    yesNo(value) {
+      return value ? '是' : '否'
+    },
+    rangeText(range) {
+      return Array.isArray(range) && range.length === 2 ? `${range[0]} 至 ${range[1]}` : emptyText
+    }
+  },
+  render() {
+    const r = this.rule || {}
+    const isOversea = r.field === '境外消费'
+    const main = isOversea
+      ? `${this.valueText(r.field)} / 消费时间 ${this.valueText(r.consumptionTimeOperator)} ${this.valueText(r.consumptionEndDate)} / ${this.valueText(r.consumptionDimension)} ${this.valueText(r.operator)} ${this.valueText(r.value)}`
+      : `${this.valueText(r.field)} / ${this.valueText(r.operator)} / ${r.field === '开户时间' ? this.rangeText(r.dateRange) : this.valueText(r.value)}`
+    const children = [
+      h('div', { class: 'panel-title' }, `规则标签${this.index + 1}`),
+      h('div', { class: 'rule-main' }, main)
+    ]
+    if (!isOversea && r.field !== '开户时间') {
+      children.push(h('div', { class: 'rule-meta' }, [
+        h('span', `开户当月必须达标:${this.yesNo(r.openingMonthRequired)}`),
+        h('span', `连续达标:${this.yesNo(r.continuous)}`),
+        h('span', `连续月份:${this.valueText(r.months)}`),
+        h('span', `允许断开:${this.yesNo(r.allowBreak)}`)
+      ]))
+    }
+    children.push(h('div', { class: 'rule-meta' }, [
+      h('span', `规则标题:${this.valueText(r.labelDescription || r.reachedText)}`),
+      h('span', `规则描述:${this.valueText(r.unreachedText)}`),
+      h('span', `提示:${this.valueText(r.progressText || r.failureReason)}`)
+    ]))
+    return h('div', { class: 'readonly-panel rule-summary' }, children)
+  }
+}
+
+const RewardSummary = {
+  name: 'RewardSummary',
+  props: {
+    reward: { type: Object, default: () => ({}) },
+    grantPlans: { type: Array, default: () => [] },
+    inner: { type: Boolean, default: false }
+  },
+  computed: {
+    planName() {
+      const planId = this.reward?.planId || this.reward?.grantPlanId
+      const target = this.grantPlans.find((item) => item.id === planId)
+      return target ? `${target.id}-${target.name}` : (planId || emptyText)
+    },
+    validityText() {
+      const range = this.reward?.validity || this.reward?.rewardValidity
+      return Array.isArray(range) && range.length === 2 ? `${range[0]} 至 ${range[1]}` : emptyText
+    }
+  },
+  render() {
+    const card = h('div', { class: ['reward-summary', { inner: this.inner }] }, [
+      h('div', { class: 'section-title reward-title' }, '达标奖励配置'),
+      h('div', { class: 'reward-grid' }, [
+        h(ReadonlyField, { label: '达标发放', value: this.reward?.type || emptyText }),
+        h(ReadonlyField, { label: '选择权益', value: this.planName }),
+        h(ReadonlyField, { label: '权益有效期', value: this.validityText }),
+        h(ReadonlyField, { label: '权益赠送次数', value: `${this.reward?.grantPeriod || emptyText} / ${this.reward?.times || 1} 次` })
+      ])
+    ])
+    if (this.inner) return card
+    return h('div', { class: 'section-card reward-summary-card' }, [card])
+  }
+}
+
 export default {
   name: 'ActivityView',
+  components: { ImagePreview, ReadonlyField, RewardSummary, RuleSummary },
+  data() {
+    return {
+      ArrowLeft,
+      ArrowRight,
+      activeStep: 0,
+      layerCopyActiveTab: '0',
+      achievementLayerActiveTab: '0',
+      assemblyPages: [],
+      stepItems: [
+        { title: '基础信息', icon: InfoFilled },
+        { title: '展示规则', icon: Setting },
+        { title: '客群与报名', icon: User },
+        { title: '进度与奖励', icon: Medal },
+        { title: '互斥与通知', icon: Bell }
+      ],
+      emptyHtml: '<span class="empty-rich-text">暂无内容</span>'
+    }
+  },
   computed: {
-    activity() {
-      return activityStore.getActivity(this.$route.params.id)
+    form() {
+      const row = activityStore.getActivity(this.$route.params.id)
+      if (!row) return null
+      return { ...activityStore.emptyActivity(), ...row }
+    },
+    crowds() { return activityStore.activityCrowds },
+    grantPlans() { return activityStore.activityGrantPlans },
+    notifications() { return activityStore.notifications },
+    isLayerActivity() {
+      return this.form?.category === '层层礼' || this.form?.achievementTemplate === 'LAYER'
+    },
+    layerContents() {
+      const list = Array.isArray(this.form?.layerContents) ? this.form.layerContents : []
+      return list.length ? list : [{ key: 'layer-copy-1', title: '层级一', content: '' }]
+    },
+    achievementLayers() {
+      return Array.isArray(this.form?.achievementLayers) ? this.form.achievementLayers : []
+    },
+    normalizedExtraForms() {
+      const list = Array.isArray(this.form?.extraForms) ? this.form.extraForms : []
+      return list.flatMap((item) => Array.isArray(item?.fields) ? item.fields : [item]).filter(Boolean).slice(0, 2)
+    },
+    ntbRules() {
+      const timeRules = Array.isArray(this.form?.ntbTimeRules) ? this.form.ntbTimeRules : []
+      const rules = Array.isArray(this.form?.ntbRules) ? this.form.ntbRules : []
+      return [...timeRules, ...rules]
+    },
+    mgmRules() {
+      return Array.isArray(this.form?.mgmRules) ? this.form.mgmRules : []
+    },
+    advancedRuleText() {
+      const cfg = this.form?.mgmReferredProgress || {}
+      return `${cfg.advancedRuleField || emptyText} / ${cfg.advancedOperator || emptyText} / ${cfg.advancedValue ?? emptyText}`
     }
   },
+  created() {
+    this.fetchAssemblyPages()
+  },
   methods: {
+    async fetchAssemblyPages() {
+      try {
+        const res = await request.get('/api/assembly/list', { params: { name: '' } })
+        this.assemblyPages = Array.isArray(res?.data) ? res.data : []
+      } catch (e) {
+        this.assemblyPages = []
+      }
+    },
+    valueText(value) {
+      return value === '' || value === undefined || value === null ? emptyText : String(value)
+    },
+    yesNo(value) {
+      return value ? '是' : '否'
+    },
+    displayText(value) {
+      return value ? '显示' : '不显示'
+    },
     rangeText(range) {
-      return range && range.length === 2 ? `${range[0]} 至 ${range[1]}` : '-'
+      return Array.isArray(range) && range.length === 2 ? `${range[0]} 至 ${range[1]}` : emptyText
+    },
+    joinText(list) {
+      return Array.isArray(list) && list.length ? list.join('、') : emptyText
+    },
+    imageUrl(value) {
+      if (Array.isArray(value) && value.length) return value[0]?.url || value[0]
+      return typeof value === 'string' ? value : ''
+    },
+    layerTitle(index) {
+      const names = ['一', '二', '三', '四', '五']
+      return `层级${names[index] || index + 1}`
+    },
+    crowdName(id) {
+      if (!id) return emptyText
+      const target = this.crowds.find((item) => item.id === id)
+      return target ? `${target.id}-${target.name}` : id
+    },
+    crowdNames(ids, singleId, fallback) {
+      if (Array.isArray(ids) && ids.length) return ids.map((id) => this.crowdName(id)).join('、')
+      if (singleId) return this.crowdName(singleId)
+      return this.joinText(fallback)
+    },
+    assemblyNames(ids) {
+      if (!Array.isArray(ids) || !ids.length) return emptyText
+      return ids.map((id) => {
+        const target = this.assemblyPages.find((item) => item.id === id)
+        return target ? `${target.name}(${target.id})` : id
+      }).join('、')
+    },
+    notificationName(id) {
+      if (!id) return emptyText
+      const target = this.notifications.find((item) => item.id === id)
+      return target ? `${target.id}-${target.name}` : id
+    },
+    fieldTypeText(type) {
+      return { text: '文本', number: '数字' }[type] || this.valueText(type)
+    },
+    templateText(type) {
+      return { NTB: 'NTB(新客达标)', MGM: 'MGM(推荐有礼)', LAYER: '层层礼' }[type] || this.valueText(type)
     },
     statusText(status) {
-      return { draft: '草稿', published: '已发布', offline: '已下线' }[status] || status
+      return { draft: '草稿', pending: '待审核', approved: '审核通过', published: '已发布', offline: '已下线' }[status] || this.valueText(status)
+    },
+    nextStep() {
+      if (this.activeStep < this.stepItems.length - 1) this.activeStep += 1
+    },
+    prevStep() {
+      if (this.activeStep > 0) this.activeStep -= 1
+    },
+    goBack() {
+      this.$router.push('/customization/hsbc/activities')
     }
   }
 }
 </script>
 
 <style scoped>
-.page-header { display: flex; justify-content: space-between; align-items: center; font-weight: 600; }
-.el-tag { margin-right: 6px; }
+.activity-view { padding:20px; }
+.page-header { display:flex; justify-content:space-between; align-items:center; font-weight:600; }
+.activity-page-card { background:transparent; border:none; box-shadow:none; }
+.activity-page-card :deep(> .el-card__header) { margin-bottom:18px; background:#fff; border:1px solid #ebeef5; border-radius:4px; box-shadow:0 1px 4px rgba(0,0,0,.04); }
+.activity-page-card :deep(> .el-card__body) { padding:0; }
+.steps-card { margin-bottom:20px; padding:12px 16px; background:#fff; border:1px solid #ebeef5; border-radius:4px; box-shadow:0 1px 4px rgba(0,0,0,.04); }
+.activity-steps { margin-bottom:0; }
+.step-pane { min-height:360px; }
+.section-card { margin-bottom:20px; border:1px solid #ebeef5; border-radius:4px; box-shadow:0 1px 4px rgba(0,0,0,.04); background:#fff; overflow:hidden; }
+.section-card :deep(.el-card__header) { padding:12px 20px; background:#fafafa; border-bottom:1px solid #ebeef5; }
+.section-title { color:#303133; font-size:16px; font-weight:600; }
+.section-title-with-action { display:flex; align-items:center; justify-content:space-between; gap:16px; }
+.toggle-section-card.is-collapsed :deep(.el-card__header) { border-bottom:none; }
+.toggle-section-card.is-collapsed :deep(.el-card__body) { display:none; }
+.readonly-grid { padding:2px 0 0; }
+.readonly-field { display:block; margin-bottom:24px; }
+.readonly-label { margin-bottom:8px; color:#303133; font-size:14px; font-weight:600; line-height:22px; white-space:nowrap; }
+.display-value {
+  min-height:32px;
+  padding:5px 11px;
+  color:#606266;
+  font-size:14px;
+  line-height:20px;
+  word-break:break-word;
+  background:#f5f7fa;
+  border:1px solid #e4e7ed;
+  border-radius:4px;
+  cursor:not-allowed;
+  box-sizing:border-box;
+}
+.sub-value { margin-top:4px; color:#606266; font-size:13px; line-height:20px; }
+.muted { color:#606266; font-size:14px; }
+.muted strong { color:#f56c6c; }
+.image-preview { width:180px; height:104px; border:1px solid #dcdfe6; border-radius:4px; overflow:hidden; background:#f7f8fa; }
+.image-preview.small { width:88px; height:88px; }
+.image-preview img { width:100%; height:100%; object-fit:cover; display:block; }
+.image-empty { width:180px; height:104px; display:flex; align-items:center; justify-content:center; color:#909399; border:1px dashed #cfd6df; border-radius:4px; background:#fafafa; }
+.image-empty.small { width:88px; height:88px; }
+.readonly-switch-line { display:flex; align-items:center; justify-content:space-between; gap:24px; margin:4px 0 24px; padding:20px 0; border-top:1px solid #ebeef5; border-bottom:1px solid #ebeef5; }
+.setting-title { margin-bottom:8px; color:#303133; font-size:15px; font-weight:600; }
+.setting-desc { color:#606266; font-size:13px; line-height:1.5; }
+.readonly-panel { margin-bottom:20px; padding:16px 18px 0; border:1px solid #ebeef5; border-radius:4px; background:#fafafa; }
+.panel-title { margin-bottom:14px; color:#303133; font-size:15px; font-weight:600; }
+.rich-text-view { min-height:160px; padding:12px 14px; color:#606266; line-height:1.7; border:1px solid #e4e7ed; border-radius:4px; background:#f5f7fa; cursor:not-allowed; }
+.compact-rich-text { min-height:120px; }
+.empty-rich-text { color:#909399; }
+.layer-copy-config { margin-top:8px; padding-top:24px; border-top:1px solid #ebeef5; }
+.layer-copy-header { display:flex; align-items:center; justify-content:space-between; gap:16px; margin-bottom:16px; }
+.layer-copy-title { color:#303133; font-size:15px; font-weight:600; }
+.required-mark { color:#f56c6c; }
+.layer-copy-tabs,
+.view-tabs { border:1px solid #dcdfe6; border-radius:4px; overflow:hidden; background:#fff; }
+.layer-copy-tabs :deep(.el-tabs__header),
+.view-tabs :deep(.el-tabs__header) { margin:0; background:#f5f7fa; }
+.layer-copy-tabs :deep(.el-tabs__nav),
+.view-tabs :deep(.el-tabs__nav) { border-top:none; border-left:none; border-radius:0; }
+.layer-copy-tabs :deep(.el-tabs__item),
+.view-tabs :deep(.el-tabs__item) { min-width:120px; text-align:center; font-weight:500; }
+.layer-copy-panel,
+.view-tabs :deep(.el-tabs__content) { padding:20px; }
+.extra-form-list { display:flex; flex-direction:column; gap:16px; }
+.extra-form-panel { margin-bottom:0; }
+.rule-summary { margin-bottom:16px; padding-bottom:16px; }
+.rule-main { margin-bottom:12px; color:#303133; line-height:22px; }
+.rule-meta { display:flex; flex-wrap:wrap; gap:10px 22px; color:#606266; font-size:13px; line-height:22px; }
+.reward-summary-card { padding:0; }
+.reward-summary { padding:0 20px 20px; background:#fff; }
+.reward-summary.inner { margin-top:18px; padding:16px 18px 0; border:1px solid #ebeef5; border-radius:4px; background:#fff; }
+.reward-title { margin:0 -20px 18px; padding:12px 20px; background:#fafafa; border-bottom:1px solid #ebeef5; }
+.reward-summary.inner .reward-title { margin:-16px -18px 18px; padding:12px 18px; }
+.reward-grid { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); column-gap:30px; }
+.layer-activity { margin-top:20px; padding:18px 18px 2px; border:1px solid #ebeef5; border-radius:4px; background:#fafafa; }
+.action-bar { display:flex; align-items:center; justify-content:space-between; margin-top:20px; padding:16px 20px; background:#fff; border:1px solid #ebeef5; border-radius:4px; box-shadow:0 1px 4px rgba(0,0,0,.04); }
+.action-left, .action-right { width:120px; }
+.action-center { display:flex; justify-content:center; gap:12px; }
+@media (max-width:1200px) {
+  .reward-grid { grid-template-columns:1fr; }
+  .activity-view :deep(.el-col) { max-width:100%; flex:0 0 100%; }
+}
 </style>

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

@@ -140,6 +140,14 @@
         </div>
       </div>
 
+      <!-- 返回首页 -->
+      <div class="block-card">
+        <div class="block-header">
+          <span><el-icon><House /></el-icon> 返回首页</span>
+          <el-checkbox v-model="editData.returnHomeConfig.enabled" @change="isModified = true" class="share-toggle-checkbox">开启</el-checkbox>
+        </div>
+      </div>
+
       <!-- 模块列表 -->
       <div v-for="(block, bIdx) in editData.blocks" :key="block.id" class="block-card">
         <!-- 模块操作栏 -->
@@ -598,6 +606,12 @@ export default {
       delete config.desc
       return config
     },
+    createDefaultReturnHomeConfig(overrides = {}) {
+      return {
+        enabled: false,
+        ...overrides
+      }
+    },
     isBlank(value) {
       return !(value || '').trim()
     },
@@ -748,6 +762,7 @@ export default {
     },
     normalizeAssemblyData(data) {
       data.shareConfig = this.createDefaultShareConfig(data.shareConfig || {})
+      data.returnHomeConfig = this.createDefaultReturnHomeConfig(data.returnHomeConfig || {})
       const blocks = data.blocks || []
       blocks.forEach(block => {
         if (block.type !== 'buttonGroup') return
@@ -761,6 +776,7 @@ export default {
       return {
         id: '', name: '', status: 'draft',
         shareConfig: this.createDefaultShareConfig(),
+        returnHomeConfig: this.createDefaultReturnHomeConfig(),
         blocks: []
       }
     },

+ 0 - 883
工作空间/产研/权益/汇丰全行/需求文档/权益平台活动模块_产品文档_后台菜单进度模板版.md

@@ -1,883 +0,0 @@
-# 权益平台活动模块产品文档(后台菜单 + 进度模板版)
-
-> 本版重点修订后台配置部分:将后台从“功能模块堆叠”改为“菜单化配置”,并新增“活动进度模板管理”,用于承接 PPT 中三种活动进度页样式。  
-> 资料依据:用户上传的 PPT、视频讲解、Excel 后台表格《活动管理/客群管理/内容管理/权益管理/渠道管理/活动进度查询》,以及上一版《权益平台活动模块\_产品文档.md》。
-
-* * *
-
-## 1\. 本版核心结论
-
-### 1.1 这版解决什么问题
-
-上一版后台配置的方向是对的,但仍有一个关键问题:**后台配置和移动端页面之间的关系还不够清楚,尤其是“活动进度页”没有单独抽象出来。**
-
-PPT 中移动端不仅有活动列表、活动详情、报名链路,还包含复杂的活动进度页:
-
-1.  多个达标条件,多次发奖;
-2.  多个达标条件,单次发奖;
-3.  MGM 专属进度展示。
-
-这三种不是简单字段,而是不同的**移动端进度页模板**。如果全部塞进“活动管理-编辑页”,会导致活动编辑页过重,也不利于后续复用和维护。
-
-因此,本版后台设计改为:
-
-```text
-活动管理 = 活动主菜单,只做活动编排
-客群管理 = 配置谁能看、谁能报名、谁能达标
-报名表单管理 = 配置报名时额外收集什么信息
-活动进度模板管理 = 配置移动端进度页长什么样
-达标规则管理 = 配置系统怎么判断是否达标
-权益发放计划管理 = 配置达标后发什么、怎么跳转
-活动互斥组管理 = 配置哪些活动互斥、命中后怎么提示
-活动投放管理 = 配置链接、二维码、渠道、首页/MobileX版位
-通知模板管理 = 配置报名、活动提醒、达标发奖通知
-活动报表/进度查询 = 查询报名、达标、发放结果
-```
-
-活动创建时不直接写死复杂规则,而是**选择引用**这些菜单中已配置好的内容。
-
-* * *
-
-### 1.2 后台能不能配置出 PPT 的移动端内容
-
-结论:**可以覆盖大部分 PPT 移动端要求,但必须新增“活动进度模板管理”,并把“达标规则”和“进度页展示”拆开。**
-
-| PPT 移动端内容 | 是否可配置 | 对应后台菜单 | 说明  |
-| --- | --- | --- | --- |
-| 权益首页活动模块 | 可配置 | 活动管理 / 活动投放管理 | 配置是否展示到首页、展示时间、展示客群、版位。 |
-| 全部活动列表 | 可配置 | 活动管理 | 配置是否展示到全部活动、活动小图、名称、时间。 |
-| 我的活动-进行中/已结束 | 可配置 | 活动管理 + 活动状态规则 | 报名活动报名后进入;无需报名活动活动期间自动进入。 |
-| 活动详情页-未报名 | 可配置 | 活动管理 | 配置头图、名称、对象、报名时间、活动时间、地点、内容、达标层级摘要。 |
-| 报名确认页 | 可配置 | 活动管理 | 配置是否需要报名确认、确认文案。 |
-| 补充信息收集页 | 可配置 | 报名表单管理 | 配置最多 2 个补充信息字段,例如航司会员号。 |
-| 报名成功/失败 | 可配置 + 系统判断 | 活动管理 / 报名规则 / 互斥组 | 名额满、不符合条件、互斥拦截等失败原因由系统判断。 |
-| 活动详情页-已报名/无需报名 | 可配置 | 活动管理 + 活动进度模板管理 + 权益发放计划 | 决定按钮展示“查看更多活动/查看达标进度/查看权益”。 |
-| 达标进度页三种样式 | 需要新增菜单 | 活动进度模板管理 | 多条件多次发奖、多条件单次发奖、MGM 专属。 |
-| 达标任务与标签判断 | 可配置 | 达标规则管理 / 客群管理 | 配置标签、条件、连续达标 X/Y/Z。 |
-| 奖励按钮与跳转 | 可配置 | 权益发放计划管理 | 根据奖励类型映射“查看积分奖励/前往领取/查看奖励”。 |
-| 互斥 Case A/B/C | 可配置 | 活动互斥组管理 | 配置互斥组、判断依据、拦截文案、T+1 修正。 |
-| 信用卡活动入口 | 可配置 | 信用卡活动入口配置 / 活动投放管理 | 先作为入口配置,不建议第一版完整纳入活动体系。 |
-
-* * *
-
-### 1.3 哪些是 PPT 明确有的,哪些是补齐的
-
-| 类型  | 内容  | 说明  |
-| --- | --- | --- |
-| PPT 明确有 | 活动列表、我的活动、活动详情、报名确认、补充信息、达标进度、互斥 Case、信用卡入口 | 这是移动端必须支持的展示和流程。 |
-| Excel 后台表已有 | 活动管理、客群管理、内容管理、权益管理、渠道管理、活动进度查询 | Excel 中已经出现大量后台字段,但部分字段混在同一张“活动管理”表里。 |
-| 本版补齐 | 活动进度模板管理、活动互斥组管理、报名表单管理、活动投放管理、通知模板管理 | 这些是为了让后台配置可复用、可维护、可被活动选择引用。 |
-| 本版拆分 | 进度模板 ≠ 达标规则 ≠ 权益发放计划 | 避免把移动端展示、达标判断、奖励发放三类不同能力混在一起。 |
-
-* * *
-
-## 2\. 后台菜单总览
-
-### 2.1 菜单结构
-
-| 菜单  | 是否独立 | 菜单定位 | 活动创建时怎么用 |
-| --- | --- | --- | --- |
-| 活动管理 | 必须独立 | 创建活动、编辑活动、审核发布、关联其他配置 | 主菜单,选择客群、表单、进度模板、达标规则、权益计划、互斥组。 |
-| 客群管理 | 必须独立 | 配置客户范围 | 活动中选择展示客群、报名客群;达标规则中选择达标客群。 |
-| 报名表单管理 | 必须独立 | 配置报名补充信息 | 活动报名设置中选择表单。 |
-| 活动进度模板管理 | 必须独立 | 配置移动端进度页样式 | 活动开启进度跟踪后选择进度模板。 |
-| 达标规则管理 | 必须独立 | 配置系统如何判断达标 | 活动或进度模板中选择达标规则。 |
-| 权益发放计划管理 | 必须独立或放在权益管理下 | 配置达标后发什么奖励 | 活动/达标层级中选择发放计划。 |
-| 活动互斥组管理 | 必须独立 | 配置活动互斥关系 | 活动报名设置中选择互斥组。 |
-| 活动投放管理 | 建议独立 | 配置链接、二维码、渠道、版位 | 选择活动后生成渠道链接和投放物料。 |
-| 通知模板管理 | 可放内容管理下 | 配置短信/通知模板 | 活动或发放计划中选择模板。 |
-| 活动报表/进度查询 | 必须独立 | 查询报名、达标、发放结果 | 不参与活动创建,只做运营查询。 |
-| 信用卡活动入口配置 | 可放投放或系统配置下 | 控制信用卡专区入口 | 配置是否展示、入口文案、跳转地址。 |
-
-* * *
-
-### 2.2 后台配置的核心关系
-
-```text
-活动管理
-  ├─ 选择展示客群 → 控制首页/全部活动是否可见
-  ├─ 选择报名客群 → 控制活动详情是否可报名
-  ├─ 选择报名表单 → 控制补充信息页展示什么字段
-  ├─ 选择进度模板 → 控制报名后进度页长什么样
-  ├─ 选择达标规则 → 控制系统怎么判断是否达标
-  ├─ 选择权益发放计划 → 控制达标后展示什么奖励按钮、跳哪里
-  ├─ 选择互斥组 → 控制是否拦截其他新客活动
-  ├─ 选择通知模板 → 控制是否发报名/开始/发奖通知
-  └─ 选择投放配置 → 控制链接、二维码、MobileX/权益首页版位
-```
-
-* * *
-
-## 3\. 菜单一:活动管理
-
-### 3.1 菜单定位
-
-活动管理是整个活动模块的主菜单,负责创建一个活动,并把其他菜单中的配置组合起来。活动管理不应该承载所有复杂规则,否则字段会非常多,且无法复用。
-
-活动管理主要解决 4 个问题:
-
-1.  这个活动是什么;
-2.  这个活动在哪里展示;
-3.  用户能不能报名、怎么报名;
-4.  报名后是否有关联进度、奖励、互斥、通知。
-
-* * *
-
-### 3.2 活动管理 - 列表页
-
-查询条件
-
-| 查询项 | 类型  | 说明  |
-| --- | --- | --- |
-| 活动 ID | 输入框 | 精确查询活动编号。 |
-| 活动名称 | 输入框 | 支持活动标题(对内)或活动名称(对客)模糊查询。 |
-| 活动类型 | 下拉框 | 普通活动、卓越迎新礼、层层礼、MGM、信用卡活动等。 |
-| 活动状态 | 下拉框 | 草稿、待审核、审核通过、已发布、已下线、已结束、无效。 |
-| 审核状态 | 下拉框 | 未提交、待审核、审核通过、审核驳回。 |
-| 活动时间 | 日期范围 | 按活动开始/结束时间筛选。 |
-| 创建人 | 输入框 | 查询某运营创建的活动。 |
-
-列表字段
-
-| 字段名称 | 字段含义 | 移动端关系 |
-| --- | --- | --- |
-| 活动 ID | 系统生成唯一编号 | 移动端查询活动详情时使用。 |
-| 活动标题(对内) | 后台识别名称 | 不对客展示。 |
-| 活动名称(对客) | 客户看到的活动名称 | 展示在首页、全部活动、我的活动、详情页。 |
-| 活动类型 | 活动分类 | 影响后台筛选、互斥组选择、报表归类。 |
-| 活动时间 | 活动进行时间 | 展示在列表和详情页。 |
-| 报名时间 | 可报名时间 | 影响报名按钮是否可点击。 |
-| 展示状态 | 是否对客展示 | 控制首页/全部活动是否可见。 |
-| 活动状态 | 生命周期状态 | 控制是否可编辑、发布、下线。 |
-| 审核状态 | 审核流转状态 | 待审核活动可置顶给审核人。 |
-| 创建人/审核人 | 操作人信息 | 不对客展示。 |
-| 创建时间/更新时间 | 后台记录 | 不对客展示。 |
-| 操作  | 查看/编辑/预览/提交审核/审核/发布/下线 | 预览用于查看移动端效果。 |
-
-操作按钮
-
-| 操作  | 显示条件 | 点击后逻辑 |
-| --- | --- | --- |
-| 新建活动 | 有活动创建权限 | 进入活动新建页。 |
-| 查看  | 有查看权限 | 进入只读详情页。 |
-| 编辑  | 草稿/驳回/未发布 | 进入编辑页。已发布活动哪些字段可改需确认。 |
-| 预览  | 活动已保存 | 生成移动端预览页。若有渠道码,预览链接携带追踪码。 |
-| 提交审核 | 必填项完整 | 弹出审核人选择,提交后变为待审核。 |
-| 审核  | 当前用户为审核人 | 审核通过或驳回。 |
-| 发布  | 审核通过且有发布权限 | 发布后按展示规则对客展示。 |
-| 下线  | 已发布活动 | 下线后前端不再新增展示;已报名用户是否仍可查看需确认。 |
-
-* * *
-
-### 3.3 活动管理 - 新建/编辑页
-
-建议使用 Tab 结构,避免一个页面字段过多。
-
-Tab 1:基础信息
-
-| 字段  | 类型  | 是否必填 | 移动端影响 | 说明  |
-| --- | --- | --- | --- | --- |
-| 活动标题(对内) | 文本  | 是   | 不展示 | 后台列表、审核、报表识别。 |
-| 活动名称(对客) | 文本  | 是   | 首页/全部活动/我的活动/详情页 | 建议限制 1-50 字。 |
-| 活动微信分享名称 | 文本  | 否   | 微信分享卡片 | 为空时默认取活动名称。 |
-| 活动描述 | 多行文本 | 否   | 可作为列表副标题或分享描述 | 是否移动端展示需确认。 |
-| 活动详情 | 富文本 | 是   | 活动详情页“活动内容” | 支持文字、图片、链接;链接需白名单。 |
-| 活动类型 | 下拉框 | 是   | 影响展示/互斥/报表 | 示例:卓越迎新礼、层层礼、MGM、普通活动。 |
-| 活动城市 | 下拉/多选 | 否   | 详情页展示 | 如地点足够,可不单独配置。 |
-| 活动地点 | 文本  | 否   | 详情页展示 | 线下活动展示,线上可为空。 |
-| 活动小图 | 图片  | 是   | 首页/列表/我的活动卡片 | 用于活动卡片展示。 |
-| 活动头图 | 图片  | 是   | 活动详情页顶部 | 与小图可不同。 |
-| 分享图片 | 图片  | 否   | 微信分享卡片 | 为空时默认取小图或头图。 |
-| 是否可转发 | 开关  | 是   | 是否允许转发 | PPT 中活动可配置“可转发/不可转发”。 |
-| SCRM activity code | 文本  | 否   | 不展示 | 后续同步/报表使用。 |
-| SCRM channel code | 文本  | 否   | 不展示 | 渠道归因使用。 |
-| Label | 多选  | 否   | 不直接展示 | 需提供 interest 列表。 |
-
-Tab 2:时间与展示
-
-| 字段  | 类型  | 是否必填 | 移动端影响 | 说明  |
-| --- | --- | --- | --- | --- |
-| 活动开始时间 | 日期时间 | 是   | 活动时间展示、状态判断 | 需早于结束时间。 |
-| 活动结束时间 | 日期时间 | 是   | 活动结束后进入已结束 | 已结束活动置灰但可点击查看。 |
-| 展示开始时间 | 日期时间 | 否   | 控制首页/全部活动是否展示 | 为空默认发布后可展示。 |
-| 展示结束时间 | 日期时间 | 否   | 控制活动何时隐藏 | 可与活动结束时间不同。 |
-| 是否展示到权益首页 | 开关  | 是   | 首页活动模块是否展示 | 还需满足展示时间、展示客群、发布状态。 |
-| 是否展示到全部活动 | 开关  | 是   | 全部活动列表是否展示 | 关闭后可仅通过链接/二维码进入。 |
-| 是否展示到我的活动 | 系统规则/开关 | 是   | 我的活动是否显示 | 报名活动报名后显示;无需报名活动活动期间自动显示。 |
-| 是否展示报名人数 | 开关  | 否   | 详情页报名人数区域 | PPT 中写报名人数前端是否展示可选。 |
-
-Tab 3:客群与报名
-
-| 字段  | 类型  | 是否必填 | 移动端影响 | 说明  |
-| --- | --- | --- | --- | --- |
-| 展示客群 | 客群选择 | 是   | 决定客户能否看到活动 | 引用客群管理。展示客群不代表可报名。 |
-| 报名客群 | 客群选择 | 需要报名时是 | 决定详情页是否显示报名按钮 | Excel 备注报名客群为单个,是否支持多选需确认。 |
-| 是否需要报名 | 开关  | 是   | 决定是否走报名链路 | 否:进入报名后/无需报名详情版本。 |
-| 报名开始时间 | 日期时间 | 需要报名时是 | 控制报名按钮 | 是否必须晚于活动开始时间需确认。 |
-| 报名结束时间 | 日期时间 | 需要报名时是 | 超过后不可报名 | 需晚于报名开始时间。 |
-| 是否需要报名确认 | 开关  | 否   | 点击报名后是否进入确认页 | PPT 写可配置不需要该 journey。 |
-| 报名确认文案 | 富文本 | 开启确认时是 | 报名确认页展示 | 支持活动名、互斥提示等变量。 |
-| 是否留资 | 开关  | 否   | 是否进入留资链路 | Excel 中区分留资与补充信息,需确认。 |
-| 关联留资表 | 选择/链接 | 开启留资时是 | 跳转或关联外部留资表 | 可能属于 AO 原生表单。 |
-| 是否需要补充信息 | 开关  | 否   | 是否展示补充信息页/弹窗 | 如航司会员号、酒店会员号。 |
-| 补充信息表 | 表单选择 | 开启补充信息时是 | 决定收集字段 | 引用报名表单管理。 |
-| 第三方跳转链接 | URL | 否   | 提交后跳第三方 | 域名需在白名单。 |
-| 报名人数上限 | 数字/不限 | 否   | 名额满提示 | 并发报名需实时判断。 |
-
-Tab 4:进度与奖励
-
-> 本 Tab 只做选择引用,不在活动管理里配置全部进度细节。
-
-| 字段  | 类型  | 是否必填 | 移动端影响 | 说明  |
-| --- | --- | --- | --- | --- |
-| 是否有进度跟踪 | 开关  | 是   | 已报名详情页按钮是否为“查看达标进度” | 开启后必须选择进度模板和达标规则。 |
-| 进度模板 | 模板选择 | 有进度时是 | 决定进度页样式 | 引用活动进度模板管理。 |
-| 达标规则 | 规则选择 | 有进度时是 | 决定系统如何判断达标 | 引用达标规则管理。 |
-| 是否需要权益发放 | 开关  | 否   | 是否展示奖励入口 | 无奖励活动不展示奖励按钮。 |
-| 权益发放计划 | 计划选择 | 需要发放时是 | 决定奖励按钮和跳转 | 引用权益发放计划管理。 |
-| 无进度是否直接发奖 | 开关  | 否   | 详情页按钮为“查看权益” | 对应 PPT 中“无进度跟踪直接发奖”。 |
-
-Tab 5:互斥与通知
-
-| 字段  | 类型  | 是否必填 | 移动端影响 | 说明  |
-| --- | --- | --- | --- | --- |
-| 是否参与互斥 | 开关  | 否   | 报名时可能被拦截 | 开启后选择互斥组。 |
-| 互斥组 | 互斥组选择 | 开启互斥时是 | 列表不符合条件、报名拦截、无法继续参与 | 引用活动互斥组管理。 |
-| 是否报名成功通知 | 开关  | 否   | 不影响页面 | 选择通知模板。 |
-| 报名成功通知模板 | 模板选择 | 否   | 短信通知 | 引用通知模板管理。 |
-| 活动开始前提醒模板 | 模板选择 | 否   | 短信通知 | 需配置触发时间。 |
-| 达标发奖通知模板 | 模板选择 | 否   | 短信通知 | 和发奖状态关联。 |
-| 活动 owner staff ID | 文本  | 否   | 不展示 | 用于通知、审核或同步。 |
-
-Tab 6:审核发布
-
-| 字段/操作 | 说明  |
-| --- | --- |
-| 审核人 | 提交审核时选择业务审核人。 |
-| 提交审核 | 必填项完整后可提交。 |
-| 审核通过 | 业务审核通过后可发布。 |
-| 审核驳回 | 返回编辑状态,需填写驳回原因。 |
-| 发布  | 发布后活动进入前端展示逻辑。 |
-| 下线  | 下线后前端不再新增展示。 |
-
-* * *
-
-## 4\. 菜单二:客群管理
-
-### 4.1 菜单定位
-
-客群管理用于配置“哪些客户能看到活动、哪些客户能报名、哪些客户算达标”。它不应该在每个活动里重复配置。
-
-### 4.2 客群管理 - 列表页
-
-| 字段  | 说明  |
-| --- | --- |
-| 客群编号 | 系统生成,供活动引用。 |
-| 客群名称 | 如“Premier 新客”“MGM 被推荐人”。 |
-| 客群描述 | 说明客群用途。 |
-| 创建人 | Excel 中提到仅创建人可编辑。 |
-| 最后更新时间 | 最近更新时间。 |
-| 操作  | 编辑/查看/预览名单。 |
-
-### 4.3 客群管理 - 新建/编辑页
-
-| 字段  | 类型  | 是否必填 | 说明  |
-| --- | --- | --- | --- |
-| 客群名称 | 文本  | 是   | 后台识别和活动选择使用。 |
-| 客群描述 | 多行文本 | 否   | 说明适用场景。 |
-| 标签组合 | 标签+条件 | 是   | 支持多个标签组合。 |
-| 操作符 | 下拉  | 是   | 等于、大于、小于、包含等。 |
-| 规则值 | 输入/枚举 | 是   | 根据标签类型决定。 |
-| 排序  | 升序/降序 | 否   | 是否需要传接口需确认。 |
-| Sizing | 数字  | 否   | 最大圈选客户数量。 |
-| 白名单/黑名单 | 文件上传/选择 | 否   | 与标签结果做交集或排除。 |
-| 人群预览 | 查询结果 | 否   | 显示 Customer ID,供 sample check。 |
-
-### 4.4 与移动端关系
-
-| 使用位置 | 移动端影响 |
-| --- | --- |
-| 展示客群 | 决定客户是否能在首页/全部活动看到活动。 |
-| 报名客群 | 决定详情页展示“报名”还是“不符合报名条件”。 |
-| 达标客群 | 决定客户是否在进度页达标。 |
-| 互斥特殊客群 | 可用于 MGM 被推荐人、NTB 新客等判断。 |
-
-* * *
-
-## 5\. 菜单三:报名表单管理
-
-### 5.1 菜单定位
-
-报名表单管理用于配置报名时额外收集的信息,例如航司会员号、酒店会员号。活动创建时只选择表单,不在活动编辑页临时创建字段。
-
-### 5.2 报名表单管理 - 列表页
-
-| 字段  | 说明  |
-| --- | --- |
-| 表单 ID | 系统生成。 |
-| 表单名称 | 后台识别,例如“航司会员号收集表”。 |
-| 表单标题 | 对客展示标题,例如“报名需要您提供 IHG 会员号”。 |
-| 字段数量 | 当前表单包含几个字段,最多建议 2 个。 |
-| 创建人 | 创建账号。 |
-| 更新时间 | 最近更新时间。 |
-| 操作  | 编辑/查看。被活动引用后是否允许编辑需确认。 |
-
-### 5.3 报名表单管理 - 新建/编辑页
-
-| 字段  | 类型  | 是否必填 | 移动端影响 |
-| --- | --- | --- | --- |
-| 表单名称 | 文本  | 是   | 不对客展示。 |
-| 表单标题 | 文本  | 是   | 补充信息页/弹窗顶部展示。 |
-| 收集字段名称 1 | 文本  | 是   | 输入框标题,如“航司会员号”。 |
-| 字段 1 输入提示 | 文本  | 否   | placeholder 或说明。 |
-| 字段 1 校验规则 | 规则配置 | 否   | 控制提交校验。 |
-| 收集字段名称 2 | 文本  | 否   | PPT 写最多收集 2 个字段。 |
-| 字段 2 输入提示 | 文本  | 否   | placeholder 或说明。 |
-| 字段 2 校验规则 | 规则配置 | 否   | 控制提交校验。 |
-| 提交按钮文案 | 文本  | 否   | 默认“提交”。 |
-
-### 5.4 需要确认
-
-| 问题  | 原因  |
-| --- | --- |
-| 留资表和补充信息表是否两个概念 | Excel 中同时出现“是否留资/关联留资表”和“补充信息表”。 |
-| 关闭补充信息弹窗是否算报名成功 | PPT 中有“关闭仍判断报名成功,下次进入继续弹窗”的逻辑。 |
-| 第三方跳转后是否需要回调 | 如果点击提交跳第三方就算报名成功,需要确认是否不校验第三方完成情况。 |
-
-* * *
-
-## 6\. 菜单四:活动进度模板管理
-
-### 6.1 为什么必须单独成菜单
-
-PPT 中的“达标进度展示样式”不是普通字段,而是移动端页面模板。至少包含三种类型:
-
-1.  多个达标条件,多次发奖;
-2.  多个达标条件,单次发奖;
-3.  MGM 专属进度展示。
-
-如果放在活动管理里,会导致以下问题:
-
-| 问题  | 影响  |
-| --- | --- |
-| 活动编辑页字段过多 | 配置复杂,运营难理解。 |
-| 进度样式无法复用 | 每个活动都要重新配置一遍。 |
-| MGM 逻辑污染普通活动 | 推荐人/被推荐人视角会让普通进度配置很乱。 |
-| 后续新增样式困难 | 比如新增“阶梯任务 + 排名”样式时需要大改活动管理。 |
-
-因此建议新增:**活动进度模板管理**。
-
-* * *
-
-### 6.2 活动进度模板管理 - 列表页
-
-| 字段  | 说明  |
-| --- | --- |
-| 模板 ID | 系统生成。 |
-| 模板名称 | 后台识别,例如“卓越新户多层达标模板”。 |
-| 进度类型 | 多条件多次发奖 / 多条件单次发奖 / MGM 专属。 |
-| 适用活动类型 | NTB、MGM、层层礼、普通活动等。 |
-| 是否连续达标 | 是/否。 |
-| 层级数量 | 当前模板配置几个进度层级。 |
-| 奖励展示方式 | 单次发奖 / 多次发奖。 |
-| 状态  | 启用 / 停用。 |
-| 关联活动数 | 当前模板被多少活动引用。 |
-| 更新时间 | 最近更新时间。 |
-| 操作  | 查看 / 编辑 / 复制 / 停用 / 预览。 |
-
-* * *
-
-### 6.3 活动进度模板管理 - 新建/编辑页
-
-配置区 1:基础信息
-
-| 字段  | 类型  | 是否必填 | 移动端影响 |
-| --- | --- | --- | --- |
-| 模板名称 | 文本  | 是   | 不对客展示。 |
-| 进度类型 | 单选  | 是   | 决定移动端进度页结构。 |
-| 适用活动类型 | 多选  | 否   | 限制哪些活动可选择该模板。 |
-| 对客标题 | 文本  | 否   | 进度页标题。 |
-| 对客说明 | 多行文本 | 否   | 进度页顶部说明。 |
-| 是否启用 | 开关  | 是   | 停用后活动不可选择。 |
-
-配置区 2:进度页状态文案
-
-| 状态  | 配置内容 | 移动端影响 |
-| --- | --- | --- |
-| 进行中 | 页面提示文案 | 进度页顶部提示区。 |
-| 未达标 | 未达标提示文案 | 任务未完成时展示。 |
-| 已达标 | 已达标提示文案 | 达标但未发奖时展示。 |
-| 已发奖 | 已发奖提示文案 | 发奖后展示。 |
-| 已结束 | 活动结束提示文案 | 已结束活动仍可查看。 |
-| 无法继续参与 | 资格关闭提示文案 | 互斥或 T+1 修正后展示。 |
-
-配置区 3:层级展示配置
-
-| 字段  | 类型  | 是否必填 | 移动端影响 |
-| --- | --- | --- | --- |
-| 是否展示层级 Tab | 开关  | 否   | 多层级时展示 tab;单层级不展示。 |
-| 层级名称 | 文本  | 是   | 如“达标 50 万”。 |
-| 层级排序 | 数字  | 是   | 控制展示顺序。 |
-| 层级对客文案 | 文本  | 是   | 进度页展示。 |
-| 问号说明 | 富文本 | 否   | 点击问号展示补充说明。 |
-| 点击对比内容 | 富文本 | 否   | 点击对比时展示弹窗。 |
-| 未达标按钮文案 | 文本  | 否   | 默认“查看达标详情”。 |
-| 达标后按钮规则 | 系统规则 | 是   | 根据权益发放计划自动生成。 |
-
-配置区 4:连续达标展示
-
-| 字段  | 类型  | 是否必填 | 移动端影响 |
-| --- | --- | --- | --- |
-| 是否连续达标 | 开关  | 否   | 决定是否按月展示进度。 |
-| 活动观察周期 X | 数字  | 连续达标时是 | 例如 12 个月。 |
-| 连续达标月份 Y | 数字  | 连续达标时是 | 例如连续 8 个月。 |
-| 最大观察周期 | 系统限制 | 是   | 建议最多 36 个月。 |
-| 最大连续达标月份 | 系统限制 | 是   | 建议最多 24 个月。 |
-| 月份展示方式 | 系统规则 | 是   | 超过 3 个月横向滑动。 |
-
-配置区 5:MGM 专属展示
-
-| 字段  | 类型  | 是否必填 | 移动端影响 |
-| --- | --- | --- | --- |
-| 是否展示推荐人进度 | 开关  | MGM 模板是 | 推荐人可查看被推荐人状态。 |
-| 是否展示被推荐人进度 | 开关  | MGM 模板是 | 被推荐人进度区展示。 |
-| 被推荐人无奖励时是否展示 | 开关  | 否   | PPT 中写无奖励则不展示进度页。 |
-| 未达标原因文案 | 富文本 | 否   | 例如“您的被推荐人已报名参与其他新客活动”。 |
-| 推荐人视角按钮 | 文本  | 否   | 默认“查看达标详情”。 |
-| 被推荐人状态来源 | 规则选择 | 是   | MGM 标签、推荐关系、活动报名记录。 |
-
-* * *
-
-### 6.4 三种进度类型如何配置
-
-| 进度类型 | 适用场景 | 配置重点 | 移动端表现 |
-| --- | --- | --- | --- |
-| 多条件多次发奖 | 层层礼、阶梯式奖励 | 多层级、多奖励计划、每层独立判断 | 达到一层发一层奖励,多个奖励可能依次亮起。 |
-| 多条件单次发奖 | 多任务达标但只发一次奖 | 多条件组合、最终只关联一个奖励计划 | 所有或指定条件满足后,只出现一次奖励按钮。 |
-| MGM 专属 | 亲友推荐 | 推荐人/被推荐人视角、互斥失败文案、推荐关系状态 | 推荐人可看到被推荐人是否达标或被互斥。 |
-
-* * *
-
-### 6.5 与活动管理的关系
-
-活动管理中只需要配置:
-
-| 活动管理字段 | 说明  |
-| --- | --- |
-| 是否有进度跟踪 | 开启后才显示进度模板选择。 |
-| 进度模板 | 选择本菜单中已启用模板。 |
-| 达标规则 | 选择达标规则管理中的规则。 |
-| 权益发放计划 | 选择发放计划,控制按钮和跳转。 |
-
-* * *
-
-## 7\. 菜单五:达标规则管理
-
-### 7.1 菜单定位
-
-达标规则管理负责“系统怎么判断客户是否达标”。它不负责页面长什么样,页面展示由“活动进度模板管理”控制。
-
-简单说:
-
-```text
-活动进度模板 = 前端怎么展示
-达标规则 = 后台怎么判断
-权益发放计划 = 达标后发什么
-```
-
-* * *
-
-### 7.2 达标规则管理 - 列表页
-
-| 字段  | 说明  |
-| --- | --- |
-| 规则 ID | 系统生成。 |
-| 规则名称 | 如“TRB 连续 8 个月达标规则”。 |
-| 规则类型 | 标签规则 / 资产规则 / 连续达标 / 组合规则。 |
-| 适用活动类型 | NTB、层层礼、MGM 等。 |
-| 条件数量 | 当前规则包含几个条件。 |
-| 是否连续达标 | 是/否。 |
-| 状态  | 启用/停用。 |
-| 关联模板数 | 被多少进度模板引用。 |
-| 关联活动数 | 被多少活动引用。 |
-| 操作  | 查看/编辑/复制/停用。 |
-
-* * *
-
-### 7.3 达标规则管理 - 新建/编辑页
-
-| 配置区 | 字段  | 类型  | 是否必填 | 说明  |
-| --- | --- | --- | --- | --- |
-| 基础信息 | 规则名称 | 文本  | 是   | 后台识别。 |
-| 基础信息 | 规则类型 | 下拉  | 是   | 标签规则、连续达标、组合规则。 |
-| 基础信息 | 适用活动类型 | 多选  | 否   | 限制可选择范围。 |
-| 条件配置 | 条件名称 | 文本  | 是   | 如“TRB 达标”。 |
-| 条件配置 | 数据来源 | 下拉  | 是   | 标签、资产、订单、积分、报名记录等。 |
-| 条件配置 | 标签/字段 | 选择  | 是   | 用于判断的标签或字段。 |
-| 条件配置 | 操作符 | 下拉  | 是   | 等于、大于、小于、包含、不包含等。 |
-| 条件配置 | 规则值 | 输入/枚举 | 是   | 例如 TRB >= 500000。 |
-| 条件配置 | 条件关系 | AND/OR | 是   | 多条件组合关系。 |
-| 连续达标 | 观察周期 X | 数字  | 视规则而定 | 例如 12 个月。 |
-| 连续达标 | 连续达标月份 Y | 数字  | 视规则而定 | 例如 8 个月。 |
-| 连续达标 | 达标条件 Z | 条件配置 | 视规则而定 | TRB 口径等。 |
-| 输出结果 | 达标层级 | 下拉/数字 | 否   | 输出给进度模板展示。 |
-| 输出结果 | 达标状态编码 | 枚举  | 是   | 未达标、已达标、已发奖等。 |
-
-### 7.4 与移动端关系
-
-| 达标规则输出 | 移动端展示 |
-| --- | --- |
-| 未达标 | 进度页任务未完成,按钮为“查看达标详情”。 |
-| 已达标未发奖 | 进度页达标状态亮起,但奖励按钮可能未出现或不可点击。 |
-| 已发奖 | 按奖励类型展示“查看积分奖励/前往领取/查看奖励”。 |
-| 无法继续参与 | 进度页或详情页展示资格关闭文案。 |
-
-* * *
-
-## 8\. 菜单六:权益发放计划管理
-
-### 8.1 菜单定位
-
-权益发放计划负责“达标后发什么”。它可放在已有“权益管理”下,但活动不应直接配置券码和发放细节,而是选择发放计划。
-
-### 8.2 发放计划列表页
-
-| 字段  | 说明  |
-| --- | --- |
-| 发放计划 ID | 系统生成。 |
-| 发放计划名称 | 如“达标 50 万发 500 积分”。 |
-| 奖励类型 | 积分、立减金、卡券、电子券、信用卡权益、第三方权益。 |
-| 发放方式 | 自动/手工/第三方。 |
-| 总数量 | 总库存。 |
-| 剩余数量 | 剩余库存。 |
-| 发放状态 | 草稿、已生效、已停用。 |
-| 关联活动数 | 当前被多少活动引用。 |
-| 操作  | 查看/编辑/停用。 |
-
-### 8.3 发放计划编辑页
-
-| 字段  | 类型  | 是否必填 | 移动端影响 |
-| --- | --- | --- | --- |
-| 发放计划名称 | 文本  | 是   | 不对客展示。 |
-| 奖励类型 | 下拉  | 是   | 决定按钮文案。 |
-| 权益/礼品 ID | 选择  | 是   | 决定跳转目标。 |
-| 单人发放数量 | 数字  | 是   | 决定客户获得数量。 |
-| 总库存 | 数字  | 否   | 库存不足影响发放。 |
-| 每日发放上限 | 数字  | 否   | 控制发放节奏。 |
-| 是否需要客户领取 | 开关  | 否   | 影响按钮为“前往领取”还是“查看奖励”。 |
-| 跳转地址 | URL/路由 | 否   | 第三方权益或外部权益使用。 |
-| 是否通知 | 开关  | 否   | 发奖后是否发通知。 |
-| 通知模板 | 选择  | 开启通知时是 | 引用通知模板管理。 |
-
-### 8.4 奖励类型与移动端按钮映射
-
-| 奖励类型 | 移动端按钮 | 跳转目标 |
-| --- | --- | --- |
-| 积分  | 查看积分奖励 | 积分明细页。 |
-| 立减金 | 前往领取 / 领取奖励 | 立减金列表或领取页。 |
-| 卡券/电子券 | 查看奖励 | 权益详情页或卡券详情页。 |
-| 信用卡权益 | 查看奖励 | 信用卡权益详情页。 |
-| 第三方发放权益 | 查看奖励 | 第三方权益说明页或权益详情页。 |
-
-* * *
-
-## 9\. 菜单七:活动互斥组管理
-
-### 9.1 菜单定位
-
-活动互斥不建议只作为“活动管理”里的一个开关。它应该独立成互斥组菜单,活动创建时选择互斥组。
-
-互斥组解决的问题是:客户已经参加某个新客活动后,是否还能参加同组其他活动;MGM 与非 MGM 新客活动如何互斥;T+1 标签更新后如何修正客户资格。
-
-* * *
-
-### 9.2 活动互斥组管理 - 列表页
-
-| 字段  | 说明  |
-| --- | --- |
-| 互斥组 ID | 系统生成。 |
-| 互斥组名称 | 如“卓越理财新户活动互斥组”。 |
-| 互斥类型 | NTB 新客互斥、MGM 互斥、自定义互斥。 |
-| 判断依据 | 报名记录、MGM 标签、开户时间、达标状态等。 |
-| 关联活动数 | 当前组下有多少活动。 |
-| 生效状态 | 启用/停用。 |
-| 更新时间 | 最近更新时间。 |
-| 操作  | 查看/编辑/复制/停用。 |
-
-* * *
-
-### 9.3 活动互斥组管理 - 新建/编辑页
-
-| 字段  | 类型  | 是否必填 | 移动端影响 |
-| --- | --- | --- | --- |
-| 互斥组名称 | 文本  | 是   | 不对客展示。 |
-| 互斥类型 | 下拉  | 是   | 决定判断逻辑。 |
-| 互斥活动范围 | 活动类型/活动选择 | 是   | 命中后其他活动不可报名。 |
-| 判断依据 | 多选  | 是   | 报名记录、MGM 标签、开户时间等。 |
-| 报名实时拦截 | 开关  | 是   | 点击报名时弹出拦截提示。 |
-| T+1 标签修正 | 开关  | MGM/NTB 场景是 | 次日可能关闭已报名资格。 |
-| 释放规则 | 枚举  | 是   | 活动结束未达标后是否释放资格。 |
-| 列表状态文案 | 文本  | 否   | 如“不符合参与条件”。 |
-| 拦截弹窗文案 | 富文本 | 是   | 报名拦截时展示。 |
-| 无法继续参与文案 | 富文本 | 是   | T+1 后资格关闭时展示。 |
-| 推荐人侧文案 | 富文本 | MGM 场景是 | 推荐人看到被推荐人互斥失败原因。 |
-
-* * *
-
-### 9.4 与 PPT 互斥 Case 的关系
-
-| PPT 场景 | 后台配置来源 | 移动端结果 |
-| --- | --- | --- |
-| Case A:先报名非 MGM,后被推荐 | 互斥组 + 报名记录 + MGM 标签 | 被推荐人无法继续参与 MGM,推荐人侧展示“被推荐人已参与其他新客活动”。 |
-| Case B:先被推荐,后报名 NTB | 互斥组 + MGM 标签 | 第二天起其他 NTB 活动展示不符合参与条件;当天极限情况次日关闭资格。 |
-| Case C:先报名航司,再报名酒店 | 同一互斥组内报名记录 | 第二个活动报名时实时拦截。 |
-
-* * *
-
-## 10\. 菜单八:活动投放管理
-
-### 10.1 菜单定位
-
-活动投放管理用于生成活动链接、二维码、太阳码,并配置活动在 MobileX、权益首页等渠道和版位的投放。它不应完全塞在活动基础信息里,因为一个活动可能有多个渠道、多个版位、多个上线时间。
-
-### 10.2 活动投放管理 - 列表页
-
-| 字段  | 说明  |
-| --- | --- |
-| 投放 ID | 系统生成。 |
-| 活动名称 | 关联活动。 |
-| Campaign ID | 活动归因 ID。 |
-| Channel ID | 渠道归因 ID。 |
-| 投放渠道 | MobileX、微信服务号、短信、Paid Media 等。 |
-| 投放版位 | MobileX 版位、权益首页 Banner 位等。 |
-| 上线时间 | 投放开始时间。 |
-| 下线时间 | 投放结束时间。 |
-| 状态  | 草稿、已发布、已下线。 |
-| 操作  | 编辑/查看/生成链接/生成二维码。 |
-
-### 10.3 活动投放管理 - 新建/编辑页
-
-| 字段  | 类型  | 是否必填 | 移动端影响 |
-| --- | --- | --- | --- |
-| 选择活动 | 活动选择 | 是   | 决定链接进入哪个活动。 |
-| Campaign ID | 搜索选择 | 否   | 链接携带,用于统计。 |
-| Channel ID | 搜索选择 | 否   | 链接携带,用于统计。 |
-| 投放渠道 | 下拉  | 是   | 决定生成物料类型。 |
-| 投放 MobileX 版位 | 下拉  | 否   | 在 MobileX 指定位置展示。 |
-| 投放权益首页版位 | 下拉  | 否   | 在权益首页 1-8 位置展示。 |
-| 投放开始时间 | 日期时间 | 是   | 控制版位何时展示。 |
-| 投放结束时间 | 日期时间 | 否   | 控制版位何时隐藏。 |
-| 生成链接 | 按钮  | \-  | 生成 H5 链接。 |
-| 生成二维码/太阳码 | 按钮  | \-  | 用于线下或微信投放。 |
-
-* * *
-
-## 11\. 菜单九:通知模板管理
-
-### 11.1 菜单定位
-
-通知模板可放在内容管理下。活动管理和发放计划中只选择模板,不重复维护短信内容。
-
-### 11.2 通知模板列表页
-
-| 字段  | 说明  |
-| --- | --- |
-| 模板编号 | 系统生成。 |
-| 模板名称 | 如“活动报名成功提醒”。 |
-| 类型  | SMS/其他,Excel 中目前主要是 SMS。 |
-| 内容  | 通知正文。 |
-| 创建人 | 创建账号。 |
-| 操作  | 编辑/查看。 |
-
-### 11.3 通知模板编辑页
-
-| 字段  | 类型  | 是否必填 | 说明  |
-| --- | --- | --- | --- |
-| 模板名称 | 文本  | 是   | 后台识别。 |
-| 模板类型 | 下拉  | 是   | SMS。 |
-| 模板内容 | 多行文本 | 是   | 是否支持活动名称、报名时间、奖励名称等变量需确认。 |
-| 提交  | 按钮  | \-  | 保存后可被活动选择。 |
-
-* * *
-
-## 12\. 菜单十:活动报表/活动进度查询
-
-### 12.1 菜单定位
-
-活动报表/进度查询不参与活动创建配置,只用于运营查询客户报名、达标、发放、积分、立减金、信用卡权益领取等结果。
-
-### 12.2 建议查询页
-
-| 查询页 | 主要字段 | 用途  |
-| --- | --- | --- |
-| 客户活动报名查询 | 报名时间、活动名称、活动编号、客户来源渠道、Customer ID、客户账户类型 | 查询客户是否报名、从哪个渠道报名。 |
-| 客户达标情况查询 | 客户号、现 MSC、升级 MSC entry date、是否首次升级、月份、已达标层级、规则/活动 | 查询客户达标进度和层级。 |
-| 客户权益发放查询 | 发放时间、权益名称、权益编号、客户来源渠道、Customer ID、客户账户类型 | 查询权益是否发放。 |
-| 积分查询 | 客户号、积分科目、积分变动额、积分总额、清算时间、交易描述 | 查询积分奖励明细。 |
-| 立减金发放明细 | 客户号、立减金类型、发放日期、金额、有效期、是否领取、是否使用 | 查询立减金领取和使用状态。 |
-| 信用卡权益领取及使用 | 客户号、卡号、客户等级、权益领取明细、商品编号、商品名称、商品数量 | 查询信用卡权益领取情况。 |
-
-* * *
-
-## 13\. 信用卡活动入口配置
-
-### 13.1 菜单定位
-
-信用卡活动在 PPT 中更像入口配置,而不是完整纳入本活动系统。第一版建议先做入口,不建议重做 CVP 信用卡活动体系。
-
-### 13.2 配置字段
-
-| 字段  | 类型  | 是否必填 | 移动端影响 |
-| --- | --- | --- | --- |
-| 是否展示信用卡入口 | 开关  | 是   | 全部活动列表底部或信用卡 tab 是否展示。 |
-| 入口名称 | 文本  | 是   | 移动端入口标题。 |
-| 提示文案 | 文本  | 否   | 如“如需查看信用卡活动进度,请点击信用卡活动专区”。 |
-| 跳转地址 | URL | 是   | 点击后跳转 CVP/信用卡权益平台。 |
-| 是否启用信用卡活动 tab | 开关  | 否   | 如启用,活动列表增加 tab;无活动时不展示。 |
-
-* * *
-
-## 14\. 后台菜单与移动端页面映射表
-
-| 后台菜单/配置 | 控制的移动端页面 | 影响内容 |
-| --- | --- | --- |
-| 活动管理-基础信息 | 首页活动模块、全部活动、我的活动、活动详情 | 活动名称、图片、时间、地点、活动内容。 |
-| 活动管理-时间与展示 | 首页、全部活动、我的活动 | 是否展示、何时展示、是否进入已结束。 |
-| 客群管理 | 首页、全部活动、活动详情、进度页 | 谁能看、谁能报名、谁能达标。 |
-| 报名表单管理 | 补充信息页/弹窗 | 收集字段、校验规则、提交逻辑。 |
-| 活动管理-报名设置 | 活动详情、报名确认页、报名结果页 | 是否报名、是否确认、名额、报名成功/失败。 |
-| 活动进度模板管理 | 活动达标进度页 | 三种进度样式、层级展示、状态文案、MGM 专属展示。 |
-| 达标规则管理 | 活动达标进度页 | 标签/资产/连续达标判断,输出达标状态。 |
-| 权益发放计划管理 | 达标进度页、奖励详情页 | 奖励类型、按钮文案、跳转目标、发放状态。 |
-| 活动互斥组管理 | 活动列表、活动详情、报名失败、MGM 进度页 | 不符合参与条件、报名拦截、无法继续参与、推荐人侧提示。 |
-| 活动投放管理 | 活动入口、Banner、二维码/链接访问 | 活动从哪个渠道进入,是否携带追踪码。 |
-| 通知模板管理 | 无直接页面 | 报名成功、开始前提醒、达标发奖通知。 |
-| 信用卡活动入口配置 | 全部活动列表底部/信用卡 tab | 信用卡活动专区入口和跳转。 |
-
-* * *
-
-## 15\. 第一版后台建议范围
-
-### 15.1 必须做
-
-| 菜单/能力 | 原因  |
-| --- | --- |
-| 活动管理 | 没有它无法创建活动和驱动移动端展示。 |
-| 客群管理 | 活动可见、报名、达标都依赖客群。 |
-| 报名表单管理 | PPT 有补充信息收集,不同活动字段不同。 |
-| 活动进度模板管理 | PPT 有三种进度页样式,必须独立配置。 |
-| 达标规则管理 | 需要把“怎么判断达标”从页面样式中拆出来。 |
-| 权益发放计划管理 | 达标后按钮和跳转必须有配置来源。 |
-| 活动互斥组管理 | NTB/MGM 互斥是核心规则,必须复用配置。 |
-| 活动报表/进度查询 | 运营需要查询报名、达标、发放结果。 |
-
-### 15.2 可以后续做
-
-| 菜单/能力 | 原因  |
-| --- | --- |
-| 活动投放多渠道效果分析 | 第一版先生成链接和渠道码,效果看板可后续。 |
-| 信用卡活动独立 tab | PPT 中仍是待讨论,第一版可先做固定入口。 |
-| 通知模板变量高级配置 | 第一版可以先支持简单模板。 |
-| 进度模板版本管理 | 有价值,但第一版可先支持复制、编辑、停用。 |
-| 投入产出比分析 | 字段口径复杂,可后续单独评估。 |
-
-### 15.3 不建议第一版做
-
-| 功能  | 原因  |
-| --- | --- |
-| 把所有配置都堆在活动管理一个编辑页里 | 字段过多,移动端和后台关系会变乱。 |
-| 让运营自由写复杂逻辑表达式 | 容易配置错误,建议先用标签/规则模板选择。 |
-| 信用卡活动完整纳入本活动体系 | 信用卡活动依赖 CVP,第一版做入口更稳。 |
-| 已发布活动随意编辑互斥组或达标规则 | 会影响已报名客户状态,容易引发投诉。 |
-| 真实接入全部发奖系统 | Demo 阶段可先模拟发奖状态和跳转。 |
-
-* * *
-
-## 16\. 仍需确认的问题
-
-| 优先级 | 待确认问题 | 影响范围 |
-| --- | --- | --- |
-| P0  | 活动时间、报名时间、展示时间三者关系如何定义 | 前端展示、报名按钮、我的活动状态。 |
-| P0  | 报名时间是否一定晚于活动开始时间 | Excel 中有该备注,但业务上可能存在提前报名。 |
-| P0  | 展示客群、报名客群、达标客群是否都支持多选 | 客群字段结构和接口结构。 |
-| P0  | 留资表和补充信息表是否两个独立概念 | 报名链路和后台菜单拆分。 |
-| P0  | 补充信息弹窗关闭后是否算报名成功 | 报名记录、我的活动、后续补填逻辑。 |
-| P0  | 进度页达标层级最多 4 层还是 5 层 | PPT 中口径不一致,影响进度模板和达标规则。 |
-| P0  | 三种进度类型是否固定,后续是否还会新增 | 影响活动进度模板管理的数据结构。 |
-| P0  | 进度模板和达标规则是一对一还是一对多 | 影响活动创建时选择逻辑。 |
-| P0  | 互斥是按活动类型自动互斥,还是按互斥组手动配置 | 影响互斥组菜单设计。 |
-| P0  | MGM 标签 T+1 后关闭资格时,已报名记录是否保留 | 我的活动、活动详情、进度页状态。 |
-| P1  | 已发布活动哪些字段允许编辑 | 审核、版本、已报名客户状态。 |
-| P1  | 活动下线后,已报名客户是否仍可在我的活动查看 | 我的活动和已结束列表。 |
-| P1  | 投放权益首页版位是否银行可自助配置 | 活动投放管理是否能完整做。 |
-| P1  | 发放计划被多个活动引用时,是否允许修改 | 奖励发放稳定性。 |
-| P1  | 互斥组被多个活动引用时是否允许编辑 | 已报名客户状态稳定性。 |
-| P2  | 是否需要活动配置版本记录 | 审计和问题追溯。 |
-| P2  | 通知模板变量范围 | 短信模板配置复杂度。 |
-
-* * *
-
-## 17\. 给 Codex / OpenCode 的开发提示词
-
-```text
-你需要基于现有后台 demo 项目,新增“权益平台活动模块”的后台配置 demo。请不要从零开发完整系统,而是在现有后台风格基础上新增菜单和页面。
-
-一、项目目标
-实现一个可演示的权益平台活动后台配置系统,用来配置移动端活动首页、全部活动、我的活动、活动详情、报名链路、达标进度页、奖励按钮、活动互斥和信用卡活动入口。
-
-二、菜单范围
-请新增或模拟以下后台菜单:
-1. 活动管理
-2. 客群管理
-3. 报名表单管理
-4. 活动进度模板管理
-5. 达标规则管理
-6. 权益发放计划管理
-7. 活动互斥组管理
-8. 活动投放管理
-9. 通知模板管理
-10. 活动报表/进度查询
-11. 信用卡活动入口配置
-
-三、重点要求
-1. 活动管理是主菜单,只做活动编排,不要把所有复杂规则都塞进活动编辑页。
-2. 客群、报名表单、进度模板、达标规则、权益发放计划、互斥组都要能独立配置,并在活动新建/编辑页中选择引用。
-3. 必须新增“活动进度模板管理”,支持三种进度类型:
-   - 多条件多次发奖
-   - 多条件单次发奖
-   - MGM 专属
-4. “活动进度模板”负责移动端进度页怎么展示;“达标规则管理”负责系统怎么判断是否达标;“权益发放计划管理”负责达标后发什么奖励。
-5. 活动互斥组需要独立菜单配置,活动创建时选择互斥组,不要只做一个是否互斥开关。
-
-四、活动管理页面要求
-活动管理需要有列表页和新建/编辑页。
-列表页字段包括:活动ID、活动名称、活动类型、活动时间、报名时间、活动状态、审核状态、创建人、审核人、创建时间、更新时间、操作。
-操作包括:查看、编辑、预览、提交审核、审核、发布、下线。
-
-活动新建/编辑页使用 Tab 结构:
-1. 基础信息
-2. 时间与展示
-3. 客群与报名
-4. 进度与奖励
-5. 互斥与通知
-6. 审核发布
-
-五、移动端联动要求
-后台配置需要能模拟影响移动端展示:
-1. 是否展示到首页影响权益首页活动模块。
-2. 是否展示到全部活动影响全部活动列表。
-3. 展示客群影响客户是否能看到活动。
-4. 报名客群影响详情页是否可报名。
-5. 报名表单影响补充信息页字段。
-6. 进度模板影响达标进度页样式。
-7. 达标规则影响进度状态:未达标、已达标、已发奖、无法继续参与。
-8. 权益发放计划影响达标后按钮:查看积分奖励、前往领取、查看奖励。
-9. 互斥组影响列表状态、报名拦截弹窗、无法继续参与提示、MGM推荐人侧文案。
-
-六、交付物
-1. 后台菜单页面 demo。
-2. 每个菜单至少有列表页和新建/编辑页。
-3. 使用 mock 数据即可,不需要真实接口。
-4. 页面风格保持现有后台系统风格。
-5. 不需要实现真实银行标签、真实发奖、真实短信发送。
-```

+ 1741 - 0
工作空间/产研/权益/汇丰全行/需求文档/活动模块需求文档.md

@@ -0,0 +1,1741 @@
+# 权益平台活动模块产品文档
+
+> 资料来源:用户上传的视频录屏与《权益平台活动模块 – CUPD》PPT。本文档按“移动端页面 + 后台配置 + 状态规则 + 测试点 + Codex/OpenCode 开发提示词”整理,用于后续需求评审、Demo 开发或交给 AI 编程工具执行。
+
+* * *
+
+## 1\. 需求范围与核心结论
+
+### 1.1 业务场景
+
+权益平台需要支持面向客户展示各类活动,客户可以在权益专区首页、全部活动、我的活动中查看活动,并根据活动规则完成报名、补充信息、查看活动详情、查看达标进度和奖励领取。部分新客活动之间存在互斥关系,需要在报名时进行实时拦截,避免客户同时参与多个互斥活动。
+
+本需求不是单一的“活动列表”,而是一个完整的活动模块,至少包含:
+
+1.  活动展示:权益专区首页活动模块、全部活动列表、我的活动列表。
+2.  活动详情:未报名详情、已报名详情、无需报名详情。
+3.  报名链路:报名确认、补充信息收集、报名成功/失败。
+4.  达标进度:多条件、多层级、单次/多次发奖、MGM 推荐活动特殊展示。
+5.  互斥判断:新客活动之间互斥、MGM 与非 MGM 活动互斥、T+1/T-1 数据延迟处理。
+6.  后台配置:活动基础信息、展示规则、报名规则、进度规则、奖励规则、互斥规则、短信/转发规则。
+7.  信用卡活动入口:作为权益平台内的固定入口或后续独立 tab。
+
+### 1.2 用户角色
+
+| 角色  | 说明  | 关注点 |
+| --- | --- | --- |
+| 客户  | 权益平台前端使用者 | 能看到自己可参与的活动、完成报名、查看进度和奖励 |
+| 潜客/新客 | 可能通过合作方二维码或链接进入活动 | 是否满足 NTB 新客活动条件、是否与其他活动互斥 |
+| 推荐人 | MGM 亲友推荐活动中的推荐方 | 查看被推荐人是否达标、是否因互斥失效 |
+| 被推荐人 | MGM 亲友推荐活动中的被推荐客户 | 可能与其他新客活动互斥 |
+| 活动运营 | 后台配置活动的人 | 配置活动内容、展示范围、报名规则、奖品、互斥组 |
+| 系统  | 标签、开户时间、活动报名、奖励发放等能力提供方 | 标签数据、客户状态、互斥判断、进度计算、奖励跳转 |
+
+### 1.3 第一版取舍建议
+
+必须做
+
+| 功能  | 原因  |
+| --- | --- |
+| 活动首页模块、全部活动、我的活动 | 没有入口,活动无法被客户访问 |
+| 活动详情未报名/已报名两套状态 | 报名前后展示内容和按钮不同 |
+| 报名规则配置 | 活动是否需要报名、是否确认、是否补充信息都不同 |
+| 报名名额控制 | 存在“报名已满”场景 |
+| 客群可见/报名限制 | 活动不是所有客户都能看或报名 |
+| 无需报名活动自动进入我的活动 | PPT 中明确说明无需报名活动也会进入我的活动 |
+| 达标进度基础展示 | 活动核心价值是查看进度和奖励 |
+| 奖励类型与按钮跳转 | 不同奖励按钮不同:积分、立减金、卡券、信用卡权益、第三方权益 |
+| 活动互斥实时判断 | 避免同一天报名多个互斥活动 |
+| 互斥后的活动状态展示 | 其他活动需展示“不符合参与条件/报名条件” |
+| 后台配置页 Demo | 该模块大量规则依赖后台配置,不只做移动端页面不完整 |
+
+可以后续做
+
+| 功能  | 原因  |
+| --- | --- |
+| 短信提醒配置 | 有业务价值,但不影响报名和查看主链路 |
+| 活动转发控制 | 可作为活动扩散能力,第一版可只做开关展示 |
+| 信用卡活动独立 tab | PPT 中标记待讨论,可先做固定入口 |
+| 人工代办修改报名信息 | PPT 提到预留系统能力,但首版不建议实现复杂后台代办 |
+| 点击对比弹窗 | 属于达标进度增强说明,首版可做简单弹窗 |
+| 连续达标逐月滚动展示 | 可在 Demo 中做静态/模拟数据,真实计算后续对接 |
+
+不建议第一版做
+
+| 功能  | 原因  |
+| --- | --- |
+| 接入真实银行客户/标签/开户接口 | 开发成本和数据权限高,不适合 Demo 阶段 |
+| 完整奖励发放系统 | 本模块只需要展示按钮与跳转,不建议做真实发奖闭环 |
+| 全量 MGM 原生能力重做 | MGM 表单和推荐关系属于既有能力,本需求只处理权益平台展示与互斥结果 |
+| 复杂审批流 | PPT 未明确活动后台审核流,Demo 阶段不需要扩展过多 |
+
+* * *
+
+## 2\. 页面与模块清单
+
+### 2.1 移动端页面
+
+| 页面/模块 | 页面类型 | 主要作用 |
+| --- | --- | --- |
+| 权益专区主页 - 活动模块 | 移动端模块 | 在权益平台首页展示配置为首页可见的活动入口 |
+| 全部活动列表页-线上活动 | 移动端页面 | 展示客户当前所有可见线上活动 |
+| 全部活动列表页-线下活动 | 移动端页面 | 展示客户当前所有可见线下活动 |
+| 全部活动列表页-信用卡活动 | 移动端页面 | 需确认,是否展示,是否后台控制展示,展示哪些内容 |
+| 我的活动页 - 进行中 | 移动端页面 | 展示客户已报名、无需报名、进行中的活动 |
+| 我的活动页 - 已结束 | 移动端页面 | 展示客户已结束活动,置灰但可查看详情/进度/奖品 |
+| 活动详情页 - 未报名 | 移动端页面 | 展示活动详情,并提供报名入口 |
+| 报名确认页/弹窗 | 移动端确认页 | 可配置是否需要客户二次确认 |
+| 补充信息收集页/弹窗 | 移动端表单 | 可配置,最多收集 2 个补充信息,例如航司会员号 |
+| 报名失败/弹窗 | 移动端报名失败提示页 | 提示失败结果 |
+| 报名结果页 | 移动端结果页 | 展示报名成功 |
+| 活动详情页 - 已报名/无需报名 | 移动端页面 | 展示报名后信息、活动内容、按钮跳转 |
+| 活动达标进度页-层层礼 | 移动端页面 | <br> |
+| 活动达标进度页-MGM | 移动端页面 | MGM 活动特殊进度展示,含推荐人/被推荐人场景 |
+| 活动达标进度页-NTB | 移动端页面 | <br> |
+| 合作方活动落地页 | 移动端 H5 | 外部二维码/链接进入的活动报名页面 |
+| 信用卡活动专区入口 | 移动端入口 | 跳转到信用卡权益平台或信用卡活动专区 |
+
+### 2.2 后台配置页面(按菜单理解)
+
+后台不再按“基础信息配置/报名规则配置/达标配置”这种零散模块理解,而是按菜单理解。活动管理是主菜单,其他可复用能力拆成独立菜单,活动创建时选择引用。
+
+| 后台菜单 | 主要作用 | 与活动管理的关系 |
+| --- | --- | --- |
+| 活动管理 | 创建活动、编辑活动、审核发布活动、预览活动 | 主菜单,活动创建时选择其他菜单的配置 |
+| 客群管理(原有菜单-人群管理) | 配置展示客群、报名客群、达标客群 | 活动管理中选择客群 |
+| 活动互斥组管理 | 配置 NTB/MGM 等互斥规则和拦截文案 | 活动报名设置中选择互斥组 |
+| 通知模板管理(原有菜单-短信模板管理) | 配置报名成功、活动开始前、达标发奖短信 | 活动通知设置中选择模板 |
+| 活动投放管理(暂时不需要) | 配置渠道码、链接、二维码、MobileX/权益首页版位 | 选择活动后生成投放物料和版位 |
+| 活动报表/进度查询 | 查询报名、达标、发奖、积分、立减金等结果 | 不参与活动创建,只做运营查询 |
+| 信用卡活动入口配置 | 控制信用卡活动专区入口和跳转 | 可作为系统配置或投放配置 |
+
+* * *
+
+## 3\. 移动端页面说明与字段说明
+
+## 3.1 权益专区主页 - 活动模块
+
+### 页面说明
+
+权益专区主页中的活动模块,用于展示运营配置为“首页展示”的活动。客户点击活动卡片后进入活动详情页。某些活动可以不展示到首页,只通过链接或二维码访问;这类活动只有客户报名后才会出现在“我的活动”中。
+
+### 展示规则
+
+1.  仅展示当前客户可见的活动。
+2.  活动需同时满足:活动状态有效、展示时间有效、客群可见、后台配置首页展示。
+3.  无需报名活动若配置到首页,也可以展示在首页活动模块。
+4.  首页活动模块为空时,不展示模块标题和空白区域。
+
+### 字段说明
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 数据来源 | 展示规则 | 编辑规则/异常 |
+| --- | --- | --- | --- | --- | --- | --- |
+| 活动小图 | 活动卡片图片 | 图片 URL | 是   | 后台活动基础信息 | 卡片左侧或顶部展示 | 图片为空则使用默认图或不展示该活动 |
+| 活动名称 | 活动标题 | 文本  | 是   | 后台活动基础信息 | 最多展示 2 行,超出省略 | 后台必填 |
+| 活动时间 | 活动进行时间 | 日期范围 | 是   | 后台活动时间配置 | 格式如 2026/1/1-2026/3/31 | 结束时间早于开始时间不允许保存 |
+| 活动状态文案 | 如已报名、不符合参与条件 | 文本/枚举 | 否   | 系统根据客户状态计算 | 按客户活动状态展示 | 不同客户看到的状态可能不同 |
+| 跳转地址 | 点击卡片后的页面 | URL/路由 | 是   | 系统生成 | 默认跳活动详情页 | 外部活动可跳合作方落地页 |
+
+### 交互逻辑
+
+1.  客户进入权益专区主页。
+2.  系统查询客户可见活动列表。
+3.  过滤未配置首页展示的活动。
+4.  客户点击活动卡片:
+    -   未报名活动:进入活动详情页(未报名)。
+    -   已报名活动:进入活动详情页(已报名)。
+    -   无需报名活动:进入活动详情页(报名后/无需报名)。
+    -   不符合参与条件活动:可进入详情页查看不符合提示,也可按业务配置不允许点击。
+
+* * *
+
+## 3.2 全部活动列表页
+
+### 页面说明
+
+全部活动列表页展示当前客户所有可见活动,包含线上/线下活动。页面下方可展示信用卡活动专区固定入口。
+
+### 展示规则
+
+1.  展示客户所有可见活动。
+2.  支持按照活动时间从新到老展示。
+3.  活动是否展示到权益平台前端由后台配置控制。
+4.  信用卡活动入口位于活动列表底部,可配置是否对客展示。
+
+### 字段说明
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 数据来源 | 展示规则 | 异常情况 |
+| --- | --- | --- | --- | --- | --- | --- |
+| 活动小图 | 列表卡片图片 | 图片 URL | 是   | 后台配置 | 每条活动展示 | 加载失败展示默认图 |
+| 活动名称 | 活动名称 | 文本  | 是   | 后台配置 | 最多 2 行 | 空值不允许活动发布 |
+| 活动时间 | 活动进行时间 | 日期范围 | 是   | 后台配置 | 统一格式展示 | 时间为空不展示活动 |
+| 活动状态 | 当前客户与活动关系 | 枚举  | 否   | 系统计算 | 如已报名、不符合参与条件、报名已满等 | 状态优先级见状态规则 |
+| 活动分类 | 线上活动/线下活动/信用卡活动 | 枚举  | 否   | 后台配置 | 可作为 tab 或标签 | 无分类时默认线上活动 |
+| 信用卡活动入口 | 跳转信用卡活动专区 | 入口卡片 | 否   | 后台入口配置 | 配置开启时展示在列表底部 | 无链接时不展示 |
+
+### 交互逻辑
+
+1.  进入全部活动列表页,系统拉取当前客户可见活动。
+2.  点击活动卡片进入活动详情页。
+3.  点击信用卡活动专区入口,跳转信用卡权益平台或信用卡活动专区。
+4.  若无活动,展示空状态文案,例如“暂无可参与活动”。
+
+* * *
+
+## 3.3 我的活动页
+
+### 页面说明
+
+我的活动用于展示客户已经报名、无需报名自动加入、或已经结束但仍可查看记录的活动。页面分为“进行中”和“已结束”两个 tab。
+
+### 展示规则
+
+1.  进行中 tab 展示客户进行中的活动。
+2.  已结束 tab 展示客户已结束的活动。
+3.  已结束活动需要置灰。
+4.  已结束活动仍可以点击进入活动详情页,查看活动详情、进度和奖品。
+5.  不需要报名的活动,在活动期间自动加入“我的活动-进行中”;活动结束后进入“已结束”。
+6.  我的活动按照时间从新到老展示。
+7.  页面可展示温馨提示:如需查看信用卡活动进度,请点击进行中-信用卡活动专区。
+
+### 字段说明
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 数据来源 | 展示规则 | 异常情况 |
+| --- | --- | --- | --- | --- | --- | --- |
+| Tab 名称 | 进行中/已结束 | 枚举  | 是   | 固定  | 顶部 tab 展示 | 无活动时仍展示 tab |
+| 活动小图 | 活动卡片图片 | 图片 URL | 是   | 后台活动配置 | 已结束活动置灰 | 图片加载失败展示默认图 |
+| 活动名称 | 活动标题 | 文本  | 是   | 后台活动配置 | 超出省略 | 空值不允许发布 |
+| 活动时间 | 活动进行时间 | 日期范围 | 是   | 后台活动配置 | 列表展示 | 时间异常则不展示 |
+| 活动状态 | 已报名/已结束/无法继续参与等 | 枚举  | 否   | 系统计算 | 可展示在卡片上 | 状态为互斥失败时展示对应文案 |
+| 信用卡活动提示 | 查看信用卡活动进度提示 | 文本  | 否   | 后台或固定文案 | 可展示在进行中 tab 顶部 | 未配置时不展示 |
+
+### 交互逻辑
+
+1.  客户进入我的活动页。
+2.  默认展示“进行中”。
+3.  点击“已结束”切换到已结束活动。
+4.  点击活动卡片:
+    -   进入活动详情页。
+    -   若活动有进度,仍可查看活动进度。
+    -   若活动有奖品,仍可查看奖品信息。
+5.  已结束活动置灰,但不禁止点击。
+
+* * *
+
+## 3.4 活动详情页 - 未报名
+
+### 页面说明
+
+客户在未报名状态进入活动详情页时,页面展示活动详细内容,并提供“报名”按钮。系统需要判断客户是否符合报名条件以及名额是否充足。
+
+### 展示内容
+
+1.  活动图片。
+2.  活动名称。
+3.  活动对象。
+4.  活动报名时间。
+5.  活动进行时间。
+6.  报名人数,是否展示可配置。
+7.  活动内容。
+8.  活动达标层级,最多四层(注意:达标配置区 PPT 中另有最多五层描述,需业务确认。Demo 建议兼容 5 层,页面按配置展示)。
+9.  报名按钮。
+
+### 字段说明
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 数据来源 | 展示规则 | 编辑/异常规则 |
+| --- | --- | --- | --- | --- | --- | --- |
+| 活动图片 | 详情页头图 | 图片 URL | 是   | 后台配置 | 顶部展示 | 图片为空使用默认图或活动不可发布 |
+| 活动名称 | 活动标题 | 文本  | 是   | 后台配置 | 页面主标题 | 后台必填 |
+| 活动对象 | 面向客户范围 | 文本/标签 | 是   | 后台配置 | 如汇丰卓越理财新户 | 可与客群标签不完全一致,是对客文案 |
+| 报名时间 | 可报名时间范围 | 日期时间 | 视活动而定 | 后台报名配置 | 需要报名活动展示 | 当前时间不在范围内时按钮置灰或隐藏 |
+| 活动进行时间 | 达标/活动有效时间 | 日期时间 | 是   | 后台配置 | 页面展示 | 决定活动进行中/已结束 |
+| 报名人数 | 当前报名人数/名额 | 数字  | 否   | 报名记录统计 | 后台可配置是否展示 | 统计失败时隐藏,不影响报名 |
+| 活动内容 | 活动规则说明 | 富文本 | 是   | 后台配置 | 正文展示 | 支持图文、换行、链接 |
+| 达标层级 | 活动奖励/达标层级说明 | 列表  | 否   | 后台达标配置 | 最多按配置展示 | 超过上限不允许保存 |
+| 报名按钮 | 触发报名 | 按钮  | 需要报名时是 | 系统生成 | 符合条件且可报名时可点击 | 不符合条件或名额满时进入失败提示 |
+
+### 交互逻辑
+
+1.  客户进入活动详情页。
+2.  系统判断客户是否可见。
+3.  系统判断客户是否符合报名条件。
+4.  系统判断是否需要报名:
+    -   需要报名:展示报名按钮。
+    -   不需要报名:按“已报名/无需报名”版本展示。
+5.  客户点击报名按钮后:
+    -   若不符合报名条件:进入报名失败提示。
+    -   若名额不足:提示“很遗憾,该活动报名已满。”
+    -   若命中互斥规则:展示互斥拦截文案。
+    -   若通过校验:进入报名确认页或补充信息页。
+
+* * *
+
+## 3.5 报名确认页/弹窗
+
+### 页面说明
+
+报名确认页用于让客户确认是否参加当前活动。此页面可配置是否启用,如果当前活动不需要客户重复确认,可以跳过该步骤。
+
+### 字段说明
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 数据来源 | 展示规则 | 异常情况 |
+| --- | --- | --- | --- | --- | --- | --- |
+| 确认标题 | 确认报名活动名称 | 文本  | 是   | 活动名称 + 后台文案模板 | 如“您确认报名【活动名称】活动?” | 活动名称为空不可发布 |
+| 确认说明 | 活动互斥/规则提示 | 富文本 | 否   | 后台配置 | 支持换行和重点提示 | 未配置时不展示 |
+| 确认按钮 | 继续报名 | 按钮  | 是   | 固定  | 点击后进入下一步 | 重复点击需防抖 |
+| 取消按钮 | 放弃报名 | 按钮  | 是   | 固定  | 返回详情页 | 取消不产生报名记录 |
+
+### 交互逻辑
+
+1.  客户点击报名。
+2.  若活动配置“需要重复确认”,展示确认页/弹窗。
+3.  点击确认:
+    -   若需要补充信息,进入补充信息收集页。
+    -   若不需要补充信息,直接提交报名。
+4.  点击取消或返回:
+    -   不创建报名记录。
+    -   返回活动详情页未报名状态。
+
+* * *
+
+## 3.6 补充信息收集页/弹窗
+
+### 页面说明
+
+部分活动需要客户补充信息,例如航司会员号、酒店会员号等。补充信息最多支持 2 个字段。也可配置提示客户前往第三方网页完成进一步信息补充。
+
+### 核心规则
+
+1.  最多配置 2 个补充信息字段。
+2.  每个字段可配置字段名称、提示文案、校验规则、最大长度。
+3.  若配置第三方跳转,客户点击提交并跳转第三方网页,即代表报名成功。
+4.  客户点击取消或右上角关闭,仍判断为报名成功。
+5.  如果客户未完成补充信息,下次进入活动时继续弹出补充信息弹窗,直到客户补全。
+
+### 字段说明
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 数据来源 | 展示规则 | 编辑/异常规则 |
+| --- | --- | --- | --- | --- | --- | --- |
+| 补充信息标题 | 表单标题 | 文本  | 是   | 后台配置 | 弹窗顶部展示 | 未配置用默认标题 |
+| 字段 1 名称 | 第一个收集字段名称 | 文本  | 否   | 后台配置 | 如“航司会员号” | 启用字段时必填 |
+| 字段 1 输入值 | 客户填写内容 | 文本  | 视配置 | 客户输入 | 根据校验规则判断 | 不合法提示错误 |
+| 字段 1 提示文案 | 输入框 placeholder 或说明 | 文本  | 否   | 后台配置 | 输入框下方/内展示 | 为空不展示 |
+| 字段 1 校验规则 | 字符、数字、长度限制 | 枚举/规则 | 否   | 后台配置 | 提交时校验 | 校验失败不可提交 |
+| 字段 2 名称 | 第二个收集字段名称 | 文本  | 否   | 后台配置 | 同字段 1 | 最多两个字段 |
+| 第三方提示文案 | 引导客户前往第三方 | 富文本 | 否   | 后台配置 | 提交前/提交后展示 | 未配置不展示 |
+| 第三方跳转链接 | 外部页面地址 | URL | 否   | 后台配置 | 点击提交后跳转 | 链接无效需提示 |
+| 提交按钮 | 提交补充信息 | 按钮  | 是   | 固定  | 点击后校验并提交 | 防止重复提交 |
+| 关闭按钮 | 关闭弹窗 | 按钮  | 是   | 固定  | 关闭后仍报名成功,但信息未补全 | 下次进入继续弹出 |
+
+* * *
+
+## 3.7 报名结果页
+
+### 页面说明
+
+报名结果页展示报名成功或失败结果。报名成功后,根据活动是否有进度跟踪进入不同后续链路。
+
+### 报名成功分支
+
+| 活动类型 | 报名成功后展示 | 后续按钮 |
+| --- | --- | --- |
+| 有进度跟踪 | 报名成功,提示可查看进度 | 查看达标进度 |
+| 无进度跟踪无发奖 | 报名成功,提示可查看更多活动 | 查看更多活动 |
+| 无进度跟踪直接发奖 | 报名成功,提示可查看权益 | 查看权益 |
+| 需要第三方补充信息 | 点击提交跳转第三方即报名成功 | 跳转第三方/稍后补充 |
+
+### 报名失败分支
+
+| 失败原因 | 展示文案 | 后续操作 |
+| --- | --- | --- |
+| 不满足报名条件 | 根据后台配置展示 | 返回上页 |
+| 名额不足 | 很遗憾,该活动报名已满。 | 返回上页 |
+| 命中互斥规则 | 展示互斥活动名称和原因 | 返回我的活动或返回上页 |
+| 活动已结束 | 活动已结束,无法报名 | 返回上页 |
+| 系统异常 | 报名失败,请稍后重试 | 重新提交/返回 |
+
+* * *
+
+## 3.8 活动详情页 - 已报名/无需报名
+
+### 页面说明
+
+客户报名成功后,或者活动本身无需报名时,进入“已报名/无需报名”活动详情版本。
+
+### 展示内容
+
+1.  活动图片。
+2.  活动名称。
+3.  活动对象。
+4.  活动进行时间。
+5.  活动地点。
+6.  活动内容。
+7.  报名信息:展示客户报名时填写的补充信息。
+8.  达标查询/达标层级。
+9.  底部操作按钮。
+
+### 按钮规则
+
+| 活动配置 | 按钮文案 | 跳转目标 |
+| --- | --- | --- |
+| 无进度跟踪且不发奖 | 查看更多活动 | 全部活动列表页 |
+| 有进度跟踪 | 查看达标进度 | 活动达标进度页 |
+| 无进度跟踪直接发奖 | 查看权益 | 对应权益页 |
+
+### 字段说明
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 数据来源 | 展示规则 | 异常情况 |
+| --- | --- | --- | --- | --- | --- | --- |
+| 活动图片 | 详情页头图 | 图片  | 是   | 后台配置 | 顶部展示 | 加载失败默认图 |
+| 活动名称 | 活动标题 | 文本  | 是   | 后台配置 | 主标题展示 | 空值不可发布 |
+| 活动对象 | 面向客户范围 | 文本  | 是   | 后台配置 | 展示文案 | 与实际标签不一致时以后台文案展示 |
+| 活动进行时间 | 活动有效期 | 日期范围 | 是   | 后台配置 | 页面展示 | 影响进行中/已结束 |
+| 活动地点 | 线下活动地点或线上渠道 | 文本  | 否   | 后台配置 | 有则展示 | 为空隐藏 |
+| 活动内容 | 活动规则 | 富文本 | 是   | 后台配置 | 正文展示 | 支持图文链接 |
+| 报名信息 | 客户填写的补充信息 | 键值对 | 否   | 报名记录 | 已填写则展示 | 客户未补全时提示补充 |
+| 达标层级 | 奖励/达标说明 | 列表  | 否   | 后台配置 | 有则展示 | 超过配置上限不允许发布 |
+| 底部按钮 | 后续操作 | 按钮  | 是   | 活动配置计算 | 根据进度/奖励类型展示 | 目标页面缺失时按钮置灰或隐藏 |
+
+* * *
+
+## 3.9 活动达标进度页
+
+### 页面说明
+
+达标进度页用于展示客户在活动中的任务完成情况、达标层级、奖励发放状态。PPT 中涉及三类展示样式:
+
+1.  多个达标条件,多次发奖。
+2.  多个达标条件,单次发奖。
+3.  MGM 专属进度展示。
+
+### 展示规则
+
+1.  需要根据现有标签能力组合实现,所有用于达标判定的标签必须在标签列表内。
+2.  最多支持 5 个层级设置。
+3.  每个层级最多支持 5 个任务。
+4.  对客文字支持后台配置。
+5.  问号说明支持后台配置补充信息。
+6.  “点击对比”展示右侧弹窗,弹窗内容支持配置。
+7.  连续达标判定支持 X/Y/Z 配置:
+    -   X:活动观察周期,例如 12 个月。
+    -   Y:连续达标月份,例如 8 个月。
+    -   Z:达标条件,例如 Avg TRB、End of day balance 大于等于指定值。
+8.  仅连续达标条件可逐月展示进度,超过 3 个月右滑展示。
+9.  进行中、已发奖、已结束三种状态的提示文案支持配置。
+
+### 达标层级字段说明
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 数据来源 | 展示规则 | 异常情况 |
+| --- | --- | --- | --- | --- | --- | --- |
+| 层级名称 | 达标层级标题 | 文本  | 是   | 后台配置 | 如第 1 档/第 2 档 | 空值不可保存 |
+| 层级排序 | 层级展示顺序 | 数字  | 是   | 后台配置 | 从小到大展示 | 不允许重复 |
+| 任务列表 | 层级下的达标任务 | 列表  | 是   | 后台配置 + 标签计算 | 每层最多 5 个任务 | 超过不允许保存 |
+| 任务对客文案 | 客户看到的任务说明 | 文本  | 是   | 后台配置 | 页面展示 | 避免直接展示系统标签名 |
+| 任务标签 | 用于判定任务完成的标签 | 标签 ID | 是   | 标签系统 | 系统计算完成状态 | 标签不存在不可保存 |
+| 问号说明 | 任务解释说明 | 富文本 | 否   | 后台配置 | 点击问号展示 | 未配置不展示问号 |
+| 完成状态 | 已完成/未完成/计算中 | 枚举  | 是   | 系统计算 | 图标/文案展示 | 数据延迟时展示计算中 |
+| 奖励类型 | 达标后奖励类型 | 枚举  | 是   | 后台奖励配置 | 决定按钮文案和跳转 | 奖励缺失时不允许发布 |
+| 奖励状态 | 未发奖/待领取/已领取/已发放 | 枚举  | 是   | 奖励系统/模拟数据 | 决定按钮状态 | 状态异常时展示查看奖励 |
+
+### 奖励按钮规则
+
+| 奖励类型 | 未达标按钮 | 达标后按钮 | 跳转目标 |
+| --- | --- | --- | --- |
+| 积分  | 查看达标详情 | 查看积分奖励 | 积分明细页 |
+| 立减金 | 查看达标详情 | 前往领取/领取奖励 | 立减金列表 |
+| 卡券类权益 | 查看达标详情 | 查看奖励 | 权益详情页(已领取) |
+| 电子券类 | 查看达标详情 | 查看奖励 | 权益详情页(已领取) |
+| 信用卡权益 | 查看达标详情 | 查看奖励 | 信用卡权益详情页 |
+| 第三方发放权益 | 查看达标详情 | 查看奖励 | 权益详情页(已领取,第三方无需兑换) |
+
+### MGM 专属规则
+
+1.  未发奖时展示按钮“查看达标详情”。
+2.  客户点击后,对应的达标层级进度区域框线高亮展示。
+3.  下方“被推荐进度详情”仅当客户同时成为被推荐人和推荐人时展示。
+4.  如果当前被推荐人没有奖励,则被推荐人没有活动进度展示页面。
+5.  MGM 页面可包含:进行中 tab、未达标 tab、已达标 tab。
+
+* * *
+
+## 4\. 移动端活动互斥逻辑
+
+本章描述客户在移动端看到的互斥状态,以及后端在移动端访问、报名、次日数据修正时返回的固定判断结果。  
+这些内容属于系统固定业务逻辑,不作为后台下拉选项或运营开关配置。后台只负责配置互斥组、互斥对象和展示文案。
+
+## 4.1 互斥总规则
+
+### 业务说明
+
+部分新客活动之间存在互斥关系。客户一旦形成某个互斥组内的有效参与记录,在该活动达标周期内,不能再参与同组其他活动。只有当当前活动结束且客户未达标时,才释放客户报名资格,客户可以参与同组其他活动,且新活动的达标周期重新计算。
+
+### 固定判断逻辑
+
+1.  移动端不自行判断互斥,统一展示后端返回的活动状态、按钮状态和提示文案。
+2.  平台内 NTB 活动以权益平台报名成功时间作为有效参与时间;客户报名成功后,系统实时生成报名记录。
+3.  客户报名同一互斥组内第二个 NTB 活动时,后端实时查询客户是否已有同组有效报名记录;若已有记录,则拦截后报名活动。
+4.  同一互斥组内,默认保留最早形成的有效参与记录。
+5.  客户报名 A 活动后,在 A 活动达标周期内,不可报名同互斥组 B 活动。
+6.  A 活动结束后,若客户未达标,释放客户报名资格;客户报名 B 活动后,B 活动达标周期重新计算。
+7.  报名 NTB 新客活动时,需要校验客户开户状态;开户状态数据时效为 T-1。
+8.  若因 T-1 时效导致当天刚开户客户未被识别,第二天获取开户数据后,需要停止客户当前 NTB 活动的进度跟踪,并在移动端展示无法继续参与。
+9.  MGM 被推荐人标签通常 T+1 进入权益平台,因此存在当天未拦截、次日修正的特殊场景。
+10.  若 MGM 与 NTB 发生在不同日期,按有效参与日期早晚判断保留对象。
+11.  若 MGM 与 NTB 发生在同一日期,且系统无法获取精确先后时间,默认保留 MGM,关闭 NTB 活动资格。
+12.  客户报名后不可自行修改报名信息,但系统可预留人工代办能力。
+
+* * *
+
+## 4.2 互斥状态优先级
+
+当客户同时满足多个状态时,建议按以下优先级展示:
+
+1.  活动下线/不可见。
+2.  客户不在可见客群。
+3.  客户命中互斥规则。
+4.  客户不在报名客群。
+5.  活动名额已满。
+6.  不在报名时间内。
+7.  已报名。
+8.  可报名。
+
+原因:互斥关系会影响多个活动展示状态,应优先于普通报名条件,否则客户可能看到错误的“可报名”。
+
+* * *
+
+## 4.3 Case A:客户先报名非 MGM 新客活动,后被朋友推荐参与 MGM
+
+### 场景说明
+
+客户先在航司合作活动中报名 NTB 活动,暂未开户。1 周后,客户又被朋友推荐,完成 MGM 留资。
+
+### 被推荐人视角
+
+1.  客户扫描航司活动二维码。
+2.  进入航司合作新户活动页面。
+3.  填写航司会员号。
+4.  确认报名。
+5.  报名成功后,航司活动进入“已报名”。
+6.  客户之后被朋友推荐并填写 MGM 表单。
+7.  第二天权益平台收到客户为 MGM 被推荐人的标签。
+8.  系统判断客户已报名航司 NTB 活动,MGM 被推荐关系无法继续生效。
+9.  客户进入权益平台时:
+    -   航司活动展示已报名。
+    -   其他互斥新户活动展示不符合参与条件。
+    -   MGM 亲友推荐活动展示无法继续参与或不符合参与条件。
+
+### 推荐人视角
+
+推荐人查看 MGM 进度时,该被推荐人展示在未达标内,未达标原因:
+
+> 您的被推荐人已报名参与其他新客活动,根据活动规则无法再被推荐。
+
+* * *
+
+## 4.4 Case B:客户先被朋友推荐,之后想报名其他 NTB 活动
+
+### 场景一:MGM 表单填写第二天后再报名其他活动
+
+1.  客户先通过朋友推荐链接填写 MGM 表单。
+2.  第二天权益平台收到 MGM 被推荐人标签。
+3.  客户进入权益平台或合作方活动页查看其他 NTB 活动。
+4.  系统判断客户已参与 MGM,被拦截报名其他 NTB 活动。
+5.  互斥活动展示“不符合参与条件”。
+6.  合作方活动页报名时展示拦截文案。
+
+示例文案:
+
+> 尊敬的客户,您已经于 XXXX 年 X 月 X 日被您的推荐人推荐,参与【亲友推荐】活动。根据活动规则,您无法再继续报名【航司合作汇丰卓越理财新户达标礼】活动。
+
+### 场景二:MGM 表单填写当天又报名其他活动
+
+这是少数极限情况,因为 MGM 标签 T+1 才进入权益平台。
+
+1.  客户当天填写 MGM 表单。
+2.  当天又进入航司/酒店活动页。
+3.  因系统暂未收到 MGM 标签,客户可能报名成功。
+4.  第二天系统收到 MGM 标签后,需要停止客户当前报名的新客活动进度跟踪。
+5.  客户再次进入权益平台时,该新客活动展示“无法继续参与”或“不符合报名条件”。
+6.  其他互斥活动均展示不符合参与条件。
+
+* * *
+
+## 4.5 Case C:客户先报名航司 NTB 活动,之后报名酒店 NTB 活动
+
+### 场景说明
+
+客户先报名航司新客活动,当天又想报名酒店新客活动。两个活动属于同一互斥组,需要实时拦截。
+
+### 交互流程
+
+1.  客户扫描航司活动二维码。
+2.  填写航司会员号。
+3.  确认报名航司活动。
+4.  系统记录客户已报名航司活动。
+5.  同一天客户扫描酒店活动二维码。
+6.  点击报名酒店活动时,系统实时查询互斥报名记录。
+7.  命中互斥后,直接拦截。
+
+### 拦截文案
+
+> 尊敬的客户,您已经于 XXXX 年 X 月 X 日参与【航司合作汇丰卓越理财新户达标礼】活动。根据活动规则,您无法再继续报名【酒店合作汇丰卓越理财新户达标礼】活动。
+
+### 权益平台展示状态
+
+1.  航司活动:已报名。
+2.  酒店活动:不符合参与条件。
+3.  其他同互斥组活动:不符合参与条件。
+
+* * *
+
+## 5\. 后台配置说明(按菜单拆分)
+
+### 5.0 后台菜单设计原则
+
+本次后台不建议继续按“活动基础信息、报名规则、达标规则、互斥规则”这种模块堆叠方式写,因为这样容易出现一个问题:**后台字段看起来很多,但不知道哪个字段控制移动端哪个页面,也不知道哪些配置可以复用。**
+
+更合理的方式是按后台菜单拆分:
+
+```text
+客群管理 / 报名表单管理 / 达标规则管理 / 互斥规则管理 / 权益发放计划 / 通知模板 / 投放渠道管理
+        ↓
+活动管理中选择引用这些配置
+        ↓
+移动端根据活动配置结果展示首页、列表、详情、报名、进度、奖励和互斥提示
+```
+
+菜单拆分原则
+
+| 原则  | 说明  |
+| --- | --- |
+| 活动管理只做活动编排 | 活动管理是主菜单,用来创建一个活动,并把客群、报名表单、互斥组、达标规则、权益发放计划等配置关联起来。 |
+| 可复用配置单独成菜单 | 客群、补充信息表单、达标规则、互斥组、通知模板、投放渠道都可能被多个活动复用,应拆成独立菜单。 |
+| 活动创建时做选择引用 | 新建活动时不重复创建复杂规则,而是选择已有配置,减少字段冗余和维护成本。 |
+| 移动端展示必须能反推后台字段 | 每个后台字段都要说明影响移动端哪个页面、哪个按钮、哪个状态或哪个提示文案。 |
+| 列表页和编辑页分开描述 | 每个后台菜单都要明确:列表页查什么、展示什么、有哪些操作;编辑页配什么、如何校验、影响哪里。 |
+
+* * *
+
+### 5.1 后台菜单总览
+
+| 一级菜单 | 二级页面/功能 | 菜单定位 | 是否建议独立菜单 | 活动创建时如何使用 |
+| --- | --- | --- | --- | --- |
+| 活动管理 | 活动列表、新建活动、编辑活动、预览、审核、发布、下线 | 核心主菜单,负责活动整体编排 | 是   | 创建活动时填写基础信息,并选择其他菜单中的配置 |
+| 客群管理 | 客群列表、新建客群、客群预览 | 配置展示客群、报名客群、达标客群 | 是   | 活动中选择展示客群、报名客群;达标规则中选择达标客群 |
+| 报名表单管理 | 表单列表、新建补充信息表单 | 配置报名时需要收集的信息,如航司会员号 | 是   | 活动报名设置中选择补充信息表单 |
+| 达标规则管理 | 达标规则列表、新建达标规则、进度页预览 | 配置进度页样式、达标层级、任务条件、连续达标等 | 是   | 活动中选择是否启用进度跟踪,并关联达标规则 |
+| 活动互斥组管理 | 互斥组列表、新建互斥组、互斥文案配置 | 配置哪些活动互斥,以及命中互斥后的处理方式 | 是,建议新增 | 活动报名设置中选择互斥组 |
+| 权益发放计划管理 | 发放计划列表、新建发放计划 | 配置达标后发什么权益、发放方式、库存、通知 | 可放在权益管理下 | 活动或达标规则中选择对应发放计划 |
+| 通知模板管理 | 模板列表、新建短信模板 | 配置报名成功、活动开始前、达标发奖等通知模板 | 可放在内容管理下 | 活动通知设置中选择模板 |
+| 活动投放管理 | 投放列表、生成链接/二维码、配置 Banner 版位 | 配置活动在哪些渠道和版位投放 | 是,建议独立或放在活动管理下 | 选择活动后生成投放链接、渠道码、二维码、版位配置 |
+| 活动报表/进度查询 | 报名查询、达标查询、发放查询 | 运营查看报名、达标、发奖结果 | 是,偏查询菜单 | 不在活动创建中配置,只查询结果 |
+| 信用卡活动入口配置 | 入口配置、跳转地址配置 | 配置信用卡活动专区入口 | 可放在活动投放管理或系统配置下 | 控制移动端是否展示信用卡活动入口 |
+
+* * *
+
+### 5.2 菜单一:活动管理
+
+活动管理是整个活动模块的主菜单。它不应该承载所有复杂规则,而是负责创建活动、维护活动基础信息,并关联客群、报名表单、达标规则、权益发放计划、互斥组、通知模板和投放配置。
+
+5.2.1 活动管理 - 列表页
+
+页面说明
+
+活动列表用于运营查询、创建、编辑、预览、提交审核、审核、发布和下线活动。
+
+查询条件
+
+| 查询项 | 类型  | 是否必填 | 说明  |
+| --- | --- | --- | --- |
+| 活动 ID | 输入框 | 否   | 支持精确查询活动编号。 |
+| 活动名称 | 输入框 | 否   | 支持按对内标题或对客名称模糊查询。 |
+| 活动类型 | 下拉框 | 否   | 如普通活动、迎新礼、层层礼、MGM、信用卡活动等。 |
+| 活动状态 | 下拉框 | 否   | 草稿、待审核、审核通过、已发布、已下线、已结束、无效。 |
+| 审核状态 | 下拉框 | 否   | 未提交、待审核、审核通过、审核驳回。 |
+| 活动时间 | 日期范围 | 否   | 按活动开始/结束时间筛选。 |
+| 创建人 | 输入框 | 否   | 查询某个运营创建的活动。 |
+
+列表字段
+
+| 字段名称 | 字段含义 | 数据来源 | 展示规则 |
+| --- | --- | --- | --- |
+| 活动 ID | 活动唯一编号 | 系统生成 | 创建后不可编辑。 |
+| 活动标题(对内) | 后台识别用名称 | 新建活动填写 | 仅后台展示。 |
+| 活动名称(对客) | 移动端展示名称 | 新建活动填写 | 展示在首页活动模块、全部活动、我的活动、详情页。 |
+| 活动类型 | 活动分类 | 新建活动选择 | 用于后台筛选,也可影响互斥或报表归类。 |
+| 活动时间 | 活动进行时间 | 新建活动填写 | 移动端列表和详情页展示。 |
+| 报名时间 | 客户可报名时间 | 新建活动填写 | 影响报名按钮是否可点击。 |
+| 活动状态 | 当前活动生命周期状态 | 系统/运营操作 | 控制是否对客展示。 |
+| 审核状态 | 当前审核进度 | 审核流程 | 待审核活动可在列表置顶。 |
+| 创建人 | 创建活动的运营账号 | 系统记录 | 用于追溯。 |
+| 审核人 | 当前审核人 | 提交审核时选择 | 用于业务审核。 |
+| 创建时间 | 活动创建时间 | 系统记录 | 后台展示。 |
+| 更新时间 | 最近一次更新时间 | 系统记录 | 后台展示。 |
+| 操作  | 当前可执行动作 | 权限/状态判断 | 查看、编辑、预览、提交审核、审核、发布、下线。 |
+
+操作按钮
+
+| 操作  | 显示条件 | 点击后逻辑 |
+| --- | --- | --- |
+| 新建活动 | 有活动创建权限 | 进入活动新建页。 |
+| 查看  | 所有有查看权限角色 | 进入只读详情页。 |
+| 编辑  | 草稿、审核驳回、未发布状态 | 进入编辑页;已发布活动是否可编辑需确认。 |
+| 预览  | 活动已保存 | 生成移动端预览页;如果配置了渠道码,则预览链接需要携带追踪码。 |
+| 提交审核 | 活动已保存且必填项完整 | 弹出审核人选择,提交后状态变为待审核。 |
+| 审核  | 当前用户为审核人 | 审核通过或驳回。 |
+| 发布  | 审核通过且有发布权限 | 发布后移动端按展示规则对客展示。 |
+| 下线  | 已发布活动 | 下线后前端不再新增展示;已报名客户是否仍可在我的活动查看需确认。 |
+
+* * *
+
+5.2.2 活动管理 - 新建/编辑页
+
+活动新建/编辑页采用步骤器结构,当前后台 demo 分为 5 步:基础信息、展示规则、客群与报名、进度与奖励、互斥与通知。页面底部固定操作区,支持上一步、下一步、取消、保存草稿、提交审核。
+
+* * *
+
+Step 1:基础信息
+
+配置区一:基础信息
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 | 说明/校验 |
+| --- | --- | --- | --- | --- | --- |
+| 活动标题(对内) | 后台内部识别名称 | 文本  | 是   | 不对客展示 | 限制 1-50 字,用于后台列表、审核、报表识别。 |
+| 活动名称(对客) | 客户看到的活动名称 | 文本  | 是   | 首页、组装页、活动详情页 | 限制 1-50 字。 |
+| 活动类型 | 活动分类 | 下拉框 | 是   | 影响后续模板、进度配置和报表归类 | 可选:普通活动、层层礼、MGM 活动、航司 NTB 活动、酒店 NTB 活动、信用卡活动。 |
+| 活动对象 | 活动面向的业务对象 | 文本  | 否   | 详情页或内部识别 | 示例:借记卡新客、潜客/新客、推荐人与被推荐人。 |
+| 活动小图 | 列表卡片图 | 图片上传 | 是   | 权益首页、组装页、活动卡片 | 建议尺寸 780x438px,支持 JPG、PNG。 |
+| 活动头图 | 详情页顶部图片 | 图片上传 | 是   | 活动详情页顶部 | 建议尺寸 780x438px,支持 JPG、PNG。 |
+| 线下活动 | 是否为线下活动 | 开关  | 否   | 决定是否展示城市和地点 | 开启后必须配置活动城市和活动地点。 |
+| 活动城市 | 线下活动所在城市 | 文本  | 线下活动开启时是 | 活动详情页展示 | 示例:上海市。 |
+| 活动地点 | 线下活动详细地址 | 文本  | 线下活动开启时是 | 活动详情页展示 | 线上活动可为空。 |
+| 活动内容 | 活动规则正文 | 富文本 | 是   | 活动详情页“活动内容”区域 | 支持加粗、列表、链接、图片等富文本能力;链接需符合白名单要求。 |
+
+活动类型为“层层礼”时,基础信息下方展示“活动达标层级内容”配置。
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 | 说明/校验 |
+| --- | --- | --- | --- | --- | --- |
+| 活动达标层级内容 | 层层礼详情页分层说明 | 分组富文本 | 层层礼必填 | 活动详情页层级 Tab 内容 | 最多配置 5 个层级,至少 1 个层级。 |
+| Tab 标题 | 单个层级的 Tab 名称 | 文本  | 是   | 活动详情页层级 Tab 名称 | 默认层级一、层级二等,可编辑。 |
+| 层级内容 | 单个层级的说明内容 | 富文本 | 是   | 活动详情页对应层级内容 | 用于配置达标要求、奖励内容、注意事项等。 |
+
+配置区二:分享设置
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 | 说明/校验 |
+| --- | --- | --- | --- | --- | --- |
+| 自定义微信分享 | 是否自定义分享卡片 | 勾选框 | 否   | 微信分享卡片 | 勾选后展示分享名称、分享图片配置。 |
+| 分享名称 | 微信分享卡片标题 | 文本  | 开启自定义分享时是 | 微信分享卡片 | 限制 1-50 字。 |
+| 分享图片 | 微信分享卡片图片 | 图片上传 | 开启自定义分享时是 | 微信分享卡片 | 建议尺寸 100x100px,支持 JPG、PNG。 |
+
+配置区三:其他设置
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 | 说明/校验 |
+| --- | --- | --- | --- | --- | --- |
+| CD staff ID | 活动负责人或同步字段 | 文本  | 否   | 不对客展示 | 用于 iHub/SCRM 同步、归属或后续运营识别。 |
+| SCRM activity code | SCRM 活动编码 | 文本  | 否   | 不对客展示 | 用于同步或报表归因。 |
+| SCRM channel code | SCRM 渠道编码 | 文本  | 否   | 不对客展示 | 用于渠道归因。 |
+| Label | 活动标签 | 多选/可新建 | 否   | 可能影响兴趣标签或报表 | 支持从已有标签中选择,也支持录入新标签。 |
+
+* * *
+
+Step 2:展示规则
+
+配置区一:时间配置
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 | 说明/校验 |
+| --- | --- | --- | --- | --- | --- |
+| 活动时间 | 活动正式进行时间 | 日期范围 | 是   | 列表/详情展示,影响活动状态 | 开始日期必须早于或等于结束日期。 |
+| 展示时间 | 活动对客展示时间 | 日期范围 | 否   | 控制入口是否展示 | 不配置时按发布状态和活动状态展示。 |
+
+配置区二:展示客群
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 | 说明/校验 |
+| --- | --- | --- | --- | --- | --- |
+| 展示客群 | 哪些客户能看到活动 | 客群多选 | 否   | 决定活动入口是否可见 | 数据来自人群管理;不选代表全部用户展示,全部展示不包括潜客。 |
+
+配置区三:入口展示
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 | 说明/校验 |
+| --- | --- | --- | --- | --- | --- |
+| 权益首页 | 是否在权益首页展示 | 单选:显示/不显示 | 是   | 权益首页活动入口 | 还需同时满足展示时间、展示客群、活动状态。 |
+| 组装页 | 是否在组装页展示 | 单选:显示/不显示 | 是   | 指定组装页中的活动模块 | 选择“显示”后必须选择至少一个组装页。 |
+| 选择组装页 | 指定投放的组装页 | 多选  | 组装页显示时是 | 影响哪些组装页展示该活动 | 数据来自组装页列表,展示名称和页面 ID。 |
+| 活动轮播图 | 是否进入活动轮播图 | 单选:显示/不显示 | 是   | 活动轮播入口 | 需结合轮播图素材和排序规则。 |
+
+* * *
+
+Step 3:客群与报名
+
+配置区一:报名设置
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 | 说明/校验 |
+| --- | --- | --- | --- | --- | --- |
+| 需要报名 | 活动是否需要客户报名 | 勾选框 | 是   | 决定详情页按钮和报名链路 | 未勾选时不展示下方报名配置。 |
+| 报名客群 | 哪些客户可以报名 | 客群多选 | 需要报名时是 | 决定详情页展示“报名”还是“不符合报名条件” | 数据来自人群管理。 |
+| 报名时间 | 客户可报名时间 | 日期范围 | 需要报名时是 | 控制报名按钮是否可点击 | 结束日期必须晚于或等于开始日期。 |
+
+配置区二:报名确认
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 | 说明/校验 |
+| --- | --- | --- | --- | --- | --- |
+| 报名确认 | 报名前是否展示确认 | 勾选框 | 否   | 点击报名后是否进入确认页 | 开启后展示报名确认文案配置。 |
+| 报名确认文案 | 确认页提示文字 | 文本  | 开启报名确认时是 | 报名确认页展示 | 默认文案:请确认您已了解活动规则,报名成功后将进入达标进度跟踪。 |
+
+配置区三:补充信息表单
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 | 说明/校验 |
+| --- | --- | --- | --- | --- | --- |
+| 补充信息 | 是否收集报名补充信息 | 勾选框 | 否   | 报名链路是否展示补充信息 | 开启后至少配置 1 项,最多配置 2 项。 |
+| 字段名称 | 补充信息字段名称 | 文本  | 开启补充信息时是 | 补充信息页字段标题 | 示例:航司会员号、酒店会员号。 |
+| 提示文案 | 输入框占位提示 | 文本  | 开启补充信息时是 | 补充信息页输入提示 | 示例:请输入航司会员号。 |
+| 字段类型 | 字段输入类型 | 下拉框 | 开启补充信息时是 | 控制输入格式 | 可选:文本、数字。 |
+| 是否必填 | 客户是否必须填写 | 开关  | 开启补充信息时是 | 控制提交校验 | 必填时为空不可提交。 |
+| 最大长度 | 输入最大字符数 | 数字  | 开启补充信息时是 | 控制输入限制 | 范围 1-500。 |
+| 第三方跳转链接 | 报名后跳转第三方页面 | URL | 否   | 报名提交后跳转 | 仅允许配置白名单域名。 |
+
+配置区四:名额控制
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 | 说明/校验 |
+| --- | --- | --- | --- | --- | --- |
+| 名额控制 | 是否限制报名人数 | 勾选框 | 否   | 满额后报名失败或按钮置灰 | 开启后展示报名人数上限、展示报名人数。 |
+| 报名人数上限 | 最大报名人数 | 数字  | 开启名额控制时是 | 名额满时不可报名 | 编辑态不能小于已报名人数。 |
+| 展示报名人数 | 是否在前端展示报名人数 | 开关  | 否   | 活动详情页报名人数区域 | 仅控制展示,不影响报名计数。 |
+| 已报名人数 | 当前已报名人数 | 数字,只读 | 编辑态展示 | 不直接对客展示 | 编辑活动时展示,不允许手工修改。 |
+
+配置区五:报名成功页配置
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 | 说明/校验 |
+| --- | --- | --- | --- | --- | --- |
+| 报名成功页配置 | 是否自定义报名成功页 | 勾选框 | 否   | 报名成功页展示 | 开启后标题必填。 |
+| 标题  | 报名成功页标题 | 文本  | 开启报名成功页配置时是 | 报名成功页主标题 | 限制 1-30 字。 |
+| 描述  | 报名成功页说明 | 多行文本 | 否   | 报名成功页描述 | 限制 200 字。 |
+
+* * *
+
+Step 4:进度与奖励
+
+该步骤按活动模板配置达标进度与奖励。活动模板可选:NTB(新客达标)、MGM(推荐有礼)、层层礼。
+
+配置区一:活动模板
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 | 说明/校验 |
+| --- | --- | --- | --- | --- | --- |
+| 活动模板 | 当前活动的达标模板 | 下拉框 | 是   | 决定达标进度页样式和配置项 | 可选:NTB(新客达标)、MGM(推荐有礼)、层层礼。 |
+| 层级数量 | 层层礼层级数量 | 数字  | 层层礼必填 | 决定层层礼进度页层级数 | 范围 1-5。 |
+
+NTB(新客达标)模板配置
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 | 说明/校验 |
+| --- | --- | --- | --- | --- | --- |
+| 未达标提示 | 未达标状态话术 | 文本  | 否   | 进度页状态提示 | 示例:您已报名本活动,请继续保持账户状态。 |
+| 已达标提示 | 已达标状态话术 | 文本  | 否   | 进度页状态提示 | 示例:您已达标活动规则,可点击下方按钮查看奖励。 |
+| 达标进度提示 | 问号弹窗说明 | 文本  | 否   | 进度页提示弹窗 | 用于解释进度统计口径。 |
+| 开户时间标签 | 开户时间判断规则 | 日期范围规则 | 否   | 进度标签展示 | 最多 1 个,默认字段为开户时间,判断条件为介于。 |
+| 进度标签 | 达标规则标签 | 规则组 | 是   | 进度页任务标签 | 支持添加多个进度标签。 |
+| 奖励配置 | 达标后的奖励 | 奖励配置组件 | 是   | 达标后奖励入口 | 选择奖励类型、发放计划、有效期、赠送次数等。 |
+
+MGM(推荐有礼)模板配置
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 | 说明/校验 |
+| --- | --- | --- | --- | --- | --- |
+| 奖励配置 | 推荐达标后的奖励 | 奖励配置组件 | 是   | 推荐人/被推荐人奖励入口 | 按发放计划配置。 |
+| 已达标展示标签 | MGM 达标状态判断标签 | 下拉框 | 是   | 进度页达标状态 | 当前 demo 支持客户 MSC Group。 |
+| 进度标签 | MGM 达标规则标签 | 规则组 | 是   | MGM 进度页标签 | 支持开户时间、TRB 值等规则。 |
+| 规则标签1描述 | 被推荐进度第一项描述 | 文本  | 是   | 被推荐人进度展示 | 默认开户时间。 |
+| 规则标签2描述 | 被推荐进度第二项描述 | 文本  | 是   | 被推荐人进度展示 | 默认基础资产达标文案。 |
+| 是否有进阶奖励 | 是否配置进阶奖励 | 下拉框:是/否 | 是   | 决定是否展示进阶奖励规则 | 选择是后展示进阶奖励规则配置。 |
+| 进阶奖励规则标签 | 进阶奖励判断规则 | 规则配置 | 进阶奖励开启时是 | 进阶奖励展示 | 默认 TRB 值(含家庭账户)大于等于 6000000。 |
+| 进阶奖励规则标签描述 | 进阶奖励规则说明 | 文本  | 进阶奖励开启时是 | 进阶奖励说明 | 示例:是否完成尊尚升级。 |
+
+层层礼模板配置
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 | 说明/校验 |
+| --- | --- | --- | --- | --- | --- |
+| 可参与人群 | 当前层级可参与客户 | 客群选择 | 否   | 决定该层级可参与范围 | 数据来自人群管理。 |
+| 层级标题 | 当前层级名称 | 文本  | 是   | 层级 Tab 或层级标题 | 示例:卓越层级礼·层级1(月日均≥50万)。 |
+| 活动标题 | 层级内单个活动标题 | 文本  | 是   | 层级内活动卡片/任务标题 | 每个层级可添加多个活动。 |
+| 活动描述 | 层级内单个活动描述 | 文本  | 否   | 活动说明 | 示例:完成以下任务获取积分。 |
+| 进度标签 | 层级内活动达标规则 | 规则组 | 是   | 层级活动任务标签 | 支持添加多个进度标签。 |
+| 达标发放计划 | 当前活动达标后的发放计划 | 奖励配置组件 | 是   | 达标后奖励入口 | 每个层级活动独立配置奖励。 |
+
+进度标签通用规则字段
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 说明/校验 |
+| --- | --- | --- | --- | --- |
+| 规则标签 | 判断字段 | 下拉框 | 是   | 可选:开户时间、TRB 值(含家庭账户)、客户 MSC Group、境外消费。 |
+| 判断条件 | 判断操作符 | 下拉框 | 是   | 可选:介于、大于等于、小于等于。 |
+| 规则值 | 判断值 | 数字/日期范围 | 视规则而定 | 开户时间通常为日期范围,TRB 和消费类通常为数字。 |
+| 开户当月必须达标 | 是否要求开户当月达标 | 开关  | 否   | 适用于非开户时间类标签。 |
+| 连续达标 | 是否要求连续达标 | 开关  | 否   | 开启后配置连续月份。 |
+| 连续月份 | 连续达标月份数 | 数字  | 连续达标开启时是 | 范围按业务规则限制。 |
+| 允许断开 | 连续达标是否允许中断 | 开关  | 否   | 用于连续达标容错。 |
+| 规则标题/标签描述 | 前端展示标题 | 文本  | 是   | 进度页标签标题。 |
+| 规则描述/未达标提示 | 前端展示说明 | 文本  | 是   | 进度页标签说明。 |
+| 标签进度提示 | 问号弹窗说明 | 文本  | 否   | 用于解释该标签的计算口径。 |
+| 不达标原因 | MGM 不达标原因 | 文本  | MGM 规则必填 | MGM 失败原因展示。 |
+
+* * *
+
+Step 5:互斥与通知
+
+配置区一:互斥设置
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 | 说明/校验 |
+| --- | --- | --- | --- | --- | --- |
+| 参与互斥 | 当前活动是否参与互斥 | 勾选框 | 否   | 影响报名拦截和活动可参与状态 | 开启后必须选择互斥组。 |
+| 互斥组 | 当前活动所属互斥组 | 下拉框 | 参与互斥时是 | 命中互斥时展示不符合参与条件或报名拦截 | 数据来自活动互斥组管理。 |
+
+配置区二:通知设置
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 | 说明/校验 |
+| --- | --- | --- | --- | --- | --- |
+| 报名成功模板 | 报名成功后发送的通知模板 | 选择项 | 否   | 不影响页面 | 数据来自通知模板管理。 |
+| 开始前提醒模板 | 活动开始前发送的提醒模板 | 选择项 | 否   | 不影响页面 | 需结合发送任务触发时间。 |
+| 达标发奖通知模板(需领取) | 达标后需客户领取时的通知模板 | 选择项 | 否   | 不影响页面 | 适用于奖励需跳转领取的场景。 |
+| 达标发奖通知模板(无需领取) | 达标后系统自动发放时的通知模板 | 选择项 | 否   | 不影响页面 | 适用于无需客户领取的奖励。 |
+
+底部操作
+
+| 操作  | 展示规则 | 说明  |
+| --- | --- | --- |
+| 上一步 | 非第一步展示 | 返回上一步配置。 |
+| 下一步 | 非最后一步展示 | 进入下一步配置。 |
+| 取消  | 全步骤展示 | 放弃当前编辑并返回活动列表。 |
+| 保存草稿 | 全步骤展示 | 保存为草稿,不提交审核。 |
+| 提交审核 | 最后一步展示 | 保存并将活动状态置为待审核。 |
+
+保存与提交校验
+
+| 校验项 | 触发场景 | 校验规则 |
+| --- | --- | --- |
+| 基础必填 | 保存草稿、提交审核 | 活动标题(对内)、活动名称(对客)、活动类型、活动小图、活动头图、活动内容、活动时间必填。 |
+| 活动时间 | 保存草稿、提交审核 | 活动结束日期必须晚于或等于活动开始日期。 |
+| 报名时间 | 需要报名时 | 报名时间必填,报名结束日期必须晚于或等于报名开始日期。 |
+| 报名成功页 | 开启报名成功页配置时 | 标题必填。 |
+| 补充信息 | 开启补充信息时 | 至少 1 项、最多 2 项;字段名称、提示文案、字段类型、最大长度必填。 |
+| 名额控制 | 开启名额控制时 | 报名人数上限不能小于已报名人数。 |
+| 进度与奖励 | 配置进度与奖励时 | 必须选择活动模板,并完整配置对应模板下必填的规则和奖励。 |
+| 层层礼奖励 | 活动模板为层层礼时 | 每个层级活动必须配置规则和达标发放计划。 |
+| 互斥设置 | 开启参与互斥时 | 必须选择互斥组。 |
+
+* * *
+
+### 5.3 菜单二:客群管理
+
+客群管理用于配置“哪些客户能看到活动、哪些客户能报名、哪些客户算达标”。它不应该散落在活动管理里重复配置。
+
+5.3.1 客群管理 - 列表页
+
+| 字段名称 | 字段含义 | 数据来源 | 说明  |
+| --- | --- | --- | --- |
+| 客群编号 | 客群唯一编号 | 系统生成 | 活动配置时引用。 |
+| 客群名称 | 客群名称 | 运营填写 | 如“Premier 新客”“MGM 被推荐人”。 |
+| 客群描述 | 客群用途说明 | 运营填写 | 帮助运营区分不同客群。 |
+| 创建人 | 创建账号 | 系统记录 | Excel 中提到仅创建人可编辑。 |
+| 最后更新时间 | 最近更新时间 | 系统记录 | 后台展示。 |
+| 操作  | 编辑/查看/预览名单 | 权限控制 | 人群预览可放在查看页。 |
+
+5.3.2 客群管理 - 新建/编辑页
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 说明  |
+| --- | --- | --- | --- | --- |
+| 客群名称 | 客群名称 | 文本  | 是   | 供活动管理选择。 |
+| 客群描述 | 客群说明 | 多行文本 | 否   | 说明该客群适用活动。 |
+| 标签组合 | 选择标签并配置条件 | 标签+条件 | 是   | 可选择多个标签组合。 |
+| 操作符 | 标签判断方式 | 下拉框 | 是   | 等于、大于、小于、包含等。 |
+| 规则值 | 标签判断值 | 输入/枚举 | 是   | 根据标签类型决定。 |
+| 排序  | 返回名单排序 | 升序/降序 | 否   | 是否实际需要传接口需确认。 |
+| Sizing | 最大圈选数量 | 数字  | 否   | 控制最大圈选客户数量。 |
+| 白名单/黑名单 | 指定包含或排除客户 | 文件上传/选择 | 否   | 可与标签圈选结果做交集或排除。 |
+| 人群预览 | 预览 customer id | 查询结果 | 否   | 供运营 sample check,非实时可能准实时。 |
+
+与移动端的关系
+
+| 使用位置 | 移动端影响 |
+| --- | --- |
+| 活动展示客群 | 决定客户是否能在首页/全部活动看到活动。 |
+| 活动报名客群 | 决定详情页是否可点击报名。 |
+| 活动达标客群 | 决定客户在进度页是否显示达标。 |
+| 互斥特殊客群 | 可用于 MGM 被推荐人、NTB 新客等特殊判断。 |
+
+* * *
+
+### 5.4 菜单三:报名表单管理/补充信息表
+
+报名表单管理用于配置报名时需要额外收集的信息,例如航司会员号、酒店会员号。它应该作为独立菜单,由活动管理引用。
+
+5.4.1 报名表单管理 - 列表页
+
+| 字段名称 | 字段含义 | 说明  |
+| --- | --- | --- |
+| 表单 ID | 表单唯一编号 | 系统生成。 |
+| 表单名称 | 表单内部名称 | 如“航司会员号收集表”。 |
+| 标题  | 对客展示标题 | 如“报名需要您提供 IHG 会员号”。 |
+| 创建人 | 创建账号 | 系统记录。 |
+| 更新时间 | 最近更新时间 | 系统记录。 |
+| 操作  | 编辑/查看 | 被活动引用后是否允许编辑需确认。 |
+
+5.4.2 报名表单管理 - 新建/编辑页
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 |
+| --- | --- | --- | --- | --- |
+| 表单名称 | 后台识别名称 | 文本  | 是   | 不对客展示。 |
+| 表单标题 | 对客标题 | 文本  | 是   | 补充信息页/弹窗顶部展示。 |
+| 收集字段名称 1 | 第一个字段名称 | 文本  | 是   | 输入框标题,如“航司会员号”。 |
+| 字段 1 输入提示 | 输入框 placeholder/说明 | 文本  | 否   | 指导客户填写。 |
+| 字段 1 校验规则 | 文本/数字/字母数字/长度 | 规则配置 | 否   | 控制提交校验。 |
+| 收集字段名称 2 | 第二个字段名称 | 文本  | 否   | PPT 写最多收集 2 个字段。 |
+| 字段 2 输入提示 | 输入提示 | 文本  | 否   | 同上。 |
+| 字段 2 校验规则 | 校验规则 | 规则配置 | 否   | 同上。 |
+| 提交按钮文案 | 按钮文字 | 文本  | 否   | 默认“提交”。 |
+
+需要确认
+
+| 问题  | 原因  |
+| --- | --- |
+| 留资表和补充信息表是否是两个概念 | Excel 中同时出现“是否留资/关联留资表”和“补充信息表”,需要确认留资是否是外部 AO 表单,补充信息是否只是会员号等轻量字段。 |
+| 关闭补充信息弹窗是否算报名成功 | PPT 中提到客户点击取消或关闭仍判断报名成功,但下次进入还要弹窗,需确认是否作为通用规则。 |
+| 第三方跳转后是否需要回调 | 如果点击提交跳第三方就算报名成功,需要确认是否不校验第三方是否真的填写完成。 |
+
+* * *
+
+### 5.5 菜单四:达标规则管理
+
+达标规则管理用于配置移动端“达标进度页”的展示样式、达标层级、任务条件、连续达标逻辑和状态文案。建议独立菜单,不要全部塞进活动管理。
+
+5.5.1 达标规则管理 - 列表页
+
+| 字段名称 | 字段含义 | 说明  |
+| --- | --- | --- |
+| 规则 ID | 达标规则编号 | 系统生成。 |
+| 规则名称 | 规则名称 | 如“卓越新户 12 个月连续达标规则”。 |
+| 进度样式 | 进度页模板 | 多条件多次发奖、多条件单次发奖、MGM 专属。 |
+| 层级数量 | 达标层级数 | PPT 中出现最多 4 层/5 层口径不一致,需确认。 |
+| 关联活动数 | 当前规则被多少活动引用 | 防止误改影响多个活动。 |
+| 状态  | 启用/停用 | 停用后不可被新活动选择。 |
+| 更新时间 | 最近更新时间 | 系统记录。 |
+| 操作  | 查看/编辑/复制/预览 | 支持复制已有规则快速创建。 |
+
+5.5.2 达标规则管理 - 新建/编辑页
+
+| 配置区 | 字段名称 | 字段含义 | 移动端影响 |
+| --- | --- | --- | --- |
+| 基础信息 | 规则名称 | 后台识别名称 | 不对客展示。 |
+| 基础信息 | 进度展示样式 | 选择进度页模板 | 决定进度页布局。 |
+| 基础信息 | 是否适用 MGM | 是否使用 MGM 推荐/被推荐人样式 | 决定是否展示推荐人/被推荐人进度。 |
+| 提示区 | 进行中文案 | 活动进行中提示 | 进度页顶部提示区。 |
+| 提示区 | 已发奖文案 | 奖励发放后提示 | 进度页顶部提示区。 |
+| 提示区 | 已结束文案 | 活动结束后提示 | 进度页顶部提示区。 |
+| 达标层级 | 层级名称 | 如“达标 50 万档” | 进度页层级标题。 |
+| 达标层级 | 层级对客文案 | 客户看到的说明 | 进度页展示。 |
+| 达标层级 | 层级排序 | 展示顺序 | 控制前端排列。 |
+| 任务条件 | 任务名称 | 如“完成风险问卷” | 进度页任务项。 |
+| 任务条件 | 任务标签/数据口径 | 用于系统判断达标 | 前端不展示标签 ID,只展示文案。 |
+| 任务条件 | 条件表达式 | 大于/等于/包含/连续达标等 | 影响达标计算结果。 |
+| 任务条件 | 问号说明 | 补充解释 | 点击问号展示弹窗。 |
+| 对比弹窗 | 点击对比内容 | 解释当前条件和目标差距 | 点击对比展示右侧弹窗。 |
+| 连续达标 | 观察周期 X | 观察多少个月 | 进度页可按月展示。 |
+| 连续达标 | 连续达标月份 Y | 需要连续达标多少个月 | 决定是否达标。 |
+| 连续达标 | 达标条件 Z | TRB 等具体条件 | 决定达标结果。 |
+| 移动端预览 | 预览按钮 | 根据配置生成进度页预览 | 运营确认样式。 |
+
+与活动管理的关系
+
+活动管理中只需要配置:
+
+| 活动管理字段 | 说明  |
+| --- | --- |
+| 是否有进度跟踪 | 开启后才显示达标规则选择。 |
+| 选择达标规则 | 从达标规则管理中选择一个规则。 |
+| 选择权益发放计划 | 规则达标后发什么奖励。 |
+
+* * *
+
+### 5.6 菜单五:活动互斥组管理
+
+活动互斥组管理用于维护“哪些活动或 MGM 推荐关系属于同一组互斥”,以及命中互斥后移动端展示哪些文案。  
+该菜单只保留真正需要运营配置的内容;PPT 已经明确的固定判断逻辑,统一在第 4 章“移动端活动互斥逻辑”中体现。
+
+* * *
+
+5.6.1 活动互斥组管理 - 列表页
+
+页面说明
+
+互斥组列表用于运营查看和维护所有互斥组。每个互斥组代表一组互斥对象,例如平台内 NTB 活动与 MGM 推荐关系。
+
+查询条件
+
+| 查询项 | 类型  | 说明  |
+| --- | --- | --- |
+| 互斥组名称 | 输入框 | 支持按互斥组名称模糊查询。 |
+| 互斥组状态 | 下拉框 | 启用、停用。 |
+| 对象角色 | 下拉框 | NTB 活动、MGM 被推荐人。 |
+| 创建时间 | 日期范围 | 查询某个时间段创建的互斥组。 |
+
+列表字段
+
+| 字段名称 | 字段含义 | 说明  |
+| --- | --- | --- |
+| 互斥组 ID | 互斥组唯一编号 | 系统生成,不可编辑。 |
+| 互斥组名称 | 后台识别名称 | 活动创建时通过该名称选择互斥组。 |
+| 互斥组说明 | 适用范围说明 | 用于说明该组适用哪些新客活动或 MGM 推荐关系。 |
+| 互斥对象数 | 当前组内配置的对象数量 | 包括平台活动、MGM 推荐关系等。 |
+| 关联活动数 | 当前被多少活动引用 | 防止误改影响已发布活动。 |
+| 状态  | 启用/停用 | 停用后不允许新活动选择。 |
+| 更新时间 | 最近更新时间 | 用于审计和问题追溯。 |
+| 操作  | 查看、编辑、复制、停用 | 支持维护互斥对象和文案。 |
+
+操作规则
+
+| 操作  | 规则  |
+| --- | --- |
+| 新建  | 创建新的互斥组。 |
+| 查看  | 查看互斥组基础信息、互斥对象、关联活动和文案。 |
+| 编辑  | 可维护互斥组名称、说明、对象和文案;已被发布活动引用时,应提示可能影响线上活动。 |
+| 复制  | 复制已有互斥组生成新规则,适合新增相似活动。 |
+| 停用  | 停用后不可被新活动选择;已发布活动是否继续生效按系统规则处理。 |
+
+* * *
+
+5.6.2 活动互斥组管理 - 新建/编辑页
+
+配置区一:基础信息
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 说明  |
+| --- | --- | --- | --- | --- |
+| 互斥组名称 | 后台识别名称 | 文本  | 是   | 例如“新客活动互斥组”。 |
+| 互斥组说明 | 适用范围说明 | 多行文本 | 否   | 说明该互斥组适用于哪些活动或 MGM 推荐关系。 |
+| 互斥组状态 | 启用/停用 | 开关  | 是   | 启用后可被活动选择;停用后新活动不可选择。 |
+
+配置区二:互斥对象配置
+
+互斥对象用于说明“哪些活动或 MGM 推荐关系属于同一组互斥”。  
+同一互斥组内的对象之间互相排斥。
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 说明  |
+| --- | --- | --- | --- | --- |
+| 对象类型 | 当前互斥对象的来源类型 | 下拉选择 | 是   | 权益平台活动 / MGM 推荐关系。 |
+| 对象名称 | 具体对象名称 | 选择项 | 是   | 对象类型为权益平台活动时,从活动管理中选择;对象类型为 MGM 推荐关系时,选择 MGM 相关来源。 |
+| 对象角色 | 对象在互斥组中的业务角色 | 下拉选择 | 是   | NTB 活动 / MGM 被推荐人。 |
+| 排序  | 后台展示顺序 | 数字  | 否   | 仅影响后台展示,不影响互斥判断。 |
+
+对象类型说明
+
+| 对象类型 | 适用场景 | 说明  |
+| --- | --- | --- |
+| 权益平台活动 | 在权益平台活动管理中创建,并由权益平台完成报名的活动,如航司 NTB、酒店 NTB | 运营需要把这些活动加入同一个互斥组。 |
+| MGM 推荐关系 | 客户通过朋友推荐链路形成的 MGM 被推荐关系 | 用于让 MGM 与同组 NTB 活动参与互斥。 |
+
+配置区三:前端展示文案
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 展示位置 |
+| --- | --- | --- | --- | --- |
+| 列表状态文案 | 命中互斥后活动卡片上的状态 | 文本  | 是   | 首页活动模块、全部活动列表。 |
+| 详情页提示文案 | 详情页不可参与说明 | 富文本 | 是   | 活动详情页。 |
+| 报名拦截文案 | 客户点击报名被拦截时展示 | 富文本 | 是   | 报名失败页/弹窗。 |
+| 无法继续参与文案 | T+1/T-1 修正后展示 | 富文本 | 是   | 我的活动、活动详情页。 |
+| 推荐人侧未达标原因 | 推荐人查看被推荐人进度时展示 | 富文本 | MGM 场景必填 | MGM 推荐人进度页。 |
+
+建议支持的文案变量:
+
+| 变量  | 含义  | 示例  |
+| --- | --- | --- |
+| `{已参与活动名称}` | 客户已经参与或被推荐的活动 | 航司合作汇丰卓越理财新户达标礼 |
+| `{当前活动名称}` | 客户当前想报名的活动 | 酒店合作汇丰卓越理财新户达标礼 |
+| `{参与日期}` | 客户报名或被推荐日期 | 2026/01/01 |
+| `{推荐日期}` | MGM 推荐日期 | 2026/01/01 |
+
+* * *
+
+5.6.3 活动管理中如何引用互斥组
+
+在“活动管理 - 新建/编辑页 - 客群与报名”中仅保留简化配置。
+
+| 字段名称 | 类型  | 是否必填 | 配置逻辑 | 移动端影响 |
+| --- | --- | --- | --- | --- |
+| 互斥组 | 下拉选择 | 否   | 选择互斥组即表示当前活动参与该组互斥;未选择则不参与互斥。 | 命中互斥后,列表、详情、报名页展示互斥状态和文案。 |
+| 当前活动互斥角色 | 下拉选择 | 选择互斥组后必填 | NTB 活动 / MGM 活动。 | 决定文案变量和互斥对象归类。 |
+| 活动级文案覆盖 | 富文本 | 否   | 默认取互斥组文案,特殊活动可覆盖。 | 点击报名被拦截时展示。 |
+
+不再单独配置“是否参与互斥”开关。  
+原因是:
+
+```text
+选择互斥组 = 参与互斥
+不选择互斥组 = 不参与互斥
+```
+
+这样可以避免“开关开启但未选互斥组”或“选了互斥组但开关关闭”的配置矛盾。
+
+* * *
+
+5.6.4 后台配置示例
+
+示例:新客活动互斥组
+
+基础信息:
+
+```text
+互斥组名称:新客活动互斥组
+互斥组说明:用于控制平台内 NTB 活动与 MGM 亲友推荐之间的互斥关系。
+互斥组状态:启用
+```
+
+互斥对象:
+
+| 对象名称 | 对象类型 | 对象角色 |
+| --- | --- | --- |
+| 航司 NTB 活动 | 权益平台活动 | NTB 活动 |
+| 酒店 NTB 活动 | 权益平台活动 | NTB 活动 |
+| MGM 亲友推荐 | MGM 推荐关系 | MGM 被推荐人 |
+
+文案配置:
+
+```text
+列表状态文案:不符合参与条件
+
+报名拦截文案:
+尊敬的客户,您已经于 {参与日期} 参与【{已参与活动名称}】活动。
+根据活动规则,您无法再继续报名【{当前活动名称}】活动。
+
+无法继续参与文案:
+尊敬的客户,由于您已参与同组新客活动,根据活动规则,您无法继续参与【{当前活动名称}】活动。
+
+推荐人侧未达标原因:
+您的被推荐人已报名参与其他新客活动,根据活动规则无法再被推荐。
+```
+
+* * *
+
+5.6.5 与移动端互斥逻辑的关系
+
+活动互斥组管理只提供互斥对象和文案配置,不承载固定判断规则。  
+固定判断规则统一由第 4 章“移动端活动互斥逻辑”描述,并由后端在以下节点执行:
+
+| 节点  | 说明  |
+| --- | --- |
+| 活动列表查询 | 返回客户对每个活动的展示状态。 |
+| 活动详情查询 | 返回客户是否可报名,以及不可报名原因。 |
+| 报名前校验 | 提交报名之前再次校验互斥。 |
+| 报名提交 | 防止并发或多端重复报名。 |
+| T+1/T-1 数据修正 | 根据 MGM 标签或开户状态更新客户活动资格。 |
+
+### 5.7 菜单六:权益发放计划管理
+
+权益发放计划可放在“权益管理”下,不建议直接堆在活动管理中。活动只选择发放计划,具体发什么、发多少、怎么发,由发放计划配置。
+
+5.7.1 发放计划列表页
+
+| 字段名称 | 字段含义 | 说明  |
+| --- | --- | --- |
+| 发放计划编号 | 计划唯一 ID | 活动或达标规则引用。 |
+| 发放计划名称 | 计划名称 | 如“达标 50 万发 500 积分”。 |
+| 发放时间 | 计划发放时间 | 用于控制发放时机。 |
+| 发放状态 | 草稿/待审核/已生效/已停用 | 控制是否可被活动选择。 |
+| 创建人 | 创建账号 | 系统记录。 |
+| 操作  | 编辑/查看 | 已关联活动后是否允许编辑需确认。 |
+
+5.7.2 发放计划编辑页
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 |
+| --- | --- | --- | --- | --- |
+| 发放计划名称 | 后台识别名称 | 文本  | 是   | 不对客展示。 |
+| 发放类型 | 手工/自动 | 下拉框 | 是   | 影响达标后是否自动发奖。 |
+| 对应活动 | 可选关联活动 | 活动选择 | 否   | 用于报表归属。 |
+| 发放客群 | 发给哪些客户 | 客群选择 | 是   | 影响客户是否收到权益。 |
+| 礼品类型 | 积分/立减金/卡券/第三方权益等 | 下拉框 | 是   | 决定移动端按钮文案。 |
+| 礼品 ID | 具体权益/卡券/商品 ID | 选择项 | 是   | 决定跳转权益详情、积分明细或领取页。 |
+| 总礼品数量 | 奖励总库存 | 数字  | 否   | 库存不足时影响发放。 |
+| 单人奖励数量 | 每人发放数量 | 数字  | 是   | 影响客户获得奖励数量。 |
+| 每日发放上限 | 每日最多发放数量 | 数字  | 否   | 影响发奖节奏。 |
+| 是否需要通知 | 发奖后是否通知客户 | 开关  | 否   | 不影响页面。 |
+| 通知模板 | 发奖通知模板 | 选择项 | 开启通知时是 | 引用通知模板。 |
+| 库存预警 | 剩余库存预警 | 数字/手机号 | 否   | 后台预警,不对客展示。 |
+
+奖励类型与移动端按钮映射
+
+| 奖励类型 | 达标后按钮 | 跳转目标 |
+| --- | --- | --- |
+| 积分  | 查看积分奖励 | 积分明细页。 |
+| 立减金 | 前往领取/领取奖励 | 立减金列表或领取页。 |
+| 卡券/电子券 | 查看奖励 | 权益详情页/卡券详情页。 |
+| 信用卡权益 | 查看奖励 | 信用卡权益详情页。 |
+| 第三方发放权益 | 查看奖励 | 权益详情页或第三方说明页。 |
+
+* * *
+
+### 5.8 菜单七:通知模板管理
+
+通知模板可放在内容管理下。活动管理和发放计划中只选择模板。
+
+5.8.1 通知模板列表页
+
+| 字段名称 | 字段含义 | 说明  |
+| --- | --- | --- |
+| 模板编号 | 模板唯一 ID | 活动配置引用。 |
+| 模板名称 | 模板名称 | 如“活动报名成功提醒”。 |
+| 类型  | SMS/其他 | Excel 中目前只有 SMS。 |
+| 内容  | 短信正文 | 支持变量需确认。 |
+| 创建人 | 创建账号 | 系统记录。 |
+| 操作  | 编辑/查看 | 已被活动引用后修改是否影响历史活动需确认。 |
+
+5.8.2 通知模板编辑页
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 说明  |
+| --- | --- | --- | --- | --- |
+| 模板名称 | 后台识别名称 | 文本  | 是   | 用于活动配置选择。 |
+| 模板类型 | 通知类型 | 下拉框 | 是   | SMS。 |
+| 模板内容 | 短信正文 | 多行文本 | 是   | 是否支持活动名称、报名时间、奖励名称等变量需确认。 |
+| 提交  | 保存模板 | 按钮  | \-  | 保存后可被活动选择。 |
+
+* * *
+
+### 5.9 菜单八:活动投放管理
+
+活动投放管理用于生成活动链接、二维码、太阳码,并配置活动投放到 MobileX 或权益首页 Banner 位。它不建议塞在活动基础信息里,因为投放可能有多个渠道、多个版位、多个上线时间。
+
+5.9.1 活动投放管理 - 列表页
+
+| 字段名称 | 字段含义 | 说明  |
+| --- | --- | --- |
+| 投放 ID | 投放记录编号 | 系统生成。 |
+| 活动名称 | 关联活动 | 选择活动管理中的活动。 |
+| Campaign ID | 活动 Campaign | 可搜索选择。 |
+| Channel ID | 渠道 ID | 引用渠道管理。 |
+| 投放渠道 | 渠道名称 | 如 MobileX、微信服务号、短信、Paid Media。 |
+| 投放版位 | 展示位置 | MobileX 版位、权益首页 Banner 位等。 |
+| 上线时间 | 投放开始时间 | 控制版位展示。 |
+| 下线时间 | 投放结束时间 | 控制版位隐藏。 |
+| 状态  | 草稿/已发布/已下线 | 控制投放是否生效。 |
+| 操作  | 编辑/查看/生成链接/生成二维码 | 生成物料给业务投放。 |
+
+5.9.2 活动投放管理 - 新建/编辑页
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 |
+| --- | --- | --- | --- | --- |
+| 选择活动 | 选择要投放的活动 | 活动选择 | 是   | 决定链接进入哪个活动详情。 |
+| Campaign ID | 活动归因 ID | 搜索选择 | 否   | 链接携带,用于统计。 |
+| Channel ID | 渠道归因 ID | 搜索选择 | 否   | 链接携带,用于统计。 |
+| 投放渠道 | 具体渠道 | 下拉框 | 是   | 决定生成物料类型。 |
+| 投放 MobileX 版位 | MobileX tab 版位 | 下拉框 | 否   | 在 MobileX 指定位置展示。 |
+| 投放权益首页版位 | 权益首页 Banner 位 | 下拉框 | 否   | 在权益首页 1-8 位置展示。 |
+| 投放开始时间 | 上线时间 | 日期时间 | 是   | 控制版位何时展示。 |
+| 投放结束时间 | 下线时间 | 日期时间 | 否   | 控制版位何时隐藏。 |
+| 生成链接 | 生成 H5 链接 | 按钮  | \-  | 通过链接/二维码访问活动。 |
+| 生成二维码/太阳码 | 生成投放物料 | 按钮  | \-  | 用于线下或微信投放。 |
+
+说明
+
+-   如果活动配置了“是否展示到首页”,但没有投放版位,则只进入活动模块列表,不一定占用 Banner 位。
+-   如果配置了权益首页 Banner 位,需要明确银行侧是否支持自助配置;Excel 中提到部分首页/组装页位置可能需要我司 BAU 工时。
+-   活动可以不展示在全部活动,但仍通过投放链接或二维码进入;客户报名后可在“我的活动”查看。
+
+* * *
+
+### 5.10 菜单九:活动报表/活动进度查询
+
+该菜单主要用于运营查询活动报名、达标、权益发放等结果,不参与活动创建配置。
+
+建议查询页
+
+| 查询页 | 主要字段 | 用途  |
+| --- | --- | --- |
+| 客户活动报名查询 | 报名时间、活动名称、活动编号、客户来源渠道、Customer ID、客户账户类型 | 查看客户是否报名、从哪个渠道报名。 |
+| 客户达标情况查询 | 客户号、现 MSC、升级 MSC entry date、是否首次升级、月份、已达标层级、规则/活动 | 查看客户达标进度和达标层级。 |
+| 客户权益发放查询 | 发放时间、权益名称、权益编号、客户来源渠道、Customer ID、客户账户类型 | 查看权益是否发放。 |
+| 积分查询 | 客户号、积分科目、积分变动额、积分总额、清算时间、交易描述 | 查看积分奖励明细。 |
+| 立减金发放明细 | 客户号、立减金类型、发放日期、金额、有效期、是否领取、是否使用 | 查看立减金领取和使用状态。 |
+| 信用卡权益领取及使用 | 客户号、卡号、客户等级、权益领取明细、商品编号、商品名称、商品数量 | 查看信用卡权益领取使用情况。 |
+
+* * *
+
+### 5.11 菜单十:信用卡活动入口配置
+
+信用卡活动在 PPT 中更像一个入口配置,而不是完整纳入本活动系统的活动。建议先作为入口配置处理,后续再判断是否扩展为独立 tab。
+
+列表页/配置页字段
+
+| 字段名称 | 字段含义 | 类型  | 是否必填 | 移动端影响 |
+| --- | --- | --- | --- | --- |
+| 是否展示信用卡入口 | 控制入口是否展示 | 开关  | 是   | 全部活动列表底部是否展示“信用卡活动专区”。 |
+| 入口名称 | 入口文案 | 文本  | 是   | 移动端入口标题。 |
+| 提示文案 | 引导说明 | 文本  | 否   | 如“如需查看信用卡活动进度,请点击信用卡活动专区”。 |
+| 跳转地址 | CVP/信用卡权益平台地址 | URL | 是   | 点击后跳转外部信用卡权益平台。 |
+| 是否启用信用卡活动 tab | 是否新增信用卡活动 tab | 开关  | 否   | 如启用,活动列表增加 tab;无活动时不展示。 |
+
+* * *
+
+### 5.12 后台菜单和移动端页面映射表
+
+| 后台菜单/配置 | 控制的移动端页面 | 影响内容 |
+| --- | --- | --- |
+| 活动管理-基础信息 | 首页活动模块、全部活动、我的活动、活动详情 | 活动名称、图片、时间、地点、活动内容。 |
+| 活动管理-时间与展示 | 首页、全部活动、我的活动 | 是否展示、何时展示、是否进入已结束。 |
+| 客群管理 | 首页、全部活动、活动详情、进度页 | 谁能看、谁能报名、谁能达标。 |
+| 报名表单管理 | 报名补充信息页/弹窗 | 收集字段、字段校验、提交逻辑。 |
+| 活动管理-报名设置 | 活动详情、报名确认页、报名结果页 | 是否报名、是否确认、名额、报名成功/失败。 |
+| 达标规则管理 | 活动达标进度页 | 进度样式、层级、任务、提示文案、问号说明。 |
+| 权益发放计划 | 达标进度页、奖励详情页 | 奖励类型、按钮文案、跳转目标、发放状态。 |
+| 活动互斥组管理 | 活动列表、活动详情、报名失败、MGM 进度页 | 不符合参与条件、报名拦截、无法继续参与、推荐人侧提示。 |
+| 通知模板管理 | 无直接页面 | 报名成功、活动开始前、达标发奖短信。 |
+| 活动投放管理 | 活动入口、Banner、二维码/链接访问 | 从哪个渠道进入活动,是否带追踪码。 |
+| 信用卡活动入口配置 | 全部活动列表底部/信用卡 tab | 信用卡活动专区入口和跳转。 |
+
+* * *
+
+### 5.13 第一版建议怎么做
+
+必须做
+
+| 菜单  | 原因  |
+| --- | --- |
+| 活动管理 | 没有它无法创建活动和驱动移动端展示。 |
+| 客群管理 | 活动可见、报名、达标都依赖客群。 |
+| 报名表单管理 | PPT 中有补充信息收集,且不同活动字段不同。 |
+| 达标规则管理 | 进度页规则复杂,必须独立管理,否则活动编辑页过重。 |
+| 活动互斥组管理 | NTB/MGM 互斥是核心规则,必须独立配置和复用。 |
+| 权益发放计划管理 | 达标后奖励发放必须有配置来源。 |
+| 活动投放管理 | PPT 中存在仅通过链接/二维码进入的活动,以及首页/MobileX 版位投放。 |
+
+可以后续做
+
+| 菜单/能力 | 原因  |
+| --- | --- |
+| 信用卡活动独立 tab | PPT 中仍是待讨论,第一版可先做固定入口。 |
+| 复杂报表分析 | 查询可以先做核心报名/达标/发放,投入产出分析后续做。 |
+| 达标规则复制/版本管理 | 有价值,但第一版可先支持新增、编辑、预览。 |
+| 活动投放多渠道效果分析 | 先生成渠道码,后续再做效果看板。 |
+
+不建议第一版做
+
+| 功能  | 原因  |
+| --- | --- |
+| 把所有配置都堆在活动管理一个编辑页里 | 字段过多,维护困难,移动端和后台关联不清。 |
+| 让运营自由配置复杂逻辑表达式 | 容易配置错误,建议先基于标签/客群/规则模板选择。 |
+| 信用卡活动完整纳入本活动体系 | 信用卡活动依赖 CVP,第一版做入口更稳。 |
+| 已发布活动随意编辑互斥规则或达标规则 | 会影响已报名客户状态,容易引发投诉。 |
+
+* * *
+
+### 5.14 当前仍需确认的问题
+
+| 优先级 | 待确认问题 | 影响范围 |
+| --- | --- | --- |
+| P0  | 活动时间、报名时间、展示时间三者关系如何定义 | 决定前端展示和报名按钮状态。 |
+| P0  | 报名时间是否一定晚于活动开始时间 | Excel 里有该说明,但常规活动可能提前报名,需要业务确认。 |
+| P0  | 展示客群和报名客群是否都支持多选 | 影响客群配置字段和接口结构。 |
+| P0  | 留资表和补充信息表是否两个独立概念 | 影响报名链路和后台菜单拆分。 |
+| P0  | 补充信息弹窗关闭后是否算报名成功 | 影响报名状态、我的活动、后续补填逻辑。 |
+| P0  | 达标层级最多 4 层还是 5 层 | PPT 中口径不一致,影响达标规则配置。 |
+| P0  | 互斥是按活动类型自动互斥,还是按互斥组手动配置 | 影响互斥菜单设计。 |
+| P0  | MGM 标签 T+1 后关闭资格时,已报名记录是否保留 | 影响我的活动和详情页状态。 |
+| P1  | 已发布活动哪些字段允许编辑 | 影响审核、版本和已报名客户状态。 |
+| P1  | 活动下线后,已报名客户是否仍可在我的活动查看 | 影响我的活动展示。 |
+| P1  | 投放权益首页版位是否银行可自助配置 | 影响是否需要做投放管理。 |
+| P1  | 发放计划被多个活动引用时,是否允许修改 | 影响奖励发放稳定性。 |
+| P2  | 是否需要活动配置版本记录 | 影响审计和问题追溯。 |
+| P2  | 通知模板变量范围 | 影响短信模板配置复杂度。 |
+
+* * *
+
+## 6\. 数据来源与模拟接口
+
+> 说明:PPT 中未给出具体接口名称。若做 Demo,可以使用以下虚拟中文接口命名。真实开发时需要替换成行内已有接口或平台接口。
+
+### 6.1 前端接口
+
+| 接口名称 | 接口路径 | 用途  | 主要入参 | 主要出参 |
+| --- | --- | --- | --- | --- |
+| 查询首页活动接口 | `/api/权益活动/查询首页活动` | 查询权益专区主页活动模块 | 客户编号、渠道、客群标签 | 活动卡片列表 |
+| 查询全部活动接口 | `/api/权益活动/查询全部活动` | 查询全部活动列表 | 客户编号、活动分类、分页 | 活动列表、信用卡入口 |
+| 查询我的活动接口 | `/api/权益活动/查询我的活动` | 查询进行中/已结束活动 | 客户编号、tab 类型 | 我的活动列表 |
+| 查询活动详情接口 | `/api/权益活动/查询活动详情` | 查询活动详情 | 客户编号、活动 ID | 活动详情、客户状态、按钮规则 |
+| 报名前校验接口 | `/api/权益活动/报名前校验` | 判断是否可报名 | 客户编号、活动 ID | 是否可报名、失败原因、互斥信息 |
+| 提交活动报名接口 | `/api/权益活动/提交报名` | 提交报名 | 客户编号、活动 ID、补充信息 | 报名结果、报名编号 |
+| 提交补充信息接口 | `/api/权益活动/提交补充信息` | 补全报名信息 | 报名编号、字段值 | 是否成功 |
+| 查询达标进度接口 | `/api/权益活动/查询达标进度` | 查询任务、层级、奖励状态 | 客户编号、活动 ID | 进度层级、任务、奖励 |
+| 查询互斥状态接口 | `/api/权益活动/查询互斥状态` | 判断客户互斥关系 | 客户编号、活动 ID | 命中互斥、互斥活动名称、拦截文案 |
+| 查询信用卡入口接口 | `/api/权益活动/查询信用卡入口` | 查询信用卡专区入口 | 客户编号、渠道 | 是否展示、跳转地址、提示文案 |
+
+### 6.2 后台接口
+
+| 接口名称 | 接口路径 | 用途  |
+| --- | --- | --- |
+| 查询活动配置列表 | `/api/后台/权益活动/查询列表` | 后台活动列表查询 |
+| 保存活动基础信息 | `/api/后台/权益活动/保存基础信息` | 创建/编辑活动基础字段 |
+| 保存展示客群配置 | `/api/后台/权益活动/保存展示客群` | 配置首页、全部活动、客群标签 |
+| 保存报名规则配置 | `/api/后台/权益活动/保存报名规则` | 配置报名、确认、名额、补充信息 |
+| 保存达标进度配置 | `/api/后台/权益活动/保存达标配置` | 配置层级、任务、标签、连续达标 |
+| 保存奖励配置 | `/api/后台/权益活动/保存奖励配置` | 配置奖励类型与跳转 |
+| 保存互斥组配置 | `/api/后台/权益活动/保存互斥组配置` | 配置互斥组、互斥对象和前端展示文案 |
+| 保存短信转发配置 | `/api/后台/权益活动/保存短信转发` | 配置短信提醒和转发开关 |
+| 发布活动 | `/api/后台/权益活动/发布` | 发布活动到前端 |
+| 下线活动 | `/api/后台/权益活动/下线` | 下线活动 |
+
+* * *
+
+## 7\. 状态流转
+
+## 7.1 客户报名状态
+
+```mermaid
+stateDiagram-v2
+    [*] --> 未报名
+    未报名 --> 不符合报名条件: 客群/开户/互斥校验失败
+    未报名 --> 报名确认: 需要重复确认
+    未报名 --> 补充信息待填写: 不需要确认但需要补充信息
+    未报名 --> 已报名: 不需要确认且不需要补充信息
+    报名确认 --> 未报名: 取消
+    报名确认 --> 补充信息待填写: 确认且需要补充信息
+    报名确认 --> 已报名: 确认且无需补充信息
+    补充信息待填写 --> 已报名信息未补全: 关闭/取消/跳转第三方未完成
+    补充信息待填写 --> 已报名: 提交成功
+    已报名信息未补全 --> 补充信息待填写: 下次进入活动再次弹窗
+    已报名 --> 进度跟踪中: 活动有进度
+    已报名 --> 无进度已完成: 活动无进度
+    进度跟踪中 --> 已达标: 任务达标
+    进度跟踪中 --> 未达标已结束: 活动结束未达标
+    已达标 --> 已发奖: 奖励发放
+    已报名 --> 无法继续参与: 次日互斥/开户状态修正
+```
+
+## 7.2 活动展示状态
+
+| 状态  | 触发条件 | 前端展示 |
+| --- | --- | --- |
+| 不可见 | 客户不在可见客群/活动未发布/展示开关关闭 | 不展示活动 |
+| 可查看未报名 | 客户可见,未报名,满足基本条件 | 展示报名按钮 |
+| 不符合报名条件 | 客户可见但不满足报名客群或命中开户限制 | 展示不符合提示 |
+| 不符合参与条件 | 命中互斥规则 | 列表卡片展示不符合参与条件,详情页展示拦截说明 |
+| 报名已满 | 名额达到上限 | 报名失败提示报名已满 |
+| 已报名 | 客户已报名且活动有效 | 我的活动进行中展示 |
+| 信息未补全 | 客户已报名但补充信息未完成 | 进入详情时继续弹补充信息 |
+| 进行中 | 活动当前处于进行时间内 | 我的活动进行中展示 |
+| 已结束 | 当前时间超过活动结束时间 | 我的活动已结束,卡片置灰 |
+| 无法继续参与 | T+1/T-1 数据修正后停止进度 | 详情页展示无法继续参与原因 |
+
+## 7.3 奖励状态
+
+| 状态  | 说明  | 按钮  |
+| --- | --- | --- |
+| 未达标 | 客户任务未完成 | 查看达标详情 |
+| 达标未发奖 | 客户达标但奖励未发放 | 查看达标详情/等待发放 |
+| 待领取 | 奖励需客户主动领取 | 前往领取/领取奖励 |
+| 已领取 | 客户已领取奖励 | 查看奖励 |
+| 自动发放完成 | 积分/第三方权益已发放 | 查看积分奖励/查看奖励 |
+| 已结束未达标 | 活动结束且客户未达标 | 查看达标详情/展示未达标原因 |
+
+* * *
+
+## 8\. 异常与边界情况
+
+| 场景  | 处理规则 |
+| --- | --- |
+| 活动无可见活动 | 首页活动模块隐藏,全部活动展示空状态 |
+| 客户可见但不可报名 | 详情页展示不符合报名条件,不展示可点击报名按钮 |
+| 活动名额已满 | 报名时提示“很遗憾,该活动报名已满。” |
+| 客户重复点击报名 | 前端按钮 loading,后端根据客户编号+活动 ID 做幂等 |
+| 客户已报名再次进入 | 直接进入已报名详情页 |
+| 客户关闭补充信息弹窗 | 报名仍成功,但标记信息未补全,下次进入继续弹窗 |
+| 第三方跳转失败 | 提示链接异常,请稍后重试;报名状态按配置决定是否已成功 |
+| 补充信息字段超过 2 个 | 后台不允许保存 |
+| 达标层级超过上限 | 后台不允许保存;移动端兼容异常数据但只展示前 5 个 |
+| 单层任务超过 5 个 | 后台不允许保存 |
+| 标签不存在或被删除 | 活动不可发布,已发布活动需提示配置异常 |
+| 活动结束后进入详情 | 仍允许查看详情、进度、奖品,列表置灰 |
+| 无需报名活动 | 活动期间自动进入我的活动进行中,结束后进入已结束 |
+| 活动不展示到首页 | 不影响通过二维码/链接进入;报名后可进入我的活动 |
+| 客户同一天报名两个互斥活动 | 非 MGM 场景必须实时拦截第二个报名 |
+| MGM 标签 T+1 | 当天可能未拦截,第二天收到标签后修正状态 |
+| 客户开户数据 T-1 | 当天开户可能无法识别,第二天识别后停止进度跟踪 |
+| 已结束活动奖励按钮 | 已领取可查看,未领取是否可领按活动规则配置 |
+| 信用卡入口未配置 | 不展示固定入口或 tab |
+| 短信发送失败 | 不影响报名主流程,记录失败日志 |
+| 活动不可转发 | 隐藏分享入口或分享按钮置灰 |
+
+* * *
+
+## 9\. 测试点
+
+## 9.1 移动端测试点
+
+| 编号  | 模块  | 测试场景 | 前置条件 | 操作步骤 | 预期结果 |
+| --- | --- | --- | --- | --- | --- |
+| M-001 | 首页活动模块 | 首页展示活动 | 活动已发布、配置首页展示、客户在可见客群 | 进入权益专区主页 | 展示活动卡片 |
+| M-002 | 首页活动模块 | 首页不展示活动 | 活动未配置首页展示 | 进入权益专区主页 | 首页不展示该活动 |
+| M-003 | 全部活动 | 展示所有可见活动 | 客户存在多个可见活动 | 进入全部活动页 | 展示全部可见活动 |
+| M-004 | 全部活动 | 不可见客群过滤 | 客户不在可见客群 | 进入全部活动页 | 不展示该活动 |
+| M-005 | 我的活动 | 已报名活动进入进行中 | 客户已报名且活动未结束 | 进入我的活动-进行中 | 展示该活动 |
+| M-006 | 我的活动 | 已结束活动置灰 | 活动已结束 | 进入我的活动-已结束 | 活动卡片置灰但可点击 |
+| M-007 | 我的活动 | 无需报名活动自动加入 | 活动配置无需报名且进行中 | 进入我的活动-进行中 | 展示该活动 |
+| M-008 | 活动详情 | 未报名详情展示 | 客户未报名且可报名 | 点击活动卡片 | 展示详情和报名按钮 |
+| M-009 | 活动详情 | 不符合报名条件 | 客户可见但不在报名客群 | 点击报名 | 展示不符合报名条件提示 |
+| M-010 | 报名名额 | 名额已满 | 活动名额已达上限 | 点击报名 | 提示活动报名已满 |
+| M-011 | 报名确认 | 需要重复确认 | 后台开启确认页 | 点击报名 | 进入确认页/弹窗 |
+| M-012 | 报名确认 | 不需要重复确认 | 后台关闭确认页 | 点击报名 | 跳过确认,进入下一步 |
+| M-013 | 补充信息 | 需要填写补充信息 | 后台配置补充信息字段 | 点击报名并确认 | 展示补充信息表单 |
+| M-014 | 补充信息 | 字段校验失败 | 字段配置数字,输入文字 | 点击提交 | 提示格式错误,不提交 |
+| M-015 | 补充信息 | 关闭弹窗后再次进入 | 客户关闭补充信息弹窗 | 再次进入活动详情 | 再次弹出补充信息 |
+| M-016 | 报名结果 | 有进度活动报名成功 | 活动配置有进度跟踪 | 提交报名 | 展示查看达标进度按钮 |
+| M-017 | 报名结果 | 无进度无发奖活动 | 活动无进度无发奖 | 提交报名 | 展示查看更多活动按钮 |
+| M-018 | 报名结果 | 无进度直接发奖活动 | 活动无进度但配置权益 | 提交报名 | 展示查看权益按钮 |
+| M-019 | 达标进度 | 多层级展示 | 后台配置多个层级 | 进入达标进度页 | 展示层级和任务 |
+| M-020 | 达标进度 | 单层级不展示 tab | 后台仅配置一个层级 | 进入达标进度页 | 不展示层级 tab |
+| M-021 | 达标进度 | 连续达标月份横滑 | 连续达标月份超过 3 个月 | 查看进度 | 月份区域支持横向滑动 |
+| M-022 | 奖励按钮 | 积分奖励跳转 | 奖励类型为积分 | 点击查看积分奖励 | 跳转积分明细页 |
+| M-023 | 奖励按钮 | 立减金跳转 | 奖励类型为立减金 | 点击前往领取 | 跳转立减金列表 |
+| M-024 | 互斥  | 航司报名后拦截酒店 | 客户已报名航司活动 | 再报名酒店活动 | 展示互斥拦截文案 |
+| M-025 | 互斥  | MGM 次日拦截其他 NTB | 客户昨日填写 MGM 表单 | 报名航司活动 | 展示已参与亲友推荐,无法报名 |
+| M-026 | 互斥  | MGM 当天极限场景次日修正 | 客户当天填写 MGM 并报名航司 | 次日进入权益平台 | 航司活动展示无法继续参与 |
+| M-027 | 信用卡入口 | 展示信用卡入口 | 后台开启信用卡入口 | 进入全部活动页 | 底部展示信用卡活动专区入口 |
+| M-028 | 信用卡入口 | 关闭信用卡入口 | 后台关闭入口 | 进入全部活动页 | 不展示信用卡入口 |
+
+## 9.2 后台测试点
+
+| 编号  | 模块  | 测试场景 | 操作步骤 | 预期结果 |
+| --- | --- | --- | --- | --- |
+| B-001 | 活动基础信息 | 必填字段校验 | 新建活动不填活动名称保存 | 提示活动名称必填 |
+| B-002 | 活动基础信息 | 时间校验 | 活动结束时间早于开始时间 | 不允许保存 |
+| B-003 | 展示配置 | 首页展示开关 | 开启/关闭首页展示 | 前端首页对应展示/隐藏 |
+| B-004 | 客群配置 | 可见客群为空 | 保存活动 | 根据规则提示必填或默认所有客户可见 |
+| B-005 | 报名配置 | 有名额上限但未填数量 | 保存  | 提示报名名额必填 |
+| B-006 | 报名配置 | 名额小于已报名人数 | 修改名额 | 不允许保存 |
+| B-007 | 补充信息 | 配置 3 个字段 | 保存  | 提示最多配置 2 个字段 |
+| B-008 | 补充信息 | 必填字段未配置名称 | 保存  | 提示字段名称必填 |
+| B-009 | 达标配置 | 层级超过 5 个 | 保存  | 提示最多 5 个层级 |
+| B-010 | 达标配置 | 单层任务超过 5 个 | 保存  | 提示每层最多 5 个任务 |
+| B-011 | 达标配置 | 标签不存在 | 保存/发布 | 提示标签不存在,不允许发布 |
+| B-012 | 连续达标 | Y 大于 X | 保存  | 提示连续达标月份不能大于观察周期 |
+| B-013 | 奖励配置 | 奖励类型与跳转不匹配 | 保存  | 提示配置错误 |
+| B-014 | 互斥配置 | 选择互斥组但未选择活动角色 | 保存活动 | 提示当前活动互斥角色必填 |
+| B-015 | 互斥配置 | 互斥组对象缺少有效参与时间字段 | 保存互斥组 | 提示有效参与时间字段必填 |
+| B-016 | 互斥配置 | 拦截文案变量 | 配置含活动名、日期变量的文案 | 前端可正确替换变量 |
+| B-017 | 短信配置 | 开启短信但无模板 | 保存  | 提示短信模板必填 |
+| B-018 | 信用卡入口 | 开启入口但无链接 | 保存  | 提示跳转地址必填 |
+| B-019 | 发布校验 | 未完成关键配置发布 | 点击发布 | 提示缺失项,不允许发布 |
+| B-020 | 下线活动 | 已发布活动下线 | 点击下线 | 前端不再展示,但已报名客户记录按规则可查 |
+| B-021 | 数据兼容 | 已发布活动修改规则 | 修改报名/互斥规则 | 需要提示影响已报名客户,要求确认 |
+
+* * *
+
+## 10\. Codex / OpenCode 开发提示词
+
+以下提示词适合直接复制给 Codex / OpenCode,用于生成一个可交互 Demo。重点是做“新增活动模块 Demo”,不是完整银行系统。
+
+```text
+你现在要基于一个现有权益平台的业务风格,开发一个“权益平台活动模块”的可交互 Demo。这个 Demo 用于产品评审和需求说明,不需要接真实银行接口,但要用 mock 数据模拟完整业务链路。
+
+一、项目目标
+开发一个前后端都可演示的活动模块 Demo,包含移动端 H5 页面和后台配置页面。移动端用于展示客户参加活动、报名、查看进度、互斥拦截;后台用于配置活动基础信息、展示规则、报名规则、补充信息、达标进度、奖励、互斥规则和信用卡活动入口。
+
+二、技术要求
+1. 使用 Vue 3 + Vite + TypeScript。
+2. 移动端页面使用适合 H5 的布局,宽度按 375px 设计,可在浏览器中居中预览。
+3. 后台页面使用 Element Plus 或同类后台组件风格。
+4. 不接真实后端,使用本地 mock 数据和 mock API 方法。
+5. 所有状态切换、报名、互斥、达标进度、奖励跳转都要能在前端演示。
+6. 页面样式尽量接近银行权益平台风格:白底、红色主按钮、卡片式列表、灰色弱化已结束状态。
+
+三、页面范围
+移动端需要做以下页面:
+1. 权益专区主页 - 活动模块:展示配置为首页展示的活动。
+2. 全部活动列表页:展示客户所有可见活动,底部可展示信用卡活动专区入口。
+3. 我的活动页:包含“进行中”和“已结束”两个 tab,已结束活动置灰但可点击。
+4. 活动详情页 - 未报名:展示活动图片、名称、对象、报名时间、活动时间、报名人数、活动内容、达标层级和报名按钮。
+5. 报名确认页/弹窗:可根据活动配置决定是否出现。
+6. 补充信息收集页/弹窗:最多支持两个字段,例如航司会员号、酒店会员号;支持校验规则。
+7. 报名结果页:支持报名成功、报名失败、名额已满、不符合报名条件、互斥拦截。
+8. 活动详情页 - 已报名/无需报名:展示报名信息、活动内容、底部按钮。
+9. 活动达标进度页:展示多个达标层级、任务完成状态、奖励状态、按钮跳转。
+10. MGM 亲友推荐进度页:展示推荐人/被推荐人互斥后的状态说明。
+11. 合作方活动落地页:模拟航司活动、酒店活动二维码进入后的报名页面。
+
+后台需要做以下页面:
+1. 活动列表页:支持查看活动、创建活动、编辑活动、发布/下线。
+2. 活动基础信息配置:活动名称、活动小图、详情图、活动对象、报名时间、活动时间、活动地点、活动内容。
+3. 展示与客群配置:是否首页展示、是否展示到全部活动、可见客群、报名客群、是否展示报名人数、是否可转发。
+4. 报名规则配置:是否需要报名、是否需要重复确认、确认文案、名额控制、是否需要补充信息。
+5. 补充信息配置:最多两个字段,字段名称、提示文案、字段类型、是否必填、最大长度、第三方跳转链接。
+6. 达标进度配置:进度展示样式、最多 5 个层级、每层最多 5 个任务、任务标签、对客文案、问号说明、连续达标 X/Y/Z 配置。
+7. 奖励配置:奖励名称、奖励类型、发放方式、按钮文案、跳转目标。
+8. 活动互斥组配置:互斥组、互斥对象、对象角色、列表状态文案、报名拦截文案、无法继续参与文案、推荐人侧未达标原因;活动管理中只选择互斥组和当前活动角色。
+9. 信用卡活动入口配置:是否展示入口、入口名称、提示文案、跳转地址。
+
+四、核心业务逻辑
+1. 活动是否展示由“活动状态 + 展示开关 + 时间 + 可见客群”共同决定。
+2. 全部活动展示客户所有可见活动;首页只展示配置为首页展示的活动。
+3. 我的活动展示客户已报名活动、无需报名活动;分为进行中和已结束。
+4. 已结束活动置灰,但仍可点击查看详情、进度和奖品。
+5. 客户点击报名时,先做报名前校验:报名时间、报名客群、名额、互斥规则、开户状态。
+6. 如果活动需要重复确认,先展示确认页;如果不需要则跳过。
+7. 如果活动需要补充信息,展示补充信息页;最多两个字段。
+8. 客户关闭补充信息弹窗时,仍视为报名成功,但标记为“信息未补全”,下次进入活动继续弹出补充信息。
+9. 无需报名活动直接进入客户的我的活动,活动期间在进行中,结束后进入已结束。
+10. 有进度跟踪的活动报名成功后按钮为“查看达标进度”。
+11. 无进度且不发奖的活动按钮为“查看更多活动”。
+12. 无进度但直接发奖的活动按钮为“查看权益”。
+13. 达标进度支持多个层级,每层最多 5 个任务,任务状态包括未完成、已完成、计算中。
+14. 奖励类型决定按钮:积分为“查看积分奖励”,立减金为“前往领取/领取奖励”,其他权益为“查看奖励”。
+15. 互斥活动必须实时判断。平台内 NTB 活动报名成功后应立即生成报名记录,客户再报名同组其他 NTB 活动时必须拦截。
+16. MGM 场景需要模拟 T+1 标签延迟:客户当天填写 MGM 表单后,可能当天仍可报名其他活动;第二天系统收到 MGM 标签后,按互斥组规则修正,该活动展示“无法继续参与”。
+17. NTB 活动需要模拟开户时间 T-1:如果第二天发现客户已开户,需要停止当前活动进度跟踪。
+18. 后台不提供“是否实时校验”“是否判断 MGM 标签”“同日冲突规则”等固定逻辑配置;活动选择互斥组即参与互斥,固定判断逻辑按移动端活动互斥逻辑执行。
+
+五、必须内置的 mock 场景
+1. 普通可报名活动:客户可以报名成功,并查看达标进度。
+2. 名额已满活动:客户点击报名后提示活动报名已满。
+3. 无需报名活动:自动出现在我的活动。
+4. 无进度无发奖活动:报名成功后按钮跳全部活动。
+5. 无进度直接发奖活动:报名成功后按钮跳权益页。
+6. 航司 NTB 活动:需要填写航司会员号。
+7. 酒店 NTB 活动:与航司活动互斥。
+8. MGM 亲友推荐活动:展示推荐人和被推荐人互斥状态。
+9. Case A:客户先报名航司活动,后被朋友推荐参与 MGM,MGM 无法继续。
+10. Case B:客户先被朋友推荐,第二天再报名航司活动,被拦截。
+11. Case B 极限情况:客户当天填写 MGM 又报名航司,第二天状态修正为无法继续参与。
+12. Case C:客户先报名航司活动,当天再报名酒店活动,被实时拦截。
+
+六、UI 与交互要求
+1. 移动端使用卡片列表、顶部 tab、底部主按钮、弹窗确认。
+2. 主按钮使用红色,次按钮使用白底描边。
+3. 已结束活动置灰展示。
+4. 不符合参与条件的活动在卡片上展示灰色状态标签。
+5. 补充信息弹窗要支持关闭按钮,关闭后下次进入继续弹。
+6. 达标进度页要能展示任务完成/未完成状态,点击“查看达标详情”时高亮对应层级。
+7. 后台配置表单要分模块展示,不要把所有字段堆在一个页面。
+8. 后台配置修改后,前端 mock 数据应能同步体现,至少在当前浏览器 session 内可演示。
+
+七、不要做什么
+1. 不要接真实银行接口。
+2. 不要做登录系统。
+3. 不要做真实短信发送。
+4. 不要做真实奖励发放。
+5. 不要重做完整 MGM 表单系统,只需要模拟 MGM 标签和权益平台展示结果。
+6. 不要把 Demo 做成纯静态页面,关键按钮和状态必须可交互。
+7. 不要引入复杂后端服务,优先前端 mock 完成。
+
+八、最终交付物
+1. 可运行的 Vue 3 项目。
+2. README.md,写清楚启动方式、页面路由、mock 场景如何切换。
+3. 移动端页面和后台页面都要可访问。
+4. mock 数据文件要结构清晰,方便产品经理修改活动配置。
+5. 关键业务逻辑要写注释,特别是报名校验、互斥组判断、平台内 NTB 报名记录实时拦截、MGM T+1 修正、NTB T-1 开户修正、奖励按钮规则。
+```
+
+* * *
+
+## 11\. 待确认问题
+
+以下问题在 PPT/视频中没有完全明确,建议需求评审时确认:
+
+1.  活动详情页达标层级上限写的是最多 4 层,达标配置区写的是最多 5 层,最终以前端展示 4 层还是后台配置 5 层为准?
+2.  “不符合参与条件”和“不符合报名条件”的文案是否需要严格区分?
+3.  已结束活动中未领取奖励是否仍允许领取,还是仅允许查看?
+4.  关闭补充信息弹窗后是否所有活动都视为报名成功,还是仅第三方跳转活动适用?
+5.  第三方跳转后是否需要回跳权益平台,还是跳出即结束?
+6.  短信提醒是否第一版必须上线,还是可以作为二期配置能力?
+7.  信用卡活动是固定入口即可,还是需要独立 tab?
+8.  MGM 推荐人侧是否所有活动都展示被推荐人进度,还是后台配置后才展示?
+9.  活动发布是否需要审核流,还是运营保存后可直接发布?
+10.  互斥释放规则中,“活动结束且未达标释放”是否适用于所有互斥活动?