소스 검색

完善活动互斥组编辑与管理原型

csk 5 일 전
부모
커밋
1de69591cb

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

@@ -1,8 +1,8 @@
 <template>
   <div class="achievement-config">
     <div class="template-row">
-      <el-form-item label="活动模板" required>
-        <el-select v-model="form.achievementTemplate" style="width:200px" @change="handleTemplateChange">
+      <el-form-item label="进度模板">
+        <el-select v-model="form.achievementTemplate" clearable placeholder="可选" style="width:200px" @change="handleTemplateChange">
           <el-option label="NTB(新客达标)" value="NTB" />
           <el-option label="MGM(推荐有礼)" value="MGM" />
           <el-option label="层层礼" value="LAYER" />
@@ -109,7 +109,7 @@
       </section>
     </template>
 
-    <template v-else>
+    <template v-else-if="form.achievementTemplate === 'LAYER'">
       <section class="config-block">
         <h3>达标层级配置</h3>
         <el-tabs v-model="activeLayer" class="layer-tabs">

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

@@ -114,6 +114,18 @@ const routes = [
         component: () => import('@/views/ActivityMutexGroup.vue'),
         meta: { title: '活动互斥组管理' }
       },
+      {
+        path: 'customization/hsbc/activity-mutex-groups/add',
+        name: 'ActivityMutexGroupAdd',
+        component: () => import('@/views/ActivityMutexGroupEdit.vue'),
+        meta: { title: '新增活动互斥组' }
+      },
+      {
+        path: 'customization/hsbc/activity-mutex-groups/edit/:id',
+        name: 'ActivityMutexGroupEdit',
+        component: () => import('@/views/ActivityMutexGroupEdit.vue'),
+        meta: { title: '编辑活动互斥组' }
+      },
       {
         path: 'customization/hsbc/activity-launches',
         name: 'ActivityLaunchList',

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

@@ -19,6 +19,7 @@ function emptyActivity() {
     category: '普通活动',
     status: 'draft',
     auditStatus: '未提交',
+    auditAction: '',
     auditor: '',
     target: '借记卡新客',
     shareTitle: '',
@@ -28,6 +29,8 @@ function emptyActivity() {
     description: '',
     scrmActivityCode: '',
     scrmChannelCode: '',
+    campaignId: '',
+    channelId: '',
     labels: [],
     thumbnail: [],
     detailImage: [],
@@ -78,7 +81,7 @@ function emptyActivity() {
     ],
     extraInfoLink: '',
     progressEnabled: true,
-    achievementTemplate: 'NTB',
+    achievementTemplate: '',
     layerCount: 2,
     progressCopy: {
       unreached: '您已报名本活动,请继续保持账户状态',
@@ -372,6 +375,97 @@ const state = reactive({
       rewardTemplateId: 'NTF002',
       launchUrl: 'https://demo.hsbc.com.cn/activity/ACT202606003',
       launchChannels: ['全部活动', 'MGM推荐链接']
+    },
+    {
+      ...emptyActivity(),
+      id: 'ACT202607004',
+      internalTitle: '暑期借记卡客户回馈活动',
+      name: '暑期客户回馈礼',
+      category: '普通活动',
+      status: 'draft',
+      auditStatus: '未提交',
+      target: '借记卡客户',
+      activityTime: ['2026-07-15', '2026-08-31'],
+      signupTime: ['2026-07-12', '2026-08-25'],
+      visibleCrowdIds: ['ACD001'],
+      signupCrowdIds: ['ACD001'],
+      creator: '李产品',
+      signedCount: 0,
+      quota: 1000
+    },
+    {
+      ...emptyActivity(),
+      id: 'ACT202607005',
+      internalTitle: '航空新客达标活动二期',
+      name: '航空新客夏日礼遇',
+      category: 'NTB活动',
+      achievementTemplate: 'NTB',
+      status: 'pending',
+      auditStatus: '待审核',
+      target: '航空联名卡新客',
+      activityTime: ['2026-08-01', '2026-10-31'],
+      signupTime: ['2026-07-20', '2026-10-15'],
+      visibleCrowdIds: ['ACD001'],
+      signupCrowdIds: ['ACD001'],
+      creator: '张运营',
+      signedCount: 86,
+      quota: 2000
+    },
+    {
+      ...emptyActivity(),
+      id: 'ACT202607006',
+      internalTitle: 'MGM推荐有礼活动三期',
+      name: '挚友推荐礼遇',
+      category: 'MGM',
+      achievementTemplate: 'MGM',
+      status: 'approved',
+      auditStatus: '审核通过',
+      target: '推荐人与被推荐人',
+      activityTime: ['2026-08-01', '2026-12-31'],
+      signupTime: ['2026-07-25', '2026-12-15'],
+      visibleCrowdIds: ['ACD002'],
+      signupCrowdIds: ['ACD002'],
+      creator: '王运营',
+      auditor: '陈审核',
+      signedCount: 126,
+      quota: 3000
+    },
+    {
+      ...emptyActivity(),
+      id: 'ACT202607007',
+      internalTitle: '卓越客户层层礼活动',
+      name: '卓越层层升级礼',
+      category: '层层礼',
+      achievementTemplate: 'LAYER',
+      status: 'published',
+      auditStatus: '已发布',
+      target: '卓越理财客户',
+      activityTime: ['2026-07-01', '2026-12-31'],
+      signupTime: ['2026-06-20', '2026-11-30'],
+      showOnAssembly: true,
+      creator: '周产品',
+      auditor: '陈审核',
+      signedCount: 1680,
+      quota: 5000
+    },
+    {
+      ...emptyActivity(),
+      id: 'ACT202607008',
+      internalTitle: '城市客户专属礼遇活动',
+      name: '上海客户专属礼遇',
+      category: '普通活动',
+      status: 'offline',
+      auditStatus: '已下线',
+      target: '上海地区客户',
+      offlineEnabled: true,
+      cityText: '上海市',
+      location: '线下合作网点',
+      activityTime: ['2026-04-01', '2026-06-30'],
+      signupTime: ['2026-03-20', '2026-06-15'],
+      creator: '李产品',
+      auditor: '陈审核',
+      signedCount: 300,
+      quota: 300
     }
   ],
   forms: [

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

@@ -17,7 +17,7 @@
             <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="活动类型" prop="category"><el-select v-model="form.category" placeholder="请选择活动类型" style="width:100%" @change="handleCategoryChange"><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">
@@ -188,6 +188,25 @@
               </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="12">
+                <el-form-item label="Campaign ID">
+                  <el-select v-model="form.campaignId" clearable placeholder="请选择 Campaign ID" style="width:100%">
+                    <el-option v-for="item in campaignOptions" :key="item.value" :label="item.label" :value="item.value" />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="Channel ID">
+                  <el-select v-model="form.channelId" clearable placeholder="请选择 Channel ID" style="width:100%">
+                    <el-option v-for="item in channelOptions" :key="item.value" :label="item.label" :value="item.value" />
+                  </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="30" class="module-form">
@@ -277,7 +296,7 @@
               <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-input v-model="form.confirmText" type="textarea" :rows="3" resize="vertical" placeholder="请输入报名确认文案" class="field-lg" />
                   </el-form-item>
                 </el-col>
               </el-row>
@@ -582,8 +601,8 @@
         </div>
         <div class="action-center">
           <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="isAudit" type="danger" @click="rejectActivity">{{ isOfflineAudit ? '驳回下线' : '驳回' }}</el-button>
+          <el-button v-if="isAudit" type="primary" @click="approveActivity">{{ isOfflineAudit ? '审核通过并下线' : '审核通过' }}</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>
@@ -602,7 +621,9 @@
 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 { fetchLinkOptions } from '@/utils/linkOptions'
 import activityStore from '@/store/activityStore'
+import smsTemplateStore from '@/store/smsTemplateStore'
 import ActivityAchievementConfig from '@/components/activity/ActivityAchievementConfig.vue'
 
 export default {
@@ -627,6 +648,8 @@ export default {
       ],
       layerActiveTab: '0',
       assemblyPages: [],
+      campaignOptions: [],
+      channelOptions: [],
       ruleFieldOptions: ['开户时间', 'TRB值(含家庭账户)', '客户MSC Group', '境外消费'],
       operatorOptions: ['介于', '大于等于', '小于等于'],
       quillOptions: {
@@ -665,8 +688,10 @@ export default {
   computed: {
     isEdit() { return this.$route.name === 'ActivityEdit' },
     isAudit() { return this.$route.name === 'ActivityAudit' },
+    isOfflineAudit() { return this.isAudit && this.form.auditAction === 'offline' },
     isReadonly() { return this.$route.name === 'ActivityView' || this.isAudit },
     pageTitle() {
+      if (this.isOfflineAudit) return '活动下线审核'
       if (this.isAudit) return '活动审核'
       if (this.isReadonly) return '查看活动'
       return this.isEdit ? '编辑活动' : '新建活动'
@@ -682,7 +707,7 @@ export default {
       return this.grantPlans.filter((item) => item.rewardType !== '积分')
     },
     mutexGroups() { return activityStore.mutexGroups },
-    notifications() { return activityStore.notifications }
+    notifications() { return smsTemplateStore.state.list }
   },
   created() {
     if (this.isEdit || this.isReadonly) {
@@ -698,6 +723,7 @@ export default {
     this.normalizeActivityImages()
     this.normalizeLayerContents()
     this.fetchAssemblyPages()
+    this.fetchChannelOptions()
   },
   methods: {
     async fetchAssemblyPages() {
@@ -796,6 +822,28 @@ export default {
     handleAchievementTemplateChange() {
       if (this.form.achievementTemplate === 'LAYER') this.syncLayerCount(this.form.layerCount || 1)
     },
+    async fetchChannelOptions() {
+      try {
+        const { campaignOptions, channelOptions } = await fetchLinkOptions()
+        this.campaignOptions = campaignOptions
+        this.channelOptions = channelOptions
+      } catch (e) {
+        this.campaignOptions = []
+        this.channelOptions = []
+      }
+    },
+    handleCategoryChange(category) {
+      const templateMap = {
+        '层层礼': 'LAYER',
+        MGM: 'MGM',
+        'MGM活动': 'MGM',
+        'NTB活动': 'NTB',
+        '航空NTB活动': 'NTB',
+        '酒店NTB活动': 'NTB'
+      }
+      this.form.achievementTemplate = templateMap[category] || ''
+      this.handleAchievementTemplateChange()
+    },
     newExtraForm() {
       return { name: '', placeholder: '', type: 'text', required: true, maxLength: 30 }
     },
@@ -919,28 +967,30 @@ export default {
     },
     async approveActivity() {
       if (!this.isAudit) return
-      await ElMessageBox.confirm(`确定审核通过活动「${this.form.name}」吗?`, '审核通过', { type: 'warning' })
+      await ElMessageBox.confirm(`确定${this.isOfflineAudit ? '审核通过并下线' : '审核通过'}活动「${this.form.name}」吗?`, '审核通过', { type: 'warning' })
       const target = activityStore.getActivity(this.form.id)
       if (target) {
-        target.status = 'approved'
-        target.auditStatus = '审核通过'
+        target.status = this.isOfflineAudit ? 'offline' : 'approved'
+        target.auditStatus = this.isOfflineAudit ? '已下线' : '审核通过'
+        target.auditAction = ''
         target.auditor = '当前审核人'
         target.updatedAt = this.nowText()
       }
-      ElMessage.success('审核通过')
+      ElMessage.success(this.isOfflineAudit ? '审核通过,活动已下线' : '审核通过')
       this.goBack()
     },
     async rejectActivity() {
       if (!this.isAudit) return
-      await ElMessageBox.confirm(`确定驳回活动「${this.form.name}」吗?`, '驳回活动', {
+      await ElMessageBox.confirm(`确定${this.isOfflineAudit ? '驳回下线申请' : '驳回活动'}「${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.status = this.isOfflineAudit ? 'published' : 'rejected'
+        target.auditStatus = this.isOfflineAudit ? '下线审核驳回' : '审核驳回'
+        target.auditAction = ''
         target.auditor = '当前审核人'
         target.updatedAt = this.nowText()
       }
@@ -962,6 +1012,7 @@ export default {
       if (toAudit) {
         this.form.status = 'pending'
         this.form.auditStatus = '待审核'
+        this.form.auditAction = ''
       }
       activityStore.saveActivity(this.form)
       ElMessage.success(toAudit ? '已保存并提交审核' : '保存成功')
@@ -985,7 +1036,6 @@ export default {
         }
       }
       if (this.form.quotaEnabled && Number(this.form.quota) < Number(this.form.signedCount || 0)) return '报名人数上限不能小于已报名人数'
-      if (this.form.progressEnabled && !this.form.achievementTemplate) return '开启进度与奖励后必须选择活动模板'
       if (this.form.progressEnabled && this.form.achievementTemplate === 'NTB' && (!this.form.ntbRules?.length || this.form.ntbRules.some((rule) => !rule.field || !rule.operator))) return '请完整配置 NTB 达标规则标签'
       if (this.form.progressEnabled && this.form.achievementTemplate === 'MGM' && this.form.mgmRules?.some((rule) => !rule.field || !rule.operator)) return '请完整配置 MGM 达标规则标签'
       if (this.form.progressEnabled && this.form.achievementTemplate === 'LAYER') {

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

@@ -1,18 +1,17 @@
 <template>
   <div class="activity-management">
-    <el-card>
+    <el-card class="activity-list-card">
       <template #header>
         <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" />
+          <el-form :model="searchForm" class="search-grid" label-width="86px">
+            <el-form-item label="活动 ID" class="search-cell">
+              <el-input v-model="searchForm.id" clearable placeholder="精确查询活动编号" />
             </el-form-item>
-            <el-form-item label="活动名称">
-              <el-input v-model="searchForm.name" clearable placeholder="对内标题/对客名称" style="width: 220px" />
+            <el-form-item label="活动名称" class="search-cell">
+              <el-input v-model="searchForm.name" clearable placeholder="对内标题/对客名称" />
             </el-form-item>
-            <el-form-item label="活动类型">
-              <el-select v-model="searchForm.category" clearable placeholder="全部" style="width: 150px">
+            <el-form-item label="活动类型" class="search-cell">
+              <el-select v-model="searchForm.category" clearable placeholder="全部">
                 <el-option label="普通活动" value="普通活动" />
                 <el-option label="卓越迎新礼" value="卓越迎新礼" />
                 <el-option label="层层礼" value="层层礼" />
@@ -20,8 +19,8 @@
                 <el-option label="信用卡活动" value="信用卡活动" />
               </el-select>
             </el-form-item>
-            <el-form-item label="活动状态">
-              <el-select v-model="searchForm.status" clearable placeholder="全部" style="width: 140px">
+            <el-form-item label="发布状态" class="search-cell">
+              <el-select v-model="searchForm.status" clearable placeholder="全部">
                 <el-option label="草稿" value="draft" />
                 <el-option label="待审核" value="pending" />
                 <el-option label="审核通过" value="approved" />
@@ -30,67 +29,62 @@
                 <el-option label="已结束" value="ended" />
               </el-select>
             </el-form-item>
-            <el-form-item label="创建人">
-              <el-input v-model="searchForm.creator" clearable placeholder="创建人" style="width: 130px" />
+            <el-form-item label="活动状态" class="search-cell">
+              <el-select v-model="searchForm.activityStatus" clearable placeholder="全部">
+                <el-option label="未开始" value="未开始" />
+                <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 label="创建人" class="search-cell">
+              <el-input v-model="searchForm.creator" clearable placeholder="创建人" />
             </el-form-item>
           </el-form>
-          </div>
-          <div class="operation-row">
+          <div class="action-row">
             <el-button type="primary" @click="handleCreate"><el-icon><Plus /></el-icon>新建活动</el-button>
+            <div class="search-actions">
+              <el-button type="primary" @click="handleSearch">查询</el-button>
+              <el-button @click="handleReset">重置</el-button>
+            </div>
           </div>
         </div>
       </template>
 
-      <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>
+      <el-table :data="pagedTableData" stripe border style="width: 100%">
+        <el-table-column label="操作" width="400" fixed="left" align="left">
+          <template #default="{ row }">
+            <div class="list-actions">
+              <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="handleView(row)">查看</el-button>
+              <el-button type="text" size="small" @click="preview(row)">预览</el-button>
+              <el-button v-if="row.status === 'draft'" type="text" size="small" @click="submitAudit(row)">提交审核</el-button>
+              <el-button v-if="row.status === 'pending'" type="text" size="small" @click="handleAudit(row)">{{ row.auditAction === 'offline' ? '审核下线' : '审核' }}</el-button>
+              <el-button v-if="row.status === 'approved'" type="text" size="small" @click="publish(row)">发布</el-button>
+              <el-button v-if="row.status === 'published'" type="text" size="small" class="offline-button" @click="requestOffline(row)">申请下线</el-button>
+            </div>
           </template>
         </el-table-column>
-        <el-table-column prop="id" label="活动 ID" width="130" />
+        <el-table-column prop="id" label="活动 ID" width="130" align="center" />
         <el-table-column prop="internalTitle" label="活动标题(对内)" min-width="170" show-overflow-tooltip />
         <el-table-column prop="name" label="活动名称(对客)" min-width="170" show-overflow-tooltip />
-        <el-table-column prop="category" label="活动类型" width="120" />
-        <el-table-column label="活动时间" width="210"><template #default="{ row }">{{ rangeText(row.activityTime) }}</template></el-table-column>
-        <el-table-column label="报名时间" width="210"><template #default="{ row }">{{ rangeText(row.signupTime) }}</template></el-table-column>
-        <el-table-column label="展示状态" width="110">
+        <el-table-column prop="category" label="活动类型" width="120" align="center" />
+        <el-table-column label="活动时间" width="210" align="center"><template #default="{ row }">{{ rangeText(row.activityTime) }}</template></el-table-column>
+        <el-table-column label="报名时间" width="210" align="center"><template #default="{ row }">{{ rangeText(row.signupTime) }}</template></el-table-column>
+        <el-table-column label="展示状态" width="110" align="center">
           <template #default="{ row }">
             <el-tag :type="row.showOnHome || row.showOnAll ? 'success' : 'info'">{{ row.showOnHome || row.showOnAll ? '对客展示' : '不展示' }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="活动状态" width="110"><template #default="{ row }"><el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag></template></el-table-column>
-        <el-table-column prop="auditStatus" label="审核状态" width="110" />
-        <el-table-column prop="creator" label="创建人" width="110" />
-        <el-table-column prop="auditor" label="审核人" width="110" />
-        <el-table-column prop="createdAt" label="创建时间" width="170" />
-        <el-table-column prop="updatedAt" label="更新时间" width="170" />
+        <el-table-column label="发布状态" width="110" align="center"><template #default="{ row }"><el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag></template></el-table-column>
+        <el-table-column label="活动状态" width="100" align="center"><template #default="{ row }"><el-tag :type="activityStatusType(row)">{{ activityStatusText(row) }}</el-tag></template></el-table-column>
+        <el-table-column prop="auditStatus" label="审核状态" width="110" align="center" />
+        <el-table-column prop="creator" label="创建人" width="110" align="center" />
+        <el-table-column prop="auditor" label="审核人" width="110" align="center" />
+        <el-table-column prop="createdAt" label="创建时间" width="170" align="center" />
+        <el-table-column prop="updatedAt" label="更新时间" width="170" align="center" />
       </el-table>
       <div class="pagination-section">
-        <span>Total {{ tableData.length }}</span>
+        <span class="total">Total {{ tableData.length }}</span>
         <el-pagination
           v-model:current-page="currentPage"
           v-model:page-size="pageSize"
@@ -111,7 +105,7 @@ export default {
   name: 'ActivityManagement',
   data() {
     return {
-      searchForm: { id: '', name: '', category: '', status: '', creator: '' },
+      searchForm: { id: '', name: '', category: '', status: '', activityStatus: '', creator: '' },
       tableData: [],
       currentPage: 1,
       pageSize: 10
@@ -129,12 +123,13 @@ export default {
     handleSearch() {
       this.tableData = activityStore.queryActivities(this.searchForm).filter((item) => {
         if (this.searchForm.creator && !String(item.creator || '').includes(this.searchForm.creator)) return false
+        if (this.searchForm.activityStatus && this.activityStatusText(item) !== this.searchForm.activityStatus) return false
         return true
       })
       this.currentPage = 1
     },
     handleReset() {
-      this.searchForm = { id: '', name: '', category: '', status: '', creator: '' }
+      this.searchForm = { id: '', name: '', category: '', status: '', activityStatus: '', creator: '' }
       this.handleSearch()
     },
     handleCreate() { this.$router.push('/customization/hsbc/activities/add') },
@@ -147,6 +142,7 @@ export default {
       if (error) return ElMessage.warning(error)
       row.status = 'pending'
       row.auditStatus = '待审核'
+      row.auditAction = ''
       ElMessage.success('已提交审核')
       this.handleSearch()
     },
@@ -155,9 +151,11 @@ export default {
       activityStore.publishActivity(row.id)
       this.handleSearch()
     },
-    async offline(row) {
-      await ElMessageBox.confirm(`确定下线活动「${row.name}」吗?`, '下线活动', { type: 'warning' })
-      activityStore.offlineActivity(row.id)
+    async requestOffline(row) {
+      await ElMessageBox.confirm(`提交后将进入下线审核流程,审核通过后活动才会下线。确定申请下线「${row.name}」吗?`, '申请下线', { type: 'warning' })
+      row.status = 'pending'
+      row.auditStatus = '下线待审核'
+      row.auditAction = 'offline'
       this.handleSearch()
     },
     validateRequired(row) {
@@ -165,13 +163,22 @@ export default {
       if (!row.internalTitle) return '活动标题(对内)必填'
       if (!row.activityTime || row.activityTime.length !== 2) return '活动时间必填'
       if (row.needSignup && (!row.signupTime || row.signupTime.length !== 2)) return '报名时间必填'
-      if (row.progressEnabled && !row.progressTemplateId) return '开启进度跟踪后必须选择进度模板'
-      if (row.progressEnabled && !row.progressRuleId) return '开启进度跟踪后必须选择达标规则'
       if (row.rewardEnabled && !row.grantPlanId) return '需要权益发放时必须选择发放计划'
       if (row.mutexEnabled && !row.mutexGroup) return '参与互斥时必须选择互斥组'
       return ''
     },
     rangeText(range) { return range && range.length === 2 ? `${range[0]} 至 ${range[1]}` : '-' },
+    activityStatusText(row) {
+      const range = row.activityTime
+      if (!Array.isArray(range) || range.length !== 2 || !range[0] || !range[1]) return '-'
+      const today = new Date().toISOString().slice(0, 10)
+      if (today < range[0]) return '未开始'
+      if (today > range[1]) return '已结束'
+      return '进行中'
+    },
+    activityStatusType(row) {
+      return { '未开始': 'info', '进行中': 'success', '已结束': 'warning' }[this.activityStatusText(row)] || 'info'
+    },
     statusText(status) {
       return { draft: '草稿', pending: '待审核', approved: '审核通过', published: '已发布', offline: '已下线', ended: '已结束', rejected: '已驳回' }[status] || status
     },
@@ -184,18 +191,36 @@ export default {
 
 <style scoped>
 .activity-management { padding:0; }
+.activity-list-card { border-radius:4px; }
 .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; }
+.search-grid {
+  display:grid;
+  grid-template-columns:repeat(4, minmax(0, 1fr));
+  column-gap:24px;
+  row-gap:20px;
+  margin-bottom:4px;
+}
+.search-grid .search-cell { margin:0; }
+.search-grid .search-cell :deep(.el-form-item__content) { width:100%; }
+.search-grid .search-cell :deep(.el-input),
+.search-grid .search-cell :deep(.el-select) { width:100%; }
+.action-row { display:flex; align-items:center; justify-content:space-between; margin-top:20px; }
+.search-actions { display:flex; align-items:center; gap:12px; }
+.activity-management :deep(.el-input:not(.el-textarea)) { --el-input-height:32px; }
+.activity-management :deep(.el-input:not(.el-textarea) .el-input__wrapper),
+.activity-management :deep(.el-select__wrapper) { min-height:32px; height:32px; }
+.activity-management :deep(.el-input:not(.el-textarea) .el-input__inner) { height:30px; line-height:30px; }
+.activity-management :deep(.el-table th.el-table__cell) { background:#fafafa; }
+.activity-management :deep(.el-table .el-table__cell) { padding:10px 0; }
 .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; }
+.pagination-section .total { color:#606266; font-size:13px; }
+.list-actions { display:inline-flex; align-items:center; justify-content:flex-start; gap:10px; width:100%; white-space:nowrap; }
+.list-actions :deep(.el-button) { margin-left:0; padding:0; }
+.offline-button { color:#f56c6c; }
+@media (max-width: 1100px) {
+  .search-grid { grid-template-columns:repeat(2, minmax(0, 1fr)); }
+}
+@media (max-width: 600px) {
+  .search-grid { grid-template-columns:1fr; }
+}
 </style>

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

@@ -5,230 +5,56 @@
         <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-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-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-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>
+          <div class="operation-row"><el-button type="primary" @click="handleCreate"><el-icon><Plus /></el-icon>新增互斥组</el-button></div>
         </div>
       </template>
 
-      <el-table :data="pagedRows" stripe style="width:100%">
-        <el-table-column label="操作" width="150" fixed="left">
+      <el-table :data="pagedRows" stripe border style="width:100%">
+        <el-table-column label="操作" width="150" fixed="left" align="center">
           <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>
+            <div class="list-actions">
+              <el-button type="text" size="small" @click="handleEdit(row)">编辑</el-button>
+              <el-button type="text" size="small" @click="toggleStatus(row)">{{ row.status === '启用' ? '停用' : '启用' }}</el-button>
+            </div>
           </template>
         </el-table-column>
-        <el-table-column prop="id" label="互斥组编号" width="120" />
+        <el-table-column prop="id" label="互斥组编号" width="120" align="center" />
         <el-table-column prop="name" label="互斥组名称" min-width="160" show-overflow-tooltip />
-        <el-table-column prop="mutexType" label="互斥类型" width="120" />
-        <el-table-column prop="businessType" label="业务类型" width="120" />
-        <el-table-column prop="scopeType" label="互斥活动范围" width="130" />
+        <el-table-column label="活动类型" width="160" align="center"><template #default="{ row }">{{ businessTypeText(row.businessType) }}</template></el-table-column>
         <el-table-column prop="activities" label="互斥活动" min-width="220" show-overflow-tooltip />
-        <el-table-column label="判断依据" min-width="190">
-          <template #default="{ row }">{{ basisText(row.judgeBasis) }}</template>
+        <el-table-column label="推荐人进度展示" width="130" align="center">
+          <template #default="{ row }"><el-tag :type="row.showReferrerProgress ? 'success' : 'info'">{{ row.showReferrerProgress ? '开启' : '关闭' }}</el-tag></template>
         </el-table-column>
-        <el-table-column label="实时拦截" width="100" align="center">
-          <template #default="{ row }">
-            <el-tag :type="row.realtimeBlock ? 'success' : 'info'">{{ row.realtimeBlock ? '开启' : '关闭' }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="T+1修正" width="100" align="center">
-          <template #default="{ row }">
-            <el-tag :type="row.tPlusOneFix ? 'warning' : 'info'">{{ row.tPlusOneFix ? '开启' : '关闭' }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="releaseRule" label="释放规则" min-width="170" show-overflow-tooltip />
-        <el-table-column prop="listStatusText" label="列表状态文案" min-width="140" show-overflow-tooltip />
-        <el-table-column label="状态" width="90" align="center">
-          <template #default="{ row }">
-            <el-tag :type="row.status === '启用' ? 'success' : 'info'">{{ row.status }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="updatedAt" label="更新时间" width="170" />
+        <el-table-column label="状态" width="90" align="center"><template #default="{ row }"><el-tag :type="row.status === '启用' ? 'success' : 'info'">{{ row.status }}</el-tag></template></el-table-column>
+        <el-table-column prop="updatedAt" label="更新时间" width="170" align="center" />
       </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"
-        />
+        <span class="total">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" :close-on-click-modal="false">
-      <el-form :model="editing" label-width="130px">
-        <el-divider content-position="left">基础规则</el-divider>
-        <el-row :gutter="16">
-          <el-col :span="12">
-            <el-form-item label="互斥组名称" required>
-              <el-input v-model="editing.name" placeholder="如:NTB新客互斥组" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="互斥类型" required>
-              <el-select v-model="editing.mutexType" style="width: 100%">
-                <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="业务类型" required>
-              <el-select v-model="editing.businessType" style="width: 100%">
-                <el-option label="NTB/MGM" value="NTB/MGM" />
-                <el-option label="NTB" value="NTB" />
-                <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="互斥活动范围" required>
-              <el-select v-model="editing.scopeType" style="width: 100%">
-                <el-option label="指定活动" value="指定活动" />
-                <el-option label="指定活动类型" value="指定活动类型" />
-                <el-option label="同业务类型全部活动" value="同业务类型全部活动" />
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="24">
-            <el-form-item label="互斥活动" required>
-              <el-input v-model="editing.activities" placeholder="填写活动名称,多个用顿号或逗号分隔" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-
-        <el-divider content-position="left">判断与拦截</el-divider>
-        <el-form-item label="判断依据" required>
-          <el-checkbox-group v-model="editing.judgeBasis">
-            <el-checkbox label="报名记录">报名记录</el-checkbox>
-            <el-checkbox label="MGM标签">MGM标签</el-checkbox>
-            <el-checkbox label="开户时间">开户时间</el-checkbox>
-            <el-checkbox label="达标状态">达标状态</el-checkbox>
-            <el-checkbox label="活动状态">活动状态</el-checkbox>
-          </el-checkbox-group>
-        </el-form-item>
-        <el-row :gutter="16">
-          <el-col :span="12">
-            <el-form-item label="报名实时拦截" required>
-              <el-switch v-model="editing.realtimeBlock" active-text="开启" inactive-text="关闭" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="T+1标签修正">
-              <el-switch v-model="editing.tPlusOneFix" active-text="开启" inactive-text="关闭" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-form-item label="拦截文案" required>
-          <el-input v-model="editing.blockText" type="textarea" :rows="3" placeholder="点击报名时弹出的拦截提示" />
-        </el-form-item>
-        <el-form-item label="T+1修正文案">
-          <el-input v-model="editing.correctionText" type="textarea" :rows="3" placeholder="次日标签更新后活动状态修正说明" />
-        </el-form-item>
-
-        <el-divider content-position="left">释放与展示</el-divider>
-        <el-row :gutter="16">
-          <el-col :span="12">
-            <el-form-item label="释放规则" required>
-              <el-select v-model="editing.releaseRule" 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="editing.listStatusText" placeholder="如:不符合参与条件" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="24">
-            <el-form-item label="详情状态文案">
-              <el-input v-model="editing.detailStatusText" type="textarea" :rows="2" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="24">
-            <el-form-item label="命中后展示规则">
-              <el-input v-model="editing.conflictDisplayRule" placeholder="如:命中后其他活动展示不符合条件" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="状态" required>
-              <el-select v-model="editing.status" style="width: 100%">
-                <el-option label="启用" value="启用" />
-                <el-option label="停用" value="停用" />
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="24">
-            <el-form-item label="备注">
-              <el-input v-model="editing.remark" type="textarea" :rows="2" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-      </el-form>
-      <template #footer>
-        <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="save">保存</el-button>
-      </template>
-    </el-dialog>
   </div>
 </template>
 
 <script>
 import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus } from '@element-plus/icons-vue'
 import activityStore from '@/store/activityStore'
 
-function emptyMutex() {
-  return {
-    name: '',
-    mutexType: '互斥报名',
-    businessType: 'NTB/MGM',
-    scopeType: '指定活动',
-    activities: '',
-    judgeBasis: ['报名记录'],
-    realtimeBlock: true,
-    tPlusOneFix: false,
-    releaseRule: '活动结束且未达标释放',
-    listStatusText: '不符合参与条件',
-    detailStatusText: '',
-    blockText: '',
-    conflictDisplayRule: '命中后其他活动展示不符合条件',
-    correctionText: '',
-    remark: '',
-    status: '启用'
-  }
-}
-
 export default {
   name: 'ActivityMutexGroup',
   data() {
     return {
-      dialogVisible: false,
-      editing: emptyMutex(),
+      Plus,
       searchForm: { name: '', status: '' },
       appliedSearch: { name: '', status: '' },
       currentPage: 1,
@@ -236,9 +62,7 @@ export default {
     }
   },
   computed: {
-    rows() {
-      return activityStore.mutexGroups
-    },
+    rows() { return activityStore.mutexGroups },
     filteredRows() {
       return this.rows.filter((row) => {
         if (this.appliedSearch.name && !String(row.name || '').includes(this.appliedSearch.name)) return false
@@ -252,31 +76,11 @@ export default {
     }
   },
   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 || '-'
-    },
-    openDialog(row) {
-      this.editing = row ? { ...emptyMutex(), ...JSON.parse(JSON.stringify(row)) } : emptyMutex()
-      if (!Array.isArray(this.editing.judgeBasis)) this.editing.judgeBasis = []
-      this.dialogVisible = true
-    },
-    save() {
-      if (!this.editing.name) return ElMessage.warning('互斥组名称必填')
-      if (!this.editing.activities) return ElMessage.warning('互斥活动必填')
-      if (!this.editing.judgeBasis.length) return ElMessage.warning('判断依据至少选择一项')
-      if (this.editing.realtimeBlock && !this.editing.blockText) return ElMessage.warning('开启报名实时拦截时,拦截文案必填')
-      activityStore.saveSimple('mutexGroups', this.editing)
-      ElMessage.success('保存成功')
-      this.dialogVisible = false
-    },
+    handleSearch() { this.appliedSearch = { ...this.searchForm }; this.currentPage = 1 },
+    handleReset() { this.searchForm = { name: '', status: '' }; this.handleSearch() },
+    handleCreate() { this.$router.push('/customization/hsbc/activity-mutex-groups/add') },
+    handleEdit(row) { this.$router.push(`/customization/hsbc/activity-mutex-groups/edit/${row.id}`) },
+    businessTypeText(value) { return Array.isArray(value) ? value.join('、') : (value || '-') },
     async toggleStatus(row) {
       const action = row.status === '启用' ? '停用' : '启用'
       await ElMessageBox.confirm(`确定${action}互斥组「${row.name}」吗?`, `${action}互斥组`, { type: 'warning' })
@@ -298,6 +102,7 @@ export default {
 .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; }
+.pagination-section .total { color:#606266; font-size:13px; }
+.list-actions { display:inline-flex; align-items:center; justify-content:center; gap:10px; width:100%; }
+.list-actions :deep(.el-button) { margin-left:0; padding:0; }
 </style>

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

@@ -0,0 +1,167 @@
+<template>
+  <div class="activity-mutex-group-edit">
+    <el-card class="activity-page-card">
+      <template #header>
+        <div class="page-header">
+          <span>{{ isEdit ? '编辑互斥组' : '新增互斥组' }}</span>
+          <el-button @click="goBack">返回</el-button>
+        </div>
+      </template>
+
+    <el-form :model="form" label-position="top" class="mutex-form">
+      <el-card shadow="never" class="section-card">
+        <template #header><div class="section-title">基础配置</div></template>
+        <el-row :gutter="30">
+          <el-col :span="12"><el-form-item label="互斥组名称" required><el-input v-model="form.name" placeholder="例如:NTB新客互斥组" /></el-form-item></el-col>
+          <el-col :span="12">
+            <el-form-item label="活动类型">
+              <el-select v-model="form.businessType" multiple clearable placeholder="请选择活动类型" style="width:100%"><el-option label="层层礼" value="层层礼" /><el-option label="NTB" value="NTB" /><el-option label="MGM" value="MGM" /><el-option label="普通活动" value="普通活动" /></el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="互斥活动">
+              <el-select v-model="form.activityIds" multiple filterable clearable placeholder="请选择已创建的活动" style="width:100%">
+                <el-option v-for="activity in activityOptions" :key="activity.id" :label="`${activity.name}(${activity.id})`" :value="activity.id" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-card>
+
+      <el-card shadow="never" class="section-card">
+        <template #header><div class="section-title">MGM推荐活动设置</div></template>
+        <el-form-item label="是否向推荐人展示推荐人进度" class="referrer-progress-switch-item">
+          <el-switch v-model="form.showReferrerProgress" active-text="开启" inactive-text="关闭" />
+        </el-form-item>
+        <el-form-item label="报名拦截文案">
+          <el-input v-model="form.signupInterceptTip" type="textarea" :rows="3" placeholder="例如:您已经于 {REF_DATE} 参与【{ACT}】活动。根据活动规则,您无法再继续报名【{CUR}】活动。" />
+          <div class="variable-tip">
+            <span class="variable-tip-label">可用变量:</span>
+            <span class="variable-token">{ACT}</span><span>已参与活动名称</span>
+            <span class="variable-token">{CUR}</span><span>当前活动名称</span>
+            <span class="variable-token">{JOIN_DATE}</span><span>参与日期</span>
+            <span class="variable-token">{REF_DATE}</span><span>推荐日期</span>
+          </div>
+        </el-form-item>
+        <el-form-item label="无法继续参与文案(被推荐人)">
+          <el-input v-model="form.referrerProgressTip" type="textarea" :rows="3" placeholder="例如:您已参与【{ACT}】活动,暂无法继续参与【{CUR}】活动" />
+          <div class="variable-tip">
+            <span class="variable-tip-label">可用变量:</span>
+            <span class="variable-token">{ACT}</span><span>已参与活动名称</span>
+            <span class="variable-token">{CUR}</span><span>当前活动名称</span>
+            <span class="variable-token">{JOIN_DATE}</span><span>参与日期</span>
+            <span class="variable-token">{REF_DATE}</span><span>推荐日期</span>
+          </div>
+        </el-form-item>
+        <el-form-item label="未达标文案(推荐人)">
+          <el-input v-model="form.referrerUnreachedTip" type="textarea" :rows="3" placeholder="例如:您推荐的客户暂未达标,请关注后续活动进度" />
+          <div class="variable-tip">
+            <span class="variable-tip-label">可用变量:</span>
+            <span class="variable-token">{ACT}</span><span>已参与活动名称</span>
+            <span class="variable-token">{CUR}</span><span>当前活动名称</span>
+            <span class="variable-token">{JOIN_DATE}</span><span>参与日期</span>
+            <span class="variable-token">{REF_DATE}</span><span>推荐日期</span>
+          </div>
+        </el-form-item>
+      </el-card>
+
+      <el-card shadow="never" class="section-card">
+        <template #header><div class="section-title">其他活动设置</div></template>
+        <el-form-item label="报名拦截文案">
+          <el-input v-model="form.otherSignupInterceptTip" type="textarea" :rows="3" placeholder="例如:您已参与【{ACT}】活动,无法继续报名【{CUR}】活动" />
+          <div class="variable-tip">
+            <span class="variable-tip-label">可用变量:</span>
+            <span class="variable-token">{ACT}</span><span>已参与活动名称</span>
+            <span class="variable-token">{CUR}</span><span>当前活动名称</span>
+            <span class="variable-token">{JOIN_DATE}</span><span>参与日期</span>
+            <span class="variable-token">{REF_DATE}</span><span>推荐日期</span>
+          </div>
+        </el-form-item>
+      </el-card>
+
+      <el-card shadow="never" class="section-card">
+        <template #header><div class="section-title">互斥组状态与备注</div></template>
+        <el-row :gutter="30">
+          <el-col :span="12"><el-form-item label="状态" required><el-select v-model="form.status" style="width:100%"><el-option label="启用" value="启用" /><el-option label="停用" value="停用" /></el-select></el-form-item></el-col>
+          <el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="2" /></el-form-item></el-col>
+        </el-row>
+      </el-card>
+    </el-form>
+
+    <div class="action-bar">
+      <el-button @click="goBack">取消</el-button>
+      <el-button type="primary" @click="save">保存</el-button>
+    </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { ElMessage } from 'element-plus'
+import activityStore from '@/store/activityStore'
+
+function emptyMutex() {
+  return {
+    name: '', businessType: [], activities: '', activityIds: [], showReferrerProgress: false,
+    referrerProgressTip: '', signupInterceptTip: '', referrerUnreachedTip: '', otherSignupInterceptTip: '', remark: '', status: '启用'
+  }
+}
+
+export default {
+  name: 'ActivityMutexGroupEdit',
+  data() { return { form: emptyMutex() } },
+  computed: {
+    isEdit() { return this.$route.name === 'ActivityMutexGroupEdit' },
+    activityOptions() { return activityStore.activities }
+  },
+  created() {
+    if (!this.isEdit) return
+    const row = activityStore.mutexGroups.find((item) => item.id === this.$route.params.id)
+    if (!row) return this.goBack()
+    this.form = { ...emptyMutex(), ...JSON.parse(JSON.stringify(row)) }
+    this.normalizeBusinessTypes()
+    if (!Array.isArray(this.form.activityIds)) {
+      const legacyNames = String(this.form.activities || '').split(/[、,,]/).map((item) => item.trim()).filter(Boolean)
+      this.form.activityIds = this.activityOptions.filter((activity) => legacyNames.includes(activity.name)).map((activity) => activity.id)
+    }
+  },
+  methods: {
+    save() {
+      if (!this.form.name) return ElMessage.warning('互斥组名称必填')
+      if (this.form.showReferrerProgress && !this.form.referrerProgressTip) return ElMessage.warning('请填写推荐人进度提示文案')
+      this.form.activities = this.form.activityIds.map((id) => this.activityOptions.find((activity) => activity.id === id)?.name || id).join('、')
+      activityStore.saveSimple('mutexGroups', this.form)
+      ElMessage.success('保存成功')
+      this.goBack()
+    },
+    normalizeBusinessTypes() {
+      if (Array.isArray(this.form.businessType)) return
+      this.form.businessType = String(this.form.businessType || '')
+        .split(/[、,, /]/)
+        .map((item) => item.trim())
+        .filter(Boolean)
+    },
+    goBack() { this.$router.push('/customization/hsbc/activity-mutex-groups') }
+  }
+}
+</script>
+
+<style scoped>
+.activity-mutex-group-edit { padding: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; }
+.page-header { display:flex; justify-content:space-between; align-items:center; font-weight:600; }
+.section-card { margin-bottom:20px; border:1px solid #ebeef5; border-radius:4px; 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; }
+.section-title { color:#303133; font-size:16px; font-weight:600; }
+.mutex-form :deep(.el-form-item) { margin-bottom:24px; }
+.mutex-form :deep(.el-form-item__label) { height:auto; margin-bottom:8px; color:#303133; font-weight:600; line-height:22px; white-space:nowrap; }
+.mutex-form :deep(.el-form-item__content) { line-height:normal; }
+.variable-tip { display:flex; flex-wrap:wrap; align-items:center; gap:6px 8px; margin-top:8px; color:#909399; font-size:12px; line-height:20px; }
+.variable-tip-label { color:#606266; }
+.variable-token { padding:0 5px; color:#409eff; background:#ecf5ff; border:1px solid #d9ecff; border-radius:3px; font-family:Consolas, Menlo, monospace; }
+.action-bar { display:flex; justify-content:center; gap:12px; padding:16px 20px; background:#fff; border:1px solid #ebeef5; border-radius:4px; box-shadow:0 1px 4px rgba(0,0,0,.04); }
+.action-bar .el-button { min-width:100px; }
+@media (max-width:1200px) { .activity-mutex-group-edit :deep(.el-col) { max-width:100%; flex:0 0 100%; } }
+</style>

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

@@ -121,6 +121,13 @@
             <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="12"><ReadonlyField label="Campaign ID" :value="campaignName(form.campaignId)" /></el-col>
+            <el-col :span="12"><ReadonlyField label="Channel ID" :value="channelName(form.channelId)" /></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">
@@ -219,9 +226,9 @@
 
       <div v-show="activeStep === 3" class="step-pane">
         <el-card shadow="never" class="section-card">
-          <template #header><div class="section-title">活动模板</div></template>
+          <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 :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>
@@ -263,7 +270,7 @@
           </el-card>
         </template>
 
-        <template v-else>
+        <template v-else-if="form.achievementTemplate === 'LAYER'">
           <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">
@@ -333,7 +340,9 @@
 import { h } from 'vue'
 import { ArrowLeft, ArrowRight, Bell, InfoFilled, Medal, Setting, User } from '@element-plus/icons-vue'
 import request from '@/utils/request'
+import { fetchLinkOptions } from '@/utils/linkOptions'
 import activityStore from '@/store/activityStore'
+import smsTemplateStore from '@/store/smsTemplateStore'
 
 const emptyText = '-'
 
@@ -456,6 +465,8 @@ export default {
       layerCopyActiveTab: '0',
       achievementLayerActiveTab: '0',
       assemblyPages: [],
+      campaignOptions: [],
+      channelOptions: [],
       stepItems: [
         { title: '基础信息', icon: InfoFilled },
         { title: '展示规则', icon: Setting },
@@ -474,7 +485,7 @@ export default {
     },
     crowds() { return activityStore.activityCrowds },
     grantPlans() { return activityStore.activityGrantPlans },
-    notifications() { return activityStore.notifications },
+    notifications() { return smsTemplateStore.state.list },
     isLayerActivity() {
       return this.form?.category === '层层礼' || this.form?.achievementTemplate === 'LAYER'
     },
@@ -504,6 +515,7 @@ export default {
   },
   created() {
     this.fetchAssemblyPages()
+    this.fetchChannelOptions()
   },
   methods: {
     async fetchAssemblyPages() {
@@ -514,6 +526,16 @@ export default {
         this.assemblyPages = []
       }
     },
+    async fetchChannelOptions() {
+      try {
+        const { campaignOptions, channelOptions } = await fetchLinkOptions()
+        this.campaignOptions = campaignOptions
+        this.channelOptions = channelOptions
+      } catch (e) {
+        this.campaignOptions = []
+        this.channelOptions = []
+      }
+    },
     valueText(value) {
       return value === '' || value === undefined || value === null ? emptyText : String(value)
     },
@@ -554,6 +576,14 @@ export default {
         return target ? `${target.name}(${target.id})` : id
       }).join('、')
     },
+    campaignName(id) {
+      if (!id) return emptyText
+      return this.campaignOptions.find((item) => item.value === id)?.label || id
+    },
+    channelName(id) {
+      if (!id) return emptyText
+      return this.channelOptions.find((item) => item.value === id)?.label || id
+    },
     notificationName(id) {
       if (!id) return emptyText
       const target = this.notifications.find((item) => item.id === id)