Ver Fonte

抽奖配置:自定义奖品

Xiao_123 há 5 meses atrás
pai
commit
3ccbf9a398

+ 8 - 3
src/api/mall/promotion/lottery/config/index.ts

@@ -54,9 +54,14 @@ export const LuckLotteryApi = {
     return await request.delete({ url: `/promotion/luck-lottery/delete?id=` + id })
   },
 
-  // 导出幸运抽奖-活动 Excel
-  exportLuckLottery: async (params) => {
-    return await request.download({ url: `/promotion/luck-lottery/export-excel`, params })
+  // 导出奖品模板 Excel
+  exportLotteryPrizeTemplate: async () => {
+    return await request.download({ url: `/promotion/luck-prize/import-template` })
+  },
+
+  // 批量导入奖品 Excel
+  importLotteryPrizeData: async (data: any) => {
+    return await request.upload({ url: `/promotion/luck-prize/import`, data })
   },
 
   // 查询幸运抽奖-奖品分页

+ 95 - 0
src/views/mall/promotion/lottery/config/ImportPrizeData.vue

@@ -0,0 +1,95 @@
+<!-- 客户导入窗口 -->
+<template>
+  <Dialog v-model="dialogVisible" title="奖品批量导入" width="400">
+    <el-upload
+      ref="uploadRef"
+      v-model:file-list="fileList"
+      :auto-upload="false"
+      :disabled="formLoading"
+      :headers="uploadHeaders"
+      :limit="1"
+      :on-error="submitFormError"
+      :on-exceed="handleExceed"
+      :on-success="submitFormSuccess"
+      accept=".xlsx, .xls"
+      action="none"
+      drag
+    >
+      <Icon icon="ep:upload" />
+      <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+    </el-upload>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { getAccessToken, getTenantId } from '@/utils/auth'
+import { LuckLotteryApi } from '@/api/mall/promotion/lottery/config'
+import type { UploadUserFile } from 'element-plus'
+
+defineOptions({ name: 'SystemUserImportForm' })
+
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中
+const uploadRef = ref()
+const uploadHeaders = ref() // 上传 Header 头
+const fileList = ref<UploadUserFile[]>([]) // 文件列表
+
+/** 打开弹窗 */
+const open = () => {
+  dialogVisible.value = true
+  fileList.value = []
+  resetForm()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const submitForm = async () => {
+  if (fileList.value.length == 0) {
+    message.error('请上传文件')
+    return
+  }
+  // 提交请求
+  uploadHeaders.value = {
+    Authorization: 'Bearer ' + getAccessToken(),
+    'tenant-id': getTenantId()
+  }
+  formLoading.value = true
+  const formData = new FormData()
+  formData.append('multipartFile', fileList.value[0].raw)
+  await LuckLotteryApi.importLotteryPrizeData(formData)
+	dialogVisible.value = false
+}
+
+/** 文件上传成功 */
+const submitFormSuccess = (response: any) => {
+  if (response.code !== 0) {
+    message.error(response.msg)
+    formLoading.value = false
+    return
+  }
+}
+
+/** 上传错误提示 */
+const submitFormError = (): void => {
+  message.error('上传失败,请您重新上传!')
+  formLoading.value = false
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  // 重置上传状态和文件
+  formLoading.value = false
+  uploadRef.value?.clearFiles()
+}
+
+/** 文件数超出提示 */
+const handleExceed = (): void => {
+  message.error('最多只能上传一个文件!')
+}
+</script>

+ 151 - 48
src/views/mall/promotion/lottery/config/PrizeForm.vue

@@ -1,11 +1,14 @@
 <template>
   <Dialog title="活动奖品配置" v-model="dialogVisible" style="width: 70%;">
-    <div class="text-right mb-20px">
+    <div class="text-right">
       <el-button type="primary" plain @click="handleAddPrize">
         <Icon icon="ep:plus" class="mr-5px" /> 新增奖品
       </el-button>
     </div>
     <div style="display: inline-block; width: 100%;">
+      <div class="mb-20px" style="color: var(--el-color-warning)">
+        提示:抽奖概率 =(奖品权重 × 奖品数量)÷ 总权重,计算结果四舍五入保留两位小数
+      </div>
       <el-table v-loading="loading" :data="list" :stripe="true">
       <el-table-column label="奖品名称" align="center" prop="name" />
       <el-table-column label="奖品图片" align="center" min-width="80" prop="image">
@@ -13,16 +16,19 @@
           <img :src="scope.row.image" alt="" class="h-70px" />
         </template>
       </el-table-column>
-      <el-table-column label="奖品数量" align="center" prop="total" />
-      <el-table-column label="权重" align="center" prop="chance" />
       <el-table-column label="类型" align="center" prop="type">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.PROMOTION_LUCK_PRIZE_TYPE" :value="scope.row.type" />
         </template>
       </el-table-column>
       <el-table-column label="提示语" align="center" prop="prompt" />
+      <el-table-column label="奖品数量" align="center" prop="total" />
+      <el-table-column label="权重" align="center" prop="chance" />
+      <el-table-column label="抽奖概率" align="center" prop="probability">
+        <template #default="scope">{{ scope.row.probability }}%</template>
+      </el-table-column>
       <el-table-column label="排序" align="center" prop="sort" />
-      <el-table-column label="操作" align="center">
+      <el-table-column label="操作" align="center" fixed="right" min-width="110">
         <template #default="scope">
           <el-button link type="primary" @click="handleEdit(scope.row.id)">编辑</el-button>
           <el-button link type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
@@ -38,45 +44,61 @@
   </Dialog>
 
   <Dialog title="添加奖品" v-model="showAddPrize">
-    <el-form
-      ref="formRef"
-      :model="addPrizeData"
-      :rules="addPrizeFormRules"
-      label-width="80px"
-    >
-      <el-form-item label="奖品" prop="type">
+    <el-form ref="formRef" :model="addPrizeData" label-width="80px">
+      <el-form-item label="奖品" prop="type" :rules="[{ required: true, message: '奖品类型不能为空', trigger: 'change' }]">
         <el-radio-group v-model="addPrizeData.type">
-          <el-radio v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_LUCK_PRIZE_TYPE)" :key="dict.value" :value="dict.value.toString()" @change="handleChangeType">{{ dict.label }}</el-radio>
+          <el-radio v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_LUCK_PRIZE_TYPE)" :key="dict.value" :value="dict.value.toString()" @change="handleChangePrizeType">{{ dict.label }}</el-radio>
         </el-radio-group>
       </el-form-item>
