Procházet zdrojové kódy

!190 完善砍价活动管理
Merge pull request !190 from puhui999/dev-to-dev

芋道源码 před 1 rokem
rodič
revize
ac0f387322

+ 62 - 0
src/api/mall/promotion/bargain/bargainActivity.ts

@@ -0,0 +1,62 @@
+import request from '@/config/axios'
+import { Sku, Spu } from '@/api/mall/product/spu'
+
+export interface BargainActivityVO {
+  id?: number
+  name?: string
+  startTime?: Date
+  endTime?: Date
+  status?: number
+  spuId?: number
+  userSize?: number // 达到该人数,才能砍到低价
+  bargainCount?: number // 最大帮砍次数
+  totalLimitCount?: number // 最大购买次数
+  stock?: number // 活动总库存
+  randomMinPrice?: number // 用户每次砍价的最小金额,单位:分
+  randomMaxPrice?: number // 用户每次砍价的最大金额,单位:分
+  successCount?: number // 砍价成功数量
+  products?: BargainProductVO[]
+}
+
+// 砍价活动所需属性
+export interface BargainProductVO {
+  spuId: number
+  skuId: number
+  bargainFirstPrice: number // 砍价起始价格,单位分
+  bargainPrice: number // 砍价底价
+  stock: number // 活动库存
+}
+
+// 扩展 Sku 配置
+export type SkuExtension = Sku & {
+  productConfig: BargainProductVO
+}
+
+export interface SpuExtension extends Spu {
+  skus: SkuExtension[] // 重写类型
+}
+
+// 查询砍价活动列表
+export const getBargainActivityPage = async (params: any) => {
+  return await request.get({ url: '/promotion/bargain-activity/page', params })
+}
+
+// 查询砍价活动详情
+export const getBargainActivity = async (id: number) => {
+  return await request.get({ url: '/promotion/bargain-activity/get?id=' + id })
+}
+
+// 新增砍价活动
+export const createBargainActivity = async (data: BargainActivityVO) => {
+  return await request.post({ url: '/promotion/bargain-activity/create', data })
+}
+
+// 修改砍价活动
+export const updateBargainActivity = async (data: BargainActivityVO) => {
+  return await request.put({ url: '/promotion/bargain-activity/update', data })
+}
+
+// 删除砍价活动
+export const deleteBargainActivity = async (id: number) => {
+  return await request.delete({ url: '/promotion/bargain-activity/delete?id=' + id })
+}

+ 220 - 0
src/views/mall/promotion/bargain/BargainActivityForm.vue

