diff --git a/src/api/mall/promotion/discount/discountActivity.ts b/src/api/mall/promotion/discount/discountActivity.ts new file mode 100644 index 00000000..e755c1bd --- /dev/null +++ b/src/api/mall/promotion/discount/discountActivity.ts @@ -0,0 +1,60 @@ +import request from '@/config/axios' +import { Sku, Spu } from '@/api/mall/product/spu' + +export interface DiscountActivityVO { + id?: number + spuId?: number + name?: string + status?: number + remark?: string + startTime?: Date + endTime?: Date + products?: DiscountProductVO[] +} +// 限时折扣相关 属性 +export interface DiscountProductVO { + spuId: number + skuId: number + discountType: number + discountPercent: number + discountPrice: number +} + +// 扩展 Sku 配置 +export type SkuExtension = Sku & { + productConfig: DiscountProductVO +} + +export interface SpuExtension extends Spu { + skus: SkuExtension[] // 重写类型 +} + +// 查询限时折扣活动列表 +export const getDiscountActivityPage = async (params) => { + return await request.get({ url: '/promotion/discount-activity/page', params }) +} + +// 查询限时折扣活动详情 +export const getDiscountActivity = async (id: number) => { + return await request.get({ url: '/promotion/discount-activity/get?id=' + id }) +} + +// 新增限时折扣活动 +export const createDiscountActivity = async (data: DiscountActivityVO) => { + return await request.post({ url: '/promotion/discount-activity/create', data }) +} + +// 修改限时折扣活动 +export const updateDiscountActivity = async (data: DiscountActivityVO) => { + return await request.put({ url: '/promotion/discount-activity/update', data }) +} + +// 关闭限时折扣活动 +export const closeDiscountActivity = async (id: number) => { + return await request.put({ url: '/promotion/discount-activity/close?id=' + id }) +} + +// 删除限时折扣活动 +export const deleteDiscountActivity = async (id: number) => { + return await request.delete({ url: '/promotion/discount-activity/delete?id=' + id }) +} diff --git a/src/views/mall/promotion/discountActivity/DiscountActivityForm.vue b/src/views/mall/promotion/discountActivity/DiscountActivityForm.vue new file mode 100644 index 00000000..7db05426 --- /dev/null +++ b/src/views/mall/promotion/discountActivity/DiscountActivityForm.vue @@ -0,0 +1,175 @@ +<template> + <Dialog v-model="dialogVisible" :title="dialogTitle" width="65%"> + <Form + ref="formRef" + v-loading="formLoading" + :isCol="true" + :rules="rules" + :schema="allSchemas.formSchema" + > + <!-- 先选择 --> + <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.discountPrice" :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.discountPercent" 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 { SpuAndSkuList, SpuProperty, SpuSelect } from '../components' +import { allSchemas, rules } from './discountActivity.data' +import { cloneDeep } from 'lodash-es' +import * as DiscountActivityApi from '@/api/mall/promotion/discount/discountActivity' +import * as ProductSpuApi from '@/api/mall/product/spu' +import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components' + +defineOptions({ name: 'PromotionDiscountActivityForm' }) + +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 ruleConfig: RuleConfig[] = [] +const spuList = ref<DiscountActivityApi.SpuExtension[]>([]) // 选择的 spu +const spuPropertyList = ref<SpuProperty<DiscountActivityApi.SpuExtension>[]>([]) +const selectSpu = (spuId: number, skuIds: number[]) => { + formRef.value.setValues({ spuId }) + getSpuDetails(spuId, skuIds) +} +/** + * 获取 SPU 详情 + */ +const getSpuDetails = async ( + spuId: number, + skuIds: number[] | undefined, + products?: DiscountActivityApi.DiscountProductVO[] +) => { + const spuProperties: SpuProperty<DiscountActivityApi.SpuExtension>[] = [] + const res = (await ProductSpuApi.getSpuDetailList([spuId])) as DiscountActivityApi.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: DiscountActivityApi.DiscountProductVO = { + skuId: sku.id!, + spuId: spu.id, + discountType: 1, + discountPercent: 0, + discountPrice: 0 + } + if (typeof products !== 'undefined') { + const product = products.find((item) => item.skuId === sku.id) + config = product || config + } + sku.productConfig = config + }) + spu.skus = selectSkus as DiscountActivityApi.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 DiscountActivityApi.getDiscountActivity( + id + )) as DiscountActivityApi.DiscountActivityVO + const supId = data.products[0].spuId + await getSpuDetails(supId!, data.products?.map((sku) => sku.skuId), data.products) + formRef.value.setValues(data) + } finally { + formLoading.value = false + } + } +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +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 DiscountActivityApi.DiscountActivityVO + // 获取 折扣商品配置 + const products = cloneDeep(spuAndSkuListRef.value.getSkuConfigs('productConfig')) + products.forEach((item: DiscountActivityApi.DiscountProductVO) => { + item.discountType = data['discountType'] + }) + data.products = products + // 真正提交 + if (formType.value === 'create') { + await DiscountActivityApi.createDiscountActivity(data) + message.success(t('common.createSuccess')) + } else { + await DiscountActivityApi.updateDiscountActivity(data) + message.success(t('common.updateSuccess')) + } + dialogVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = async () => { + spuList.value = [] + spuPropertyList.value = [] + await nextTick() + formRef.value.getElFormRef().resetFields() +} +</script> diff --git a/src/views/mall/promotion/discountActivity/discountActivity.data.ts b/src/views/mall/promotion/discountActivity/discountActivity.data.ts new file mode 100644 index 00000000..abc1bc9e --- /dev/null +++ b/src/views/mall/promotion/discountActivity/discountActivity.data.ts @@ -0,0 +1,118 @@ +import type { CrudSchema } from '@/hooks/web/useCrudSchemas' +import { dateFormatter2 } from '@/utils/formatTime' + +// 表单校验 +export const rules = reactive({ + spuId: [required], + name: [required], + startTime: [required], + endTime: [required], + discountType: [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: 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: 'discountType', + dictType: DICT_TYPE.PROMOTION_DISCOUNT_TYPE, + dictClass: 'number', + isSearch: true, + form: { + component: 'Radio', + value: 1 + } + }, + { + label: '活动商品', + field: 'spuId', + isTable: true, + isSearch: false, + form: { + colProps: { + span: 24 + } + }, + table: { + width: 300 + } + }, + { + label: '备注', + field: 'remark', + isSearch: false, + form: { + component: 'Input', + componentProps: { + type: 'textarea', + rows: 4 + }, + colProps: { + span: 24 + } + }, + table: { + width: 300 + } + } +]) +export const { allSchemas } = useCrudSchemas(crudSchemas) diff --git a/src/views/mall/promotion/discountActivity/index.vue b/src/views/mall/promotion/discountActivity/index.vue new file mode 100644 index 00000000..b7dd5c20 --- /dev/null +++ b/src/views/mall/promotion/discountActivity/index.vue @@ -0,0 +1,237 @@ +<template> + <ContentWrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="活动名称" prop="name"> + <el-input + v-model="queryParams.name" + placeholder="请输入活动名称" + clearable + @keyup.enter="handleQuery" + class="!w-240px" + /> + </el-form-item> + <el-form-item label="活动状态" prop="status"> + <el-select + v-model="queryParams.status" + placeholder="请选择活动状态" + clearable + class="!w-240px" + > + <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="activeTime"> + <el-date-picker + v-model="queryParams.activeTime" + 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 + type="primary" + plain + @click="openForm('create')" + v-hasPermi="['promotion:discount-activity:create']" + > + <Icon icon="ep:plus" class="mr-5px" /> 新增活动 + </el-button> + </el-form-item> + </el-form> + </ContentWrap> + <!-- 列表 --> + <ContentWrap> + <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> + <el-table-column label="活动编号" prop="id" min-width="80" /> + <el-table-column label="活动名称" prop="name" min-width="140" /> + <el-table-column label="活动时间" min-width="210"> + <template #default="scope"> + {{ formatDate(scope.row.startTime, 'YYYY-MM-DD') }} + ~ {{ formatDate(scope.row.endTime, 'YYYY-MM-DD') }} + </template> + </el-table-column> + <el-table-column label="商品图片" prop="spuName" min-width="80"> + <template #default="scope"> + <el-image + :src="scope.row.picUrl" + class="h-40px w-40px" + :preview-src-list="[scope.row.picUrl]" + preview-teleported + /> + </template> + </el-table-column> + <el-table-column label="商品标题" prop="spuName" min-width="300" /> + <el-table-column label="活动状态" align="center" prop="status" min-width="100"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column + label="创建时间" + align="center" + prop="createTime" + :formatter="dateFormatter" + width="180px" + /> + <el-table-column label="操作" align="center" width="150px" fixed="right"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openForm('update', scope.row.id)" + v-hasPermi="['promotion:discount-activity:update']" + > + 编辑 + </el-button> + <el-button + link + type="danger" + @click="handleClose(scope.row.id)" + v-if="scope.row.status === 0" + v-hasPermi="['promotion:discount-activity:close']" + > + 关闭 + </el-button> + <el-button + link + type="danger" + @click="handleDelete(scope.row.id)" + v-else + v-hasPermi="['promotion:discount-activity:delete']" + > + 删除 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + <!-- 表单弹窗:添加/修改 --> + <DiscountActivityForm ref="formRef" @success="getList" /> +</template> + +<script setup lang="ts"> +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { dateFormatter } from '@/utils/formatTime' +import * as DiscountActivity from '@/api/mall/promotion/discount/discountActivity' +import DiscountActivityForm from './DiscountActivityForm.vue' +import { formatDate } from '@/utils/formatTime' +import { fenToYuanFormat } from '@/utils/formatter' +import { fenToYuan } from '@/utils' + +defineOptions({ name: 'DiscountActivity' }) + +const message = useMessage() // 消息弹窗 +const { t } = useI18n() // 国际化 + +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref([]) // 列表的数据 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + activeTime: null, + name: null, + status: null +}) +const queryFormRef = ref() // 搜索的表单 +const exportLoading = ref(false) // 导出的加载中 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await DiscountActivity.getDiscountActivityPage(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 handleClose = async (id: number) => { + try { + // 关闭的二次确认 + await message.confirm('确认关闭该限时折扣活动吗?') + // 发起关闭 + await DiscountActivity.closeDiscountActivity(id) + message.success('关闭成功') + // 刷新列表 + await getList() + } catch {} +} + +/** 删除按钮操作 */ +const handleDelete = async (id: number) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await DiscountActivity.deleteDiscountActivity(id) + message.success(t('common.delSuccess')) + // 刷新列表 + await getList() + } catch {} +} + +const configList = ref([]) // 时段配置精简列表 +// const formatConfigNames = (configId) => { +// const config = configList.value.find((item) => item.id === configId) +// return config != null ? `${config.name}[${config.startTime} ~ ${config.endTime}]` : '' +// } + +const formatSeckillPrice = (products) => { + // const seckillPrice = Math.min(...products.map((item) => item.seckillPrice)) + console.log(products) + const seckillPrice = 200 + return `¥${fenToYuan(seckillPrice)}` +} + +/** 初始化 **/ +onMounted(async () => { + await getList() +}) +</script>