-      <el-form-item v-if="isShowSpu" label="商品" prop="productId">
-        <div style="width: 100%;">
-          <el-image v-if="Object.keys(spu).length && spu?.id" class="w-100px h-100px" :src="spu?.picUrl" />
+      <el-form-item v-if="prizeType === '6'" label="商品" prop="productId">
+        <div class="w-full">
+          <el-image v-if="spu && Object.keys(spu).length && spu?.id" class="w-100px h-100px" :src="spu?.picUrl" />
         </div>
         <el-button :icon="Plus" type="primary" @click.stop="spuQueryParams.pageNo = 1,showSpu = true">选择商品</el-button>
       </el-form-item>
-      <el-form-item label="奖品名称" prop="name">
+      <el-form-item label="奖品名称" prop="name" :rules="[{ required: true, message: '奖品名称不能为空', trigger: 'blur' }]">
         <el-input v-model="addPrizeData.name" placeholder="请输入奖品名称" />
       </el-form-item>
-      <el-form-item label="奖品图片" prop="image">
-        <UploadImg v-model="addPrizeData.image" :validSpecifications="true" height="150px" width="150px" :fileSize="10" :maxWidth="150" :maxHeight="150" />
+      <el-form-item label="奖品图片" prop="image" :rules="[{ required: true, message: '奖品图片不能为空', trigger: 'change' }]">
+        <UploadImg v-model="addPrizeData.image" :validSpecifications="true" height="150px" width="150px" :fileSize="10" :maxWidth="800" :maxHeight="800" />
       </el-form-item>
       <el-form-item label="排序" prop="sort">
