Преглед изворни кода

1、调整优惠卷领取记录处理

yys пре 2 недеља
родитељ
комит
32356e7c3b

+ 15 - 0
src/directive/select/elSelectLoadMore.js

@@ -0,0 +1,15 @@
+
+export default {
+  inserted(el, binding) {
+    const SELECT_WRAP_DOM = el.querySelector('.el-select-dropdown .el-select-dropdown__wrap')
+    if (!SELECT_WRAP_DOM) {
+      return
+    }
+    SELECT_WRAP_DOM.addEventListener('scroll', function () {
+      const condition = this.scrollHeight - this.scrollTop <= this.clientHeight + 1
+      if (condition && typeof binding.value === 'function') {
+        binding.value()
+      }
+    })
+  }
+}

+ 8 - 0
src/directive/select/index.js

@@ -0,0 +1,8 @@
+
+import elSelectLoadMore from "@/directive/select/elSelectLoadMore";
+
+const install = function(Vue) {
+  Vue.directive("select-load-more", elSelectLoadMore);
+};
+
+export default install

+ 2 - 0
src/main.js

@@ -10,6 +10,7 @@ import App from './App'
 import store from './store'
 import router from './router'
 import directive from './directive' //directive
+import elementDirective from './directive/select'
 
 import './assets/icons' // icon
 import './permission' // permission control
@@ -84,6 +85,7 @@ import H5Editor from "@/components/H5Editor";
 Vue.component('H5Editor', H5Editor)
 
 Vue.use(directive)
