Kaynağa Gözat

统一权益编号与发放计划 ID 格式,新增权益有效期字段并完善发放报表筛选

- 权益编号统一加 Q 前缀,发放计划 ID 改为 P + 6 位序号
- 权益新增"权益有效期"字段,支持取自发放计划或自定义输入
- 新建权益支持 copyFrom 复制源权益信息
- 客户权益发放、立减金发放报表新增多维度筛选与导出

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fade 1 ay önce
ebeveyn
işleme
51a43ea4e6

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

@@ -12,7 +12,10 @@
       "Bash(npm run *)",
       "Bash(grep -n \"  },$\\\\|computed:\\\\|methods:\\\\|^  }\" /Users/apple/workspace/rongshu/pc/src/views/AddEquityProduct.vue)",
       "Bash(sed -i '' 's/label=\"首页\"/label=\"权益首页\\\\/列表页\"/g; s/pages.push\\(.首页.\\)/pages.push\\(\"权益首页\\\\/列表页\"\\)/g' components/ShowPagesDialog.vue views/AddEquityProduct.vue views/EditEquityProduct.vue views/ViewEquityProduct.vue views/AuditEquityProduct.vue views/EquityProducts.vue views/CreditCardEquity.vue)",
-      "Bash(grep -rn \"label=\\\\\"首页\\\\\"\\\\|pages.push\\('首页'\\)\\\\|pages.push\\(\\\\\"首页\\\\\"\\)\" .)"
+      "Bash(grep -rn \"label=\\\\\"首页\\\\\"\\\\|pages.push\\('首页'\\)\\\\|pages.push\\(\\\\\"首页\\\\\"\\)\" .)",
+      "Bash(npx eslint *)",
+      "Bash(echo \"exit=$?\")",
+      "Bash(openssl s_client -connect hub.banwotech.cn:443 -servername hub.banwotech.cn)"
     ],
     "additionalDirectories": []
   }

+ 28 - 30
pc/src/mock/index.js

