Kaynağa Gözat

新增报表中心模块:积分明细查询、客户权益发放、立减金发放明细;移除短信发送记录失败原因列

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fade 1 ay önce
ebeveyn
işleme
26d76747a0

+ 12 - 0
pc/src/components/Layout.vue

@@ -45,6 +45,18 @@
                 发送记录查询
               </el-menu-item>
             </el-submenu>
+            <el-submenu index="customization-hsbc-report">
+              <template slot="title">报表中心</template>
+              <el-menu-item index="/customization/hsbc/points-detail">
+                积分明细查询
+              </el-menu-item>
+              <el-menu-item index="/customization/hsbc/equity-grant">
+                客户权益发放
+              </el-menu-item>
+              <el-menu-item index="/customization/hsbc/cash-discount-grant">
+                立减金发放明细
+              </el-menu-item>
+            </el-submenu>
           </el-submenu>
         </el-submenu>
         <el-submenu index="system">

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

@@ -99,6 +99,24 @@ const routes = [
         name: 'SmsSendRecord',
         component: () => import('@/views/SmsSendRecord.vue'),
         meta: { title: '发送记录查询', icon: 'el-icon-document-copy' }
+      },
+      {
+        path: 'customization/hsbc/equity-grant',
+        name: 'CustomerEquityGrant',
+        component: () => import('@/views/CustomerEquityGrant.vue'),
+        meta: { title: '客户权益发放', icon: 'el-icon-s-data' }
+      },
+      {
+        path: 'customization/hsbc/cash-discount-grant',
+        name: 'CashDiscountGrant',
+        component: () => import('@/views/CashDiscountGrant.vue'),
+        meta: { title: '立减金发放明细', icon: 'el-icon-s-finance' }
+      },
+      {
+        path: 'customization/hsbc/points-detail',
+        name: 'PointsDetail',
+        component: () => import('@/views/PointsDetail.vue'),
+        meta: { title: '积分明细查询', icon: 'el-icon-s-order' }
       }
     ]
   },

+ 169 - 0
pc/src/views/CashDiscountGrant.vue