+Vue.use(elementDirective)
 Vue.use(VueMeta)
 
 /**

+ 15 - 1
src/views/live/live/index.vue

@@ -784,6 +784,7 @@ export default {
         popupPosition: [{ required: true, message: "不能为空", trigger: "change" }],
         liveType: [{ required: true, message: "不能为空", trigger: "burl" }],
         startTime: [{ required: true, message: "不能为空", trigger: "burl" }],
+        finishTime: [{ required: true, message: "结束时间不能为空", trigger: "change" }],
         liveImgUrl: [{ required: true, message: "不能为空", trigger: "burl" }],
         isShow: [{ required: true, message: "不能为空", trigger: "change" }],
         talentId: [{ required: true, message: "不能为空", trigger: "change" }],
@@ -821,7 +822,10 @@ export default {
       handler(newVal) {
         if (!newVal || typeof newVal !== "string") return;
         // 已是 :01 结尾则不再回写,避免干扰日期选择器导致无法改时间
-        if (/:\d{2}$/.test(newVal) && newVal.slice(-2) === "01") return;
+        if (/:\d{2}$/.test(newVal) && newVal.slice(-2) === "01") {
+          this.timeChange();
+          return;
+        }
 
         const normalized = newVal.replace("T", " ").substring(0, 19);
         const timeObj = new Date(normalized.replace(/-/g, "/"));
@@ -836,6 +840,7 @@ export default {
         if (formatted !== newVal) {
           this.form.startTime = formatted;
         }
+        this.timeChange();
       },
       immediate: true,
     },
@@ -1190,6 +1195,15 @@ export default {
       if (this.form.liveType == 2 && this.videoUrl.length == 0) {
         return this.$message.error("请上传视频");
       }
+      // 提交前再算一次,避免开始时间/时长异步回填后结束时间为空
+      this.timeChange();
+      if (!this.form.finishTime) {
+        return this.$message.error(
+          this.form.liveType == 2
+            ? "结束时间未生成,请确认已选择开始时间并上传视频"
+            : "请选择结束时间"
+        );
+      }
       console.log(this.form);
       this.$refs["form"].validate((valid) => {
         if (valid) {

+ 12 - 6
src/views/live/liveConfig/completionReward.vue

@@ -285,6 +285,7 @@ export default {
         finishQuestionIds: ''
       },
       finishCouponSelectedIds: [],
+      couponTypeOptions: [],
       rules: {
         completionRate: [
           {
@@ -368,6 +369,11 @@ export default {
       tempSelectedQuestions: []
     };
   },
+  created() {
+    this.getDicts('store_coupon_type').then(response => {
+      this.couponTypeOptions = response.data || [];
+    });
+  },
   watch: {
     '$route.query': {
       handler() {
@@ -390,10 +396,9 @@ export default {
   },
   methods: {
     couponTypeLabel(type) {
-      if (type === 0) return '普通券';
-      if (type === 1) return '套餐券';
-      if (type === 2) return '制单券';
-      return '无门槛券';
+      // 以字典 store_coupon_type 为准(如 3=核销券、4=普通券),勿写死 0/1/2/其余=无门槛
+      const label = this.selectDictLabel(this.couponTypeOptions, type);
+      return label || (type != null ? `类型${type}` : '-');
     },
     resolveCouponTitle(couponId) {
       const coupon = this.couponListData.find(item => item.couponId === couponId);
@@ -405,8 +410,8 @@ export default {
         return [];
       }
       return [
-        `面值?${coupon.couponPrice}`,
-        `满?${coupon.useMinPrice}`,
+        `面值¥${coupon.couponPrice}`,
+        `满¥${coupon.useMinPrice}`,
         `${coupon.couponTime}天`,
         this.couponTypeLabel(coupon.type)
       ];
@@ -550,6 +555,7 @@ export default {
       });
     },
     loadCouponList() {
+      // 不传 pageNum/pageSize,与原先一致拉全量可选优惠券,避免漏券影响配置
       return listLiveCouponOn({ liveId: this.liveId }).then(response => {
         const list = response.rows || [];
         this.couponListData = list;

+ 95 - 33
src/views/live/liveConfig/task.vue

@@ -203,15 +203,12 @@
           </el-select>
         </el-form-item>
         <el-form-item label="优惠券" prop="content" v-if="form.taskType == 5">
-          <el-select v-model="form.content" placeholder="请选择优惠券" ref="selectCoupon" >
+          <el-select v-model="form.content" placeholder="请选择优惠券" ref="selectCoupon" filterable clearable>
             <el-option v-for="i in couponOptions" :key="i.value" :label="i.label" :value="i.value"></el-option>
-            <!-- 加载载中状态 -->
             <div v-if="isLoading" class="loading-indicator">
               <i class="el-icon-loading"></i>
               <span>加载中...</span>
             </div>
-
-            <!-- 没有更多数据 -->
             <div v-if="!hasMore && !isLoading" class="no-more">
               没有更多数据了
             </div>
@@ -247,7 +244,7 @@
                 <el-option v-for="i in redOptions" :key="i.value" :label="i.label" :value="i.value"></el-option>
               </el-select>
               <template v-else-if="item.rewardType == 5">
-                <el-select v-model="item.rewardId" placeholder="请选择优惠券" style="width: 160px; margin-right: 8px;">
+                <el-select v-model="item.rewardId" placeholder="请选择优惠券" filterable clearable style="width: 180px; margin-right: 8px;">
                   <el-option v-for="i in couponOptions" :key="i.value" :label="i.label" :value="i.value"></el-option>
                 </el-select>
                 <el-input-number v-model="item.couponCount" :min="1" :max="99" controls-position="right" placeholder="数量" style="width: 110px; margin-right: 8px;"></el-input-number>
@@ -259,10 +256,10 @@
             <el-button type="text" icon="el-icon-plus" @click="addSignReward">添加奖励</el-button>
           </div>
         </el-form-item>
-        <el-form-item label="触发时间" prop="content">
+        <el-form-item label="触发时间" prop="triggerValue">
           <el-time-picker
-            default-value="2025-01-01 00:00:00"
             v-model="form.triggerValue"
+            value-format="yyyy-MM-dd HH:mm:ss"
             :picker-options="{
       selectableRange: '00:00:00 - 23:59:59'
     }"
@@ -513,7 +510,12 @@ export default {
       }
     },
     productNameFormatter(row, column, value){
-      let content = JSON.parse(row.content)
+      let content = {};
+      try {
+        content = typeof row.content === 'string' ? JSON.parse(row.content || '{}') : (row.content || {});
+      } catch (e) {
+        return '--';
+      }
       if(content.productName) {
         return content.productName
       }
@@ -745,26 +747,29 @@ export default {
       });
     },
     addCouponList() {
-      if(this.haveData.coupon) return
-      listLiveCouponOn(this.listParams).then(res => {
-        if(res.rows.length > 0) {
-          res.rows.forEach(item => {
-            // 根据productName和goodsId组装成为label和value
-            this.couponOptions.push({
-              value: item.couponId,
-              label: item.title
-            })
-          })
+      if (this.haveData.coupon) {
+        return Promise.resolve();
+      }
+      return listLiveCouponOn(this.listParams).then(res => {
+        const rows = res.rows || [];
+        if (rows.length > 0) {
+          rows.forEach(item => {
+            if (!this.couponOptions.some(o => Number(o.value) === Number(item.couponId))) {
+              this.couponOptions.push({
+                value: item.couponId,
+                label: item.title
+              });
+            }
+          });
           this.listParams.pageNum++;
-          // 判断是否还有更多数据
-          this.hasMore = this.couponOptions.length < res.total;
+          // 按页递归拉全量,保证下拉可选券与原先一致
+          this.hasMore = (this.listParams.pageNum - 1) * this.listParams.pageSize < Number(res.total || 0);
           if (this.hasMore) {
-            this.addCouponList();
-          } else {
-            this.haveData.coupon = true
+            return this.addCouponList();
           }
+          this.haveData.coupon = true;
         } else {
-          this.haveData.coupon = true
+          this.haveData.coupon = true;
           this.hasMore = false;
         }
       });
@@ -854,8 +859,22 @@ export default {
       }
       const id = row.id || this.ids
       getTask(id).then(async response => {
-        this.form = response.data;
-        let content = JSON.parse( response.data.content)
+        const data = response.data || {};
+        let content = {};
+        try {
+          content = typeof data.content === 'string' ? JSON.parse(data.content || '{}') : (data.content || {});
+        } catch (e) {
+          content = {};
+        }
+        // 统一回填,保证 rewards 等字段具备响应式
+        this.form = {
+          ...data,
+          content: data.content,
+          goodsId: null,
+          goodsStatus: null,
+          rewards: [],
+          triggerValue: this.normalizeTriggerValue(data.triggerValue)
+        };
         if (this.form.taskType == 1) {
           this.form.content = content.goodsId;
         }else if (this.form.taskType == 2) {
@@ -865,8 +884,8 @@ export default {
         }else if(this.form.taskType == 5){
           this.form.content = content.couponId;
         }else if(this.form.taskType == 6){
-          this.form.goodsId = content.goodsId;
-          this.form.goodsStatus = content.status;
+          this.$set(this.form, 'goodsId', content.goodsId);
+          this.$set(this.form, 'goodsStatus', content.status);
         }else if(this.form.taskType == 7){
           await this.preloadAllSignRewardOptions();
           const rewards = [];
@@ -877,11 +896,17 @@ export default {
                 if (r.rewardType == 2) rewardId = r.reward.redId;
                 else if (r.rewardType == 5) rewardId = r.reward.couponId;
               }
+              // 已触发任务的积分红包可能不在 listOn 中,补进下拉选项
+              if (r.rewardType == 2 && rewardId != null) {
+                this.ensureRedOption(rewardId, r.reward);
+              } else if (r.rewardType == 5 && rewardId != null) {
+                this.ensureCouponOption(rewardId, r.reward);
+              }
               rewards.push({
                 rewardType: r.rewardType != null ? Number(r.rewardType) : null,
-                rewardId: rewardId,
+                rewardId: rewardId != null ? Number(rewardId) : null,
                 couponCount: r.couponCount > 0 ? Number(r.couponCount) : 1,
-                amount: r.amount != null ? r.amount : null
+                amount: r.amount != null ? String(r.amount) : null
               });
             });
           } else if (content.rewardType != null) {
@@ -891,19 +916,56 @@ export default {
               if (content.rewardType == 2) rewardId = content.reward.redId;
               else if (content.rewardType == 5) rewardId = content.reward.couponId;
             }
+            if (content.rewardType == 2 && rewardId != null) {
+              this.ensureRedOption(rewardId, content.reward);
+            } else if (content.rewardType == 5 && rewardId != null) {
+              this.ensureCouponOption(rewardId, content.reward);
+            }
             rewards.push({
               rewardType: Number(content.rewardType),
-              rewardId: rewardId,
+              rewardId: rewardId != null ? Number(rewardId) : null,
               couponCount: content.couponCount > 0 ? Number(content.couponCount) : 1,
-              amount: content.amount != null ? content.amount : null
+              amount: content.amount != null ? String(content.amount) : null
             });
           }
-          this.form.rewards = rewards.length ? rewards : [{ rewardType: null, rewardId: null, couponCount: 1, amount: null }];
+          this.$set(this.form, 'rewards', rewards.length ? rewards : [{ rewardType: null, rewardId: null, couponCount: 1, amount: null }]);
         }
         this.open = true;
         this.title = "修改直播间自动化任务配置";
       });
     },
+    normalizeTriggerValue(val) {
+      if (!val) return null;
+      if (val instanceof Date) return this.parseTime(val, '{y}-{m}-{d} {h}:{i}:{s}');
+      if (typeof val === 'number') {
+        const date = new Date(String(val).length === 10 ? val * 1000 : val);
+        return this.parseTime(date, '{y}-{m}-{d} {h}:{i}:{s}');
+      }
+      if (typeof val === 'string') {
+        // 仅时分秒时补默认日期,供 value-format 使用
+        if (/^\d{2}:\d{2}:\d{2}$/.test(val)) {
+          return '2000-01-01 ' + val;
+        }
+        return val;
+      }
+      return val;
+    },
+    ensureRedOption(rewardId, reward) {
+      const id = Number(rewardId);
+      if (!id || this.redOptions.some(o => Number(o.value) === id)) return;
+      this.redOptions.unshift({
+        value: id,
+        label: (reward && (reward.desc || reward.Desc)) || ('积分红包#' + id)
+      });
+    },
+    ensureCouponOption(rewardId, reward) {
+      const id = Number(rewardId);
+      if (!id || this.couponOptions.some(o => Number(o.value) === id)) return;
+      this.couponOptions.unshift({
+        value: id,
+        label: (reward && (reward.title || reward.Title)) || ('优惠券#' + id)
+      });
+    },
     /** 提交按钮 */
     submitForm() {
       this.form.liveId = this.liveId;

+ 11 - 5
src/views/live/liveConfig/watchReward.vue

@@ -263,9 +263,15 @@ export default {
         { label: '优惠券', value: '3', icon: 'el-icon-ticket', tone: 'tone-green', desc: '发放指定优惠券' }
       ],
       couponSelectOptions: [],
-      couponListData: []
+      couponListData: [],
+      couponTypeOptions: []
     };
   },