@@ -0,0 +1,220 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="65%">
+    <Form
+      ref="formRef"
+      v-loading="formLoading"
+      :is-col="true"
+      :rules="rules"
+      :schema="allSchemas.formSchema"
+      class="mt-10px"
+    >
+      <template #spuId>
+        <el-button @click="spuSelectRef.open()">选择商品</el-button>
+        <SpuAndSkuList
+          ref="spuAndSkuListRef"
+          :rule-config="ruleConfig"
+          :spu-list="spuList"
+          :spu-property-list-p="spuPropertyList"
+        >
+          <el-table-column align="center" label="砍价起始价格(元)" min-width="168">
+            <template #default="{ row: sku }">
+              <el-input-number
+                v-model="sku.productConfig.bargainFirstPrice"
+                :min="0"
+                :precision="2"
+                :step="0.1"
+                class="w-100%"
+              />
+            </template>
+          </el-table-column>
+          <el-table-column align="center" label="砍价底价(元)" min-width="168">
+            <template #default="{ row: sku }">
+              <el-input-number
+                v-model="sku.productConfig.bargainPrice"
+                :min="0"
+                :precision="2"
+                :step="0.1"
+                class="w-100%"
+              />
+            </template>
+          </el-table-column>
+          <el-table-column align="center" label="活动库存" min-width="168">
+            <template #default="{ row: sku }">
+              <el-input-number v-model="sku.productConfig.stock" class="w-100%" />
+            </template>
+          </el-table-column>
+        </SpuAndSkuList>
+      </template>
+    </Form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+  <SpuSelect ref="spuSelectRef" :isSelectSku="true" @confirm="selectSpu" />
+</template>
+<script lang="ts" setup>
+import * as BargainActivityApi from '@/api/mall/promotion/bargain/bargainActivity'
+import { BargainProductVO } from '@/api/mall/promotion/bargain/bargainActivity'
+import { allSchemas, rules } from './bargainActivity.data'
+import { SpuAndSkuList, SpuProperty, SpuSelect } from '@/views/mall/promotion/components'
+import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import { convertToInteger, formatToFraction } from '@/utils'
+
+defineOptions({ name: 'PromotionBargainActivityForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formRef = ref() // 表单 Ref
+
+// ================= 商品选择相关 =================
+
+const spuSelectRef = ref() // 商品和属性选择 Ref
+const spuAndSkuListRef = ref() // sku 秒杀配置组件Ref
+const spuList = ref<BargainActivityApi.SpuExtension[]>([]) // 选择的 spu
+const spuPropertyList = ref<SpuProperty<BargainActivityApi.SpuExtension>[]>([])
+const ruleConfig: RuleConfig[] = [
+  {
+    name: 'productConfig.bargainFirstPrice',
+    rule: (arg) => arg > 0,
+    message: '商品砍价起始价格不能小于0 !!!'
+  },
+  {
+    name: 'productConfig.bargainPrice',
+    rule: (arg) => arg > 0,
+    message: '商品砍价底价不能小于0 !!!'
+  },
+  {
+    name: 'productConfig.stock',
+    rule: (arg) => arg > 1,
+    message: '商品活动库存不能小于1 !!!'
+  }
+]
+const selectSpu = (spuId: number, skuIds: number[]) => {
+  formRef.value.setValues({ spuId })
+  getSpuDetails(spuId, skuIds)
+}
+/**
+ * 获取 SPU 详情
+ */
+const getSpuDetails = async (
+  spuId: number,
+  skuIds: number[] | undefined,
+  products?: BargainProductVO[]
+) => {
+  const spuProperties: SpuProperty<BargainActivityApi.SpuExtension>[] = []
+  const res = (await ProductSpuApi.getSpuDetailList([spuId])) as BargainActivityApi.SpuExtension[]
+  if (res.length == 0) {
+    return
+  }
+  spuList.value = []
+  // 因为只能选择一个
+  const spu = res[0]
+  const selectSkus =
+    typeof skuIds === 'undefined' ? spu?.skus : spu?.skus?.filter((sku) => skuIds.includes(sku.id!))
+  selectSkus?.forEach((sku) => {
+    let config: BargainProductVO = {
+      spuId: spu.id!,
+      skuId: sku.id!,
+      bargainFirstPrice: 1,
+      bargainPrice: 1,
+      stock: 1
+    }
+    if (typeof products !== 'undefined') {
+      const product = products.find((item) => item.skuId === sku.id)
+      if (product) {
+        // 分转元
+        product.bargainFirstPrice = formatToFraction(product.bargainFirstPrice)
+        product.bargainPrice = formatToFraction(product.bargainPrice)
+      }
+      config = product || config
+    }
+    sku.productConfig = config
+  })
+  spu.skus = selectSkus as BargainActivityApi.SkuExtension[]
+  spuProperties.push({
+    spuId: spu.id!,
+    spuDetail: spu,
+    propertyList: getPropertyList(spu)
+  })
+  spuList.value.push(spu)
+  spuPropertyList.value = spuProperties
+}
+
+// ================= end =================
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  await resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      const data = (await BargainActivityApi.getBargainActivity(
+        id
+      )) as BargainActivityApi.BargainActivityVO
+      // 用户每次砍价金额分转元, 分转元
+      data.randomMinPrice = formatToFraction(data.randomMinPrice)
+      data.randomMaxPrice = formatToFraction(data.randomMaxPrice)
+      await getSpuDetails(data.spuId!, data.products?.map((sku) => sku.skuId), data.products)
+      formRef.value.setValues(data)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 重置表单 */
+const resetForm = async () => {
+  spuList.value = []
+  spuPropertyList.value = []
+  await nextTick()
+  formRef.value.getElFormRef().resetFields()
+}
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.getElFormRef().validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formRef.value.formModel as BargainActivityApi.BargainActivityVO
+    const products = spuAndSkuListRef.value.getSkuConfigs('productConfig')
+    products.forEach((item: BargainProductVO) => {
+      // 砍价价格元转分
+      item.bargainFirstPrice = convertToInteger(item.bargainFirstPrice)
+      item.bargainPrice = convertToInteger(item.bargainPrice)
+    })
+    // 用户每次砍价金额分转元, 元转分
+    data.randomMinPrice = convertToInteger(data.randomMinPrice)
+    data.randomMaxPrice = convertToInteger(data.randomMaxPrice)
+    data.products = products
+    if (formType.value === 'create') {
+      await BargainActivityApi.createBargainActivity(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await BargainActivityApi.updateBargainActivity(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+</script>

+ 165 - 0
src/views/mall/promotion/bargain/bargainActivity.data.ts

@@ -0,0 +1,165 @@
+import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
+import { dateFormatter2 } from '@/utils/formatTime'
+
+// 表单校验
+export const rules = reactive({
+  name: [required],
+  startTime: [required],
+  endTime: [required],
+  userSize: [required],
+  bargainCount: [required],
+  singleLimitCount: [required]
+})
+
+// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
+const crudSchemas = reactive<CrudSchema[]>([
+  {
+    label: '砍价活动名称',
+    field: 'name',
+    isSearch: true,
+    isTable: false,
+    form: {
+      colProps: {
+        span: 24
+      }
+    }
+  },
+  {
+    label: '活动开始时间',
+    field: 'startTime',
+    formatter: dateFormatter2,
+    isSearch: true,
+    search: {
+      component: 'DatePicker',
+      componentProps: {
+        valueFormat: 'YYYY-MM-DD',
+        type: 'daterange'
+      }
+    },
+    form: {
+      component: 'DatePicker',
+      componentProps: {
+        type: 'date',
+        valueFormat: 'x'
+      }
+    },
+    table: {
+      width: 120
+    }
+  },
+  {
+    label: '活动结束时间',
+    field: 'endTime',
+    formatter: dateFormatter2,
+    isSearch: true,
+    search: {
+      component: 'DatePicker',
+      componentProps: {
+        valueFormat: 'YYYY-MM-DD',
+        type: 'daterange'
+      }
+    },
+    form: {
+      component: 'DatePicker',
+      componentProps: {
+        type: 'date',
+        valueFormat: 'x'
+      }
+    },
+    table: {
+      width: 120
+    }
+  },
+  {
+    label: '砍价人数',
+    field: 'userSize',
+    isSearch: false,
+    form: {
+      component: 'InputNumber',
+      labelMessage: '参与人数不能少于两人',
+      value: 2
+    }
+  },
+  {
+    label: '最大帮砍次数',
+    field: 'bargainCount',
+    isSearch: false,
+    form: {
+      component: 'InputNumber',
+      labelMessage: '参与人数不能少于两人',
+      value: 2
+    }
+  },
+  {
+    label: '总限购数量',
+    field: 'totalLimitCount',
+    isSearch: false,
+    form: {
+      component: 'InputNumber',
+      labelMessage: '用户最大能发起砍价的次数',
+      value: 0
+    }
+  },
+  {
+    label: '砍价的最小金额',
+    field: 'randomMinPrice',
+    isSearch: false,
+    isTable: false,
+    form: {
+      component: 'InputNumber',
+      componentProps: {
+        min: 0,
+        precision: 2,
+        step: 0.1
+      },
+      labelMessage: '用户每次砍价的最小金额',
+      value: 0
+    }
+  },
+  {
+    label: '砍价的最大金额',
+    field: 'randomMaxPrice',
+    isSearch: false,
+    isTable: false,
+    form: {
+      component: 'InputNumber',
+      componentProps: {
+        min: 0,
+        precision: 2,
+        step: 0.1
+      },
+      labelMessage: '用户每次砍价的最大金额',
+      value: 0
+    }
+  },
+  {
+    label: '砍价成功数量',
+    field: 'successCount',
+    isSearch: false,
+    isForm: false
+  },
+  {
+    label: '活动状态',
+    field: 'status',
+    dictType: DICT_TYPE.COMMON_STATUS,
+    dictClass: 'number',
+    isSearch: true,
+    isForm: false
+  },
+  {
+    label: '拼团商品',
+    field: 'spuId',
+    isSearch: false,
+    form: {
+      colProps: {
+        span: 24
+      }
+    }
+  },
+  {
+    label: '操作',
+    field: 'action',
+    isForm: false
+  }
+])
+export const { allSchemas } = useCrudSchemas(crudSchemas)

+ 117 - 0
src/views/mall/promotion/bargain/index.vue

@@ -0,0 +1,117 @@
+<template>
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <Search :schema="allSchemas.searchSchema" @reset="setSearchParams" @search="setSearchParams">
+      <!-- 新增等操作按钮 -->
+      <template #actionMore>
+        <el-button
+          v-hasPermi="['promotion:bargain-activity:create']"
+          plain
+          type="primary"
+          @click="openForm('create')"
+        >
+          <Icon class="mr-5px" icon="ep:plus" />
+          新增
+        </el-button>
+      </template>
+    </Search>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <Table
+      v-model:currentPage="tableObject.currentPage"
+      v-model:pageSize="tableObject.pageSize"
+      :columns="allSchemas.tableColumns"
+      :data="tableObject.tableList"
+      :loading="tableObject.loading"
+      :pagination="{
+        total: tableObject.total
+      }"
+    >
+      <template #spuId="{ row }">
+        <el-image
+          :src="row.picUrl"
+          class="w-30px h-30px align-middle mr-5px"
+          @click="imagePreview(row.picUrl)"
+        />
+        <span class="align-middle">{{ row.spuName }}</span>
+      </template>
+      <template #action="{ row }">
+        <el-button
+          v-hasPermi="['promotion:bargain-activity:update']"
+          link
+          type="primary"
+          @click="openForm('update', row.id)"
+        >
+          编辑
+        </el-button>
+        <el-button
+          v-hasPermi="['promotion:bargain-activity:delete']"
+          link
+          type="danger"
+          @click="handleDelete(row.id)"
+        >
+          删除
+        </el-button>
+      </template>
+    </Table>
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <BargainActivityForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { allSchemas } from './bargainActivity.data'
+import * as BargainActivityApi from '@/api/mall/promotion/bargain/bargainActivity'
+import BargainActivityForm from './BargainActivityForm.vue'
+import { cloneDeep } from 'lodash-es'
+import { createImageViewer } from '@/components/ImageViewer'
+
+defineOptions({ name: 'PromotionBargainActivity' })
+
+// tableObject:表格的属性对象,可获得分页大小、条数等属性
+// tableMethods:表格的操作对象,可进行获得分页、删除记录等操作
+// 详细可见:https://doc.iocoder.cn/vue3/crud-schema/
+const { tableObject, tableMethods } = useTable({
+  getListApi: BargainActivityApi.getBargainActivityPage, // 分页接口
+  delListApi: BargainActivityApi.deleteBargainActivity // 删除接口
+})
+// 获得表格的各种操作
+const { getList, setSearchParams } = tableMethods
+
+/** 商品图预览 */
+const imagePreview = (imgUrl: string) => {
+  createImageViewer({
+    urlList: [imgUrl]
+  })
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = (id: number) => {
+  tableMethods.delList(id, false)
+}
+
+// TODO @puhui999:要不还是使用原生的 element plus 做。感觉 crud schema 复杂界面,做起来麻烦
+/** 初始化 **/
+onMounted(() => {
+  /**
+   TODO
+   后面准备封装成一个函数来操作 tableColumns 重新排列:比如说需求是表单上商品选择是在后面的而列表展示的时候需要调到位置。
+   封装效果支持批量操作,给出 field 和需要插入的位置,例:[{field:'spuId',index: 1}] 效果为把 field 为 spuId 的 column 移动到第一个位置
+   */
+  // 处理一下表格列让商品往前
+  const index = allSchemas.tableColumns.findIndex((item) => item.field === 'spuId')
+  const column = cloneDeep(allSchemas.tableColumns[index])
+  allSchemas.tableColumns.splice(index, 1)
+  // 添加到开头
+  allSchemas.tableColumns.unshift(column)
+  getList()
+})
+</script>