Przeglądaj źródła

1、调整优惠卷领取记录

yys 1 tydzień temu
rodzic
commit
cb3e88c838

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

@@ -196,15 +196,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>
@@ -240,7 +237,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>
@@ -252,10 +249,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'
     }"
@@ -519,7 +516,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
       }
@@ -752,26 +754,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;
         }
       });
@@ -861,8 +866,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) {
@@ -872,8 +891,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 = [];
@@ -884,11 +903,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) {
@@ -898,19 +923,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;

+ 84 - 2
src/views/live/liveCouponUser/index.vue

@@ -10,6 +10,16 @@
           @keyup.enter.native="handleQuery"
         />
       </el-form-item>
+      <el-form-item label="优惠券类型" prop="couponType">
+        <el-select v-model="queryParams.couponType" placeholder="请选择类型" clearable size="small">
+          <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 size="small">
           <el-option
@@ -69,6 +79,13 @@
           <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)">
+            {{ couponTypeLabel(scope.row.couponType) }}
+          </el-tag>
+        </template>
+      </el-table-column>
       <el-table-column label="领取/有效期" min-width="150">
         <template slot-scope="scope">
           <div class="cell-sub">领:{{ formatTime(scope.row.createTime) }}</div>
@@ -89,7 +106,7 @@
       <el-table-column label="操作" width="70" align="center">
         <template slot-scope="scope">
           <el-button
-            v-if="String(scope.row.status) === '0'"
+            v-if="canVerify(scope.row)"
             size="mini"
             type="text"
             @click="handleVerify(scope.row)"
@@ -110,6 +127,8 @@
 </template>
 
 <script>
+import axios from "axios";
+import { getToken } from "@/utils/auth";
 import { listStoreCouponUser, exportStoreCouponUser, verifyStoreCouponUser } from "@/api/live/liveCouponUser";
 
 export default {
@@ -117,6 +136,7 @@ export default {
   data() {
     return {
       statusOptions: [],
+      couponTypeOptions: [],
       loading: true,
       dateRange: [],
       showSearch: true,
@@ -126,6 +146,7 @@ export default {
         pageNum: 1,
         pageSize: 10,
         couponTitle: null,
+        couponType: null,
         status: null
       }
     };
@@ -134,9 +155,34 @@ export default {
     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: {
+    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}`;
+    },
+    couponTypeTagType(type) {
+      const label = this.couponTypeLabel(type) || '';
+      if (label.includes('核销') || label.includes('代金券')) {
+        return 'warning';
+      }
+      return 'info';
+    },
+    /** 仅核销类券且未核销可操作;普通券不可核销 */
+    isVerifyCouponType(type) {
+      const label = this.couponTypeLabel(type) || '';
+      return label.includes('核销') || label.includes('代金券');
+    },
+    canVerify(row) {
+      return row && String(row.status) === '0' && this.isVerifyCouponType(row.couponType);
+    },
     statusLabel(status) {
       const hit = (this.statusOptions || []).find(item => String(item.dictValue) === String(status));
       if (hit) {
@@ -176,6 +222,10 @@ export default {
       this.handleQuery();
     },
     handleVerify(row) {
+      if (!this.isVerifyCouponType(row.couponType)) {
+        this.$message.warning('普通优惠券不可核销,仅核销券支持核销');
+        return;
+      }
       this.$confirm('是否确认核销"' + row.couponTitle + '"?', "警告", {
         confirmButtonText: "确定",
         cancelButtonText: "取消",
@@ -189,6 +239,7 @@ export default {
         }
       }).catch(() => {});
     },
+    /** 本页单独带 Authorization 下载,避免 location 跳转丢 token,不影响全局 download */
     handleExport() {
       const queryParams = this.addDateRange({ ...this.queryParams }, this.dateRange);
       this.$confirm('是否确认导出所有优惠券发放记录数据项?', "警告", {
@@ -198,8 +249,39 @@ export default {
       }).then(() => {
         return exportStoreCouponUser(queryParams);
       }).then(response => {
-        this.download(response.msg);
+        return this.downloadFileWithAuth(response.msg);
       }).catch(() => {});
+    },
+    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: true },
+        responseType: "blob",
+        headers: {
+          Authorization: "Bearer " + getToken(),
+          "X-Frontend-Type": "company"
+        }
+      }).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);
+      });
     }
   }
 };