Browse Source

活动商品优化

yjwang 5 days ago
parent
commit
001fe79710
2 changed files with 564 additions and 1 deletions
  1. 26 0
      src/api/hisStore/productActivity.js
  2. 538 1
      src/views/hisStore/storeProduct/index.vue

+ 26 - 0
src/api/hisStore/productActivity.js

@@ -0,0 +1,26 @@
+import request from '@/utils/request'
+
+// 根据商品ID查询活动设置(含活动状态、是否进行中)
+export function getActivityByProductId(productId) {
+  return request({
+    url: '/store/store/productActivity/getByProductId/' + productId,
+    method: 'get'
+  })
+}
+
+// 保存活动设置(批量保存)
+export function saveActivity(data) {
+  return request({
+    url: '/store/store/productActivity/save',
+    method: 'post',
+    data: data
+  })
+}
+
+// 删除活动设置
+export function removeActivity(productId) {
+  return request({
+    url: '/store/store/productActivity/remove/' + productId,
+    method: 'delete'
+  })
+}

+ 538 - 1
src/views/hisStore/storeProduct/index.vue

@@ -7,7 +7,7 @@
       </el-form-item>
 
       <el-form-item label="商品名称" prop="productName">
-        <el-input
+        <el-inputa
           v-model="queryParams.productName"
           placeholder="请输入商品名称"
           clearable
@@ -329,6 +329,13 @@
       </el-table-column>
       <el-table-column label="销量" align="center" prop="sales" />
       <el-table-column label="库存" align="center" prop="stock" />
+      <el-table-column label="活动类型" align="center" prop="activityType" width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.activityType === 6" type="danger" size="small">秒杀</el-tag>
+          <el-tag v-else-if="scope.row.activityType === 7" type="warning" size="small">限时折扣</el-tag>
+          <el-tag v-else type="info" size="small">无</el-tag>
+        </template>
+      </el-table-column>
       <el-table-column label="类型" align="center" prop="productType" >
         <template slot-scope="scope">
           <el-tag prop="productType" v-for="(item, index) in productTypeOptions"    v-if="scope.row.productType==item.dictValue">{{item.dictLabel}}</el-tag>
@@ -343,6 +350,14 @@
       </el-table-column>
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-s-flag"
+            @click="handleSetActivity(scope.row)"
+            v-hasPermi="['store:storeProduct:edit']"
+            v-if="scope.row.isAudit === '1'"
+          >设置活动</el-button>
           <el-button
             size="mini"
             type="text"
@@ -960,6 +975,131 @@
       </div>
     </el-dialog>
 
