瀏覽代碼

!197 同步商城实现
Merge pull request !197 from 芋道源码/dev

芋道源码 1 年之前
父節點
當前提交
16369c001c
共有 29 個文件被更改,包括 1326 次插入755 次删除
  1. 62 0
      src/api/mall/promotion/bargain/bargainActivity.ts
  2. 3 3
      src/api/mall/promotion/combination/combinationActivity.ts
  3. 0 0
      src/api/mall/promotion/coupon/coupon.ts
  4. 0 0
      src/api/mall/promotion/coupon/couponTemplate.ts
  5. 1 1
      src/api/mall/promotion/seckill/seckillConfig.ts
  6. 10 0
      src/hooks/web/useCrudSchemas.ts
  7. 1 1
      src/views/bpm/group/index.vue
  8. 1 1
      src/views/infra/apiAccessLog/index.vue
  9. 1 1
      src/views/infra/apiErrorLog/index.vue
  10. 1 1
      src/views/infra/codegen/index.vue
  11. 219 0
      src/views/mall/promotion/bargain/activity/BargainActivityForm.vue
  12. 165 0
      src/views/mall/promotion/bargain/activity/bargainActivity.data.ts
  13. 107 0
      src/views/mall/promotion/bargain/activity/index.vue
  14. 12 11
      src/views/mall/promotion/combination/activity/CombinationActivityForm.vue
  15. 2 2
      src/views/mall/promotion/combination/activity/combinationActivity.data.ts
  16. 6 15
      src/views/mall/promotion/combination/activity/index.vue
  17. 0 0
      src/views/mall/promotion/combination/record/index.vue
  18. 37 43
      src/views/mall/promotion/coupon/index.vue
  19. 348 0
      src/views/mall/promotion/coupon/template/CouponTemplateForm.vue
  20. 295 0
      src/views/mall/promotion/coupon/template/index.vue
  21. 0 614
      src/views/mall/promotion/couponTemplate/index.vue
  22. 18 16
      src/views/mall/promotion/seckill/activity/SeckillActivityForm.vue
  23. 11 16
      src/views/mall/promotion/seckill/activity/index.vue
  24. 2 2
      src/views/mall/promotion/seckill/activity/seckillActivity.data.ts
  25. 11 9
      src/views/mall/promotion/seckill/config/SeckillConfigForm.vue
  26. 10 16
      src/views/mall/promotion/seckill/config/index.vue
  27. 1 1
      src/views/mp/message/index.vue
  28. 1 1
      src/views/pay/notify/index.vue
  29. 1 1
      src/views/system/dict/index.vue

+ 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 })
+}

+ 3 - 3
src/api/mall/promotion/combination/combinationActivity.ts

@@ -10,8 +10,8 @@ export interface CombinationActivityVO {
   startTime?: Date
   endTime?: Date
   userSize?: number
-  totalNum?: number
-  successNum?: number
+  totalCount?: number
+  successCount?: number
   orderUserCount?: number
   virtualGroup?: number
   status?: number
@@ -23,7 +23,7 @@ export interface CombinationActivityVO {
 export interface CombinationProductVO {
   spuId: number
   skuId: number
-  activePrice: number // 拼团价格
+  combinationPrice: number // 拼团价格
 }
 
 // 扩展 Sku 配置

+ 0 - 0
src/api/mall/promotion/coupon.ts → src/api/mall/promotion/coupon/coupon.ts


+ 0 - 0
src/api/mall/promotion/couponTemplate.ts → src/api/mall/promotion/coupon/couponTemplate.ts


+ 1 - 1
src/api/mall/promotion/seckill/seckillConfig.ts

@@ -20,7 +20,7 @@ export const getSeckillConfig = async (id: number) => {
 }
 
 // 获得所有开启状态的秒杀时段精简列表
-export const getListAllSimple = async () => {
+export const getSimpleSeckillConfigList = async () => {
   return await request.get({ url: '/promotion/seckill-config/list-all-simple' })
 }
 

+ 10 - 0
src/hooks/web/useCrudSchemas.ts

@@ -9,6 +9,7 @@ import { TableColumn } from '@/types/table'
 import { DescriptionsSchema } from '@/types/descriptions'
 import { ComponentOptions, ComponentProps } from '@/types/components'
 import { DictTag } from '@/components/DictTag'
+import { cloneDeep } from 'lodash-es'
 
 export type CrudSchema = Omit<TableColumn, 'children'> & {
   isSearch?: boolean // 是否在查询显示
@@ -306,3 +307,12 @@ const filterOptions = (options: Recordable, labelField?: string) => {
     return v
   })
 }
+
+// 将 tableColumns 指定 fields 放到最前面
+export const sortTableColumns = (tableColumns: TableColumn[], field: string) => {
+  const fieldIndex = tableColumns.findIndex((item) => item.field === field)
+  const fieldColumn = cloneDeep(tableColumns[fieldIndex])
+  tableColumns.splice(fieldIndex, 1)
+  // 添加到开头
+  tableColumns.unshift(fieldColumn)
+}

+ 1 - 1
src/views/bpm/group/index.vue

@@ -30,7 +30,7 @@
       <el-form-item label="创建时间" prop="createTime">
         <el-date-picker
           v-model="queryParams.createTime"
-          value-format="yyyy-MM-dd HH:mm:ss"
+          value-format="YYYY-MM-DD HH:mm:ss"
           type="daterange"
           start-placeholder="开始日期"
           end-placeholder="结束日期"

+ 1 - 1
src/views/infra/apiAccessLog/index.vue

@@ -46,7 +46,7 @@
       <el-form-item label="请求时间" prop="beginTime">
         <el-date-picker
           v-model="queryParams.beginTime"
-          value-format="yyyy-MM-dd HH:mm:ss"
+          value-format="YYYY-MM-DD HH:mm:ss"
           type="daterange"
           start-placeholder="开始日期"
           end-placeholder="结束日期"

+ 1 - 1
src/views/infra/apiErrorLog/index.vue

@@ -46,7 +46,7 @@
       <el-form-item label="异常时间" prop="exceptionTime">
         <el-date-picker
           v-model="queryParams.exceptionTime"
-          value-format="yyyy-MM-dd HH:mm:ss"
+          value-format="YYYY-MM-DD HH:mm:ss"
           type="daterange"
           start-placeholder="开始日期"
           end-placeholder="结束日期"

+ 1 - 1
src/views/infra/codegen/index.vue

@@ -37,7 +37,7 @@
           end-placeholder="结束日期"
           start-placeholder="开始日期"
           type="daterange"
-          value-format="YYYY-MM-dd HH:mm:ss"
+          value-format="YYYY-MM-DD HH:mm:ss"
         />
       </el-form-item>
       <el-form-item>

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

@@ -0,0 +1,219 @@
+<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/activity/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)

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

@@ -0,0 +1,107 @@
+<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 { createImageViewer } from '@/components/ImageViewer'
+import { sortTableColumns } from '@/hooks/web/useCrudSchemas'
+
+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)
+}
+
+/** 初始化 **/
+onMounted(() => {
+  // 获得活动列表
+  sortTableColumns(allSchemas.tableColumns, 'spuId')
+  getList()
+})
+</script>

+ 12 - 11
src/views/mall/promotion/combination/CombinationActivityForm.vue → src/views/mall/promotion/combination/activity/CombinationActivityForm.vue

@@ -19,7 +19,7 @@
           <el-table-column align="center" label="拼团价格(元)" min-width="168">
             <template #default="{ row: sku }">
               <el-input-number
-                v-model="sku.productConfig.activePrice"
+                v-model="sku.productConfig.combinationPrice"
                 :min="0"
                 :precision="2"
                 :step="0.1"