-        <el-input-number v-model="addPrizeData.sort" :min="1" />
+        <el-input-number v-model="addPrizeData.sort" :min="0" />
       </el-form-item>
-      <el-form-item label="奖品数量" prop="total">
-        <el-input-number v-model="addPrizeData.total" :min="1" />
+      <el-form-item label="奖品数量" prop="total" :rules="[{ required: true, message: '商品数量不能为空', trigger: 'blur' }]">
+        <el-input-number v-model="addPrizeData.total" :min="0" />
       </el-form-item>
-      <el-form-item label="奖品权重" prop="chance">
-        <el-input-number v-model="addPrizeData.chance" :min="1" />
+      <el-form-item label="奖品权重" prop="chance" :rules="[{ required: true, message: '奖品权重不能为空', trigger: 'blur' }]">
+        <el-input-number v-model="addPrizeData.chance" :min="0" />
       </el-form-item>
-      <el-form-item label="提示语" prop="prompt">
+      <el-form-item label="提示语" prop="prompt" :rules="[{ required: true, message: '提示语不能为空', trigger: 'blur' }]">
         <el-input v-model="addPrizeData.prompt" placeholder="请输入提示语" />
       </el-form-item>
+      <div v-if="prizeType === '99'">
+        <el-form-item label="省市区" required>
+          <el-cascader 
+            ref="areaRef" 
+            v-model="area"
+            class="w-full"
+            clearable 
+            placeholder="省市区 *" 
+            :props="{ value: 'id', label: 'name', emitPath: false }" 
+            :options="areaTreeData" 
+            @change="handleChangeArea"
+            @clear="handleClearArea"
+          />
+        </el-form-item>
+        <el-form-item label="集团名称" prop="extend.bloc" :rules="[{ required: true, message: '集团名称不能为空', trigger: 'blur' }]">
+          <el-input v-model="addPrizeData.extend.bloc" placeholder="请输入集团名称" />
+        </el-form-item>
+        <el-form-item label="品牌名称" prop="extend.brand" :rules="[{ required: true, message: '品牌名称不能为空', trigger: 'blur' }]">
+          <el-input v-model="addPrizeData.extend.brand" placeholder="请输入品牌名称" />
+        </el-form-item>
+      </div>
     </el-form>
     <template #footer>
       <el-button @click="submitPrize" type="primary">确 定</el-button>
-      <el-button @click="showAddPrize = false, spu = {}">取 消</el-button>
+      <el-button @click="showAddPrize = false; spu = {}">取 消</el-button>
     </template>
   </Dialog>
 
@@ -96,6 +118,7 @@
   import { LuckLotteryApi } from '@/api/mall/promotion/lottery/config'
   import * as ProductSpuApi from '@/api/mall/product/spu'
   import SpuList from './components/spuList.vue'
+  import { getDict } from '@/hooks/web/useDictionaries'
               
   /** 幸运抽奖-活动 表单 */
   defineOptions({ name: 'LuckLotteryForm' })
@@ -111,6 +134,22 @@
     pageSize: 10,
     lotteryId: undefined as any
   })
+  const area = ref()
+  const areaRef = ref()
+  const areaTreeData = ref([])
+
+  const getDictData = async () => {
+    const { data } = await getDict('areaTreeData', {}, 'areaTreeData')
+    const obj = data.find(e => e.name === '中国')
+    const list = obj?.children ? obj.children.map(e =>{
+      // 市辖区直接显示区
+      const municipality = e.children && e.children.length && e.children[0].name === '市辖区'
+      if (municipality && e.children[0].children?.length) e.children = e.children[0].children
+      return e
+    }) : []
+    areaTreeData.value = list || []
+  }
+  getDictData()
 
   // 获取奖品分页
   const getList = async () => {
@@ -118,7 +157,7 @@
     try {
       const res = await LuckLotteryApi.getLuckLotteryPrizePage(queryParams)
       list.value = res.list
-      total.value = res.total
+      // total.value = res.total
     } finally {
       loading.value = false
     }
@@ -143,6 +182,35 @@
       spuLoading.value = false
     }
   }