@@ -370,7 +370,7 @@ Mock.mock(/\/api\/campaign\/delete/, 'post', (options) => {
 //   已上架+下架待审核   = 已上架·下架待审核
 const equityList = [
   {
-    productId: '100001',
+    productId: 'Q100001',
     equityName: '权益A',
     source: '银行自有',
     jumpMethod: '-',
@@ -391,7 +391,7 @@ const equityList = [
     assemblyPages: []
   },
   {
-    productId: '100002',
+    productId: 'Q100002',
     equityName: '权益B',
     source: '第三方',
     jumpMethod: '没有',
@@ -412,7 +412,7 @@ const equityList = [
     assemblyPages: []
   },
   {
-    productId: '100003',
+    productId: 'Q100003',
     equityName: '权益C',
     source: '第三方',
     jumpMethod: '有',
@@ -433,7 +433,7 @@ const equityList = [
     assemblyPages: []
   },
   {
-    productId: '100004',
+    productId: 'Q100004',
     equityName: '权益D',
     source: '第三方',
     jumpMethod: '有',
@@ -454,7 +454,7 @@ const equityList = [
     assemblyPages: []
   },
   {
-    productId: '100005',
+    productId: 'Q100005',
     equityName: '权益E',
     source: '银行自有',
     jumpMethod: '-',
@@ -475,7 +475,7 @@ const equityList = [
     assemblyPages: []
   },
   {
-    productId: '100006',
+    productId: 'Q100006',
     equityName: '权益F(草稿)',
     source: '银行自有',
     jumpMethod: '-',
@@ -505,7 +505,7 @@ const auditList = [
     auditType: 'new',
     bizModule: 'equity_product',
     bizModuleName: '借记卡权益',
-    bizId: '100002',
+    bizId: 'Q100002',
     bizName: '权益B',
     snapshot: {
       equityName: '权益B',
@@ -532,7 +532,7 @@ const auditList = [
     auditType: 'change',
     bizModule: 'equity_product',
     bizModuleName: '借记卡权益',
-    bizId: '100003',
+    bizId: 'Q100003',
     bizName: '权益C',
     snapshot: {
       equityName: '权益C(变更)',
@@ -559,7 +559,7 @@ const auditList = [
     auditType: 'offshelf',
     bizModule: 'equity_product',
     bizModuleName: '借记卡权益',
-    bizId: '100004',
+    bizId: 'Q100004',
     bizName: '权益D',
     snapshot: {
       equityName: '权益D',
@@ -586,7 +586,7 @@ const auditList = [
     auditType: 'new',
     bizModule: 'equity_product',
     bizModuleName: '借记卡权益',
-    bizId: '100001',
+    bizId: 'Q100001',
     bizName: '权益A',
     snapshot: {
       equityName: '权益A',
@@ -677,7 +677,7 @@ Mock.mock(/\/api\/equity\/save-draft/, 'post', (options) => {
   }
   // 新建草稿
   equityIdCounter += 1
-  const newId = String(equityIdCounter)
+  const newId = 'Q' + String(equityIdCounter)
   const newRow = {
     productId: newId,
     equityName: body.equityName || '未命名权益',
@@ -721,7 +721,7 @@ Mock.mock(/\/api\/equity\/submit/, 'post', (options) => {
   // 新建 + 直接提审
   if (!cur) {
     equityIdCounter += 1
-    const newId = String(equityIdCounter)
+    const newId = 'Q' + String(equityIdCounter)
     const newRow = {
       productId: newId,
       equityName: body.equityName || '未命名权益',
@@ -970,39 +970,37 @@ Mock.mock(/\/api\/audit\/reject/, 'post', (options) => {
 // =============================================
 const equityGrantCampaignList = [
   {
-    id: 'GC202604010001',
-    name: '春季会员尊享礼遇',
-    startTime: '2026-04-01 00:00:00',
-    endTime: '2026-06-30 23:59:59',
-    crowdList: ['A0000120251224'],
-    equityList: ['100001', '100003'],
-    grantCycle: 'monthly',
-    grantQuantity: 2,
-    validity: { type: 'fixed', fixedRange: ['2026-04-01', '2026-06-30'], relativeDays: 30 },
-    createTime: '2026-03-25 10:20:30',
-    updateTime: '2026-03-25 10:20:30'
-  },
-  {
-    id: 'GC202605010001',
+    id: 'P000002',
     name: '夏日畅享权益月',
     startTime: '2026-05-01 00:00:00',
     endTime: '2026-08-31 23:59:59',
     crowdList: [],
-    equityList: ['100001', '100004', '100005'],
+    equityList: ['Q100001', 'Q100004', 'Q100005'],
     grantCycle: 'total',
     grantQuantity: 1,
     validity: { type: 'relative', fixedRange: [], relativeDays: 30 },
     createTime: '2026-04-20 14:10:00',
     updateTime: '2026-04-20 14:10:00'
+  },
+  {
+    id: 'P000001',
+    name: '春季会员尊享礼遇',
+    startTime: '2026-04-01 00:00:00',
+    endTime: '2026-06-30 23:59:59',
+    crowdList: ['A0000120251224'],
+    equityList: ['Q100001', 'Q100003'],
+    grantCycle: 'monthly',
+    grantQuantity: 2,
+    validity: { type: 'fixed', fixedRange: ['2026-04-01', '2026-06-30'], relativeDays: 30 },
+    createTime: '2026-03-25 10:20:30',
+    updateTime: '2026-03-25 10:20:30'
   }
 ]
 let equityGrantCampaignCounter = equityGrantCampaignList.length
 
 function nextGrantCampaignId() {
   equityGrantCampaignCounter += 1
-  const d = new Date()
-  const ymd = d.getFullYear().toString() + String(d.getMonth() + 1).padStart(2, '0') + String(d.getDate()).padStart(2, '0')
-  return 'GC' + ymd + String(equityGrantCampaignCounter).padStart(4, '0')
+  return 'P' + String(equityGrantCampaignCounter).padStart(6, '0')
 }
 
 Mock.mock(/\/api\/equity-grant-campaign\/list/, 'get', (options) => {

+ 196 - 0
pc/src/views/AddEquityProduct.vue

@@ -243,6 +243,33 @@
                 </el-form-item>
               </el-col>
             </el-row>
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="权益有效期" required>
+                  <template v-if="form.source === '银行自有' || form.source === '荣数商品'">
+                    <el-radio-group v-model="form.validityType">
+                      <el-radio label="fromGrantPlan">取自发放计划</el-radio>
+                      <el-radio label="custom">自定义输入</el-radio>
+                    </el-radio-group>
+                    <el-input
+                      v-if="form.validityType === 'custom'"
+                      v-model="form.validityText"
+                      maxlength="50"
+                      show-word-limit
+                      placeholder="请输入权益有效期"
+                      style="margin-top: 8px;"
+                    ></el-input>
+                  </template>
+                  <el-input
+                    v-else
+                    v-model="form.validityText"
+                    maxlength="50"
+                    show-word-limit
+                    placeholder="请输入权益有效期"
+                  ></el-input>
+                </el-form-item>
+              </el-col>
+            </el-row>
             <el-row :gutter="20">
               <el-col :span="12">
                 <el-form-item label="权益内容" required>
@@ -576,6 +603,8 @@ export default {
         listIcon: [],
         mainImage: [],
         introduction: '',
+        validityType: 'fromGrantPlan',
+        validityText: '',
         overview: '',
         details: '',
         smsEnabled: false,
@@ -610,7 +639,171 @@ export default {
       return false
     }
   },
+  created() {
+    const copyFrom = this.$route.query.copyFrom
+    if (copyFrom) {
+      this.loadCopySource(copyFrom)
+    }
+  },
   methods: {
+    loadCopySource(id) {
+      const mockMap = {
+        'Q100001': {
+          source: '银行自有',
+          externalJumpMethod: '',
+          externalLink: '',
+          customLink: '',
+          miniAppId: '',
+          miniAppPath: '',
+          skuNo: 'SKU001',
+          equityName: '权益A',
+          subTitle: '副标题示例',
+          listIcon: [],
+          mainImage: [],
+          overview: '<p>权益内容内容</p>',
+          details: '<p>权益说明内容</p>',
+          smsEnabled: true,
+          smsTemplateId: 'SMS_TPL_001',
+          smsTemplate: '【银行】您购买的${productName}已支付成功,卡号:${exchangeNum},卡密:${exchangePwd},有效期至${expireDate},详情:${qrCodeDownPaths}',
+          successButtonText: '马上去使用',
+          showTimeEnabled: true,
+          showStartTime: '2026-01-01 00:00:00',
+          showEndTime: '2026-12-31 23:59:59',
+          showGroupEnabled: false,
+          showGroupTagIds: [],
+          claimTimeEnabled: true,
+          claimStartTime: '2026-01-01 00:00:00',
+          claimEndTime: '2026-12-31 23:59:59',
+          claimGroupEnabled: false,
+          claimGroupTagId: '',
+          claimDenyMessage: '',
+          claimDenyButtonText: '',
+          claimDenyButtonLink: ''
+        },
+        'Q100002': {
+          source: '第三方',
+          externalJumpMethod: '没有',
+          externalLink: '太古地产',
+          customLink: '',
+          miniAppId: '',
+          miniAppPath: '',
+          skuNo: 'SKU002',
+          equityName: '权益B',
+          subTitle: '外部列表页副标题',
+          listIcon: [],
+          mainImage: [],
+          overview: '',
+          details: '',
+          smsEnabled: false,
+          smsTemplateId: '',
+          smsTemplate: '',
+          successButtonText: '',
+          showTimeEnabled: false,
+          showStartTime: '',
+          showEndTime: '',
+          showGroupEnabled: true,
+          showGroupTagIds: ['客群A'],
+          claimTimeEnabled: true,
+          claimStartTime: '2026-02-01 00:00:00',
+          claimEndTime: '2026-11-30 23:59:59',
+          claimGroupEnabled: true,
+          claimGroupIsPotential: false,
+          claimGroupTagId: '客群奖励活动A',
+          claimDenyMessage: '您暂不满足领取条件',
+          claimDenyButtonText: '去升级',
+          claimDenyButtonLink: 'https://example.com/upgrade'
+        },
+        'Q100003': {
+          source: '第三方',
+          externalJumpMethod: '有',
+          externalLink: '龙腾',
+          customLink: '',
+          miniAppId: '',
+          miniAppPath: '',
+          skuNo: 'SKU003',
+          equityName: '权益C',
+          subTitle: '外部详情页副标题',
+          listIcon: [],
+          mainImage: [],
+          overview: '<p>外部详情页权益内容</p>',
+          details: '<p>外部详情页权益说明</p>',
+          smsEnabled: false,
+          smsTemplateId: '',
+          smsTemplate: '',
+          successButtonText: '',
+          showTimeEnabled: false,
+          showStartTime: '',
+          showEndTime: '',
+          showGroupEnabled: false,
+          showGroupTagIds: [],
+          claimTimeEnabled: false,
+          claimStartTime: '',
+          claimEndTime: '',
+          claimGroupEnabled: false,
+          claimGroupTagId: '',
+          claimDenyMessage: '',
+          claimDenyButtonText: '',
+          claimDenyButtonLink: ''
+        },
+        'Q100004': {
+          source: '第三方',
+          externalJumpMethod: '有',
+          externalLink: 'h5链接',
+          customLink: 'https://example.com/custom',
+          skuNo: 'SKU004',
+          equityName: '权益D',
+          subTitle: '外部自定义链接副标题',
+          listIcon: [],
+          mainImage: [],
+          overview: '<p>自定义链接权益内容</p>',
+          details: '<p>自定义链接权益说明</p>',
+          smsEnabled: false,
+          smsTemplateId: '',
+          smsTemplate: '',
+          successButtonText: '',
+          showTimeEnabled: false,
+          showStartTime: '',
+          showEndTime: '',
+          showGroupEnabled: false,
+          showGroupTagIds: [],
+          claimTimeEnabled: false,
+          claimStartTime: '',
+          claimEndTime: '',
+          claimGroupEnabled: false,
+          claimGroupTagId: '',
+          claimDenyMessage: '',
+          claimDenyButtonText: '',
+          claimDenyButtonLink: ''
+        }
+      }
+
+      request.get(`/api/equity/detail?id=${id}`).then(res => {
+        const core = res.data || {}
+        const rich = mockMap[id] || {}
+        const merged = { ...rich }
+        const coreFields = ['equityName', 'source', 'productType', 'skuNo', 'merchantName', 'subTitle']
+        coreFields.forEach(k => {
+          if (core[k] !== undefined && core[k] !== null && core[k] !== '') {
+            merged[k] = core[k]
+          }
+        })
+        if (core.source === '第三方') {
+          if (core.jumpMethod) merged.externalJumpMethod = core.jumpMethod
+          if (core.jumpLink) merged.externalLink = core.jumpLink
+        }
+        if (core.showOnHome) merged.showOnHome = core.showOnHome
+        if (core.showOnAssembly) merged.showOnAssembly = core.showOnAssembly
+        if (Array.isArray(core.assemblyPages)) merged.assemblyPages = [...core.assemblyPages]
+        // 复制后清空标识字段,让用户重新命名并由后端生成新编号
+        delete merged.productId
+        Object.keys(merged).forEach(k => {
+          this.$set(this.form, k, merged[k])
+        })
+        this.$message.success(`已复制「${core.equityName || id}」的内容,请确认后保存`)
+      }).catch(() => {
+        this.$message.warning('未能加载源权益信息,将以空白表单创建')
+      })
+    },
     nextStep() {
       if (this.activeStep < 2) {
         this.activeStep++
@@ -623,6 +816,7 @@ export default {
     },
     buildPayload() {
       const f = this.form
+      const isCustomValidity = f.source === '第三方' || f.validityType === 'custom'
       return {
         equityName: f.equityName,
         source: f.source,
@@ -635,6 +829,8 @@ export default {
         exchangeMethod: f.source === '第三方'
           ? (f.externalJumpMethod === '有' ? '第三方(详情页跳转)' : '第三方(列表跳转)')
           : '权益平台',
+        validityType: isCustomValidity ? 'custom' : 'fromGrantPlan',
+        validityText: isCustomValidity ? (f.validityText || '') : '',
         showOnHome: f.showOnHome,
         showOnAssembly: f.showOnAssembly,
         assemblyPages: [...(f.assemblyPages || [])]

+ 16 - 4
pc/src/views/AuditDetail.vue

@@ -101,6 +101,16 @@
                 </el-form-item>
               </el-col>
             </el-row>
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="权益有效期">
+                  <el-input
+                    :value="form.validityType === 'fromGrantPlan' ? '取自发放计划' : (form.validityText || '-')"
+                    readonly
+                  ></el-input>
+                </el-form-item>
+              </el-col>
+            </el-row>
             <el-row :gutter="20">
               <el-col :span="12">
                 <el-form-item label="权益内容">
@@ -256,7 +266,7 @@
 import request from '@/utils/request'
 
 const detailMockMap = {
-  100001: {
+  'Q100001': {
     listIcon: [], mainImage: [],
     introduction: '权益A 介绍内容示例',
     overview: '<p>权益内容内容</p>',
@@ -272,7 +282,7 @@ const detailMockMap = {
     claimEndTime: '2026-12-31 23:59:59',
     claimGroupEnabled: false
   },
-  100002: {
+  'Q100002': {
     externalJumpMethod: '没有', externalLink: '太古地产',
     listIcon: [], mainImage: [], introduction: '',
     overview: '', details: '',
@@ -284,7 +294,7 @@ const detailMockMap = {
     claimEndTime: '2026-11-30 23:59:59',
     claimGroupEnabled: true
   },
-  100003: {
+  'Q100003': {
     externalJumpMethod: '有', externalLink: '龙腾',
     listIcon: [], mainImage: [],
     introduction: '权益C 介绍内容示例',
@@ -294,7 +304,7 @@ const detailMockMap = {
     claimTimeEnabled: false,
     claimGroupEnabled: false
   },
-  100004: {
+  'Q100004': {
     externalJumpMethod: '有', externalLink: 'h5链接',
     listIcon: [], mainImage: [],
     introduction: '权益D 介绍内容示例',
@@ -324,6 +334,8 @@ export default {
         listIcon: [],
         mainImage: [],
         introduction: '',
+        validityType: 'fromGrantPlan',
+        validityText: '',
         overview: '',
         details: '',
         successButtonText: '',

+ 16 - 4
pc/src/views/AuditEquityProduct.vue

@@ -200,6 +200,16 @@
                 </el-form-item>
               </el-col>
             </el-row>
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="权益有效期">
+                  <el-input
+                    :value="form.validityType === 'fromGrantPlan' ? '取自发放计划' : (form.validityText || '-')"
+                    readonly
+                  ></el-input>
+                </el-form-item>
+              </el-col>
+            </el-row>
             <el-row :gutter="20">
               <el-col :span="12">
                 <el-form-item label="权益内容">
@@ -435,6 +445,8 @@ export default {
         listIcon: [],
         mainImage: [],
         introduction: '',
+        validityType: 'fromGrantPlan',
+        validityText: '',
         overview: '',
         details: '',
         smsEnabled: false,
@@ -477,7 +489,7 @@ export default {
       const id = this.$route.params.id
       // TODO: 根据 id 从接口获取数据,这里用 mock 数据示例
       const mockMap = {
-        100001: {
+        'Q100001': {
           source: '银行自有',
           externalJumpMethod: '',
           externalLink: '',
@@ -509,7 +521,7 @@ export default {
           claimDenyButtonText: '',
           claimDenyButtonLink: ''
         },
-        100002: {
+        'Q100002': {
           source: '第三方',
           externalJumpMethod: '没有',
           externalLink: '太古地产',
@@ -542,7 +554,7 @@ export default {
           claimDenyButtonText: '了解更多',
           claimDenyButtonLink: 'https://example.com'
         },
-        100003: {
+        'Q100003': {
           source: '第三方',
           externalJumpMethod: '有',
           externalLink: '龙腾',
@@ -574,7 +586,7 @@ export default {
           claimDenyButtonText: '',
           claimDenyButtonLink: ''
         },
-        100004: {
+        'Q100004': {
           source: '第三方',
           externalJumpMethod: '有',
           externalLink: 'h5链接',

+ 94 - 27
pc/src/views/CashDiscountGrant.vue

@@ -2,21 +2,62 @@
   <div class="cash-discount-grant">
     <el-card>
       <div slot="header" class="search-section">
-        <div class="search-row">
-          <el-form :model="searchForm" label-width="70px" inline>
-            <el-form-item label="客户号:">
-              <el-input v-model="searchForm.customerId" placeholder="请输入客户号" clearable style="width: 220px;"></el-input>
-            </el-form-item>
-            <div class="button-group" style="margin-left: 20px;">
+        <el-form :model="searchForm" label-width="100px">
+          <el-row :gutter="16">
+            <el-col :span="8">
+              <el-form-item label="客户号:">
+                <el-input v-model="searchForm.customerId" placeholder="请输入客户号" clearable></el-input>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="立减金类型:">
+                <el-select v-model="searchForm.discountType" placeholder="请选择立减金类型" clearable style="width: 100%;">
+                  <el-option v-for="item in discountTypeOptions" :key="item" :label="item" :value="item"></el-option>
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="发放时间:">
+                <el-date-picker
+                  v-model="searchForm.grantDateRange"
+                  type="daterange"
+                  range-separator="至"
+                  start-placeholder="开始日期"
+                  end-placeholder="结束日期"
+                  value-format="yyyy-MM-dd"
+                  style="width: 100%;"
+                ></el-date-picker>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="16">
+            <el-col :span="8">
+              <el-form-item label="是否领取:">
+                <el-select v-model="searchForm.claimed" placeholder="请选择" clearable style="width: 100%;">
+                  <el-option label="是" value="是"></el-option>
+                  <el-option label="否" value="否"></el-option>
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="是否使用:">
+                <el-select v-model="searchForm.used" placeholder="请选择" clearable style="width: 100%;">
+                  <el-option label="是" value="是"></el-option>
+                  <el-option label="否" value="否"></el-option>
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8" class="action-col">
               <el-button type="primary" @click="handleSearch">查询</el-button>
               <el-button @click="handleReset">重置</el-button>
               <el-button type="success" icon="el-icon-download" @click="handleExport">导出</el-button>
-            </div>
-          </el-form>
-        </div>
+            </el-col>
+          </el-row>
+        </el-form>
       </div>
 
       <el-table :data="pagedData" stripe border style="width: 100%;" v-loading="loading">
+        <el-table-column prop="grantTime" label="发放时间" width="170"></el-table-column>
         <el-table-column prop="customerId" label="客户号" width="160"></el-table-column>
         <el-table-column prop="discountType" label="立减金类型" width="130"></el-table-column>
         <el-table-column prop="binRestriction" label="是否有bin卡限制" width="140">
@@ -24,20 +65,19 @@
             <el-tag :type="scope.row.binRestriction === '是' ? 'warning' : 'info'" size="small">{{ scope.row.binRestriction }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="grantDate" label="发放日期" width="120"></el-table-column>
         <el-table-column prop="grantAmount" label="发放金额" width="110">
           <template slot-scope="scope">
             <span>¥{{ scope.row.grantAmount }}</span>
           </template>
         </el-table-column>
         <el-table-column prop="grantReason" label="发放事由" min-width="160" show-overflow-tooltip></el-table-column>
-        <el-table-column prop="validPeriod" label="使用有效期" width="220"></el-table-column>
         <el-table-column prop="claimed" label="是否领取" width="100">
           <template slot-scope="scope">
             <el-tag :type="scope.row.claimed === '是' ? 'success' : 'danger'" size="small">{{ scope.row.claimed }}</el-tag>
           </template>
         </el-table-column>
         <el-table-column prop="claimTime" label="领取时间" width="160"></el-table-column>
+        <el-table-column prop="validPeriod" label="使用有效期" width="220"></el-table-column>
         <el-table-column prop="used" label="是否使用" width="100">
           <template slot-scope="scope">
             <el-tag :type="scope.row.used === '是' ? 'success' : 'danger'" size="small">{{ scope.row.used }}</el-tag>
@@ -83,8 +123,8 @@ function generateMockData() {
   const amounts = [10, 20, 30, 50, 88, 100, 128, 200]
 
   for (let i = 1; i <= 65; i++) {
-    const grantDate = randomDate(base, 90)
-    const validStart = grantDate
+    const grantTime = randomDateTime(base, 90)
+    const grantDate = grantTime.slice(0, 10)
     const validEnd = new Date(grantDate)
     validEnd.setDate(validEnd.getDate() + 30 + Math.floor(Math.random() * 60))
     const validEndStr = `${validEnd.getFullYear()}-${pad(validEnd.getMonth() + 1)}-${pad(validEnd.getDate())}`
@@ -97,17 +137,18 @@ function generateMockData() {
       customerId: 'CUST' + String(100000 + Math.floor(Math.random() * 900000)),
       discountType: DISCOUNT_TYPES[Math.floor(Math.random() * DISCOUNT_TYPES.length)],
       binRestriction: Math.random() > 0.5 ? '是' : '否',
+      grantTime: grantTime,
       grantDate: grantDate,
       grantAmount: amounts[Math.floor(Math.random() * amounts.length)],
       grantReason: GRANT_REASONS[Math.floor(Math.random() * GRANT_REASONS.length)],
-      validPeriod: `${validStart} 至 ${validEndStr}`,
+      validPeriod: claimed === '是' ? `${grantDate} 至 ${validEndStr}` : '-',
       claimed: claimed,
       claimTime: claimed === '是' ? randomDateTime(base, 60) : '-',
       used: used,
       useTime: used === '是' ? randomDateTime(base, 30) : '-'
     })
   }
-  list.sort((a, b) => b.grantDate.localeCompare(a.grantDate))
+  list.sort((a, b) => b.grantTime.localeCompare(a.grantTime))
   return list
 }
 
@@ -115,18 +156,41 @@ export default {
   name: 'CashDiscountGrant',
   data() {
     return {
-      searchForm: { customerId: '' },
+      searchForm: {
+        customerId: '',
+        discountType: '',
+        grantDateRange: [],
+        claimed: '',
+        used: ''
+      },
       loading: false,
       currentPage: 1,
       pageSize: 10,
-      tableData: generateMockData()
+      tableData: generateMockData(),
+      discountTypeOptions: DISCOUNT_TYPES
     }
   },
   computed: {
     filteredData() {
-      const keyword = this.searchForm.customerId.trim()
-      if (!keyword) return this.tableData
-      return this.tableData.filter(item => item.customerId.includes(keyword))
+      let list = this.tableData
+      const customerId = this.searchForm.customerId.trim()
+      if (customerId) {
+        list = list.filter(item => item.customerId.includes(customerId))
+      }
+      if (this.searchForm.discountType) {
+        list = list.filter(item => item.discountType === this.searchForm.discountType)
+      }
+      if (this.searchForm.grantDateRange && this.searchForm.grantDateRange.length === 2) {
+        const [start, end] = this.searchForm.grantDateRange
+        list = list.filter(item => item.grantDate >= start && item.grantDate <= end)
+      }
+      if (this.searchForm.claimed) {
+        list = list.filter(item => item.claimed === this.searchForm.claimed)
+      }
+      if (this.searchForm.used) {
+        list = list.filter(item => item.used === this.searchForm.used)
+      }
+      return list
     },
     pagedData() {
       const start = (this.currentPage - 1) * this.pageSize
@@ -138,7 +202,13 @@ export default {
       this.currentPage = 1
     },
     handleReset() {
-      this.searchForm.customerId = ''
+      this.searchForm = {
+        customerId: '',
+        discountType: '',
+        grantDateRange: [],
+        claimed: '',
+        used: ''
+      }
       this.currentPage = 1
     },
     handlePageChange(page) {
@@ -154,16 +224,13 @@ export default {
 <style scoped>
 .search-section {
   display: flex;
-  align-items: center;
+  flex-direction: column;
+  align-items: stretch;
 }
 
-.search-row {
+.action-col {
   display: flex;
   align-items: center;
-}
-
-.button-group {
-  display: inline-flex;
   gap: 8px;
 }
 </style>

+ 145 - 28
pc/src/views/CustomerEquityGrant.vue

@@ -2,27 +2,75 @@
   <div class="customer-equity-grant">
     <el-card>
       <div class="search-section" slot="header">
-        <div class="search-row">
-          <el-form :model="searchForm" label-width="70px" inline>
-            <el-form-item label="客户号:">
-              <el-input v-model="searchForm.customerId" placeholder="请输入客户号" style="width: 220px;" clearable></el-input>
-            </el-form-item>
-            <div class="button-group" style="margin-left: 20px;">
+        <el-form :model="searchForm" label-width="130px">
+          <el-row :gutter="16">
+            <el-col :span="8">
+              <el-form-item label="客户号:">
+                <el-input v-model="searchForm.customerId" placeholder="请输入客户号" clearable></el-input>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="权益编号:">
+                <el-input v-model="searchForm.equityCode" placeholder="请输入权益编号" clearable></el-input>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="发放时间:">
+                <el-date-picker
+                  v-model="searchForm.grantTimeRange"
+                  type="daterange"
+                  range-separator="至"
+                  start-placeholder="开始日期"
+                  end-placeholder="结束日期"
+                  value-format="yyyy-MM-dd"
+                  style="width: 100%;"
+                ></el-date-picker>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="16">
+            <el-col :span="8">
+              <el-form-item label="Channel 名称:">
+                <el-select v-model="searchForm.channelName" placeholder="请选择 Channel 名称" clearable filterable style="width: 100%;">
+                  <el-option v-for="item in channelOptions" :key="item.id" :label="item.name" :value="item.name"></el-option>
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="Campaign 名称:">
+                <el-select v-model="searchForm.campaignName" placeholder="请选择 Campaign 名称" clearable filterable style="width: 100%;">
+                  <el-option v-for="item in campaignOptions" :key="item.id" :label="item.name" :value="item.name"></el-option>
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="客户账户类型:">
+                <el-select v-model="searchForm.accountType" placeholder="请选择客户账户类型" clearable style="width: 100%;">
+                  <el-option v-for="item in accountTypeOptions" :key="item" :label="item" :value="item"></el-option>
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="16">
+            <el-col :span="24" class="action-col">
               <el-button type="primary" @click="handleSearch">查询</el-button>
               <el-button @click="handleReset">重置</el-button>
               <el-button type="success" icon="el-icon-download" @click="handleExport">导出</el-button>
-            </div>
-          </el-form>
-        </div>
+            </el-col>
+          </el-row>
+        </el-form>
       </div>
 
       <el-table :data="pagedData" stripe style="width: 100%;" v-loading="loading">
-        <el-table-column prop="grantTime" label="发放时间"></el-table-column>
+        <el-table-column prop="grantTime" label="发放时间" width="170"></el-table-column>
         <el-table-column prop="equityName" label="权益名称"></el-table-column>
-        <el-table-column prop="equityCode" label="权益编号"></el-table-column>
-        <el-table-column prop="sourceChannel" label="客户来源渠道"></el-table-column>
-        <el-table-column prop="customerId" label="客户号"></el-table-column>
-        <el-table-column prop="accountType" label="客户账户类型">
+        <el-table-column prop="equityCode" label="权益编号" width="130"></el-table-column>
+        <el-table-column prop="campaignId" label="Campaign ID" width="120"></el-table-column>
+        <el-table-column prop="campaignName" label="Campaign 名称" show-overflow-tooltip></el-table-column>
+        <el-table-column prop="channelId" label="Channel ID" width="120"></el-table-column>
+        <el-table-column prop="channelName" label="Channel 名称"></el-table-column>
+        <el-table-column prop="customerId" label="客户号" width="140"></el-table-column>
+        <el-table-column prop="accountType" label="客户账户类型" width="130">
           <template slot-scope="scope">
             <el-tag :type="accountTypeTag[scope.row.accountType]" size="small">{{ scope.row.accountType }}</el-tag>
           </template>
@@ -43,11 +91,40 @@
 
 <script>
 const ACCOUNT_TYPES = ['Premier', 'Elite', 'PB', 'Prospect']
-const CHANNELS = ['Mobile App', 'Online Banking', 'Branch', 'WeChat Mini Program', 'API']
+const CHANNELS = [
+  { id: 'CH0001', name: '汇丰中国客户服务号' },
+  { id: 'CH0002', name: 'MGM - Mortgage' },
+  { id: 'CH0003', name: 'MGM - Premier' },
+  { id: 'CH0004', name: 'CCSS SMS' },
+  { id: 'CH0005', name: 'GPB Referral' },
+  { id: 'CH0006', name: 'HK referral' },
+  { id: 'CH0007', name: 'Non WPB staff Referral' },
+  { id: 'CH0008', name: 'Pinnacle Referral' },
+  { id: 'CH0009', name: 'RBB Refferal' },
+  { id: 'CH0010', name: 'New staff' },
+  { id: 'CH0011', name: 'IBC referral staff code' },
+  { id: 'CH0012', name: 'HTC' },
+  { id: 'CH0013', name: 'NIVIDA' },
+  { id: 'CH0014', name: 'Mobile X' },
+  { id: 'CH0015', name: 'PWS site' },
+  { id: 'CH0016', name: '汇丰中国小程序' },
+  { id: 'CH0017', name: '汇丰中国订阅号' },
+  { id: 'CH0018', name: '推文尾部入口' },
+  { id: 'CH0019', name: 'Chatbot' },
+  { id: 'CH0020', name: 'SMS' },
+  { id: 'CH0021', name: 'River APP' }
+]
 const EQUITY_NAMES = [
   '机场贵宾厅', '酒店权益', '高尔夫权益', '健康体检', '保险权益',
   '积分兑换', '消费返现', '生日礼遇', '出行权益', '餐饮优惠'
 ]
+const CAMPAIGNS = [
+  { id: 'CP9001', name: 'Q3 大客户回馈' },
+  { id: 'CP9002', name: '新春开户活动' },
+  { id: 'CP9003', name: '618 消费季营销' },
+  { id: 'CP9004', name: '双十一专享权益' },
+  { id: 'CP9005', name: 'Premier 升级礼遇' }
+]
 
 function generateMockData() {
   const list = []
@@ -58,12 +135,17 @@ function generateMockData() {
     const pad = n => String(n).padStart(2, '0')
     const timeStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
 
+    const campaign = CAMPAIGNS[Math.floor(Math.random() * CAMPAIGNS.length)]
+    const channel = CHANNELS[Math.floor(Math.random() * CHANNELS.length)]
     list.push({
       id: i,
       grantTime: timeStr,
       equityName: EQUITY_NAMES[Math.floor(Math.random() * EQUITY_NAMES.length)],
-      equityCode: 'EQ' + String(202600000 + i),
-      sourceChannel: CHANNELS[Math.floor(Math.random() * CHANNELS.length)],
+      equityCode: 'Q' + String(202600000 + i),
+      campaignId: campaign.id,
+      campaignName: campaign.name,
+      channelId: channel.id,
+      channelName: channel.name,
       customerId: 'CUST' + String(100000 + Math.floor(Math.random() * 900000)),
       accountType: ACCOUNT_TYPES[Math.floor(Math.random() * ACCOUNT_TYPES.length)]
     })
@@ -76,11 +158,21 @@ export default {
   name: 'CustomerEquityGrant',
   data() {
     return {
-      searchForm: { customerId: '' },
+      searchForm: {
+        customerId: '',
+        grantTimeRange: [],
+        channelName: '',
+        campaignName: '',
+        equityCode: '',
+        accountType: ''
+      },
       loading: false,
       currentPage: 1,
       pageSize: 10,
       tableData: generateMockData(),
+      channelOptions: CHANNELS,
+      campaignOptions: CAMPAIGNS,
+      accountTypeOptions: ACCOUNT_TYPES,
       accountTypeTag: {
         Premier: '',
         Elite: 'success',
@@ -91,9 +183,30 @@ export default {
   },
   computed: {
     filteredData() {
-      const keyword = this.searchForm.customerId.trim()
-      if (!keyword) return this.tableData
-      return this.tableData.filter(item => item.customerId.includes(keyword))
+      let list = this.tableData
+      const customerId = this.searchForm.customerId.trim()
+      if (customerId) {
+        list = list.filter(item => item.customerId.includes(customerId))
+      }
+      if (this.searchForm.grantTimeRange && this.searchForm.grantTimeRange.length === 2) {
+        const start = this.searchForm.grantTimeRange[0]
+        const end = this.searchForm.grantTimeRange[1] + ' 23:59:59'
+        list = list.filter(item => item.grantTime >= start && item.grantTime <= end)
+      }
+      if (this.searchForm.channelName) {
+        list = list.filter(item => item.channelName === this.searchForm.channelName)
+      }
+      if (this.searchForm.campaignName) {
+        list = list.filter(item => item.campaignName === this.searchForm.campaignName)
+      }
+      const equityCode = this.searchForm.equityCode.trim()
+      if (equityCode) {
+        list = list.filter(item => item.equityCode.includes(equityCode))
+      }
+      if (this.searchForm.accountType) {
+        list = list.filter(item => item.accountType === this.searchForm.accountType)
+      }
+      return list
     },
     pagedData() {
       const start = (this.currentPage - 1) * this.pageSize
@@ -105,7 +218,14 @@ export default {
       this.currentPage = 1
     },
     handleReset() {
-      this.searchForm.customerId = ''
+      this.searchForm = {
+        customerId: '',
+        grantTimeRange: [],
+        channelName: '',
+        campaignName: '',
+        equityCode: '',
+        accountType: ''
+      }
       this.currentPage = 1
     },
     handlePageChange(page) {
@@ -121,16 +241,13 @@ export default {
 <style scoped>
 .search-section {
   display: flex;
-  align-items: center;
+  flex-direction: column;
+  align-items: stretch;
 }
 
-.search-row {
+.action-col {
   display: flex;
   align-items: center;
-}
-
-.button-group {
-  display: inline-flex;
   gap: 8px;
 }
 </style>

+ 36 - 4
pc/src/views/EditEquityProduct.vue

@@ -244,6 +244,33 @@
               </el-form-item>
             </el-col>
           </el-row>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="权益有效期" required>
+                <template v-if="form.source === '银行自有' || form.source === '荣数商品'">
+                  <el-radio-group v-model="form.validityType">
+                    <el-radio label="fromGrantPlan">取自发放计划</el-radio>
+                    <el-radio label="custom">自定义输入</el-radio>
+                  </el-radio-group>
+                  <el-input
+                    v-if="form.validityType === 'custom'"
+                    v-model="form.validityText"
+                    maxlength="50"
+                    show-word-limit
+                    placeholder="请输入权益有效期"
+                    style="margin-top: 8px;"
+                  ></el-input>
+                </template>
+                <el-input
+                  v-else
+                  v-model="form.validityText"
+                  maxlength="50"
+                  show-word-limit
+                  placeholder="请输入权益有效期"
+                ></el-input>
+              </el-form-item>
+            </el-col>
+          </el-row>
           <el-row :gutter="20">
             <el-col :span="12">
               <el-form-item label="权益内容" required>
@@ -581,6 +608,8 @@ export default {
         listIcon: [],
         mainImage: [],
         introduction: '',
+        validityType: 'fromGrantPlan',
+        validityText: '',
         overview: '',
         details: '',
         smsEnabled: false,
@@ -640,6 +669,7 @@ export default {
     },
     buildPayload() {
       const f = this.form
+      const isCustomValidity = f.source === '第三方' || f.validityType === 'custom'
       return {
         productId: f.productId || this.$route.params.id,
         equityName: f.equityName,
@@ -653,6 +683,8 @@ export default {
         exchangeMethod: f.source === '第三方'
           ? (f.externalJumpMethod === '有' ? '第三方(详情页跳转)' : '第三方(列表跳转)')
           : '权益平台',
+        validityType: isCustomValidity ? 'custom' : 'fromGrantPlan',
+        validityText: isCustomValidity ? (f.validityText || '') : '',
         showOnHome: f.showOnHome,
         showOnAssembly: f.showOnAssembly,
         assemblyPages: [...(f.assemblyPages || [])]
@@ -685,7 +717,7 @@ export default {
     loadData() {
       const id = this.$route.params.id
       const mockMap = {
-        100001: {
+        'Q100001': {
           source: '银行自有',
           externalJumpMethod: '',
           externalLink: '',
@@ -717,7 +749,7 @@ export default {
           claimDenyButtonText: '',
           claimDenyButtonLink: ''
         },
-        100002: {
+        'Q100002': {
           source: '第三方',
           externalJumpMethod: '没有',
           externalLink: '太古地产',
@@ -750,7 +782,7 @@ export default {
           claimDenyButtonText: '去升级',
           claimDenyButtonLink: 'https://example.com/upgrade'
         },
-        100003: {
+        'Q100003': {
           source: '第三方',
           externalJumpMethod: '有',
           externalLink: '龙腾',
@@ -782,7 +814,7 @@ export default {
           claimDenyButtonText: '',
           claimDenyButtonLink: ''
         },
-        100004: {
+        'Q100004': {
           source: '第三方',
           externalJumpMethod: '有',
           externalLink: 'h5链接',

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

@@ -71,7 +71,7 @@
             <el-tag :type="statusTag(scope.row)" size="small">{{ statusLabel(scope.row) }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="发放数量" width="150" align="center">
+        <el-table-column label="权益可领取数" width="150" align="center">
           <template slot-scope="scope">
             {{ grantSummary(scope.row) }}
           </template>
@@ -90,7 +90,7 @@
 <script>
 import request from '@/utils/request'
 
-const grantCycleLabelMap = { total: '累计', monthly: '每月', quarterly: '每季度', yearly: '每年' }
+const grantCycleLabelMap = { total: '整个周期', monthly: '每月', quarterly: '每季度', yearly: '每年' }
 
 export default {
   name: 'EquityGrantCampaign',
@@ -176,7 +176,7 @@ export default {
     },
     grantSummary(row) {
       if (!row) return '-'
-      const cycle = grantCycleLabelMap[row.grantCycle] || '累计'
+      const cycle = grantCycleLabelMap[row.grantCycle] || '整个周期'
       return `${cycle} ${row.grantQuantity || 0} 个`
     },
     statusLabel(row) {

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

@@ -76,10 +76,10 @@
             />
           </el-select>
         </el-form-item>
-        <el-form-item label="发放数量" required>
+        <el-form-item label="权益可领取数" required>
           <div class="grant-row">
             <el-select v-model="form.grantCycle" placeholder="类型" style="width:140px">
-              <el-option label="累计" value="total" />
+              <el-option label="整个周期" value="total" />
               <el-option label="每月" value="monthly" />
               <el-option label="每季度" value="quarterly" />
               <el-option label="每年" value="yearly" />
@@ -240,7 +240,7 @@ export default {
           return
         }
         if (!this.form.grantQuantity || this.form.grantQuantity < 1) {
-          this.$message.warning('请输入发放数量')
+          this.$message.warning('请输入权益可领取数')
           return
         }
         await request.post('/api/equity-grant-campaign/save', this.form)

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

@@ -11,7 +11,7 @@
         <el-descriptions-item label="状态">
           <el-tag :type="statusTag" size="small">{{ statusLabel }}</el-tag>
         </el-descriptions-item>
-        <el-descriptions-item label="发放数量">{{ grantSummary }}</el-descriptions-item>
+        <el-descriptions-item label="权益可领取数">{{ grantSummary }}</el-descriptions-item>
         <el-descriptions-item label="开始时间">{{ current.startTime || '-' }}</el-descriptions-item>
         <el-descriptions-item label="结束时间">{{ current.endTime || '-' }}</el-descriptions-item>
         <el-descriptions-item label="权益有效期" :span="2">{{ validitySummary }}</el-descriptions-item>
@@ -53,7 +53,7 @@
 import request from '@/utils/request'
 import crowdNameListStore from '@/store/crowdNameListStore'
 
-const grantCycleLabelMap = { total: '累计', monthly: '每月', quarterly: '每季度', yearly: '每年' }
+const grantCycleLabelMap = { total: '整个周期', monthly: '每月', quarterly: '每季度', yearly: '每年' }
 
 export default {
   name: 'EquityGrantCampaignView',
@@ -78,7 +78,7 @@ export default {
     },
     grantSummary() {
       if (!this.current) return '-'
-      const cycle = grantCycleLabelMap[this.current.grantCycle] || '累计'
+      const cycle = grantCycleLabelMap[this.current.grantCycle] || '整个周期'
       return `${cycle} ${this.current.grantQuantity || 0} 个`
     },
     validitySummary() {

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

@@ -269,11 +269,11 @@ export default {
     },
     equityName() {
       const map = {
-        100001: '权益A',
-        100002: '权益B',
-        100003: '权益C',
-        100004: '权益D',
-        100005: '权益E'
+        'Q100001': '权益A',
+        'Q100002': '权益B',
+        'Q100003': '权益C',
+        'Q100004': '权益D',
+        'Q100005': '权益E'
       }
       return map[this.productId] || this.productId
     },

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

@@ -89,6 +89,10 @@
                   </div>
                   <div slot="reference" class="more-actions-item">调整显示顺序</div>
                 </el-popover>
+                <div
+                  class="more-actions-item"
+                  @click="handleCopyFromMore(scope.row, scope.$index)"
+                >复制</div>
                 <div
                   class="more-actions-item"
                   @click="openLinkDialogFromMore(scope.row, scope.$index)"
@@ -221,8 +225,7 @@ export default {
     },
     // 按钮可见性
     canEdit(row) {
-      return (row.shelfStatus === '未上架' && row.auditStatus === '草稿') ||
-             (row.shelfStatus === '已上架' && row.auditStatus === '审核通过')
+      return row.shelfStatus === '未上架' && row.auditStatus === '草稿'
     },
     canRequestOffshelf(row) {
       return row.shelfStatus === '已上架' && row.auditStatus === '审核通过'
@@ -316,6 +319,14 @@ export default {
       if (moreRef) moreRef.doClose()
       this.showLinkDialog(row)
     },
+    handleCopyFromMore(row, index) {
+      const moreRef = this.getPopoverInstance(`morePopover-${index}`)
+      if (moreRef) moreRef.doClose()
+      this.$router.push({
+        path: '/customization/hsbc/equity-products/add',
+        query: { copyFrom: row.productId }
+      })
+    },
     handleManageCouponsFromMore(row, index) {
       const moreRef = this.getPopoverInstance(`morePopover-${index}`)
       if (moreRef) moreRef.doClose()

+ 33 - 36
pc/src/views/PointsDetail.vue

@@ -2,29 +2,32 @@
   <div class="points-detail">
     <el-card>
       <div slot="header" class="search-section">
-        <div class="search-row">
-          <el-form :model="searchForm" label-width="100px" inline>
-            <el-form-item label="客户号:">
-              <el-input v-model="searchForm.customerId" placeholder="请输入客户号" clearable style="width: 220px;"></el-input>
-            </el-form-item>
-            <el-form-item label="查询日期范围:">
-              <el-date-picker
-                v-model="searchForm.dateRange"
-                type="daterange"
-                range-separator="至"
-                start-placeholder="开始日期"
-                end-placeholder="结束日期"
-                value-format="yyyy-MM-dd"
-                style="width: 360px;"
-              ></el-date-picker>
-            </el-form-item>
-            <div class="button-group" style="margin-left: 20px;">
-              <el-button type="primary" @click="handleSearch">查询</el-button>
-              <el-button @click="handleReset">重置</el-button>
-              <el-button type="success" icon="el-icon-download" @click="handleExport">导出</el-button>
-            </div>
-          </el-form>
-        </div>
+        <el-form :model="searchForm" label-width="100px" inline>
+          <el-form-item label="客户号:">
+            <el-input v-model="searchForm.customerId" placeholder="请输入客户号" clearable style="width: 220px;"></el-input>
+          </el-form-item>
+          <el-form-item label="变动类型:">
+            <el-select v-model="searchForm.changeType" placeholder="请选择变动类型" clearable style="width: 220px;">
+              <el-option v-for="item in changeTypeOptions" :key="item" :label="item" :value="item"></el-option>
+            </el-select>
+          </el-form-item>
+          <el-form-item label="查询日期范围:">
+            <el-date-picker
+              v-model="searchForm.dateRange"
+              type="daterange"
+              range-separator="至"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              value-format="yyyy-MM-dd"
+              style="width: 360px;"
+            ></el-date-picker>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="handleSearch">查询</el-button>
+            <el-button @click="handleReset">重置</el-button>
+            <el-button type="success" icon="el-icon-download" @click="handleExport">导出</el-button>
+          </el-form-item>
+        </el-form>
       </div>
 
       <el-table :data="pagedData" stripe border style="width: 100%;" v-loading="loading">
@@ -39,7 +42,7 @@
             <el-tag :type="changeTypeTag[scope.row.changeType]" size="small">{{ scope.row.changeType }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="projectName" label="积分项目名称" show-overflow-tooltip></el-table-column>
+        <el-table-column prop="projectName" label="积分项目类别" show-overflow-tooltip></el-table-column>
       </el-table>
 
       <el-pagination
@@ -107,11 +110,12 @@ export default {
   name: 'PointsDetail',
   data() {
     return {
-      searchForm: { customerId: '', dateRange: [] },
+      searchForm: { customerId: '', changeType: '', dateRange: [] },
       loading: false,
       currentPage: 1,
       pageSize: 10,
       tableData: generateMockData(),
+      changeTypeOptions: CHANGE_TYPES,
       changeTypeTag: {
         '获取积分': 'success',
         '使用积分': '',
@@ -126,6 +130,9 @@ export default {
       if (keyword) {
         list = list.filter(item => item.customerId.includes(keyword))
       }
+      if (this.searchForm.changeType) {
+        list = list.filter(item => item.changeType === this.searchForm.changeType)
+      }
       if (this.searchForm.dateRange && this.searchForm.dateRange.length === 2) {
         const start = this.searchForm.dateRange[0].replace(/-/g, '/')
         const end = this.searchForm.dateRange[1].replace(/-/g, '/')
@@ -143,7 +150,7 @@ export default {
       this.currentPage = 1
     },
     handleReset() {
-      this.searchForm = { customerId: '', dateRange: [] }
+      this.searchForm = { customerId: '', changeType: '', dateRange: [] }
       this.currentPage = 1
     },
     handlePageChange(page) {
@@ -161,14 +168,4 @@ export default {
   display: flex;
   align-items: center;
 }
-
-.search-row {
-  display: flex;
-  align-items: center;
-}
-
-.button-group {
-  display: inline-flex;
-  gap: 8px;
-}
 </style>

+ 22 - 10
pc/src/views/ViewEquityProduct.vue

@@ -203,6 +203,16 @@
                 </el-form-item>
               </el-col>
             </el-row>
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="权益有效期">
+                  <el-input
+                    :value="form.validityType === 'fromGrantPlan' ? '取自发放计划' : (form.validityText || '-')"
+                    readonly
+                  ></el-input>
+                </el-form-item>
+              </el-col>
+            </el-row>
             <el-row :gutter="20">
               <el-col :span="12">
                 <el-form-item label="权益内容">
@@ -462,6 +472,8 @@ export default {
         listIcon: [],
         mainImage: [],
         introduction: '',
+        validityType: 'fromGrantPlan',
+        validityText: '',
         overview: '',
         details: '',
         smsEnabled: false,
@@ -509,7 +521,7 @@ export default {
       const id = this.$route.params.id
       // TODO: 根据 id 从接口获取数据,这里用 mock 数据示例
       const mockMap = {
-        100001: {
+        'Q100001': {
           source: '银行自有',
           externalJumpMethod: '',
           externalLink: '',
@@ -541,7 +553,7 @@ export default {
           claimDenyButtonText: '',
           claimDenyButtonLink: ''
         },
-        100002: {
+        'Q100002': {
           source: '第三方',
           externalJumpMethod: '没有',
           externalLink: '太古地产',
@@ -574,7 +586,7 @@ export default {
           claimDenyButtonText: '去升级',
           claimDenyButtonLink: 'https://example.com/upgrade'
         },
-        100003: {
+        'Q100003': {
           source: '第三方',
           externalJumpMethod: '有',
           externalLink: '龙腾',
@@ -606,7 +618,7 @@ export default {
           claimDenyButtonText: '',
           claimDenyButtonLink: ''
         },
-        100004: {
+        'Q100004': {
           source: '第三方',
           externalJumpMethod: '有',
           externalLink: 'h5链接',
@@ -640,30 +652,30 @@ export default {
       if (id && mockMap[id]) {
         Object.assign(this.form, mockMap[id])
       } else if (id) {
-        Object.assign(this.form, mockMap['100001'])
+        Object.assign(this.form, mockMap['Q100001'])
       }
 
       const logsMap = {
-        100001: [
+        'Q100001': [
           { operator: '张三', operateTime: '2026-01-10 09:32:15', action: '创建' },
           { operator: '李四', operateTime: '2026-01-10 14:08:42', action: '审核' },
           { operator: '赵六', operateTime: '2026-03-01 16:45:30', action: '下架' }
         ],
-        100002: [
+        'Q100002': [
           { operator: '张三', operateTime: '2026-01-12 10:15:00', action: '创建' },
           { operator: '李四', operateTime: '2026-01-12 15:22:18', action: '审核' }
         ],
-        100003: [
+        'Q100003': [
           { operator: '钱七', operateTime: '2026-01-15 11:03:27', action: '创建' },
           { operator: '李四', operateTime: '2026-01-15 17:30:09', action: '审核' }
         ],
-        100004: [
+        'Q100004': [
           { operator: '钱七', operateTime: '2026-02-01 09:48:12', action: '创建' },
           { operator: '李四', operateTime: '2026-02-01 14:11:36', action: '审核' },
           { operator: '赵六', operateTime: '2026-04-05 18:22:47', action: '下架' }
         ]
       }
-      this.operationLogs = (id && logsMap[id]) ? logsMap[id] : (id ? logsMap['100001'] : [])
+      this.operationLogs = (id && logsMap[id]) ? logsMap[id] : (id ? logsMap['Q100001'] : [])
     },
     actionTagType(action) {
       const map = {