+    <!-- 设置活动弹窗 -->
+    <el-dialog title="设置活动" :visible.sync="activityDialogVisible" width="780px" append-to-body :close-on-click-modal="false">
+      <!-- 活动进行中提示 -->
+      <el-alert
+        v-if="activityIsOngoing"
+        :title="'当前商品正在' + (activityOngoingType === 6 ? '秒杀' : '限时折扣') + '活动中,活动进行期间无法修改活动设置'"
+        type="error"
+        :closable="false"
+        show-icon
+        style="margin-bottom:15px;"
+      />
+
+      <el-form ref="activityForm" :model="activityForm" :rules="activityRules" label-width="100px" :disabled="activityIsOngoing">
+        <!-- 活动类型 -->
+        <el-form-item label="活动类型" prop="activityType">
+          <el-radio-group v-model="activityForm.activityType">
+            <el-radio :label="0">不参与活动</el-radio>
+            <el-radio :label="6">
+              <i class="el-icon-lightning" style="color:#F56C6C;margin-right:2px;"></i>秒杀
+            </el-radio>
+            <el-radio :label="7">
+              <i class="el-icon-time" style="color:#E6A23C;margin-right:2px;"></i>限时折扣
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+
+        <!-- 统一活动时间(所有规格共用) -->
+        <template v-if="activityForm.activityType === 6 || activityForm.activityType === 7">
+          <el-form-item label="活动时间" prop="startTime">
+            <el-date-picker v-model="activityForm.startTime" type="datetime" placeholder="开始时间" value-format="yyyy-MM-dd HH:mm:ss" style="width:200px" size="small"/>
+            <span style="margin:0 8px;color:#909399;">至</span>
+            <el-date-picker v-model="activityForm.endTime" type="datetime" placeholder="结束时间" value-format="yyyy-MM-dd HH:mm:ss" style="width:200px" size="small"/>
+          </el-form-item>
+        </template>
+
+        <template v-if="activityForm.activityType === 6 || activityForm.activityType === 7">
+          <!-- 选择参与规格 -->
+          <el-divider content-position="left">
+            <span style="font-size:14px;font-weight:500;">选择参与规格</span>
+          </el-divider>
+          <div class="spec-select-list">
+            <div
+              v-for="(spec, idx) in activityAllSpecs"
+              :key="idx"
+              :class="['spec-select-item', spec.selected ? 'spec-select-active' : '']"
+              @click="toggleSpecSelect(spec)"
+            >
+              <el-popover placement="right" trigger="hover" v-if="spec.image">
+                <img slot="reference" :src="spec.image" class="spec-thumb" />
+                <img :src="spec.image" style="max-width:200px;max-height:200px;" />
+              </el-popover>
+              <div class="spec-select-info">
+                <span class="spec-select-name">{{ spec.specName }}</span>
+                <span class="spec-select-detail">¥{{ spec.price }} · 库存{{ spec.specStock }}</span>
+              </div>
+              <i :class="spec.selected ? 'el-icon-circle-check' : 'el-icon-circle-plus-outline'" :style="{color: spec.selected ? '#409EFF' : '#C0C4CC', fontSize: '20px'}"></i>
+            </div>
+          </div>
+
+          <!-- 已选规格活动设置 -->
+          <template v-if="selectedSpecList.length > 0">
+            <el-divider content-position="left">
+              <span style="font-size:14px;font-weight:500;">规格活动设置</span>
+              <span style="font-size:12px;color:#909399;margin-left:8px;">已选 {{ selectedSpecList.length }} 项</span>
+            </el-divider>
+            <div class="activity-spec-list">
+              <div v-for="(spec, idx) in selectedSpecList" :key="idx" class="activity-spec-card">
+                <div class="spec-card-header">
+                  <div class="spec-header-left">
+                    <el-popover placement="right" trigger="hover" v-if="spec.image">
+                      <img slot="reference" :src="spec.image" class="spec-thumb" />
+                      <img :src="spec.image" style="max-width:200px;max-height:200px;" />
+                    </el-popover>
+                    <span class="spec-name">{{ spec.specName }}</span>
+                    <i class="el-icon-remove-outline" style="color:#F56C6C;font-size:16px;cursor:pointer;margin-left:8px;" title="移除" @click="toggleSpecSelect(spec)"></i>
+                  </div>
+                </div>
+                <div class="spec-detail-row">
+                  <span class="spec-detail-item">售价 <b>¥{{ spec.price }}</b></span>
+                  <span class="spec-detail-item">代理价 <b>¥{{ spec.agentPrice }}</b></span>
+                  <span class="spec-detail-item">成本价 <b>¥{{ spec.cost }}</b></span>
+                  <span class="spec-detail-item">原价 <b>¥{{ spec.otPrice }}</b></span>
+                  <span class="spec-detail-item">库存 <b>{{ spec.specStock }}</b></span>
+                  <span class="spec-detail-item" v-if="spec.barCode">编号 <b>{{ spec.barCode }}</b></span>
+                </div>
+                <el-row :gutter="16" class="spec-card-body">
+                  <!-- 秒杀价 -->
+                  <el-col :span="12" v-if="activityForm.activityType === 6">
+                    <div class="spec-field">
+                      <label>秒杀价</label>
+                      <el-input-number v-model="spec.flashPrice" :precision="2" :min="0" :max="spec.originalPrice" size="small" placeholder="秒杀价" style="width:100%" controls-position="right"/>
+                    </div>
+                  </el-col>
+                  <!-- 折扣设置 -->
+                  <el-col :span="8" v-if="activityForm.activityType === 7">
+                    <div class="spec-field">
+                      <label>折扣 <span style="color:#E6A23C;font-weight:500;">{{ spec.discountValue ? spec.discountValue.toFixed(1) : '10.0' }}折</span></label>
+                      <el-slider v-model="spec.discountValue" :min="1" :max="10" :step="0.1" :show-tooltip="false" @change="handleSpecDiscountChange(spec)"/>
+                    </div>
+                  </el-col>
+                  <el-col :span="8" v-if="activityForm.activityType === 7">
+                    <div class="spec-field">
+                      <label>折扣价</label>
+                      <el-input-number v-model="spec.discountPrice" :precision="2" :min="0" size="small" placeholder="折扣价" style="width:100%" controls-position="right" @change="handleSpecDiscountPriceChange(spec)"/>
+                    </div>
+                  </el-col>
+                </el-row>
+              </div>
+            </div>
+          </template>
+          <div v-else style="text-align:center;padding:20px 0;color:#C0C4CC;font-size:13px;">
+            请在上方选择需要参与活动的规格
+          </div>
+        </template>
+
+        <el-form-item label="备注" style="margin-top:15px;">
+          <el-input v-model="activityForm.remark" type="textarea" :rows="2" placeholder="请输入备注" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer" v-if="!activityIsOngoing">
+        <el-button @click="activityDialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="submitActivityForm" :loading="activitySubmitLoading">确 定</el-button>
+      </div>
+    </el-dialog>
+
   </div>
 </template>
 