+
+  // 地区选择
+  const handleChangeArea = () => {
+    const key = {
+      value: ['provinceId', 'cityId', 'districtId'],
+      label: ['provinceName', 'cityName', 'districtName']
+    }
+    const node = areaRef.value.getCheckedNodes() ? areaRef.value.getCheckedNodes()[0] : null
+    if (!node) return
+    for (let i in addPrizeData.value.extend) {
+      const index = key.value.indexOf(i)
+      if (index > -1) {
+        addPrizeData.value.extend[i] = node.pathValues[index]
+        addPrizeData.value.extend[key.label[index]] = node.pathLabels[index]
+      }
+    }
+  }
+  const handleClearArea = () => {
+    addPrizeData.value.extend = {
+      bloc: undefined,
+      brand: undefined,
+      provinceId: undefined,
+      provinceName: undefined,
+      cityId: undefined,
+      cityName: undefined,
+      districtId: undefined,
+      districtName: undefined
+    }
+  }
               
   /** 打开弹窗 */
   const open = async (id: number) => {
@@ -172,7 +240,20 @@
       const data = await LuckLotteryApi.getLuckLotteryPrize(id)
       addPrizeData.value = data
       spu.value = data.spu
-      isShowSpu.value = data.type === '6'
+      prizeType.value = data.type
+      // 没有自定义参数,则初始化
+      if (!data.extend || !Object.keys(data.extend).length) {
+        addPrizeData.value.extend = {
+          bloc: undefined,
+          brand: undefined,
+          provinceId: undefined,
+          provinceName: undefined,
+          cityId: undefined,
+          cityName: undefined,
+          districtId: undefined,
+          districtName: undefined
+        }
+      }
       showAddPrize.value = true
     } catch {}
   }
@@ -182,45 +263,57 @@
     type: '1',
     productId: undefined,
     name: undefined,
-    total: 10,
-    chance: 10,
+    total: 0,
+    chance: 0,
     prompt: undefined,
     image: undefined,
     status: 0,
     sort: 0,
-    lotteryId: undefined as any
+    lotteryId: undefined as any,
+    extend: {
+      bloc: undefined,
+      brand: undefined,
+      provinceId: undefined,
+      provinceName: undefined,
+      cityId: undefined,
+      cityName: undefined,
+      districtId: undefined,
+      districtName: undefined
+    }
   })
   const showAddPrize = ref(false)
   const formRef = ref()
-  const addPrizeFormRules = reactive({
-    type: [{ required: true, message: '奖品类型不能为空', trigger: 'change' }],
-    name: [{ required: true, message: '奖品名称不能为空', trigger: 'blur' }],
-    chance: [{ required: true, message: '奖品权重不能为空', trigger: 'blur' }],
-    image: [{ required: true, message: '奖品图片不能为空', trigger: 'blur' }],
-    total: [{ required: true, message: '商品数量不能为空', trigger: 'blur' }],
-    prompt: [{ required: true, message: '提示语不能为空', trigger: 'blur' }]
-  })
   const handleAddPrize = async () => {
     formType.value = 'add'
-    isShowSpu.value = false
+    prizeType.value = '1'
     resetForm()
     showAddPrize.value = true
     await getSpuList()
   }
 
   // 只有站内商品才展示商品信息