+  created() {
+    this.getDicts('store_coupon_type').then(response => {
+      this.couponTypeOptions = response.data || [];
+    });
+  },
   watch: {
     '$route.query': {
       handler() {
@@ -296,10 +302,9 @@ export default {
   },
   methods: {
     couponTypeLabel(type) {
-      if (type === 0) return '普通券';
-      if (type === 1) return '套餐券';
-      if (type === 2) return '制单券';
-      return '无门槛券';
+      // 以字典 store_coupon_type 为准(如 3=核销券、4=普通券),勿写死 0/1/2/其余=无门槛
+      const label = this.selectDictLabel(this.couponTypeOptions, type);
+      return label || (type != null ? `类型${type}` : '-');
     },
     resolveCouponTitle(couponId) {
       const coupon = this.couponListData.find(item => item.couponId === couponId);
@@ -453,6 +458,7 @@ export default {
       });
     },
     loadCouponList() {
+      // 不传 pageNum/pageSize,与原先一致拉全量可选优惠券,避免漏券影响配置
       return listLiveCouponOn({ liveId: this.liveId }).then(response => {
         const list = response.rows || [];
         this.couponListData = list;

+ 252 - 255
src/views/live/liveCouponUser/index.vue

@@ -1,134 +1,151 @@
 <template>
-  <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+  <div class="app-container coupon-user-page">
+    <el-card shadow="never" class="search-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="88px" size="small">
+        <el-form-item label="优惠券名称" prop="couponTitle">
+          <el-input
+            v-model="queryParams.couponTitle"
+            placeholder="请输入优惠券名称"
+            clearable
+            style="width: 180px"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="优惠券类型" prop="couponType">
+          <el-select v-model="queryParams.couponType" placeholder="全部类型" clearable style="width: 140px">
+            <el-option
+              v-for="item in couponTypeOptions"
+              :key="item.dictValue"
+              :label="item.dictLabel"
+              :value="item.dictValue"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="queryParams.status" placeholder="全部状态" clearable style="width: 150px">
+            <el-option
+              v-for="item in statusFilterOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="领取时间">
+          <el-date-picker
+            v-model="dateRange"
+            style="width: 240px"
+            value-format="yyyy-MM-dd"
+            type="daterange"
+            range-separator="-"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
 
-      <el-form-item label="优惠券名称" prop="couponTitle">
-        <el-input
-          v-model="queryParams.couponTitle"
-          placeholder="请输入优惠券名称"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
-      </el-form-item>
-       <el-form-item label="状态" prop="status">
-         <el-select   v-model="queryParams.status" placeholder="请选择状态" clearable size="small" >
-         <el-option
-                v-for="item in statusOptions"
-                :key="item.dictValue"
-                :label="item.dictLabel"
-                :value="item.dictValue"
-              />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="领取时间">
-            <el-date-picker v-model="dateRange" size="small" style="width: 220px" value-format="yyyy-MM-dd" type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
-      </el-form-item>
-      <el-form-item>
-        <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
-        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
-      </el-form-item>
-    </el-form>
-    <el-row :gutter="10" class="mb8">
-      <!-- <el-col :span="1.5">
-        <el-button
-          type="danger"
-          icon="el-icon-delete"
-          size="mini"
-          :disabled="multiple"
-          @click="handleDelete"
-          v-hasPermi="['company:companySmsLogs:remove']"
-        >删除</el-button>
-      </el-col> -->
-      <el-col :span="1.5">
-        <el-button
-          type="warning"
-          icon="el-icon-download"
-          size="mini"
-          @click="handleExport"
-          v-hasPermi="['store:storeCouponUser:export']"
-        >导出</el-button>
-      </el-col>
-	  <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
-    </el-row>
+    <el-card shadow="never" class="table-card">
+      <div class="toolbar">
+        <div class="toolbar-left">
+          <el-button
+            type="warning"
+            plain
+            icon="el-icon-download"
+            size="mini"
+            @click="handleExport"
+            v-hasPermi="['store:storeCouponUser:export', 'live:issue:export']"
+          >导出</el-button>
+          <span class="toolbar-tip">普通券状态为待使用/已使用;核销券为未核销/已核销</span>
+        </div>
+        <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
+      </div>
 
-    <el-table
-      height="500"
-      border
-      size="mini"
-      v-loading="loading"
-      :data="storeCouponUserList"
-    >
-      <el-table-column label="会员" min-width="120" show-overflow-tooltip>
-        <template slot-scope="scope">
-          <div class="cell-main">{{ scope.row.nickname || '-' }}</div>
-          <div class="cell-sub">{{ scope.row.phone || '-' }}</div>
-        </template>
-      </el-table-column>
-      <el-table-column label="优惠券" min-width="140" show-overflow-tooltip>
-        <template slot-scope="scope">
-          <div class="cell-main">{{ scope.row.couponTitle || '-' }}</div>
-          <div class="cell-sub">面值 {{ scope.row.couponPrice }} / 满 {{ scope.row.useMinPrice }}</div>
-        </template>
-      </el-table-column>
-      <el-table-column label="领取/有效期" min-width="150">
-        <template slot-scope="scope">
-          <div class="cell-sub">领:{{ formatTime(scope.row.createTime) }}</div>
-          <div class="cell-sub">止:{{ formatTime(scope.row.limitTime) }}</div>
-        </template>
-      </el-table-column>
-      <el-table-column label="核销信息" min-width="150">
-        <template slot-scope="scope">
-          <div class="cell-main">{{ scope.row.verifyUserName || '-' }}</div>
-          <div class="cell-sub">{{ formatTime(scope.row.verifyTime || scope.row.useTime) }}</div>
-        </template>
-      </el-table-column>
-      <el-table-column label="状态" width="80" align="center">
-        <template slot-scope="scope">
-          <el-tag size="mini" :type="statusTagType(scope.row.status)">{{ statusLabel(scope.row.status) }}</el-tag>
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <pagination
-      v-show="total>0"
-      :total="total"
-      :page.sync="queryParams.pageNum"
-      :limit.sync="queryParams.pageSize"
-      @pagination="getList"
-    />
+      <el-table
+        height="520"
+        border
+        size="mini"
+        v-loading="loading"
+        :data="storeCouponUserList"
+        class="coupon-table"
+      >
+        <el-table-column label="会员" min-width="130" show-overflow-tooltip>
+          <template slot-scope="scope">
+            <div class="cell-main">{{ scope.row.nickname || '-' }}</div>
+            <div class="cell-sub">{{ scope.row.phone || '-' }}</div>
+          </template>
+        </el-table-column>
+        <el-table-column label="优惠券" min-width="160" show-overflow-tooltip>
+          <template slot-scope="scope">
+            <div class="cell-main">{{ scope.row.couponTitle || '-' }}</div>
+            <div class="cell-sub">面值 ¥{{ scope.row.couponPrice }} · 满 ¥{{ scope.row.useMinPrice }}</div>
+          </template>
+        </el-table-column>
+        <el-table-column label="类型" width="100" align="center">
+          <template slot-scope="scope">
+            <el-tag size="mini" :type="couponTypeTagType(scope.row.couponType)" effect="plain">
+              {{ couponTypeLabel(scope.row.couponType) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="领取 / 有效期" min-width="160">
+          <template slot-scope="scope">
+            <div class="cell-sub">领 {{ formatTime(scope.row.createTime) }}</div>
+            <div class="cell-sub">止 {{ formatTime(scope.row.limitTime) }}</div>
+          </template>
+        </el-table-column>
+        <el-table-column label="使用信息" min-width="150">
+          <template slot-scope="scope">
+            <template v-if="isVerifyCouponType(scope.row.couponType)">
+              <div class="cell-main">{{ scope.row.verifyUserName || '-' }}</div>
+              <div class="cell-sub">{{ formatTime(scope.row.verifyTime || scope.row.useTime) }}</div>
+            </template>
+            <template v-else>
+              <div class="cell-main cell-muted">普通券不可核销</div>
+              <div class="cell-sub">{{ formatTime(scope.row.useTime) !== '-' ? ('使用 ' + formatTime(scope.row.useTime)) : '-' }}</div>
+            </template>
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" width="96" align="center">
+          <template slot-scope="scope">
+            <el-tag size="mini" :type="statusTagType(scope.row.status)">
+              {{ rowStatusLabel(scope.row) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+      </el-table>
 
+      <pagination
+        v-show="total > 0"
+        :total="total"
+        :page.sync="queryParams.pageNum"
+        :limit.sync="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </el-card>
   </div>
 </template>
 
 <script>
-import { listStoreCouponUser, getStoreCouponUser, delStoreCouponUser, addStoreCouponUser, updateStoreCouponUser, exportStoreCouponUser } from "@/api/live/liveCouponUser";
+import axios from "axios";
+import { getToken } from "@/utils/auth";
+import { listStoreCouponUser, exportStoreCouponUser } from "@/api/live/liveCouponUser";
 
 export default {
   name: "StoreCouponUser",
   data() {
     return {
-      statusOptions:[],
-      // 遮罩层
+      statusOptions: [],
+      couponTypeOptions: [],
       loading: true,
-      // 选中数组
-      ids: [],
-      dateRange:[],
-      // 非单个禁用
-      single: true,
-      // 非多个禁用
-      multiple: true,
-      // 显示搜索条件
+      dateRange: [],
       showSearch: true,
-      // 总条数
       total: 0,
-      // 优惠券发放记录表格数据
       storeCouponUserList: [],
-      // 弹出层标题
-      title: "",
-      // 是否显示弹出层
-      open: false,
-      // 查询参数
       queryParams: {
         pageNum: 1,
         pageSize: 10,
@@ -140,65 +157,60 @@ export default {
         endTime: null,
         useTime: null,
         type: null,
+        couponType: null,
         status: null,
         isFail: null,
         isDel: null
       },
-      // 表单参数
-      form: {},
-      // 表单校验
-      rules: {
-        couponId: [
-          { required: true, message: "兑换的项目id不能为空", trigger: "blur" }
-        ],
-        userId: [
-          { required: true, message: "优惠券所属用户不能为空", trigger: "blur" }
-        ],
-        couponTitle: [
-          { required: true, message: "优惠券名称不能为空", trigger: "blur" }
-        ],
-        couponPrice: [
-          { required: true, message: "优惠券的面值不能为空", trigger: "blur" }
-        ],
-        useMinPrice: [
-          { required: true, message: "最低消费多少金额可用优惠券不能为空", trigger: "blur" }
-        ],
-        createTime: [
-          { required: true, message: "优惠券创建时间不能为空", trigger: "blur" }
-        ],
-        endTime: [
-          { required: true, message: "优惠券结束时间不能为空", trigger: "blur" }
-        ],
-        type: [
-          { required: true, message: "获取方式不能为空", trigger: "change" }
-        ],
-        status: [
-          { required: true, message: "状态不能为空", trigger: "blur" }
-        ],
-        isFail: [
-          { required: true, message: "是否有效不能为空", trigger: "blur" }
-        ],
-      }
+      // 筛选用中性文案(同一 status 码,展示因券类型不同)
+      statusFilterOptions: [
+        { value: "0", label: "待使用 / 未核销" },
+        { value: "1", label: "已使用 / 已核销" },
+        { value: "2", label: "已过期" }
+      ]
     };
   },
   created() {
     this.getDicts("live_coupon_user_status").then((response) => {
       this.statusOptions = response.data || [];
     });
+    this.getDicts("store_coupon_type").then((response) => {
+      this.couponTypeOptions = response.data || [];
+    });
     this.getList();
   },
   methods: {
-    statusLabel(status) {
-      const hit = (this.statusOptions || []).find(item => String(item.dictValue) === String(status));
-      if (hit) {
-        return hit.dictLabel;
+    couponTypeLabel(type) {
+      if (type === null || type === undefined || type === "") {
+        return "-";
+      }
+      const hit = (this.couponTypeOptions || []).find(item => String(item.dictValue) === String(type));
+      return hit ? hit.dictLabel : `类型${type}`;
+    },
+    isVerifyCouponType(type) {
+      const label = this.couponTypeLabel(type) || "";
+      return label.includes("核销") || label.includes("代金券");
+    },
+    couponTypeTagType(type) {
+      return this.isVerifyCouponType(type) ? "warning" : "";
+    },
+    /** 按券类型展示状态:普通券=待使用/已使用;核销券=未核销/已核销 */
+    rowStatusLabel(row) {
+      const status = row == null ? null : row.status;
+      if (status === null || status === undefined || status === "") {
+        return "-";
       }
-      const fallback = { 0: "未核销", 1: "已核销", 2: "已过期" };
-      return fallback[status] != null ? fallback[status] : status;
+      const code = Number(status);
+      if (this.isVerifyCouponType(row.couponType)) {
+        const map = { 0: "未核销", 1: "已核销", 2: "已过期" };
+        return map[code] != null ? map[code] : String(status);
+      }
+      const map = { 0: "待使用", 1: "已使用", 2: "已过期" };
+      return map[code] != null ? map[code] : String(status);
     },
     statusTagType(status) {
       const map = { 0: "info", 1: "success", 2: "danger" };
-      return map[status] || "info";
+      return map[Number(status)] || "info";
     },
     formatTime(val) {
       if (!val) {
@@ -207,135 +219,120 @@ export default {
       const text = String(val).replace("T", " ");
       return text.length >= 16 ? text.substring(0, 16) : text;
     },
-    /** 查询优惠券发放记录列表 */
     getList() {
       this.loading = true;
       listStoreCouponUser(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
-        this.storeCouponUserList = response.rows;
-        this.total = response.total;
+        this.storeCouponUserList = response.rows || [];
+        this.total = response.total || 0;
+        this.loading = false;
+      }).catch(() => {
         this.loading = false;
       });
     },
-    // 取消按钮
-    cancel() {
-      this.open = false;
-      this.reset();
-    },
-    // 表单重置
-    reset() {
-      this.form = {
-        id: null,
-        couponId: null,
-        userId: null,
-        couponTitle: null,
-        couponPrice: null,
-        useMinPrice: null,
-        createTime: null,
-        updateTime: null,
-        endTime: null,
-        useTime: null,
-        type: null,
-        status: 0,
-        isFail: null,
-        isDel: null
-      };
-      this.resetForm("form");
-    },
-    /** 搜索按钮操作 */
     handleQuery() {
       this.queryParams.pageNum = 1;
       this.getList();
     },
-    /** 重置按钮操作 */
     resetQuery() {
+      this.dateRange = [];
       this.resetForm("queryForm");
       this.handleQuery();
     },
-    // 多选框选中数据
-    handleSelectionChange(selection) {
-      this.ids = selection.map(item => item.id)
-      this.single = selection.length!==1
-      this.multiple = !selection.length
-    },
-    /** 新增按钮操作 */
-    handleAdd() {
-      this.reset();
-      this.open = true;
-      this.title = "添加优惠券发放记录";
-    },
-    /** 修改按钮操作 */
-    handleUpdate(row) {
-      this.reset();
-      const id = row.id || this.ids
-      getStoreCouponUser(id).then(response => {
-        this.form = response.data;
-        this.open = true;
-        this.title = "修改优惠券发放记录";
-      });
+    handleExport() {
+      const queryParams = this.addDateRange({ ...this.queryParams }, this.dateRange);
+      this.$confirm("是否确认导出所有优惠券发放记录数据项?", "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        return exportStoreCouponUser(queryParams);
+      }).then(response => {
+        return this.downloadFileWithAuth(response.msg);
+      }).catch(() => {});
     },
-    /** 提交按钮 */
-    submitForm() {
-      this.$refs["form"].validate(valid => {
-        if (valid) {
-          if (this.form.id != null) {
-            updateStoreCouponUser(this.form).then(response => {
-              if (response.code === 200) {
-                this.msgSuccess("修改成功");
-                this.open = false;
-                this.getList();
-              }
-            });
-          } else {
-            addStoreCouponUser(this.form).then(response => {
-              if (response.code === 200) {
-                this.msgSuccess("新增成功");
-                this.open = false;
-                this.getList();
-              }
-            });
-          }
+    downloadFileWithAuth(fileName) {
+      if (!fileName) {
+        this.msgError("导出失败,未获取到文件名");
+        return Promise.reject("empty fileName");
+      }
+      return axios({
+        method: "get",
+        url: process.env.VUE_APP_BASE_API + "/common/download",
+        params: { fileName: fileName, delete: false },
+        responseType: "blob",
+        headers: {
+          Authorization: "Bearer " + getToken(),
+          "X-Frontend-Type": "admin"
+        }
+      }).then(res => {
+        const disposition = res.headers["content-disposition"] || "";
+        let downloadName = fileName.indexOf("_") > -1 ? fileName.substring(fileName.indexOf("_") + 1) : fileName;
+        const match = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(disposition);
+        if (match && match[1]) {
+          downloadName = decodeURIComponent(match[1].replace(/['"]/g, ""));
         }
+        const blob = new Blob([res.data]);
+        const link = document.createElement("a");
+        link.href = window.URL.createObjectURL(blob);
+        link.setAttribute("download", downloadName);
+        document.body.appendChild(link);
+        link.click();
+        document.body.removeChild(link);
+        window.URL.revokeObjectURL(link.href);
       });
-    },
-    /** 删除按钮操作 */
-    handleDelete(row) {
-      const ids = row.id || this.ids;
-      this.$confirm('是否确认删除优惠券发放记录编号为"' + ids + '"的数据项?', "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(function() {
-          return delStoreCouponUser(ids);
-        }).then(() => {
-          this.getList();
-          this.msgSuccess("删除成功");
-        }).catch(function() {});
-    },
-    /** 导出按钮操作 */
-    handleExport() {
-      const queryParams = this.queryParams;
-      this.$confirm('是否确认导出所有优惠券发放记录数据项?', "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(function() {
-          return exportStoreCouponUser(queryParams);
-        }).then(response => {
-          this.download(response.msg);
-        }).catch(function() {});
     }
   }
 };
 </script>
 
 <style scoped>
+.coupon-user-page {
+  padding-bottom: 12px;
+}
+.search-card {
+  margin-bottom: 12px;
+  border-radius: 8px;
+}
+.search-card >>> .el-card__body {
+  padding: 14px 16px 2px;
+}
+.table-card {
+  border-radius: 8px;
+}
+.table-card >>> .el-card__body {
+  padding: 12px 16px 8px;
+}
+.toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12px;
+}
+.toolbar-left {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+.toolbar-tip {
+  font-size: 12px;
+  color: #909399;
+}
+.coupon-table {
+  width: 100%;
+}
 .cell-main {
   line-height: 18px;
   color: #303133;
+  font-weight: 500;
+}
+.cell-muted {
+  color: #909399;
+  font-weight: 400;
 }
 .cell-sub {
   line-height: 18px;
   font-size: 12px;
   color: #909399;
+  margin-top: 2px;
 }
 </style>

+ 57 - 8
src/views/live/liveData/UserClaimTypeActions.vue

@@ -106,7 +106,8 @@ export default {
       dialogTitle: '领取记录',
       loading: false,
       recordList: [],
-      claimType: null
+      claimType: null,
+      couponTypeOptions: []
     }
   },
   computed: {
@@ -115,6 +116,11 @@ export default {
       return this.claimType === 'verifyCoupon' ? '900px' : '640px'
     }
   },
+  created() {
+    this.getDicts('store_coupon_type').then(response => {
+      this.couponTypeOptions = response.data || []
+    })
+  },
   methods: {
     handleViewClaim(claimType) {
       if (!this.liveId || !this.user || !this.user.userId) {
@@ -129,29 +135,72 @@ export default {
       getUserClaimRecords(this.liveId, this.user.userId).then(response => {
         if (response.code === 200) {
           const list = response.data || []
-          this.recordList = list.filter(item => this.matchClaimType(item.opType, claimType))
+          this.recordList = list.filter(item => this.matchClaimType(item, claimType))
         }
         this.loading = false
       }).catch(() => {
         this.loading = false
       })
     },
-    /** 与后端 LiveConsoleOpLog.opType 保持一致 */
-    matchClaimType(opType, claimType) {
-      const type = Number(opType)
+    /** 与后端 LiveConsoleOpLog.opType + verifyCoupon/couponType 保持一致 */
+    matchClaimType(item, claimType) {
+      if (!item) {
+        return false
+      }
+      const type = Number(item.opType)
       if (claimType === 'redPoints') {
         // 5红包发放 6抽奖发放 7完课积分 9观看奖励积分 11签到金额红包
         return [5, 6, 7, 9, 11].includes(type)
       }
+      const isVerify = this.isVerifyCouponRecord(item)
       if (claimType === 'verifyCoupon') {
-        return type === 2
+        // 中控核销券展示(2) + 完课/观看奖励中的核销类券(8/10)
+        if (type === 2) {
+          return true
+        }
+        if ([8, 10].includes(type)) {
+          return isVerify
+        }
+        return false
       }
       if (claimType === 'coupon') {
-        // 1优惠券展示 8完课优惠券 10观看奖励优惠券
-        return [1, 8, 10].includes(type)
+        // 中控普通优惠券(1) + 完课/观看奖励中的非核销券(8/10)
+        if (type === 1) {
+          return true
+        }
+        if ([8, 10].includes(type)) {
+          return !isVerify
+        }
+        return false
       }
       return true
     },
+    /**
+     * 核销券判定:opType=2,或后端 verifyCoupon,或字典 store_coupon_type 标签含核销/代金券。
+     * 完课/观看奖励统一是 opType=8/10,必须靠券模板类型(live_coupon.type)区分,
+     * 切勿把 live_coupon_user.type(获取方式,如 4-151 完课、3 看课)当成券类型。
+     */
+    isVerifyCouponRecord(item) {
+      const type = Number(item.opType)
+      if (type === 2) {
+        return true
+      }
+      if (type === 1) {
+        return false
+      }
+      if (item.verifyCoupon === true || item.verifyCoupon === 1 || item.verifyCoupon === 'true') {
+        return true
+      }
+      if (item.verifyCoupon === false || item.verifyCoupon === 0 || item.verifyCoupon === 'false') {
+        return false
+      }
+      const label = this.selectDictLabel(this.couponTypeOptions, item.couponType) || ''
+      if (label.includes('核销') || label.includes('代金券')) {
+        return true
+      }
+      // 兼容旧数据:字典未加载时仅 type=3 视为核销/无门槛
+      return Number(item.couponType) === 3
+    },
     verifyStatusTagType(status) {
       const map = { 0: 'info', 1: 'success', 2: 'danger' }
       return map[Number(status)] || 'info'