@@ -45,6 +45,7 @@ import { SpuAndSkuList, SpuProperty, SpuSelect } from '@/views/mall/promotion/co
 import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components'
 import * as ProductSpuApi from '@/api/mall/product/spu'
 import { convertToInteger, formatToFraction } from '@/utils'
+import { cloneDeep } from 'lodash-es'
 
 defineOptions({ name: 'PromotionCombinationActivityForm' })
 
@@ -65,8 +66,8 @@ const spuList = ref<CombinationActivityApi.SpuExtension[]>([]) // 选择的 spu
 const spuPropertyList = ref<SpuProperty<CombinationActivityApi.SpuExtension>[]>([])
 const ruleConfig: RuleConfig[] = [
   {
-    name: 'productConfig.activePrice',
-    rule: (arg) => arg > 0.01,
+    name: 'productConfig.combinationPrice',
+    rule: (arg) => arg >= 0.01,
     message: '商品拼团价格不能小于0.01 !!!'
   }
 ]
@@ -98,13 +99,12 @@ const getSpuDetails = async (
     let config: CombinationProductVO = {
       spuId: spu.id!,
       skuId: sku.id!,
-      activePrice: 0
+      combinationPrice: 0
     }
     if (typeof products !== 'undefined') {
       const product = products.find((item) => item.skuId === sku.id)
       if (product) {
-        // 分转元
-        product.activePrice = formatToFraction(product.activePrice)
+        product.combinationPrice = formatToFraction(product.combinationPrice)
       }
       config = product || config
     }
@@ -162,13 +162,14 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    const data = formRef.value.formModel as CombinationActivityApi.CombinationActivityVO
-    const products = spuAndSkuListRef.value.getSkuConfigs('productConfig')
-    products.forEach((item: CombinationProductVO) => {
-      // 拼团价格元转分
-      item.activePrice = convertToInteger(item.activePrice)
+    // 获得拼团商品配置
+    const products = cloneDeep(spuAndSkuListRef.value.getSkuConfigs('productConfig'))
+    products.forEach((item: CombinationActivityApi.CombinationProductVO) => {
+      item.combinationPrice = convertToInteger(item.combinationPrice)
     })
+    const data = formRef.value.formModel as CombinationActivityApi.CombinationActivityVO
     data.products = products
+    // 真正提交
     if (formType.value === 'create') {
       await CombinationActivityApi.createCombinationActivity(data)
       message.success(t('common.createSuccess'))

+ 2 - 2
src/views/mall/promotion/combination/combinationActivity.data.ts → src/views/mall/promotion/combination/activity/combinationActivity.data.ts

@@ -122,13 +122,13 @@ const crudSchemas = reactive<CrudSchema[]>([
   },
   {
     label: '开团组数',
-    field: 'totalNum',
+    field: 'totalCount',
     isSearch: false,
     isForm: false
   },
   {
     label: '成团组数',
-    field: 'successNum',
+    field: 'successCount',
     isSearch: false,
     isForm: false
   },

+ 6 - 15
src/views/mall/promotion/combination/index.vue → src/views/mall/promotion/combination/activity/index.vue

@@ -1,4 +1,6 @@
 <template>
+  <doc-alert title="功能开启" url="https://doc.iocoder.cn/mall/build/" />
+
   <!-- 搜索工作栏 -->
   <ContentWrap>
     <Search :schema="allSchemas.searchSchema" @reset="setSearchParams" @search="setSearchParams">
@@ -10,8 +12,7 @@
           type="primary"
           @click="openForm('create')"
         >
-          <Icon class="mr-5px" icon="ep:plus" />
-          新增
+          <Icon class="mr-5px" icon="ep:plus" /> 新增
         </el-button>
       </template>
     </Search>
@@ -65,7 +66,7 @@
 import { allSchemas } from './combinationActivity.data'
 import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
 import CombinationActivityForm from './CombinationActivityForm.vue'
-import { cloneDeep } from 'lodash-es'
+import { sortTableColumns } from '@/hooks/web/useCrudSchemas'
 import { createImageViewer } from '@/components/ImageViewer'
 
 defineOptions({ name: 'PromotionCombinationActivity' })
@@ -98,20 +99,10 @@ 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)
+  // 获得活动列表
+  sortTableColumns(allSchemas.tableColumns, 'spuId')
   getList()
 })
 </script>

+ 0 - 0
src/views/mall/promotion/combination/record/index.vue


+ 37 - 43
src/views/mall/promotion/coupon/index.vue

@@ -1,12 +1,19 @@
 <template>
   <doc-alert title="功能开启" url="https://doc.iocoder.cn/mall/build/" />
 
+  <!-- 搜索工作栏 -->
   <ContentWrap>
-    <!-- 搜索工作栏 -->
-    <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
       <el-form-item label="会员昵称" prop="nickname">
         <el-input
           v-model="queryParams.nickname"
+          class="!w-240px"
           placeholder="请输入会员昵称"
           clearable
           @keyup="handleQuery"
@@ -15,27 +22,19 @@
       <el-form-item label="创建时间" prop="createTime">
         <el-date-picker
           v-model="queryParams.createTime"
-          style="width: 240px"
-          type="datetimerange"
           value-format="YYYY-MM-DD HH:mm:ss"
-          range-separator="-"
+          type="daterange"
           start-placeholder="开始日期"
           end-placeholder="结束日期"
-          :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)]"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
         />
       </el-form-item>
       <el-form-item>
-        <el-button type="primary" @click="handleQuery">
-          <Icon icon="ep:search" class="mr-5px" /> 搜索
-        </el-button>
+        <el-button @click="handleQuery"> <Icon icon="ep:search" class="mr-5px" />搜索 </el-button>
         <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" />重置 </el-button>
       </el-form-item>
     </el-form>
-
-    <!-- 操作工具栏 -->
-    <!-- <el-row :gutter="10" class="mb8">
-      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
-    </el-row> -->
   </ContentWrap>
 
   <ContentWrap>
@@ -86,43 +85,38 @@
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template #default="scope">
           <el-button
-            size="small"
-            type="primary"
-            link
-            @click="handleDelete(scope.row)"
             v-hasPermi="['promotion:coupon:delete']"
-            ><Icon icon="ep:delete" :size="12" class="mr-1px" />回收</el-button
+            type="danger"
+            link
+            @click="handleDelete(scope.row.id)"
           >
+            回收
+          </el-button>
         </template>
       </el-table-column>
     </el-table>
-
-    <!-- 分页组件 -->
-    <pagination
-      v-show="total > 0"
-      :total="total"
-      v-model:page="queryParams.pageNo"
+    <!-- 分页 -->
+    <Pagination
       v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
       @pagination="getList"
     />
   </ContentWrap>
 </template>
 
 <script setup lang="ts" name="PromotionCoupon">
-import { deleteCoupon, getCouponPage } from '@/api/mall/promotion/coupon'
+import { deleteCoupon, getCouponPage } from '@/api/mall/promotion/coupon/coupon'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
-import { FormInstance } from 'element-plus'
 
-// 消息弹窗
-const message = useMessage()
+defineOptions({ name: 'PromotionCoupon' })
+
+const message = useMessage() // 消息弹窗
 