@@ -986,6 +1126,8 @@ import singleImg from '@/components/Material/single'
 import { getCompanyList } from "@/api/company/company";
 import { listStore } from '@/api/hisStore/store'
 import {list as getAppMallOptions} from "@/api/course/coursePlaySourceConfig";
+import { getActivityByProductId, saveActivity } from "@/api/hisStore/productActivity";
+import { listStoreProductAttrValue } from "@/api/hisStore/storeProductAttrValue";
 export default {
   name: "HisStoreProduct",
   components: {
@@ -1005,6 +1147,11 @@ export default {
       this.form.drugImage = val.join(',');
     }
   },
+  computed: {
+    selectedSpecList() {
+      return this.activityAllSpecs.filter(s => s.selected);
+    }
+  },
   data() {
     return {
       isMedicalMall: this.$store.state.user.medicalMallConfig.medicalMall,
@@ -1084,6 +1231,24 @@ export default {
       // 企业列表
       companyOptions:[],
       storeOptions:[],
+      // 活动设置相关
+      activityDialogVisible: false,
+      activityIsOngoing: false,
+      activityOngoingType: null,
+      activityCurrentProduct: null,
+      activitySubmitLoading: false,
+      activityForm: {
+        activityType: 0,
+        startTime: null,
+        endTime: null,
+        remark: null
+      },
+      activitySpecList: [],
+      activityAllSpecs: [],
+      activityRules: {
+        activityType: [{ required: true, message: '请选择活动类型', trigger: 'change' }],
+        startTime: [{ required: true, message: '请选择活动开始时间', trigger: 'change' }]
+      },
       // 遮罩层
       loading: true,
       // 选中数组
@@ -1816,6 +1981,246 @@ export default {
         this.msgSuccess("复制成功");
       }).catch(function() {});
     },
+    /** 设置活动按钮操作 */
+    handleSetActivity(row) {
+      // // 校验审核状态:只有审核通过的商品才能设置活动
+      // if (row.isAudit !== 1) {
+      //   this.$message.warning('商品审核通过后才能设置活动');
+      //   return;
+      // }
+      this.activityCurrentProduct = row;
+      this.activityIsOngoing = false;
+      this.activityOngoingType = null;
+      this.activityForm = {
+        activityType: 0,
+        startTime: null,
+        endTime: null,
+        remark: null
+      };
+      this.activitySpecList = [];
+      this.activityAllSpecs = [];
+
+      // 先查询已有活动设置
+      getActivityByProductId(row.productId).then(res => {
+        const activityList = res.data || [];
+        this.activityIsOngoing = res.isOngoing || false;
+        this.activityOngoingType = res.ongoingActivityType || null;
+
+        if (activityList.length > 0) {
+          // 回显已有活动设置
+          const first = activityList[0];
+          this.activityForm.activityType = first.activityType || 0;
+          this.activityForm.remark = first.remark;
+          this.activityForm.startTime = first.startTime || null;
+          this.activityForm.endTime = first.endTime || null;
+        } else {
+          // 从商品表获取activityType
+          this.activityForm.activityType = row.activityType || 0;
+        }
+
+        // 加载商品规格数据
+        this.loadActivitySpecList(row.productId, activityList);
+
+        this.activityDialogVisible = true;
+      }).catch(() => {
+        // 查询失败时仍然打开弹窗,使用商品行数据
+        this.activityForm.activityType = row.activityType || 0;
+        this.loadActivitySpecList(row.productId, []);
+        this.activityDialogVisible = true;
+      });
+    },
+
+    /** 加载活动规格列表 */
+    loadActivitySpecList(productId, existActivityList) {
+      // 先获取商品基本信息(确定specType),再获取规格值列表
+      getStoreProduct(productId).then(res => {
+        const productData = res.data;
+        listStoreProductAttrValue({ productId: productId, pageNum: 1, pageSize: 999 }).then(attrRes => {
+          const attrValues = attrRes.rows || [];
+          let specList = attrValues.map(v => ({
+            specId: v.id,
+            specName: v.sku || ('规格' + v.id),
+            image: v.image || productData.image || null,
+            price: v.price || 0,
+            agentPrice: v.agentPrice || 0,
+            cost: v.cost || 0,
+            otPrice: v.otPrice || 0,
+            specStock: v.stock || 0,
+            barCode: v.barCode || '',
+            originalPrice: v.price || 0,
+            startTime: null,
+            endTime: null,
+            flashPrice: null,
+            discountValue: 10,
+            discount: null,
+            discountPrice: null,
+            stock: null,
+            selected: false
+          }));
+
+          // 回显已有活动数据
+          this.mergeExistActivity(specList, existActivityList);
+          this.activityAllSpecs = specList;
+        })
+      });
+    },
+
+    /** 切换规格选中状态 */
+    toggleSpecSelect(spec) {
+      spec.selected = !spec.selected;
+    },
+
+    /** 合并已有活动数据到规格列表 */
+    mergeExistActivity(specList, existActivityList) {
+      if (existActivityList && existActivityList.length > 0) {
+        specList.forEach(spec => {
+          const matched = existActivityList.find(a => {
+            if (spec.specId !== null) {
+              return a.specId === spec.specId;
+            } else {
+              return true;
+            }
+          });
+          if (matched) {
+            spec.selected = true;
+            spec.flashPrice = matched.flashPrice;
+            spec.discount = matched.discount;
+            spec.discountPrice = matched.discountPrice;
+            spec.stock = matched.stock;
+            spec.originalPrice = matched.originalPrice || spec.originalPrice;
+            spec.startTime = matched.startTime;
+            spec.endTime = matched.endTime;
+            // 折扣值转换:discount存的是0.8格式,滑块需要8格式
+            if (matched.discount) {
+              spec.discountValue = Number((matched.discount * 10).toFixed(1));
+            }
+          }
+        });
+      }
+    },
+
+    /** 折扣滑块变化时计算折扣价 */
+    handleSpecDiscountChange(row) {
+      if (row.originalPrice && row.discountValue) {
+        const discount = row.discountValue / 10;
+        row.discount = discount;
+        row.discountPrice = Math.round(row.originalPrice * discount * 100) / 100;
+      }
+    },
+
+    /** 折扣价变化时反算折扣 */
+    handleSpecDiscountPriceChange(row) {
+      if (row.originalPrice && row.discountPrice) {
+        const discount = row.discountPrice / row.originalPrice;
+        row.discount = Math.round(discount * 1000) / 1000;
+        row.discountValue = Math.round(discount * 100) / 10;
+      }
+    },
+
+    /** 提交活动设置 */
+    submitActivityForm() {
+      this.$refs['activityForm'].validate(valid => {
+        if (!valid) return;
+
+        const { activityType, remark } = this.activityForm;
+
+        if (activityType === 0) {
+          // 不参与活动,直接保存
+          this.activitySubmitLoading = true;
+          saveActivity({
+            productId: this.activityCurrentProduct.productId,
+            activityType: 0,
+            activityList: []
+          }).then(res => {
+            if (res.code === 200) {
+              this.$message.success('活动设置已清除');
+              this.activityDialogVisible = false;
+              this.getList();
+            } else {
+              this.$message.error(res.msg || '保存失败');
+            }
+          }).finally(() => {
+            this.activitySubmitLoading = false;
+          });
+          return;
+        }
+
+        // 校验统一活动时间
+        if (!this.activityForm.startTime) {
+          this.$message.warning('请选择活动开始时间');
+          return;
+        }
+        if (!this.activityForm.endTime) {
+          this.$message.warning('请选择活动结束时间');
+          return;
+        }
+
+        // 校验规格活动数据
+        let hasError = false;
+        const activityList = this.selectedSpecList.map(spec => {
+          const item = {
+            specId: spec.specId,
+            originalPrice: spec.originalPrice,
+            stock: spec.stock,
+            startTime: this.activityForm.startTime,
+            endTime: this.activityForm.endTime
+          };
+          if (activityType === 6) {
+            // 秒杀
+            if (!spec.flashPrice && spec.flashPrice !== 0) {
+              this.$message.warning('请填写规格【' + spec.specName + '】的秒杀价');
+              hasError = true;
+              return null;
+            }
+            item.flashPrice = spec.flashPrice;
+          } else if (activityType === 7) {
+            // 限时折扣
+            if (!spec.discount && spec.discount !== 0) {
+              this.$message.warning('请填写规格【' + spec.specName + '】的折扣');
+              hasError = true;
+              return null;
+            }
+            item.discount = spec.discount;
+            item.discountPrice = spec.discountPrice;
+          }
+
+          // if (!spec.stock && spec.stock !== 0) {
+          //   this.$message.warning('请填写规格【' + spec.specName + '】的活动库存');
+          //   hasError = true;
+          //   return null;
+          // }
+
+          return item;
+        });
+
+        if (hasError) return;
+
+        // 过滤掉null
+        const validList = activityList.filter(item => item !== null);
+        if (validList.length === 0) {
+          this.$message.warning('请至少设置一个规格的活动信息');
+          return;
+        }
+
+        this.activitySubmitLoading = true;
+        saveActivity({
+          productId: this.activityCurrentProduct.productId,
+          activityType: activityType,
+          activityList: validList
+        }).then(res => {
+          if (res.code === 200) {
+            this.$message.success('活动设置保存成功');
+            this.activityDialogVisible = false;
+            this.getList();
+          } else {
+            this.$message.error(res.msg || '保存失败');
+          }
+        }).finally(() => {
+          this.activitySubmitLoading = false;
+        });
+      });
+    },
+
     /** 导出按钮操作 */
     handleExport() {
       const queryParams = this.queryParams;
@@ -1832,3 +2237,135 @@ export default {
   }
 };
 </script>
+
+<style scoped>
+/* 规格选择列表 */
+.spec-select-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin-bottom: 4px;
+}
+.spec-select-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 12px;
+  border: 1px dashed #DCDFE6;
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.2s;
+  background: #fff;
+  min-width: 160px;
+}
+.spec-select-item:hover {
+  border-color: #409EFF;
+  background: #ECF5FF;
+}
+.spec-select-active {
+  border-style: solid;
+  border-color: #409EFF;
+  background: #ECF5FF;
+}
+.spec-select-info {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+  flex: 1;
+  min-width: 0;
+}
+.spec-select-name {
+  font-size: 13px;
+  font-weight: 500;
+  color: #303133;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.spec-select-detail {
+  font-size: 11px;
+  color: #909399;
+}
+
+/* 活动规格卡片列表 */
+.activity-spec-list {
+  max-height: 400px;
+  overflow-y: auto;
+  padding-right: 5px;
+}
+.activity-spec-card {
+  border: 1px solid #EBEEF5;
+  border-radius: 6px;
+  margin-bottom: 12px;
+  background: #FAFAFA;
+  transition: all 0.2s;
+}
+.activity-spec-card:hover {
+  border-color: #409EFF;
+  box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
+}
+.spec-card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 10px 16px;
+  border-bottom: 1px solid #EBEEF5;
+  background: #F5F7FA;
+  border-radius: 6px 6px 0 0;
+}
+.spec-card-header .spec-name {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+}
+.spec-header-left {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+.spec-thumb {
+  width: 32px;
+  height: 32px;
+  border-radius: 4px;
+  object-fit: cover;
+  border: 1px solid #EBEEF5;
+  flex-shrink: 0;
+}
+.spec-card-header .spec-info {
+  font-size: 12px;
+  color: #909399;
+}
+.spec-card-header .spec-info b {
+  color: #606266;
+}
+.spec-detail-row {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 4px 16px;
+  padding: 8px 16px;
+  background: #F5F7FA;
+  border-bottom: 1px solid #EBEEF5;
+}
+.spec-detail-item {
+  font-size: 12px;
+  color: #909399;
+  white-space: nowrap;
+}
+.spec-detail-item b {
+  color: #606266;
+  font-weight: 500;
+}
+.spec-card-body {
+  padding: 12px 16px 8px;
+}
+.spec-field {
+  margin-bottom: 4px;
+}
+.spec-field label {
+  display: block;
+  font-size: 12px;
+  color: #909399;
+  margin-bottom: 6px;
+  line-height: 1;
+}
+</style>