!197 同步商城实现

Merge pull request !197 from 芋道源码/dev
This commit is contained in:
芋道源码 2023-08-13 03:03:21 +00:00 committed by Gitee
commit 16369c001c
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
29 changed files with 1326 additions and 755 deletions

View File

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

View File

@ -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 配置

View File

@ -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' })
}

View File

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

View File

@ -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="结束日期"

View File

@ -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="结束日期"

View File

@ -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="结束日期"

View File

@ -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>

View File

@ -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) // 12
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>

View File

@ -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)

View File

@ -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>

View File

@ -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'))

View File

@ -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
},

View File

@ -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>

View File

@ -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 loading = ref(true)
//
const total = ref(0)
//
const list = ref([])
const message = useMessage() //
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

View File

@ -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) // 12
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>

View File

@ -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>

View File

@ -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>

View File

@ -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 {

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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="开始日期"

View File

@ -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="开始日期"

View File

@ -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>