commit
fa71d3e7dd
@ -0,0 +1,67 @@
|
||||
import request from '@/config/axios'
|
||||
import { Sku, SpuRespVO } from '@/api/mall/product/spu'
|
||||
|
||||
export interface SeckillActivityVO {
|
||||
id: number
|
||||
spuIds: number[]
|
||||
name: string
|
||||
status: number
|
||||
remark: string
|
||||
startTime: Date
|
||||
endTime: Date
|
||||
sort: number
|
||||
configIds: string
|
||||
orderCount: number
|
||||
userCount: number
|
||||
totalPrice: number
|
||||
totalLimitCount: number
|
||||
singleLimitCount: number
|
||||
stock: number
|
||||
totalStock: number
|
||||
products: SeckillProductVO[]
|
||||
}
|
||||
|
||||
export interface SeckillProductVO {
|
||||
spuId: number
|
||||
skuId: number
|
||||
seckillPrice: number
|
||||
stock: number
|
||||
}
|
||||
|
||||
type SkuExtension = Sku & {
|
||||
productConfig: SeckillProductVO
|
||||
}
|
||||
|
||||
export interface SpuExtension extends SpuRespVO {
|
||||
skus: SkuExtension[] // 重写类型
|
||||
}
|
||||
|
||||
// 查询秒杀活动列表
|
||||
export const getSeckillActivityPage = async (params) => {
|
||||
return await request.get({ url: '/promotion/seckill-activity/page', params })
|
||||
}
|
||||
|
||||
// 查询秒杀活动详情
|
||||
export const getSeckillActivity = async (id: number) => {
|
||||
return await request.get({ url: '/promotion/seckill-activity/get?id=' + id })
|
||||
}
|
||||
|
||||
// 新增秒杀活动
|
||||
export const createSeckillActivity = async (data: SeckillActivityVO) => {
|
||||
return await request.post({ url: '/promotion/seckill-activity/create', data })
|
||||
}
|
||||
|
||||
// 修改秒杀活动
|
||||
export const updateSeckillActivity = async (data: SeckillActivityVO) => {
|
||||
return await request.put({ url: '/promotion/seckill-activity/update', data })
|
||||
}
|
||||
|
||||
// 删除秒杀活动
|
||||
export const deleteSeckillActivity = async (id: number) => {
|
||||
return await request.delete({ url: '/promotion/seckill-activity/delete?id=' + id })
|
||||
}
|
||||
|
||||
// 导出秒杀活动 Excel
|
||||
export const exportSeckillActivityApi = async (params) => {
|
||||
return await request.download({ url: '/promotion/seckill-activity/export-excel', params })
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export interface SeckillConfigVO {
|
||||
id: number
|
||||
name: string
|
||||
startTime: string
|
||||
endTime: string
|
||||
picUrl: string
|
||||
status: number
|
||||
}
|
||||
|
||||
// 查询秒杀时段配置列表
|
||||
export const getSeckillConfigPage = async (params) => {
|
||||
return await request.get({ url: '/promotion/seckill-config/page', params })
|
||||
}
|
||||
|
||||
// 查询秒杀时段配置详情
|
||||
export const getSeckillConfig = async (id: number) => {
|
||||
return await request.get({ url: '/promotion/seckill-config/get?id=' + id })
|
||||
}
|
||||
|
||||
// 获得所有开启状态的秒杀时段精简列表
|
||||
export const getListAllSimple = async () => {
|
||||
return await request.get({ url: '/promotion/seckill-config/list-all-simple' })
|
||||
}
|
||||
|
||||
// 新增秒杀时段配置
|
||||
export const createSeckillConfig = async (data: SeckillConfigVO) => {
|
||||
return await request.post({ url: '/promotion/seckill-config/create', data })
|
||||
}
|
||||
|
||||
// 修改秒杀时段配置
|
||||
export const updateSeckillConfig = async (data: SeckillConfigVO) => {
|
||||
return await request.put({ url: '/promotion/seckill-config/update', data })
|
||||
}
|
||||
|
||||
// 修改时段配置状态
|
||||
export const updateSeckillConfigStatus = (id: number, status: number) => {
|
||||
const data = {
|
||||
id,
|
||||
status
|
||||
}
|
||||
return request.put({ url: '/promotion/seckill-config/update-status', data: data })
|
||||
}
|
||||
|
||||
// 删除秒杀时段配置
|
||||
export const deleteSeckillConfig = async (id: number) => {
|
||||
return await request.delete({ url: '/promotion/seckill-config/delete?id=' + id })
|
||||
}
|
||||
|
||||
// 导出秒杀时段配置 Excel
|
||||
export const exportSeckillConfigApi = async (params) => {
|
||||
return await request.download({ url: '/promotion/seckill-config/export-excel', params })
|
||||
}
|
||||
@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<el-table :data="spuData">
|
||||
<el-table-column type="expand" width="30">
|
||||
<template #default="{ row }">
|
||||
<SkuList
|
||||
ref="skuListRef"
|
||||
:is-activity-component="true"
|
||||
:prop-form-data="spuPropertyList.find((item) => item.spuId === row.id)?.spuDetail"
|
||||
:property-list="spuPropertyList.find((item) => item.spuId === row.id)?.propertyList"
|
||||
:rule-config="ruleConfig"
|
||||
>
|
||||
<template #extension>
|
||||
<el-table-column align="center" label="秒杀库存" min-width="168">
|
||||
<template #default="{ row: sku }">
|
||||
<el-input-number v-model="sku.productConfig.stock" :min="0" 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.seckillPrice"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-100%"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</SkuList>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column key="id" align="center" label="商品编号" prop="id" />
|
||||
<el-table-column label="商品图" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<el-image :src="row.picUrl" class="w-30px h-30px" @click="imagePreview(row.picUrl)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :show-overflow-tooltip="true" label="商品名称" min-width="300" prop="name" />
|
||||
<el-table-column align="center" label="商品售价" min-width="90" prop="price">
|
||||
<template #default="{ row }">
|
||||
{{ formatToFraction(row.price) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
|
||||
<el-table-column align="center" label="库存" min-width="90" prop="stock" />
|
||||
</el-table>
|
||||
</template>
|
||||
<script lang="ts" name="SpuAndSkuList" setup>
|
||||
// TODO 后续计划重新封装作为活动商品配置通用组件
|
||||
import { formatToFraction } from '@/utils'
|
||||
import { createImageViewer } from '@/components/ImageViewer'
|
||||
import * as ProductSpuApi from '@/api/mall/product/spu'
|
||||
import { SpuRespVO } from '@/api/mall/product/spu'
|
||||
import {
|
||||
getPropertyList,
|
||||
Properties,
|
||||
RuleConfig,
|
||||
SkuList
|
||||
} from '@/views/mall/product/spu/components'
|
||||
import { SeckillProductVO, SpuExtension } from '@/api/mall/promotion/seckill/seckillActivity'
|
||||
|
||||
const props = defineProps({
|
||||
spuList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
const spuData = ref<SpuRespVO[]>([]) // spu 详情数据列表
|
||||
const skuListRef = ref() // 商品属性列表Ref
|
||||
interface spuProperty {
|
||||
spuId: number
|
||||
spuDetail: SpuExtension
|
||||
propertyList: Properties[]
|
||||
}
|
||||
|
||||
const spuPropertyList = ref<spuProperty[]>([]) // spuId 对应的 sku 的属性列表
|
||||
/**
|
||||
* 获取 SPU 详情
|
||||
* @param spuIds
|
||||
*/
|
||||
const getSpuDetails = async (spuIds: number[]) => {
|
||||
const spuProperties: spuProperty[] = []
|
||||
// TODO puhui999: 考虑后端添加通过 spuIds 批量获取
|
||||
for (const spuId of spuIds) {
|
||||
// 获取 SPU 详情
|
||||
const res = (await ProductSpuApi.getSpu(spuId)) as SpuExtension
|
||||
if (!res) {
|
||||
continue
|
||||
}
|
||||
// 初始化每个 sku 秒杀配置
|
||||
res.skus?.forEach((sku) => {
|
||||
const config: SeckillProductVO = {
|
||||
spuId,
|
||||
skuId: sku.id!,
|
||||
stock: 0,
|
||||
seckillPrice: 0
|
||||
}
|
||||
sku.productConfig = config
|
||||
})
|
||||
spuProperties.push({ spuId, spuDetail: res, propertyList: getPropertyList(res) })
|
||||
}
|
||||
spuPropertyList.value = spuProperties
|
||||
}
|
||||
const ruleConfig: RuleConfig[] = [
|
||||
{
|
||||
name: 'stock',
|
||||
geValue: 10
|
||||
},
|
||||
{
|
||||
name: 'seckillPrice',
|
||||
geValue: 0.01
|
||||
}
|
||||
]
|
||||
const message = useMessage() // 消息弹窗
|
||||
/**
|
||||
* 获取所有 sku 秒杀配置
|
||||
*/
|
||||
const getSkuConfigs = (): SeckillProductVO[] => {
|
||||
if (!skuListRef.value.validateSku()) {
|
||||
// TODO 作为通用组件是需要进一步完善
|
||||
message.warning('请检查商品相关属性配置!!')
|
||||
throw new Error('请检查商品相关属性配置!!')
|
||||
}
|
||||
const seckillProducts: SeckillProductVO[] = []
|
||||
spuPropertyList.value.forEach((item) => {
|
||||
item.spuDetail.skus.forEach((sku) => {
|
||||
seckillProducts.push(sku.productConfig)
|
||||
})
|
||||
})
|
||||
return seckillProducts
|
||||
}
|
||||
// 暴露出给表单提交时使用
|
||||
defineExpose({ getSkuConfigs })
|
||||
/** 商品图预览 */
|
||||
const imagePreview = (imgUrl: string) => {
|
||||
createImageViewer({
|
||||
zIndex: 99999999,
|
||||
urlList: [imgUrl]
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 将传进来的值赋值给 skuList
|
||||
*/
|
||||
watch(
|
||||
() => props.spuList,
|
||||
(data) => {
|
||||
if (!data) return
|
||||
spuData.value = data as SpuRespVO[]
|
||||
getSpuDetails(spuData.value.map((spu) => spu.id!))
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@ -0,0 +1,4 @@
|
||||
import SpuAndSkuSelectForm from './SpuAndSkuSelectForm.vue'
|
||||
import SpuAndSkuList from './SpuAndSkuList.vue'
|
||||
|
||||
export { SpuAndSkuSelectForm, SpuAndSkuList }
|
||||
@ -0,0 +1,261 @@
|
||||
import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { getListAllSimple } from '@/api/mall/promotion/seckill/seckillConfig'
|
||||
|
||||
// 表单校验
|
||||
export const rules = reactive({
|
||||
spuId: [required],
|
||||
name: [required],
|
||||
startTime: [required],
|
||||
endTime: [required],
|
||||
sort: [required],
|
||||
configIds: [required],
|
||||
totalLimitCount: [required],
|
||||
singleLimitCount: [required],
|
||||
totalStock: [required]
|
||||
})
|
||||
|
||||
// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
|
||||
const crudSchemas = reactive<CrudSchema[]>([
|
||||
{
|
||||
label: '秒杀活动名称',
|
||||
field: 'name',
|
||||
isSearch: true,
|
||||
form: {
|
||||
colProps: {
|
||||
span: 24
|
||||
}
|
||||
},
|
||||
table: {
|
||||
width: 120
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '活动开始时间',
|
||||
field: 'startTime',
|
||||
formatter: dateFormatter,
|
||||
isSearch: true,
|
||||
search: {
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
type: 'daterange',
|
||||
defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')]
|
||||
}
|
||||
},
|
||||
form: {
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
type: 'date',
|
||||
valueFormat: 'x'
|
||||
}
|
||||
},
|
||||
table: {
|
||||
width: 300
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '活动结束时间',
|
||||
field: 'endTime',
|
||||
formatter: dateFormatter,
|
||||
isSearch: true,
|
||||
search: {
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
type: 'daterange',
|
||||
defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')]
|
||||
}
|
||||
},
|
||||
form: {
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
type: 'date',
|
||||
valueFormat: 'x'
|
||||
}
|
||||
},
|
||||
table: {
|
||||
width: 300
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '秒杀时段',
|
||||
field: 'configIds',
|
||||
form: {
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
multiple: true,
|
||||
optionsAlias: {
|
||||
labelField: 'name',
|
||||
valueField: 'id'
|
||||
}
|
||||
},
|
||||
api: getListAllSimple
|
||||
},
|
||||
table: {
|
||||
width: 300
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '新增订单数',
|
||||
field: 'orderCount',
|
||||
isForm: false,
|
||||
form: {
|
||||
component: 'InputNumber',
|
||||
value: 0
|
||||
},
|
||||
table: {
|
||||
width: 300
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '付款人数',
|
||||
field: 'userCount',
|
||||
isForm: false,
|
||||
form: {
|
||||
component: 'InputNumber',
|
||||
value: 0
|
||||
},
|
||||
table: {
|
||||
width: 300
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '订单实付金额',
|
||||
field: 'totalPrice',
|
||||
isForm: false,
|
||||
form: {
|
||||
component: 'InputNumber',
|
||||
value: 0
|
||||
},
|
||||
table: {
|
||||
width: 300
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '总限购数量',
|
||||
field: 'totalLimitCount',
|
||||
form: {
|
||||
component: 'InputNumber',
|
||||
value: 0
|
||||
},
|
||||
table: {
|
||||
width: 300
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '单次限够数量',
|
||||
field: 'singleLimitCount',
|
||||
form: {
|
||||
component: 'InputNumber',
|
||||
value: 0
|
||||
},
|
||||
table: {
|
||||
width: 300
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '秒杀库存',
|
||||
field: 'stock',
|
||||
isForm: false,
|
||||
form: {
|
||||
component: 'InputNumber',
|
||||
value: 0
|
||||
},
|
||||
table: {
|
||||
width: 300
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '秒杀总库存',
|
||||
field: 'totalStock',
|
||||
form: {
|
||||
component: 'InputNumber',
|
||||
value: 0
|
||||
},
|
||||
table: {
|
||||
width: 300
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '秒杀活动商品',
|
||||
field: 'spuId',
|
||||
form: {
|
||||
colProps: {
|
||||
span: 24
|
||||
}
|
||||
},
|
||||
table: {
|
||||
width: 200
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
field: 'createTime',
|
||||
formatter: dateFormatter,
|
||||
search: {
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
type: 'daterange',
|
||||
defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')]
|
||||
}
|
||||
},
|
||||
isForm: false,
|
||||
table: {
|
||||
width: 300
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '排序',
|
||||
field: 'sort',
|
||||
form: {
|
||||
component: 'InputNumber',
|
||||
value: 0
|
||||
},
|
||||
table: {
|
||||
width: 300
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
field: 'status',
|
||||
dictType: DICT_TYPE.COMMON_STATUS,
|
||||
dictClass: 'number',
|
||||
isForm: false,
|
||||
isSearch: true,
|
||||
form: {
|
||||
component: 'Radio'
|
||||
},
|
||||
table: {
|
||||
width: 80
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
field: 'remark',
|
||||
form: {
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
rows: 4
|
||||
},
|
||||
colProps: {
|
||||
span: 24
|
||||
}
|
||||
},
|
||||
table: {
|
||||
width: 300
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '操作',
|
||||
field: 'action',
|
||||
isForm: false,
|
||||
table: {
|
||||
width: 120,
|
||||
fixed: 'right'
|
||||
}
|
||||
}
|
||||
])
|
||||
export const { allSchemas } = useCrudSchemas(crudSchemas)
|
||||
@ -0,0 +1,79 @@
|
||||
import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
|
||||
// 表单校验
|
||||
export const rules = reactive({
|
||||
name: [required],
|
||||
startTime: [required],
|
||||
endTime: [required],
|
||||
picUrl: [required],
|
||||
status: [required]
|
||||
})
|
||||
|
||||
// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
|
||||
const crudSchemas = reactive<CrudSchema[]>([
|
||||
{
|
||||
label: '秒杀时段名称',
|
||||
field: 'name',
|
||||
isSearch: true
|
||||
},
|
||||
{
|
||||
label: '开始时间点',
|
||||
field: 'startTime',
|
||||
isSearch: false,
|
||||
search: {
|
||||
component: 'TimePicker'
|
||||
},
|
||||
form: {
|
||||
component: 'TimePicker',
|
||||
componentProps: {
|
||||
valueFormat: 'HH:mm:ss'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '结束时间点',
|
||||
field: 'endTime',
|
||||
isSearch: false,
|
||||
search: {
|
||||
component: 'TimePicker'
|
||||
},
|
||||
form: {
|
||||
component: 'TimePicker',
|
||||
componentProps: {
|
||||
valueFormat: 'HH:mm:ss'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '秒杀主图',
|
||||
field: 'picUrl',
|
||||
isSearch: false,
|
||||
form: {
|
||||
component: 'UploadImg'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
field: 'status',
|
||||
dictType: DICT_TYPE.COMMON_STATUS,
|
||||
dictClass: 'number',
|
||||
isSearch: true,
|
||||
form: {
|
||||
component: 'Radio'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
field: 'createTime',
|
||||
isForm: false,
|
||||
isSearch: false,
|
||||
formatter: dateFormatter
|
||||
},
|
||||
{
|
||||
label: '操作',
|
||||
field: 'action',
|
||||
isForm: false
|
||||
}
|
||||
])
|
||||
export const { allSchemas } = useCrudSchemas(crudSchemas)
|
||||
Loading…
Reference in New Issue