-  const isShowSpu = ref(false)
-  const handleChangeType = (val: string) => {
-    isShowSpu.value = val === '6'
+  const prizeType = ref('1')
+  const handleChangePrizeType = (val: string) => {
+    prizeType.value = val
   }
+
   const submitPrize = async () => {
+    console.log(addPrizeData.value, 'submit');
+    
     // 校验表单
     await formRef.value.validate()
-    if (isShowSpu.value && !Object.keys(spu.value).length && !spu.value?.id) return message.warning('请选择商品')
+
+    if (prizeType.value === '6' && !Object.keys(spu.value).length && !spu.value?.id) return message.warning('请选择商品')
+    if (prizeType.value !== '99') handleClearArea() // 奖品类型不是自定义时清空extend数据
+
+    // 奖品类型为自定义时,效验是否有选省市区
+    if (prizeType.value === '99' && !addPrizeData.value.extend.provinceId) return message.warning('请选择省市区')
+
     addPrizeData.value.lotteryId = queryParams.lotteryId
-    addPrizeData.value.productId = spu.value.id
-    addPrizeData.value.image = spu.value?.picUrl
+    if (prizeType.value === '6') addPrizeData.value.productId = spu.value.id
+    if (addPrizeData.value.type === '6' && !addPrizeData.value.image) addPrizeData.value.image = spu.value?.picUrl
     delete addPrizeData.value.spu
+
     try {
       formType.value === 'add' ? await LuckLotteryApi.createLuckLotteryPrize(addPrizeData.value) : await LuckLotteryApi.updateLuckLotteryPrize(addPrizeData.value)
       message.success(formType.value === 'add' ? '添加成功' : '修改成功')
@@ -236,13 +329,23 @@
       type: '1',
       productId: undefined,
       name: undefined,
-      total: 10,
-      chance: 10,
+      total: 0,
+      chance: 0,
       prompt: undefined,
       image: undefined,
       status: 0,
       sort: 0,
-      lotteryId: undefined as any
+      lotteryId: undefined as any,
+      extend: {
+        bloc: undefined,
+        brand: undefined,
+        provinceId: undefined,
+        provinceName: undefined,
+        cityId: undefined,
+        cityName: undefined,
+        districtId: undefined,
+        districtName: undefined
+      }
     }
   }
 

+ 19 - 5
src/views/mall/promotion/lottery/config/index.vue

@@ -76,9 +76,12 @@
           plain
           @click="handleExport"
           :loading="exportLoading"
-          v-hasPermi="['promotion:luck-lottery:export']"
         >
-          <Icon icon="ep:download" class="mr-5px" /> 导出
+          <Icon icon="ep:download" class="mr-5px" /> 下载奖品模板
+        </el-button>
+        <el-button plain type="warning" :loading="importLoading" @click="handleImport">
+          <Icon icon="ep:upload" />
+          奖品批量导入
         </el-button>
       </el-form-item>
     </el-form>
@@ -138,7 +141,10 @@
   <LuckLotteryForm ref="formRef" @success="getList" />
 
   <!-- 奖品配置 -->
-  <PrizeForm ref="prizeRef" @success="getList" />
+  <PrizeForm ref="prizeRef" />
+
+  <!-- 奖品导入 -->
+  <ImportPrizeData ref="importFormRef" />
 </template>
 
 <script setup lang="ts">
@@ -148,6 +154,7 @@ import download from '@/utils/download'
 import { LuckLotteryApi, LuckLotteryVO } from '@/api/mall/promotion/lottery/config'
 import LuckLotteryForm from './LuckLotteryForm.vue'
 import PrizeForm from './PrizeForm.vue'
+import ImportPrizeData from './ImportPrizeData.vue'
 
 /** 幸运抽奖-活动 列表 */
 defineOptions({ name: 'LotteryConfigIndex' })
@@ -178,6 +185,7 @@ const queryParams = reactive({
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
+const importLoading = ref(false)
 
 /** 查询列表 */
 const getList = async () => {
@@ -235,14 +243,20 @@ const handleExport = async () => {
     await message.exportConfirm()
     // 发起导出
     exportLoading.value = true
-    const data = await LuckLotteryApi.exportLuckLottery(queryParams)
-    download.excel(data, '幸运抽奖-活动.xls')
+    const data = await LuckLotteryApi.exportLotteryPrizeTemplate()
+    download.excel(data, '奖品批量模版.xls')
   } catch {
   } finally {
     exportLoading.value = false
   }
 }
 
+/** 导入按钮操作 */
+const importFormRef = ref<InstanceType<typeof ImportPrizeData>>()
+const handleImport = () => {
+  importFormRef.value?.open()
+}
+
 /** 初始化 **/
 onMounted(() => {
   getList()