@@ -0,0 +1,169 @@
+<template>
+  <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-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>
+      </div>
+
+      <el-table :data="pagedData" stripe border style="width: 100%;" v-loading="loading">
+        <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">
+          <template slot-scope="scope">
+            <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="used" label="是否使用" width="100">
+          <template slot-scope="scope">
+            <el-tag :type="scope.row.used === '是' ? 'success' : 'danger'" size="small">{{ scope.row.used }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="useTime" label="使用时间" width="160"></el-table-column>
+      </el-table>
+
+      <el-pagination
+        style="margin-top: 20px; text-align: right;"
+        @current-change="handlePageChange"
+        :current-page="currentPage"
+        :page-size="pageSize"
+        layout="total, prev, pager, next, jumper"
+        :total="filteredData.length"
+      ></el-pagination>
+    </el-card>
+  </div>
+</template>
+
+<script>
+const DISCOUNT_TYPES = ['借记卡', '信用卡']
+const GRANT_REASONS = ['新开户礼遇', '月度消费达标奖励', '生日专属福利', '双十一营销活动', '服务补偿', '升级Premier礼遇', '推荐好友奖励', '季度活跃奖励']
+
+function pad(n) { return String(n).padStart(2, '0') }
+
+function randomDate(baseDate, offsetDays) {
+  const d = new Date(baseDate)
+  d.setDate(d.getDate() - Math.floor(Math.random() * offsetDays))
+  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
+}
+
+function randomDateTime(baseDate, offsetDays) {
+  const d = new Date(baseDate)
+  d.setDate(d.getDate() - Math.floor(Math.random() * offsetDays))
+  d.setHours(Math.floor(Math.random() * 14 + 8), Math.floor(Math.random() * 60), Math.floor(Math.random() * 60))
+  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
+}
+
+function generateMockData() {
+  const list = []
+  const base = new Date(2026, 4, 15)
+  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 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())}`
+
+    const claimed = Math.random() > 0.25 ? '是' : '否'
+    const used = claimed === '是' && Math.random() > 0.35 ? '是' : '否'
+
+    list.push({
+      id: i,
+      customerId: 'CUST' + String(100000 + Math.floor(Math.random() * 900000)),
+      discountType: DISCOUNT_TYPES[Math.floor(Math.random() * DISCOUNT_TYPES.length)],
+      binRestriction: Math.random() > 0.5 ? '是' : '否',
+      grantDate: grantDate,
+      grantAmount: amounts[Math.floor(Math.random() * amounts.length)],
+      grantReason: GRANT_REASONS[Math.floor(Math.random() * GRANT_REASONS.length)],
+      validPeriod: `${validStart} 至 ${validEndStr}`,
+      claimed: claimed,
+      claimTime: claimed === '是' ? randomDateTime(base, 60) : '-',
+      used: used,
+      useTime: used === '是' ? randomDateTime(base, 30) : '-'
+    })
+  }
+  list.sort((a, b) => b.grantDate.localeCompare(a.grantDate))
+  return list
+}
+
+export default {
+  name: 'CashDiscountGrant',
+  data() {
+    return {
+      searchForm: { customerId: '' },
+      loading: false,
+      currentPage: 1,
+      pageSize: 10,
+      tableData: generateMockData()
+    }
+  },
+  computed: {
+    filteredData() {
+      const keyword = this.searchForm.customerId.trim()
+      if (!keyword) return this.tableData
+      return this.tableData.filter(item => item.customerId.includes(keyword))
+    },
+    pagedData() {
+      const start = (this.currentPage - 1) * this.pageSize
+      return this.filteredData.slice(start, start + this.pageSize)
+    }
+  },
+  methods: {
+    handleSearch() {
+      this.currentPage = 1
+    },
+    handleReset() {
+      this.searchForm.customerId = ''
+      this.currentPage = 1
+    },
+    handlePageChange(page) {
+      this.currentPage = page
+    },
+    handleExport() {
+      this.$message.success('导出任务已提交,请稍后在下载中心查看')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.search-section {
+  display: flex;
+  align-items: center;
+}
+
+.search-row {
+  display: flex;
+  align-items: center;
+}
+
+.button-group {
+  display: inline-flex;
+  gap: 8px;
+}
+</style>

+ 136 - 0
pc/src/views/CustomerEquityGrant.vue

@@ -0,0 +1,136 @@
+<template>
+  <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-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>
+      </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="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="客户账户类型">
+          <template slot-scope="scope">
+            <el-tag :type="accountTypeTag[scope.row.accountType]" size="small">{{ scope.row.accountType }}</el-tag>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <el-pagination
+        style="margin-top: 20px; text-align: right;"
+        @current-change="handlePageChange"
+        :current-page="currentPage"
+        :page-size="pageSize"
+        layout="total, prev, pager, next, jumper"
+        :total="filteredData.length"
+      ></el-pagination>
+    </el-card>
+  </div>
+</template>
+
+<script>
+const ACCOUNT_TYPES = ['Premier', 'Elite', 'PB', 'Prospect']
+const CHANNELS = ['Mobile App', 'Online Banking', 'Branch', 'WeChat Mini Program', 'API']
+const EQUITY_NAMES = [
+  '机场贵宾厅', '酒店权益', '高尔夫权益', '健康体检', '保险权益',
+  '积分兑换', '消费返现', '生日礼遇', '出行权益', '餐饮优惠'
+]
+
+function generateMockData() {
+  const list = []
+  for (let i = 1; i <= 86; i++) {
+    const d = new Date(2026, 4, 15)
+    d.setDate(d.getDate() - Math.floor(Math.random() * 60))
+    d.setHours(Math.floor(Math.random() * 24), Math.floor(Math.random() * 60), Math.floor(Math.random() * 60))
+    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())}`
+
+    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)],
+      customerId: 'CUST' + String(100000 + Math.floor(Math.random() * 900000)),
+      accountType: ACCOUNT_TYPES[Math.floor(Math.random() * ACCOUNT_TYPES.length)]
+    })
+  }
+  list.sort((a, b) => b.grantTime.localeCompare(a.grantTime))
+  return list
+}
+
+export default {
+  name: 'CustomerEquityGrant',
+  data() {
+    return {
+      searchForm: { customerId: '' },
+      loading: false,
+      currentPage: 1,
+      pageSize: 10,
+      tableData: generateMockData(),
+      accountTypeTag: {
+        Premier: '',
+        Elite: 'success',
+        PB: 'warning',
+        Prospect: 'info'
+      }
+    }
+  },
+  computed: {
+    filteredData() {
+      const keyword = this.searchForm.customerId.trim()
+      if (!keyword) return this.tableData
+      return this.tableData.filter(item => item.customerId.includes(keyword))
+    },
+    pagedData() {
+      const start = (this.currentPage - 1) * this.pageSize
+      return this.filteredData.slice(start, start + this.pageSize)
+    }
+  },
+  methods: {
+    handleSearch() {
+      this.currentPage = 1
+    },
+    handleReset() {
+      this.searchForm.customerId = ''
+      this.currentPage = 1
+    },
+    handlePageChange(page) {
+      this.currentPage = page
+    },
+    handleExport() {
+      this.$message.success('导出任务已提交,请稍后在下载中心查看')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.search-section {
+  display: flex;
+  align-items: center;
+}
+
+.search-row {
+  display: flex;
+  align-items: center;
+}
+
+.button-group {
+  display: inline-flex;
+  gap: 8px;
+}
+</style>

+ 174 - 0
pc/src/views/PointsDetail.vue

@@ -0,0 +1,174 @@
+<template>
+  <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>
+      </div>
+
+      <el-table :data="pagedData" stripe border style="width: 100%;" v-loading="loading">
+        <el-table-column prop="customerId" label="客户号"></el-table-column>
+        <el-table-column prop="changeDate" label="变动日期"></el-table-column>
+        <el-table-column prop="changeAmount" label="变动积分金额"></el-table-column>
+        <el-table-column prop="totalPoints" label="积分总数"></el-table-column>
+        <el-table-column prop="hyhPoints" label="HYH积分"></el-table-column>
+        <el-table-column prop="creditCardPoints" label="信用卡积分"></el-table-column>
+        <el-table-column prop="changeType" label="变动类别">
+          <template slot-scope="scope">
+            <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>
+
+      <el-pagination
+        style="margin-top: 20px; text-align: right;"
+        @current-change="handlePageChange"
+        :current-page="currentPage"
+        :page-size="pageSize"
+        layout="total, prev, pager, next, jumper"
+        :total="filteredData.length"
+      ></el-pagination>
+    </el-card>
+  </div>
+</template>
+
+<script>
+const CHANGE_TYPES = ['获取积分', '使用积分', '积分过期']
+const EARN_PROJECTS = ['消费返积分', '开户礼积分', '生日奖励积分', '活动赠送积分', '推荐好友奖励', '月度达标奖励']
+const SPEND_PROJECTS = ['积分兑换商品', '积分抵现', '积分兑换券码', '积分兑换里程', '积分兑换权益']
+
+function pad(n) { return String(n).padStart(2, '0') }
+
+function generateMockData() {
+  const list = []
+  const customerIds = ['CUST100231', 'CUST100458', 'CUST100672', 'CUST100899', 'CUST101024', 'CUST101337', 'CUST101582', 'CUST101790']
+
+  for (let i = 1; i <= 80; i++) {
+    const d = new Date(2026, 4, 15)
+    d.setDate(d.getDate() - Math.floor(Math.random() * 120))
+    const dateStr = `${d.getFullYear()}/${pad(d.getMonth() + 1)}/${pad(d.getDate())}`
+
+    const changeType = CHANGE_TYPES[Math.floor(Math.random() * CHANGE_TYPES.length)]
+    let changeAmount, projectName
+    if (changeType === '获取积分') {
+      changeAmount = Math.floor(Math.random() * 500 + 1)
+      projectName = EARN_PROJECTS[Math.floor(Math.random() * EARN_PROJECTS.length)]
+    } else if (changeType === '使用积分') {
+      changeAmount = Math.floor(Math.random() * 300 + 1)
+      projectName = SPEND_PROJECTS[Math.floor(Math.random() * SPEND_PROJECTS.length)]
+    } else {
+      changeAmount = Math.floor(Math.random() * 200 + 50)
+      projectName = '/'
+    }
+
+    const totalPoints = Math.floor(Math.random() * 5000 + 100)
+    const hyhPoints = Math.floor(totalPoints * (Math.random() * 0.4 + 0.1))
+    const creditCardPoints = totalPoints - hyhPoints
+
+    list.push({
+      id: i,
+      customerId: customerIds[Math.floor(Math.random() * customerIds.length)],
+      changeDate: dateStr,
+      changeAmount: changeAmount,
+      totalPoints: totalPoints,
+      hyhPoints: hyhPoints,
+      creditCardPoints: creditCardPoints,
+      changeType: changeType,
+      projectName: projectName
+    })
+  }
+  list.sort((a, b) => b.changeDate.localeCompare(a.changeDate))
+  return list
+}
+
+export default {
+  name: 'PointsDetail',
+  data() {
+    return {
+      searchForm: { customerId: '', dateRange: [] },
+      loading: false,
+      currentPage: 1,
+      pageSize: 10,
+      tableData: generateMockData(),
+      changeTypeTag: {
+        '获取积分': 'success',
+        '使用积分': '',
+        '积分过期': 'info'
+      }
+    }
+  },
+  computed: {
+    filteredData() {
+      let list = this.tableData
+      const keyword = this.searchForm.customerId.trim()
+      if (keyword) {
+        list = list.filter(item => item.customerId.includes(keyword))
+      }
+      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, '/')
+        list = list.filter(item => item.changeDate >= start && item.changeDate <= end)
+      }
+      return list
+    },
+    pagedData() {
+      const start = (this.currentPage - 1) * this.pageSize
+      return this.filteredData.slice(start, start + this.pageSize)
+    }
+  },
+  methods: {
+    handleSearch() {
+      this.currentPage = 1
+    },
+    handleReset() {
+      this.searchForm = { customerId: '', dateRange: [] }
+      this.currentPage = 1
+    },
+    handlePageChange(page) {
+      this.currentPage = page
+    },
+    handleExport() {
+      this.$message.success('导出任务已提交,请稍后在下载中心查看')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.search-section {
+  display: flex;
+  align-items: center;
+}
+
+.search-row {
+  display: flex;
+  align-items: center;
+}
+
+.button-group {
+  display: inline-flex;
+  gap: 8px;
+}
+</style>

+ 1 - 7
pc/src/views/SmsSendRecord.vue

@@ -57,13 +57,7 @@
             </el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="failReason" label="失败原因" min-width="160" show-overflow-tooltip>
-          <template slot-scope="scope">
-            <span v-if="scope.row.status === 'failed'">{{ scope.row.failReason }}</span>
-            <span v-else>-</span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="sentAt" label="发送时间" width="160" />
+<el-table-column prop="sentAt" label="发送时间" width="160" />
       </el-table>
 
       <!-- 分页 -->