-// 遮罩层
-const loading = ref(true)
-// 总条数
-const total = ref(0)
-// 优惠劵列表
-const list = ref([])
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 字典表格数据
 // 查询参数
 const queryParams = reactive({
   pageNo: 1,
@@ -130,9 +124,9 @@ const queryParams = reactive({
   createTime: [],
   status: undefined
 })
-// Tab 筛选
-const activeTab = ref('all')
+const queryFormRef = ref() // 搜索的表单
 
+const activeTab = ref('all') // Tab 筛选
 const statusTabs = reactive([
   {
     label: '全部',
@@ -140,8 +134,6 @@ const statusTabs = reactive([
   }
 ])
 
-const queryFormRef = ref<FormInstance | null>(null)
-
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
@@ -168,16 +160,17 @@ const resetQuery = () => {
 }
 
 /** 删除按钮操作 */
-const handleDelete = async (row) => {
-  const id = row.id
-
+const handleDelete = async (id: number) => {
   try {
+    // 二次确认
     await message.confirm(
       '回收将会收回会员领取的待使用的优惠券,已使用的将无法回收,确定要回收所选优惠券吗?'
     )
+    // 发起删除
     await deleteCoupon(id)
-    getList()
     message.notifySuccess('回收成功')
+    // 重新加载列表
+    await getList()
   } catch {}
 }
 
@@ -187,6 +180,7 @@ const onTabChange = (tabName) => {
   getList()
 }
 
+/** 初始化 **/
 onMounted(() => {
   getList()
   // 设置 statuses 过滤

+ 348 - 0
src/views/mall/promotion/coupon/template/CouponTemplateForm.vue

@@ -0,0 +1,348 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="140px"
+    >
+      <el-form-item label="优惠券名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入优惠券名称" />
+      </el-form-item>
+      <el-form-item label="优惠券类型" prop="discountType">
+        <el-radio-group v-model="formData.discountType">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_DISCOUNT_TYPE)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item
+        v-if="formData.discountType === PromotionDiscountTypeEnum.PRICE.type"
+        label="优惠券面额"
+        prop="discountPrice"
+      >
+        <el-input-number
+          v-model="formData.discountPrice"
+          placeholder="请输入优惠金额,单位:元"
+          style="width: 400px"
+          :precision="2"
+          :min="0"
+        />
+        元
+      </el-form-item>
+      <el-form-item
+        v-if="formData.discountType === PromotionDiscountTypeEnum.PERCENT.type"
+        label="优惠券折扣"
+        prop="discountPercent"
+      >
+        <el-input-number
+          v-model="formData.discountPercent"
+          placeholder="优惠券折扣不能小于 1 折,且不可大于 9.9 折"
+          style="width: 400px"
+          :precision="1"
+          :min="1"
+          :max="9.9"
+        />
+        折
+      </el-form-item>
+      <el-form-item
+        v-if="formData.discountType === PromotionDiscountTypeEnum.PERCENT.type"
+        label="最多优惠"
+        prop="discountLimitPrice"
+      >
+        <el-input-number
+          v-model="formData.discountLimitPrice"
+          placeholder="请输入最多优惠"
+          style="width: 400px"
+          :precision="2"
+          :min="0"
+        />
+        元
+      </el-form-item>
+      <el-form-item label="满多少元可以使用" prop="usePrice">
+        <el-input-number
+          v-model="formData.usePrice"
+          placeholder="无门槛请设为 0"
+          style="width: 400px"
+          :precision="2"
+          :min="0"
+        />
+        元
+      </el-form-item>
+      <el-form-item label="领取方式" prop="takeType">
+        <el-radio-group v-model="formData.takeType">
+          <el-radio :key="1" :label="1">直接领取</el-radio>
+          <el-radio :key="2" :label="2">指定发放</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item v-if="formData.takeType === 1" label="发放数量" prop="totalCount">
+        <el-input-number
+          v-model="formData.totalCount"
+          placeholder="发放数量,没有之后不能领取或发放,-1 为不限制"
+          style="width: 400px"
+          :precision="0"
+          :min="-1"
+        />
+        张
+      </el-form-item>
+      <el-form-item v-if="formData.takeType === 1" label="每人限领个数" prop="takeLimitCount">
+        <el-input-number
+          v-model="formData.takeLimitCount"
+          placeholder="设置为 -1 时,可无限领取"
+          style="width: 400px"
+          :precision="0"
+          :min="-1"
+        />
+        张
+      </el-form-item>
+      <el-form-item label="有效期类型" prop="validityType">
+        <el-radio-group v-model="formData.validityType">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item
+        v-if="formData.validityType === CouponTemplateValidityTypeEnum.DATE.type"
+        label="固定日期"
+        prop="validTimes"
+      >
+        <el-date-picker
+          v-model="formData.validTimes"
+          style="width: 240px"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="datetimerange"
+          :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)]"
+        />
+      </el-form-item>
+      <el-form-item
+        v-if="formData.validityType === CouponTemplateValidityTypeEnum.TERM.type"
+        label="领取日期"
+        prop="fixedStartTerm"
+      >
+        第
+        <el-input-number
+          v-model="formData.fixedStartTerm"
+          placeholder="0 为今天生效"
+          style="width: 165px"
+          :precision="0"
+          :min="0"
+        />
+        至
+        <el-input-number
+          v-model="formData.fixedEndTerm"
+          placeholder="请输入结束天数"
+          style="width: 165px"
+          :precision="0"
+          :min="0"
+        />
+        天有效
+      </el-form-item>
+      <el-form-item label="活动商品" prop="productScope">
+        <el-radio-group v-model="formData.productScope">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_PRODUCT_SCOPE)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item
+        v-if="formData.productScope === PromotionProductScopeEnum.SPU.scope"
+        prop="productSpuIds"
+      >
+        <el-select
+          v-model="formData.productSpuIds"
+          placeholder="请选择活动商品"
+          clearable
+          multiple
+          filterable
+          style="width: 400px"
+        >
+          <el-option v-for="item in productSpus" :key="item.id" :label="item.name" :value="item.id">
+            <span style="float: left">{{ item.name }}</span>
+            <span style="float: right; font-size: 13px; color: #8492a6">
+              ¥{{ (item.minPrice / 100.0).toFixed(2) }}
+            </span>
+          </el-option>
+        </el-select>
+      </el-form-item>
+    </el-form>
+    <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 { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import {
+  CouponTemplateValidityTypeEnum,
+  PromotionDiscountTypeEnum,
+  PromotionProductScopeEnum
+} from '@/utils/constants'
+
+defineOptions({ name: 'CouponTemplateForm' })
+
+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 formData = ref({
+  id: undefined,
+  name: undefined,
+  discountType: PromotionDiscountTypeEnum.PRICE.type,
+  discountPrice: undefined,
+  discountPercent: undefined,
+  discountLimitPrice: undefined,
+  usePrice: undefined,
+  takeType: 1,
+  totalCount: undefined,
+  takeLimitCount: undefined,
+  validityType: CouponTemplateValidityTypeEnum.DATE.type,
+  validTimes: [],
+  validStartTime: undefined,
+  validEndTime: undefined,
+  fixedStartTerm: undefined,
+  fixedEndTerm: undefined,
+  productScope: PromotionProductScopeEnum.ALL.scope,
+  productSpuIds: []
+})
+const formRules = reactive({
+  name: [{ required: true, message: '优惠券名称不能为空', trigger: 'blur' }],
+  discountType: [{ required: true, message: '优惠券类型不能为空', trigger: 'change' }],
+  discountPrice: [{ required: true, message: '优惠券面额不能为空', trigger: 'blur' }],
+  discountPercent: [{ required: true, message: '优惠券折扣不能为空', trigger: 'blur' }],
+  discountLimitPrice: [{ required: true, message: '最多优惠不能为空', trigger: 'blur' }],
+  usePrice: [{ required: true, message: '满多少元可以使用不能为空', trigger: 'blur' }],
+  takeType: [{ required: true, message: '领取方式不能为空', trigger: 'change' }],
+  totalCount: [{ required: true, message: '发放数量不能为空', trigger: 'blur' }],
+  takeLimitCount: [{ required: true, message: '每人限领个数不能为空', trigger: 'blur' }],
+  validityType: [{ required: true, message: '有效期类型不能为空', trigger: 'change' }],
+  validTimes: [{ required: true, message: '固定日期不能为空', trigger: 'change' }],
+  fixedStartTerm: [{ required: true, message: '开始领取天数不能为空', trigger: 'blur' }],
+  fixedEndTerm: [{ required: true, message: '开始领取天数不能为空', trigger: 'blur' }],
+  productScope: [{ required: true, message: '商品范围不能为空', trigger: 'blur' }],
+  productSpuIds: [{ required: true, message: '商品范围不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+const productSpus = ref([]) // 商品列表
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      const data = await CouponTemplateApi.getCouponTemplate(id)
+      formData.value = {
+        ...data,
+        discountPrice: data.discountPrice !== undefined ? data.discountPrice / 100.0 : undefined,
+        discountPercent:
+          data.discountPercent !== undefined ? data.discountPercent / 10.0 : undefined,
+        discountLimitPrice:
+          data.discountLimitPrice !== undefined ? data.discountLimitPrice / 100.0 : undefined,
+        usePrice: data.usePrice !== undefined ? data.usePrice / 100.0 : undefined,
+        validTimes: [data.validStartTime, data.validEndTime]
+      }
+    } finally {
+      formLoading.value = false
+    }
+  }
+  // 获得商品列表
+  productSpus.value = await ProductSpuApi.getSpuSimpleList()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = {
+      ...formData.value,
+      discountPrice:
+        formData.value.discountPrice !== undefined ? formData.value.discountPrice * 100 : undefined,
+      discountPercent:
+        formData.value.discountPercent !== undefined
+          ? formData.value.discountPercent * 10
+          : undefined,
+      discountLimitPrice:
+        formData.value.discountLimitPrice !== undefined
+          ? formData.value.discountLimitPrice * 100
+          : undefined,
+      usePrice: formData.value.usePrice !== undefined ? formData.value.usePrice * 100 : undefined,
+      validStartTime:
+        formData.value.validTimes && formData.value.validTimes.length === 2
+          ? formData.value.validTimes[0]
+          : undefined,
+      validEndTime:
+        formData.value.validTimes && formData.value.validTimes.length === 2
+          ? formData.value.validTimes[1]
+          : undefined
+    } as CouponTemplateApi.CouponTemplateVO
+    if (formType.value === 'create') {
+      await CouponTemplateApi.createCouponTemplate(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await CouponTemplateApi.updateCouponTemplate(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    discountType: PromotionDiscountTypeEnum.PRICE.type,
+    discountPrice: undefined,
+    discountPercent: undefined,
+    discountLimitPrice: undefined,
+    usePrice: undefined,
+    takeType: 1,
+    totalCount: undefined,
+    takeLimitCount: undefined,
+    validityType: CouponTemplateValidityTypeEnum.DATE.type,
+    validTimes: [],
+    validStartTime: undefined,
+    validEndTime: undefined,
+    fixedStartTerm: undefined,
+    fixedEndTerm: undefined,
+    productScope: PromotionProductScopeEnum.ALL.scope,
+    productSpuIds: []
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 295 - 0
src/views/mall/promotion/coupon/template/index.vue

@@ -0,0 +1,295 @@
+<template>
+  <doc-alert title="功能开启" url="https://doc.iocoder.cn/mall/build/" />
+
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="82px"
+    >
+      <el-form-item label="优惠券名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          class="!w-240px"
+          placeholder="请输入优惠劵名"
+          clearable
+          @keyup="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="优惠券类型" prop="discountType">
+        <el-select
+          v-model="queryParams.discountType"
+          class="!w-240px"
+          placeholder="请选择优惠券类型"
+          clearable
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_DISCOUNT_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="优惠券状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          class="!w-240px"
+          placeholder="请选择优惠券状态"
+          clearable
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"> <Icon icon="ep:search" class="mr-5px" />搜索 </el-button>
+        <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" />重置 </el-button>
+        <el-button
+          v-hasPermi="['promotion:coupon-template:create']"
+          plain
+          type="primary"
+          @click="openForm('create')"
+        >
+          <Icon class="mr-5px" icon="ep:plus" /> 新增
+        </el-button>
+        <el-button
+          plain
+          type="success"
+          @click="$router.push('/promotion/coupon')"
+          v-hasPermi="['promotion:coupon:query']"
+        >
+          <Icon icon="ep:operation" class="mr-5px" />会员优惠劵
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="优惠券名称" align="center" prop="name" />
+      <el-table-column label="优惠券类型" align="center" prop="discountType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE" :value="scope.row.discountType" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="优惠金额 / 折扣"
+        align="center"
+        prop="discount"
+        :formatter="discountFormat"
+      />
+      <el-table-column label="发放数量" align="center" prop="totalCount" />
+      <el-table-column
+        label="剩余数量"
+        align="center"
+        prop="totalCount"
+        :formatter="(row) => row.totalCount - row.takeCount"
+      />
+      <el-table-column
+        label="领取上限"
+        align="center"
+        prop="takeLimitCount"
+        :formatter="takeLimitCountFormat"
+      />
+      <el-table-column
+        label="有效期限"
+        align="center"
+        prop="validityType"
+        width="190"
+        :formatter="validityTypeFormat"
+      />
+      <el-table-column label="状态" align="center" prop="status">
+        <template #default="scope">
+          <el-switch
+            v-model="scope.row.status"
+            :active-value="0"
+            :inactive-value="1"
+            @change="handleStatusChange(scope.row)"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180"
+      />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template #default="scope">
+          <el-button
+            v-hasPermi="['promotion:coupon-template:update']"
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+          >
+            修改
+          </el-button>
+          <el-button
+            v-hasPermi="['promotion:coupon-template:delete']"
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <CouponTemplateForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate'
+import {
+  CommonStatusEnum,
+  CouponTemplateValidityTypeEnum,
+  PromotionDiscountTypeEnum
+} from '@/utils/constants'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter, formatDate } from '@/utils/formatTime'
+import CouponTemplateForm from './CouponTemplateForm.vue'
+
+defineOptions({ name: 'PromotionCouponTemplate' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 字典表格数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: null,
+  status: null,
+  type: null,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    // 执行查询
+    const data = await CouponTemplateApi.getCouponTemplatePage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef?.value?.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 优惠劵模板状态修改 */
+const handleStatusChange = async (row: any) => {
+  // 此时,row 已经变成目标状态了,所以可以直接提交请求和提示
+  let text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用'
+
+  try {
+    await message.confirm('确认要"' + text + '""' + row.name + '"优惠劵吗?')
+    await CouponTemplateApi.updateCouponTemplateStatus(row.id, row.status)
+    message.success(text + '成功')
+  } catch {
+    // 异常时,需要将 row.status 状态重置回之前的
+    row.status =
+      row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
+  }
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.confirm('是否确认删除优惠劵编号为"' + id + '"的数据项?')
+    // 发起删除
+    await CouponTemplateApi.deleteCouponTemplate(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+// 格式化【优惠金额/折扣】
+const discountFormat = (row: any) => {
+  if (row.discountType === PromotionDiscountTypeEnum.PRICE.type) {
+    return `¥${(row.discountPrice / 100.0).toFixed(2)}`
+  }
+  if (row.discountType === PromotionDiscountTypeEnum.PERCENT.type) {
+    return `¥${(row.discountPrice / 100.0).toFixed(2)}`
+  }
+  return '未知【' + row.discountType + '】'
+}
+
+// 格式化【领取上限】
+const takeLimitCountFormat = (row: any) => {
+  if (row.takeLimitCount === -1) {
+    return '无领取限制'
+  }
+  return `${row.takeLimitCount} 张/人`
+}
+
+// 格式化【有效期限】
+const validityTypeFormat = (row: any) => {
+  if (row.validityType === CouponTemplateValidityTypeEnum.DATE.type) {
+    return `${formatDate(row.validStartTime)} 至 ${formatDate(row.validEndTime)}`
+  }
+  if (row.validityType === CouponTemplateValidityTypeEnum.TERM.type) {
+    return `领取后第 ${row.fixedStartTerm} - ${row.fixedEndTerm} 天内可用`
+  }
+  return '未知【' + row.validityType + '】'
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 0 - 614
src/views/mall/promotion/couponTemplate/index.vue

@@ -1,614 +0,0 @@
-<template>
-  <doc-alert title="功能开启" url="https://doc.iocoder.cn/mall/build/" />
-
-  <!-- 搜索工作栏 -->
-  <ContentWrap>
-    <el-form
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      v-show="showSearch"
-      label-width="82px"
-    >
-      <el-form-item label="优惠券名称" prop="name">
-        <el-input
-          v-model="queryParams.name"
-          placeholder="请输入优惠劵名"
-          clearable
-          @keyup="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="优惠券类型" prop="discountType">
-        <el-select v-model="queryParams.discountType" placeholder="请选择优惠券类型" clearable>
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_DISCOUNT_TYPE)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="优惠券状态" prop="status">
-        <el-select v-model="queryParams.status" placeholder="请选择优惠券状态" clearable>
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="创建时间" prop="createTime">
-        <el-date-picker
-          v-model="queryParams.createTime"
-          style="width: 240px"
-          type="datetimerange"
-          value-format="YYYY-MM-DD HH:mm:ss"
-          range-separator="-"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)]"
-        />
-      </el-form-item>
-      <el-form-item>
-        <el-button type="primary" @click="handleQuery">
-          <Icon icon="ep:search" class="mr-5px" /> 搜索
-        </el-button>
-        <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" />重置 </el-button>
-      </el-form-item>
-    </el-form>
-
-    <!-- 操作工具栏 -->
-    <el-row :gutter="10" class="mb8">
-      <el-col :span="1.5">
-        <el-button
-          type="primary"
-          plain
-          @click="handleAdd"
-          v-hasPermi="['promotion:coupon-template:create']"
-        >
-          <Icon icon="ep:plus" class="mr-5px" />新增
-        </el-button>
-        <el-button
-          type="info"
-          plain
-          @click="$router.push('/promotion/coupon')"
-          v-hasPermi="['promotion:coupon:query']"
-        >
-          <Icon icon="ep:operation" class="mr-5px" />会员优惠劵
-        </el-button>
-      </el-col>
-      <!-- <right-toolbar v-model:showSearch="showSearch" @query-table="getList" /> -->
-    </el-row>
-  </ContentWrap>
-
-  <!-- 列表 -->
-  <ContentWrap>
-    <el-table v-loading="loading" :data="list">
-      <el-table-column label="优惠券名称" align="center" prop="name" />
-      <el-table-column label="优惠券类型" align="center" prop="discountType">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE" :value="scope.row.discountType" />
-        </template>
-      </el-table-column>
-      <el-table-column
-        label="优惠金额 / 折扣"
-        align="center"
-        prop="discount"
-        :formatter="discountFormat"
-      />
-      <el-table-column label="发放数量" align="center" prop="totalCount" />
-      <el-table-column
-        label="剩余数量"
-        align="center"
-        prop="totalCount"
-        :formatter="(row) => row.totalCount - row.takeCount"
-      />
-      <el-table-column
-        label="领取上限"
-        align="center"
-        prop="takeLimitCount"
-        :formatter="takeLimitCountFormat"
-      />
-      <el-table-column
-        label="有效期限"
-        align="center"
-        prop="validityType"
-        width="180"
-        :formatter="validityTypeFormat"
-      />
-      <el-table-column label="状态" align="center" prop="status">
-        <template #default="scope">
-          <el-switch
-            v-model="scope.row.status"
-            :active-value="0"
-            :inactive-value="1"
-            @change="handleStatusChange(scope.row)"
-          />
-        </template>
-      </el-table-column>
-      <el-table-column
-        label="创建时间"
-        align="center"
-        prop="createTime"
-        :formatter="dateFormatter"
-        width="180"
-      />
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
-        <template #default="scope">
-          <el-button
-            size="small"
-            type="primary"
-            link
-            @click="handleUpdate(scope.row)"
-            v-hasPermi="['promotion:coupon-template:update']"
-          >
-            <Icon icon="ep:edit" :size="12" class="mr-1px" />
-            修改
-          </el-button>
-          <el-button
-            size="small"
-            type="primary"
-            link
-            @click="handleDelete(scope.row)"
-            v-hasPermi="['promotion:coupon-template:delete']"
-          >
-            <Icon icon="ep:delete" :size="12" class="mr-1px" />
-            删除
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-  </ContentWrap>
-
-  <!-- 分页组件 -->
-  <pagination
-    v-show="total > 0"
-    :total="total"
-    v-model:page="queryParams.pageNo"
-    v-model:limit="queryParams.pageSize"
-    @pagination="getList"
-  />
-
-  <!-- 对话框(添加 / 修改) -->
-  <el-dialog :title="title" v-model="open" width="600px" append-to-body>
-    <el-form ref="formRef" :model="form" :rules="rules" label-width="140px">
-      <el-form-item label="优惠券名称" prop="name">
-        <el-input v-model="form.name" placeholder="请输入优惠券名称" />
-      </el-form-item>
-      <el-form-item label="优惠券类型" prop="discountType">
-        <el-radio-group v-model="form.discountType">
-          <el-radio
-            v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_DISCOUNT_TYPE)"
-            :key="dict.value"
-            :label="parseInt(dict.value)"
-            >{{ dict.label }}</el-radio
-          >
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item
-        v-if="form.discountType === PromotionDiscountTypeEnum.PRICE.type"
-        label="优惠券面额"
-        prop="discountPrice"
-      >
-        <el-input-number
-          v-model="form.discountPrice"
-          placeholder="请输入优惠金额,单位:元"
-          style="width: 400px"
-          :precision="2"
-          :min="0"
-        />
-        元
-      </el-form-item>
-      <el-form-item
-        v-if="form.discountType === PromotionDiscountTypeEnum.PERCENT.type"
-        label="优惠券折扣"
-        prop="discountPercent"
-      >
-        <el-input-number
-          v-model="form.discountPercent"
-          placeholder="优惠券折扣不能小于 1 折,且不可大于 9.9 折"
-          style="width: 400px"
-          :precision="1"
-          :min="1"
-          :max="9.9"
-        />
-        折
-      </el-form-item>
-      <el-form-item
-        v-if="form.discountType === PromotionDiscountTypeEnum.PERCENT.type"
-        label="最多优惠"
-        prop="discountLimitPrice"
-      >
-        <el-input-number
-          v-model="form.discountLimitPrice"
-          placeholder="请输入最多优惠"
-          style="width: 400px"
-          :precision="2"
-          :min="0"
-        />
-        元
-      </el-form-item>
-      <el-form-item label="满多少元可以使用" prop="usePrice">
-        <el-input-number
-          v-model="form.usePrice"
-          placeholder="无门槛请设为 0"
-          style="width: 400px"
-          :precision="2"
-          :min="0"
-        />
-        元
-      </el-form-item>
-      <el-form-item label="领取方式" prop="takeType">
-        <el-radio-group v-model="form.takeType">
-          <el-radio :key="1" :label="1">直接领取</el-radio>
-          <el-radio :key="2" :label="2">指定发放</el-radio>
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item v-if="form.takeType === 1" label="发放数量" prop="totalCount">
-        <el-input-number
-          v-model="form.totalCount"
-          placeholder="发放数量,没有之后不能领取或发放,-1 为不限制"
-          style="width: 400px"
-          :precision="0"
-          :min="-1"
-        />
-        张
-      </el-form-item>
-      <el-form-item v-if="form.takeType === 1" label="每人限领个数" prop="takeLimitCount">
-        <el-input-number
-          v-model="form.takeLimitCount"
-          placeholder="设置为 -1 时,可无限领取"
-          style="width: 400px"
-          :precision="0"
-          :min="-1"
-        />
-        张
-      </el-form-item>
-      <el-form-item label="有效期类型" prop="validityType">
-        <el-radio-group v-model="form.validityType">
-          <el-radio
-            v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE)"
-            :key="dict.value"
-            :label="parseInt(dict.value)"
-            >{{ dict.label }}</el-radio
-          >
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item
-        v-if="form.validityType === CouponTemplateValidityTypeEnum.DATE.type"
-        label="固定日期"
-        prop="validTimes"
-      >
-        <el-date-picker
-          v-model="form.validTimes"
-          style="width: 240px"
-          value-format="yyyy-MM-dd HH:mm:ss"
-          type="datetimerange"
-          :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)]"
-        />
-      </el-form-item>
-      <el-form-item
-        v-if="form.validityType === CouponTemplateValidityTypeEnum.TERM.type"
-        label="领取日期"
-        prop="fixedStartTerm"
-      >
-        第
-        <el-input-number
-          v-model="form.fixedStartTerm"
-          placeholder="0 为今天生效"
-          style="width: 165px"
-          :precision="0"
-          :min="0"
-        />
-        至
-        <el-input-number
-          v-model="form.fixedEndTerm"
-          placeholder="请输入结束天数"
-          style="width: 165px"
-          :precision="0"
-          :min="0"
-        />
-        天有效
-      </el-form-item>
-      <el-form-item label="活动商品" prop="productScope">
-        <el-radio-group v-model="form.productScope">
-          <el-radio
-            v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_PRODUCT_SCOPE)"
-            :key="dict.value"
-            :label="parseInt(dict.value)"
-            >{{ dict.label }}</el-radio
-          >
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item
-        v-if="form.productScope === PromotionProductScopeEnum.SPU.scope"
-        prop="productSpuIds"
-      >
-        <el-select
-          v-model="form.productSpuIds"
-          placeholder="请选择活动商品"
-          clearable
-          size="small"
-          multiple
-          filterable
-          style="width: 400px"
-        >
-          <el-option v-for="item in productSpus" :key="item.id" :label="item.name" :value="item.id">
-            <span style="float: left">{{ item.name }}</span>
-            <span style="float: right; font-size: 13px; color: #8492a6"
-              >¥{{ (item.minPrice / 100.0).toFixed(2) }}</span
-            >
-          </el-option>
-        </el-select>
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <div class="dialog-footer">
-        <el-button type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
-      </div>
-    </template>
-  </el-dialog>
-</template>
-
-<script setup lang="ts" name="PromotionCouponTemplate">
-import {
-  createCouponTemplate,
-  updateCouponTemplate,
-  deleteCouponTemplate,
-  getCouponTemplate,
-  getCouponTemplatePage,
-  updateCouponTemplateStatus
-} from '@/api/mall/promotion/couponTemplate'
-import {
-  CommonStatusEnum,
-  CouponTemplateValidityTypeEnum,
-  PromotionDiscountTypeEnum,
-  PromotionProductScopeEnum
-} from '@/utils/constants'
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { getSpuSimpleList } from '@/api/mall/product/spu'
-import { dateFormatter, formatDate } from '@/utils/formatTime'
-import { FormInstance } from 'element-plus'
-
-// 消息弹窗
-const message = useMessage()
-
-// 遮罩层
-const loading = ref(true)
-// 显示搜索条件
-const showSearch = ref(true)
-// 总条数
-const total = ref(0)
-// 优惠劵列表
-const list = ref([])
-// 弹出层标题
-const title = ref('')
-// 是否显示弹出层
-const open = ref(false)
-// 查询参数
-const queryParams = reactive({
-  pageNo: 1,
-  pageSize: 10,
-  name: null,
-  status: null,
-  type: null,
-  createTime: []
-})
-// 表单参数
-const form = ref<any>({})
-// 表单校验
-const rules = {
-  name: [{ required: true, message: '优惠券名称不能为空', trigger: 'blur' }],
-  discountType: [{ required: true, message: '优惠券类型不能为空', trigger: 'change' }],
-  discountPrice: [{ required: true, message: '优惠券面额不能为空', trigger: 'blur' }],
-  discountPercent: [{ required: true, message: '优惠券折扣不能为空', trigger: 'blur' }],
-  discountLimitPrice: [{ required: true, message: '最多优惠不能为空', trigger: 'blur' }],
-  usePrice: [{ required: true, message: '满多少元可以使用不能为空', trigger: 'blur' }],
-  takeType: [{ required: true, message: '领取方式不能为空', trigger: 'change' }],
-  totalCount: [{ required: true, message: '发放数量不能为空', trigger: 'blur' }],
-  takeLimitCount: [{ required: true, message: '每人限领个数不能为空', trigger: 'blur' }],
-  validityType: [{ required: true, message: '有效期类型不能为空', trigger: 'change' }],
-  validTimes: [{ required: true, message: '固定日期不能为空', trigger: 'change' }],
-  fixedStartTerm: [{ required: true, message: '开始领取天数不能为空', trigger: 'blur' }],
-  fixedEndTerm: [{ required: true, message: '开始领取天数不能为空', trigger: 'blur' }],
-  productScope: [{ required: true, message: '商品范围不能为空', trigger: 'blur' }],
-  productSpuIds: [{ required: true, message: '商品范围不能为空', trigger: 'blur' }]
-}
-// 商品列表
-const productSpus = ref([])
-const queryFormRef = ref<FormInstance | null>(null)
-const formRef = ref<FormInstance | null>(null)
-
-onMounted(() => {
-  getList()
-})
-
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    // 执行查询
-    const data = await getCouponTemplatePage(queryParams)
-    list.value = data.list
-    total.value = data.total
-    // 查询商品列表
-    productSpus.value = await getSpuSimpleList()
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 取消按钮 */
-const cancel = () => {
-  open.value = false
-  reset()
-}
-
-/** 表单重置 */
-const reset = () => {
-  form.value = {
-    id: undefined,
-    name: undefined,
-    discountType: PromotionDiscountTypeEnum.PRICE.type,
-    discountPrice: undefined,
-    discountPercent: undefined,
-    discountLimitPrice: undefined,
-    usePrice: undefined,
-    takeType: 1,
-    totalCount: undefined,
-    takeLimitCount: undefined,
-    validityType: CouponTemplateValidityTypeEnum.DATE.type,
-    validTimes: [],
-    validStartTime: undefined,
-    validEndTime: undefined,
-    fixedStartTerm: undefined,
-    fixedEndTerm: undefined,
-    productScope: PromotionProductScopeEnum.ALL.scope,
-    productSpuIds: []
-  }
-  formRef.value?.resetFields()
-}
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.pageNo = 1
-  getList()
-}
-
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef?.value?.resetFields()
-  handleQuery()
-}
-
-/** 新增按钮操作 */
-const handleAdd = () => {
-  reset()
-  open.value = true
-  title.value = '添加优惠劵'
-}
-
-/** 修改按钮操作 */
-const handleUpdate = async (row: any) => {
-  reset()
-  const id = row.id
-  try {
-    const data = await getCouponTemplate(id)
-    form.value = {
-      ...data,
-      discountPrice: data.discountPrice !== undefined ? data.discountPrice / 100.0 : undefined,
-      discountPercent: data.discountPercent !== undefined ? data.discountPercent / 10.0 : undefined,
-      discountLimitPrice:
-        data.discountLimitPrice !== undefined ? data.discountLimitPrice / 100.0 : undefined,
-      usePrice: data.usePrice !== undefined ? data.usePrice / 100.0 : undefined,
-      validTimes: [data.validStartTime, data.validEndTime]
-    }
-    open.value = true
-    title.value = '修改优惠劵'
-  } catch {}
-}
-
-/** 提交按钮 */
-const submitForm = async () => {
-  const valid = await formRef.value?.validate()
-  if (!valid) {
-    return
-  }
-
-  // 金额相关字段的缩放
-  let data = {
-    ...form.value,
-    discountPrice:
-      form.value.discountPrice !== undefined ? form.value.discountPrice * 100 : undefined,
-    discountPercent:
-      form.value.discountPercent !== undefined ? form.value.discountPercent * 10 : undefined,
-    discountLimitPrice:
-      form.value.discountLimitPrice !== undefined ? form.value.discountLimitPrice * 100 : undefined,
-    usePrice: form.value.usePrice !== undefined ? form.value.usePrice * 100 : undefined,
-    validStartTime:
-      form.value.validTimes && form.value.validTimes.length === 2
-        ? form.value.validTimes[0]
-        : undefined,
-    validEndTime:
-      form.value.validTimes && form.value.validTimes.length === 2
-        ? form.value.validTimes[1]
-        : undefined
-  }
-
-  // 修改的提交
-  if (form.value.id != null) {
-    try {
-      await updateCouponTemplate(data)
-      message.success('修改成功')
-      open.value = false
-      getList()
-    } catch {}
-
-    return
-  }
-
-  try {
-    await createCouponTemplate(data)
-    message.success('新增成功')
-    open.value = false
-    getList()
-  } catch {}
-}
-
-/** 优惠劵模板状态修改 */
-const handleStatusChange = async (row: any) => {
-  // 此时,row 已经变成目标状态了,所以可以直接提交请求和提示
-  let text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用'
-
-  try {
-    await message.confirm('确认要"' + text + '""' + row.name + '"优惠劵吗?')
-    await updateCouponTemplateStatus(row.id, row.status)
-    message.success(text + '成功')
-  } catch {
-    // 异常时,需要将 row.status 状态重置回之前的
-    row.status =
-      row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
-  }
-}
-
-/** 删除按钮操作 */
-const handleDelete = async (row: any) => {
-  const id = row.id
-  try {
-    await message.confirm('是否确认删除优惠劵编号为"' + id + '"的数据项?')
-    await deleteCouponTemplate(id)
-  } catch {}
-}
-
-// 格式化【优惠金额/折扣】
-const discountFormat = (row: any) => {
-  if (row.discountType === PromotionDiscountTypeEnum.PRICE.type) {
-    return `¥${(row.discountPrice / 100.0).toFixed(2)}`
-  }
-  if (row.discountType === PromotionDiscountTypeEnum.PERCENT.type) {
-    return `¥${(row.discountPrice / 100.0).toFixed(2)}`
-  }
-  return '未知【' + row.discountType + '】'
-}
-
-// 格式化【领取上限】
-const takeLimitCountFormat = (row: any) => {
-  if (row.takeLimitCount === -1) {
-    return '无领取限制'
-  }
-  return `${row.takeLimitCount} 张/人`
-}
-
-// 格式化【有效期限】
-const validityTypeFormat = (row: any) => {
-  if (row.validityType === CouponTemplateValidityTypeEnum.DATE.type) {
-    return `${formatDate(row.validStartTime)} 至 ${formatDate(row.validEndTime)}`
-  }
-  if (row.validityType === CouponTemplateValidityTypeEnum.TERM.type) {
-    return `领取后第 ${row.fixedStartTerm} - ${row.fixedEndTerm} 天内可用`
-  }
-  return '未知【' + row.validityType + '】'
-}
-</script>

+ 18 - 16
src/views/mall/promotion/seckill/activity/SeckillActivityForm.vue

@@ -45,6 +45,7 @@
 <script lang="ts" setup>
 import { SpuAndSkuList, SpuProperty, SpuSelect } from '../../components'
 import { allSchemas, rules } from './seckillActivity.data'
+import { cloneDeep } from 'lodash-es'
 
 import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
 import { SeckillProductVO } from '@/api/mall/promotion/seckill/seckillActivity'
@@ -70,13 +71,13 @@ const spuAndSkuListRef = ref() // sku 秒杀配置组件Ref
 const ruleConfig: RuleConfig[] = [
   {
     name: 'productConfig.stock',
-    rule: (arg) => arg > 1,
-    message: '商品秒杀库存必须大于 1 !!!'
+    rule: (arg) => arg >= 1,
+    message: '商品秒杀库存必须大于等于 1 !!!'
   },
   {
     name: 'productConfig.seckillPrice',
-    rule: (arg) => arg > 0.01,
-    message: '商品秒杀价格必须大于 0.01 !!!'
+    rule: (arg) => arg >= 0.01,
+    message: '商品秒杀价格必须大于等于 0.01 !!!'
   }
 ]
 const spuList = ref<SeckillActivityApi.SpuExtension[]>([]) // 选择的 spu
@@ -112,7 +113,6 @@ const getSpuDetails = async (
     if (typeof products !== 'undefined') {
       const product = products.find((item) => item.skuId === sku.id)
       if (product) {
-        // 分转元
         product.seckillPrice = formatToFraction(product.seckillPrice)
       }
       config = product || config
@@ -153,13 +153,6 @@ const open = async (type: string, id?: number) => {
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
-/** 重置表单 */
-const resetForm = async () => {
-  spuList.value = []
-  spuPropertyList.value = []
-  await nextTick()
-  formRef.value.getElFormRef().resetFields()
-}
 /** 提交表单 */
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 const submitForm = async () => {
@@ -170,14 +163,14 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    const data = formRef.value.formModel as SeckillActivityApi.SeckillActivityVO
-    const products = spuAndSkuListRef.value.getSkuConfigs('productConfig')
+    // 获取秒杀商品配置
+    const products = cloneDeep(spuAndSkuListRef.value.getSkuConfigs('productConfig'))
     products.forEach((item: SeckillProductVO) => {
-      // 秒杀价格元转分
       item.seckillPrice = convertToInteger(item.seckillPrice)
     })
-    // 获取秒杀商品配置
+    const data = formRef.value.formModel as SeckillActivityApi.SeckillActivityVO
     data.products = products
+    // 真正提交
     if (formType.value === 'create') {
       await SeckillActivityApi.createSeckillActivity(data)
       message.success(t('common.createSuccess'))
@@ -192,6 +185,15 @@ const submitForm = async () => {
     formLoading.value = false
   }
 }
+
+/** 重置表单 */
+const resetForm = async () => {
+  spuList.value = []
+  spuPropertyList.value = []
+  await nextTick()
+  formRef.value.getElFormRef().resetFields()
+}
+// TODO @puhui999:下面的 css 名字,是不是可以改下;demo-table-expand
 </script>
 <style lang="scss" scoped>
 .demo-table-expand {

+ 11 - 16
src/views/mall/promotion/seckill/activity/index.vue

@@ -1,4 +1,6 @@
 <template>
+  <doc-alert title="功能开启" url="https://doc.iocoder.cn/mall/build/" />
+
   <!-- 搜索工作栏 -->
   <ContentWrap>
     <Search :schema="allSchemas.searchSchema" @reset="setSearchParams" @search="setSearchParams">
@@ -10,8 +12,7 @@
           type="primary"
           @click="openForm('create')"
         >
-          <Icon class="mr-5px" icon="ep:plus" />
-          新增
+          <Icon class="mr-5px" icon="ep:plus" /> 新增
         </el-button>
       </template>
     </Search>
@@ -71,11 +72,11 @@
 </template>
 <script lang="ts" setup>
 import { allSchemas } from './seckillActivity.data'
-import { getListAllSimple } from '@/api/mall/promotion/seckill/seckillConfig'
+import { getSimpleSeckillConfigList } from '@/api/mall/promotion/seckill/seckillConfig'
 import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
 import SeckillActivityForm from './SeckillActivityForm.vue'
-import { cloneDeep } from 'lodash-es'
 import { createImageViewer } from '@/components/ImageViewer'
+import { sortTableColumns } from '@/hooks/web/useCrudSchemas'
 
 defineOptions({ name: 'PromotionSeckillActivity' })
 
@@ -99,12 +100,14 @@ const openForm = (type: string, id?: number) => {
 const handleDelete = (id: number) => {
   tableMethods.delList(id, false)
 }
+
 /** 商品图预览 */
 const imagePreview = (imgUrl: string) => {
   createImageViewer({
     urlList: [imgUrl]
   })
 }
+
 const configList = ref([]) // 时段配置精简列表
 const convertSeckillConfigNames = computed(
   () => (row) =>
@@ -120,18 +123,10 @@ const expandChange = (row, expandedRows) => {
 
 /** 初始化 **/
 onMounted(async () => {
-  /*
-   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)
+  // 获得活动列表
+  sortTableColumns(allSchemas.tableColumns, 'spuId')
   await getList()
-  configList.value = await getListAllSimple()
+  // 获得秒杀时间段
+  configList.value = await getSimpleSeckillConfigList()
 })
 </script>

+ 2 - 2
src/views/mall/promotion/seckill/activity/seckillActivity.data.ts

@@ -1,6 +1,6 @@
 import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
 import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
-import { getListAllSimple } from '@/api/mall/promotion/seckill/seckillConfig'
+import { getSimpleSeckillConfigList } from '@/api/mall/promotion/seckill/seckillConfig'
 
 // 表单校验
 export const rules = reactive({
@@ -88,7 +88,7 @@ const crudSchemas = reactive<CrudSchema[]>([
           valueField: 'id'
         }
       },
-      api: getListAllSimple
+      api: getSimpleSeckillConfigList
     },
     table: {
       width: 300

+ 11 - 9
src/views/mall/promotion/seckill/config/SeckillConfigForm.vue

@@ -10,7 +10,6 @@
 <script lang="ts" name="SeckillConfigForm" setup>
 import * as SeckillConfigApi from '@/api/mall/promotion/seckill/seckillConfig'
 import { allSchemas, rules } from './seckillConfig.data'
-import { cloneDeep } from 'lodash-es'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -53,19 +52,22 @@ const submitForm = async () => {
   formLoading.value = true
   try {
     // 处理轮播图列表
-    const data = formRef.value.formModel as SeckillConfigApi.SeckillConfigVO
-    const cloneData = cloneDeep(data)
-    const newSliderPicUrls = []
-    cloneData.sliderPicUrls.forEach((item) => {
+    const sliderPicUrls = []
+    formRef.value.formModel.sliderPicUrls.forEach((item) => {
       // 如果是前端选的图
-      typeof item === 'object' ? newSliderPicUrls.push(item.url) : newSliderPicUrls.push(item)
+      typeof item === 'object' ? sliderPicUrls.push(item.url) : sliderPicUrls.push(item)
     })
-    cloneData.sliderPicUrls = newSliderPicUrls
+
+    // 真正提交
+    const data = {
+      ...formRef.value.formModel,
+      sliderPicUrls
+    } as SeckillConfigApi.SeckillConfigVO
     if (formType.value === 'create') {
-      await SeckillConfigApi.createSeckillConfig(cloneData)
+      await SeckillConfigApi.createSeckillConfig(data)
       message.success(t('common.createSuccess'))
     } else {
-      await SeckillConfigApi.updateSeckillConfig(cloneData)
+      await SeckillConfigApi.updateSeckillConfig(data)
       message.success(t('common.updateSuccess'))
     }
     dialogVisible.value = false

+ 10 - 16
src/views/mall/promotion/seckill/config/index.vue

@@ -1,4 +1,6 @@
 <template>
+  <doc-alert title="功能开启" url="https://doc.iocoder.cn/mall/build/" />
+
   <!-- 搜索工作栏 -->
   <ContentWrap>
     <Search :schema="allSchemas.searchSchema" @reset="setSearchParams" @search="setSearchParams">
@@ -76,7 +78,6 @@ import * as SeckillConfigApi from '@/api/mall/promotion/seckill/seckillConfig'
 import SeckillConfigForm from './SeckillConfigForm.vue'
 import { createImageViewer } from '@/components/ImageViewer'
 import { CommonStatusEnum } from '@/utils/constants'
-import { isArray } from '@/utils/is'
 
 const message = useMessage() // 消息弹窗
 // tableObject:表格的属性对象,可获得分页大小、条数等属性
@@ -89,21 +90,6 @@ const { tableObject, tableMethods } = useTable({
 // 获得表格的各种操作
 const { getList, setSearchParams } = tableMethods
 
-/** 轮播图预览预览 */
-const imagePreview = (args) => {
-  const urlList = []
-  if (isArray(args)) {
-    args.forEach((item) => {
-      urlList.push(item)
-    })
-  } else {
-    urlList.push(args)
-  }
-  createImageViewer({
-    urlList
-  })
-}
-
 /** 添加/修改操作 */
 const formRef = ref()
 const openForm = (type: string, id?: number) => {
@@ -131,6 +117,14 @@ const handleStatusChange = async (row: SeckillConfigApi.SeckillConfigVO) => {
       row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
   }
 }
+
+/** 轮播图预览预览 */
+const imagePreview = (args) => {
+  createImageViewer({
+    urlList: args
+  })
+}
+
 /** 初始化 **/
 onMounted(() => {
   getList()

+ 1 - 1
src/views/mp/message/index.vue

@@ -34,7 +34,7 @@
         <el-date-picker
           v-model="queryParams.createTime"
           style="width: 240px"
-          value-format="yyyy-MM-dd HH:mm:ss"
+          value-format="YYYY-MM-DD HH:mm:ss"
           type="daterange"
           range-separator="-"
           start-placeholder="开始日期"

+ 1 - 1
src/views/pay/notify/index.vue

@@ -73,7 +73,7 @@
         <el-date-picker
           v-model="queryParams.createTime"
           style="width: 240px"
-          value-format="yyyy-MM-dd HH:mm:ss"
+          value-format="YYYY-MM-DD HH:mm:ss"
           type="daterange"
           range-separator="-"
           start-placeholder="开始日期"

+ 1 - 1
src/views/system/dict/index.vue

@@ -49,7 +49,7 @@
           end-placeholder="结束日期"
           start-placeholder="开始日期"
           type="daterange"
-          value-format="yyyy-MM-dd HH:mm:ss"
+          value-format="YYYY-MM-DD HH:mm:ss"
         />
       </el-form-item>
       <el-form-item>