From 3198688eb53ae39f76472643c8ee80db903e7ab2 Mon Sep 17 00:00:00 2001 From: owen <owen@evolsun.com> Date: Sun, 19 Nov 2023 18:27:25 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E8=90=A5=E9=94=80=EF=BC=9A=E9=80=82?= =?UTF-8?q?=E9=85=8D=E5=95=86=E5=9F=8E=E8=A3=85=E4=BF=AE=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E3=80=90=E5=95=86=E5=93=81=E6=A0=8F=E3=80=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/mobile/ProductList/config.ts | 64 +++++++++ .../components/mobile/ProductList/index.vue | 128 ++++++++++++++++++ .../mobile/ProductList/property.vue | 99 ++++++++++++++ src/components/DiyEditor/util.ts | 2 +- .../product/spu/components/SpuShowcase.vue | 1 - 5 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 src/components/DiyEditor/components/mobile/ProductList/config.ts create mode 100644 src/components/DiyEditor/components/mobile/ProductList/index.vue create mode 100644 src/components/DiyEditor/components/mobile/ProductList/property.vue diff --git a/src/components/DiyEditor/components/mobile/ProductList/config.ts b/src/components/DiyEditor/components/mobile/ProductList/config.ts new file mode 100644 index 00000000..019be74e --- /dev/null +++ b/src/components/DiyEditor/components/mobile/ProductList/config.ts @@ -0,0 +1,64 @@ +import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' + +/** 商品卡片属性 */ +export interface ProductListProperty { + // 布局类型:双列 | 三列 | 水平滑动 + layoutType: 'twoCol' | 'threeCol' | 'horizSwiper' + // 商品字段 + fields: { + // 商品名称 + name: ProductListFieldProperty + // 商品价格 + price: ProductListFieldProperty + } + // 角标 + badge: { + // 是否显示 + show: boolean + // 角标图片 + imgUrl: string + } + // 上圆角 + borderRadiusTop: number + // 下圆角 + borderRadiusBottom: number + // 间距 + space: number + // 商品编号列表 + spuIds: number[] + // 组件样式 + style: ComponentStyle +} +// 商品字段 +export interface ProductListFieldProperty { + // 是否显示 + show: boolean + // 颜色 + color: string +} + +// 定义组件 +export const component = { + id: 'ProductList', + name: '商品栏', + icon: 'system-uicons:carousel', + property: { + layoutType: 'twoCol', + fields: { + name: { show: true, color: '#000' }, + price: { show: true, color: '#ff3000' } + }, + badge: { show: false, imgUrl: '' }, + borderRadiusTop: 8, + borderRadiusBottom: 8, + space: 8, + spuIds: [], + style: { + bgType: 'color', + bgColor: '', + marginLeft: 8, + marginRight: 8, + marginBottom: 8 + } as ComponentStyle + } +} as DiyComponent<ProductListProperty> diff --git a/src/components/DiyEditor/components/mobile/ProductList/index.vue b/src/components/DiyEditor/components/mobile/ProductList/index.vue new file mode 100644 index 00000000..cf644ea1 --- /dev/null +++ b/src/components/DiyEditor/components/mobile/ProductList/index.vue @@ -0,0 +1,128 @@ +<template> + <el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef"> + <!-- 商品网格 --> + <div + class="grid overflow-x-auto" + :style="{ + gridGap: `${property.space}px`, + gridTemplateColumns, + width: scrollbarWidth, + }" + > + <!-- 商品 --> + <div + class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white" + :style="{ + borderTopLeftRadius: `${property.borderRadiusTop}px`, + borderTopRightRadius: `${property.borderRadiusTop}px`, + borderBottomLeftRadius: `${property.borderRadiusBottom}px`, + borderBottomRightRadius: `${property.borderRadiusBottom}px` + }" + v-for="(spu, index) in spuList" + :key="index" + > + <!-- 角标 --> + <div + v-if="property.badge.show" + class="absolute left-0 top-0 z-1 items-center justify-center" + > + <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" /> + </div> + <!-- 商品封面图 --> + <el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" /> + <div + :class="[ + 'flex flex-col gap-8px p-8px box-border', + { + 'w-[calc(100%-64px)]': columns === 2, + 'w-full': columns === 3 + } + ]" + > + <!-- 商品名称 --> + <div + v-if="property.fields.name.show" + class="truncate text-12px" + :style="{ color: property.fields.name.color }" + > + {{ spu.name }} + </div> + <div> + <!-- 商品价格 --> + <span + v-if="property.fields.price.show" + class="text-12px" + :style="{ color: property.fields.price.color }" + > + ¥{{ spu.price }} + </span> + </div> + </div> + </div> + </div> + </el-scrollbar> +</template> +<script setup lang="ts"> +import { ProductListProperty } from "./config" +import * as ProductSpuApi from "@/api/mall/product/spu" + +/** 商品卡片 */ +defineOptions({ name: "ProductList" }) +// 定义属性 +const props = defineProps<{ property: ProductListProperty }>() +// 商品列表 +const spuList = ref<ProductSpuApi.Spu[]>([]) +watch( + () => props.property.spuIds, + async () => { + spuList.value = await ProductSpuApi.getSpuDetailList(props.property.spuIds) + }, + { + immediate: true, + deep: true + } +) +// 手机宽度 +const phoneWidth = ref(375) +// 容器 +const containerRef = ref() +// 商品的列数 +const columns = ref(2) +// 滚动条宽度 +const scrollbarWidth = ref("100%") +// 商品图大小 +const imageSize = ref("0") +// 商品网络列数 +const gridTemplateColumns = ref("") +// 计算布局参数 +watch( + () => [props.property, phoneWidth, spuList.value.length], + () => { + // 计算列数 + columns.value = props.property.layoutType === "twoCol" ? 2 : 3 + // 提取手机宽度 + // 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数 + const productWidth = (phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value + // 商品图布局:2列时,左右布局 3列时,上下布局 + imageSize.value = columns.value === 2 ? "64px" : `${productWidth}px` + // 根据布局类型,计算行数、列数 + if (props.property.layoutType === "horizSwiper") { + // 单行显示 + gridTemplateColumns.value = `repeat(auto-fill, ${productWidth}px)` + // 显示滚动条 + scrollbarWidth.value = `${productWidth * spuList.value.length + props.property.space * (spuList.value.length - 1)}px` + } else { + // 指定列数 + gridTemplateColumns.value = `repeat(${columns.value}, auto)` + // 不滚动 + scrollbarWidth.value = "100%" + } + }, + { immediate: true, deep: true } +) +onMounted(() => { + phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375; +}) +</script> + +<style scoped lang="scss"></style> diff --git a/src/components/DiyEditor/components/mobile/ProductList/property.vue b/src/components/DiyEditor/components/mobile/ProductList/property.vue new file mode 100644 index 00000000..872affc3 --- /dev/null +++ b/src/components/DiyEditor/components/mobile/ProductList/property.vue @@ -0,0 +1,99 @@ +<template> + <ComponentContainerProperty v-model="formData.style"> + <el-form label-width="80px" :model="formData"> + <el-card header="商品列表" class="property-group" shadow="never"> + <SpuShowcase v-model="formData.spuIds" /> + </el-card> + <el-card header="商品样式" class="property-group" shadow="never"> + <el-form-item label="布局" prop="type"> + <el-radio-group v-model="formData.layoutType"> + <el-tooltip class="item" content="双列" placement="bottom"> + <el-radio-button label="twoCol"> + <Icon icon="fluent:text-column-two-24-filled" /> + </el-radio-button> + </el-tooltip> + <el-tooltip class="item" content="三列" placement="bottom"> + <el-radio-button label="threeCol"> + <Icon icon="fluent:text-column-three-24-filled" /> + </el-radio-button> + </el-tooltip> + <el-tooltip class="item" content="水平滑动" placement="bottom"> + <el-radio-button label="horizSwiper"> + <Icon icon="system-uicons:carousel" /> + </el-radio-button> + </el-tooltip> + </el-radio-group> + </el-form-item> + <el-form-item label="商品名称" prop="fields.name.show"> + <div class="flex gap-8px"> + <ColorInput v-model="formData.fields.name.color" /> + <el-checkbox v-model="formData.fields.name.show" /> + </div> + </el-form-item> + <el-form-item label="商品价格" prop="fields.price.show"> + <div class="flex gap-8px"> + <ColorInput v-model="formData.fields.price.color" /> + <el-checkbox v-model="formData.fields.price.show" /> + </div> + </el-form-item> + </el-card> + <el-card header="角标" class="property-group" shadow="never"> + <el-form-item label="角标" prop="badge.show"> + <el-switch v-model="formData.badge.show" /> + </el-form-item> + <el-form-item label="角标" prop="badge.imgUrl" v-if="formData.badge.show"> + <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px"> + <template #tip> 建议尺寸:36 * 22 </template> + </UploadImg> + </el-form-item> + </el-card> + <el-card header="商品样式" class="property-group" shadow="never"> + <el-form-item label="上圆角" prop="borderRadiusTop"> + <el-slider + v-model="formData.borderRadiusTop" + :max="100" + :min="0" + show-input + input-size="small" + :show-input-controls="false" + /> + </el-form-item> + <el-form-item label="下圆角" prop="borderRadiusBottom"> + <el-slider + v-model="formData.borderRadiusBottom" + :max="100" + :min="0" + show-input + input-size="small" + :show-input-controls="false" + /> + </el-form-item> + <el-form-item label="间隔" prop="space"> + <el-slider + v-model="formData.space" + :max="100" + :min="0" + show-input + input-size="small" + :show-input-controls="false" + /> + </el-form-item> + </el-card> + </el-form> + </ComponentContainerProperty> +</template> + +<script setup lang="ts"> +import { ProductListProperty } from './config' +import { usePropertyForm } from '@/components/DiyEditor/util' +import SpuShowcase from '@/views/mall/product/spu/components/SpuShowcase.vue' + +// 商品卡片属性面板 +defineOptions({ name: 'ProductListProperty' }) + +const props = defineProps<{ modelValue: ProductListProperty }>() +const emit = defineEmits(['update:modelValue']) +const { formData } = usePropertyForm(props.modelValue, emit) +</script> + +<style scoped lang="scss"></style> diff --git a/src/components/DiyEditor/util.ts b/src/components/DiyEditor/util.ts index 8f8c2c0c..5dfcea9e 100644 --- a/src/components/DiyEditor/util.ts +++ b/src/components/DiyEditor/util.ts @@ -107,7 +107,7 @@ export const PAGE_LIBS = [ extended: true, components: ['ImageBar', 'Carousel', 'TitleBar', 'VideoPlayer', 'Divider', 'MagicCube'] }, - { name: '商品组件', extended: true, components: ['ProductCard'] }, + { name: '商品组件', extended: true, components: ['ProductCard', 'ProductList'] }, { name: '会员组件', extended: true, diff --git a/src/views/mall/product/spu/components/SpuShowcase.vue b/src/views/mall/product/spu/components/SpuShowcase.vue index a3f16fba..3d2caf9a 100644 --- a/src/views/mall/product/spu/components/SpuShowcase.vue +++ b/src/views/mall/product/spu/components/SpuShowcase.vue @@ -59,7 +59,6 @@ watch( productSpus.value.length === 0 || productSpus.value.some((spu) => !props.modelValue.includes(spu.id)) ) { - debugger productSpus.value = await ProductSpuApi.getSpuDetailList(props.modelValue) } }, From e0a731dd86f76292a4b6a7aef3cad1f188c1ff0c Mon Sep 17 00:00:00 2001 From: owen <owen@evolsun.com> Date: Mon, 20 Nov 2023 10:53:02 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E5=95=86=E5=93=81=E8=AF=84=E8=AE=BA?= =?UTF-8?q?=E3=80=81=E4=BC=98=E6=83=A0=E5=88=B8=E6=A8=A1=E6=9D=BF=E5=A4=8D?= =?UTF-8?q?=E7=94=A8=E7=BB=84=E4=BB=B6=E3=80=90=E5=95=86=E5=93=81=E6=A9=B1?= =?UTF-8?q?=E7=AA=97=E3=80=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mall/product/comment/CommentForm.vue | 31 ++-------- src/views/mall/product/comment/index.vue | 2 +- .../product/spu/components/SpuShowcase.vue | 51 ++++++++++------ .../coupon/template/CouponTemplateForm.vue | 58 +------------------ 4 files changed, 41 insertions(+), 101 deletions(-) diff --git a/src/views/mall/product/comment/CommentForm.vue b/src/views/mall/product/comment/CommentForm.vue index 284e9a61..e9913ade 100644 --- a/src/views/mall/product/comment/CommentForm.vue +++ b/src/views/mall/product/comment/CommentForm.vue @@ -8,14 +8,7 @@ v-loading="formLoading" > <el-form-item label="商品" prop="spuId"> - <div @click="handleSelectSpu" class="h-60px w-60px"> - <div v-if="spuData && spuData.picUrl"> - <el-image :src="spuData.picUrl" /> - </div> - <div v-else class="select-box"> - <Icon icon="ep:plus" /> - </div> - </div> + <SpuShowcase v-model="formData.spuId" :limit="1" /> </el-form-item> <el-form-item label="商品规格" prop="skuId" v-if="formData.spuId"> <div @click="handleSelectSku" class="h-60px w-60px"> @@ -51,12 +44,11 @@ <el-button @click="dialogVisible = false">取 消</el-button> </template> </Dialog> - <SpuTableSelect ref="spuTableSelectRef" @change="handleSpuChange" /> - <SkuTableSelect ref="skuTableSelectRef" @change="handleSkuChange" :spu-id="spuData.id" /> + <SkuTableSelect ref="skuTableSelectRef" @change="handleSkuChange" :spu-id="formData.spuId" /> </template> <script setup lang="ts"> import * as CommentApi from '@/api/mall/product/comment' -import SpuTableSelect from '@/views/mall/product/spu/components/SpuTableSelect.vue' +import SpuShowcase from "@/views/mall/product/spu/components/SpuShowcase.vue"; import * as ProductSpuApi from '@/api/mall/product/spu' import SkuTableSelect from '@/views/mall/product/spu/components/SkuTableSelect.vue' @@ -72,8 +64,7 @@ const formData = ref({ userId: undefined, userNickname: undefined, userAvatar: undefined, - spuId: undefined, - spuName: undefined, + spuId: 0, skuId: undefined, descriptionScores: 5, benefitScores: 5, @@ -90,7 +81,6 @@ const formRules = reactive({ benefitScores: [{ required: true, message: '服务星级不能为空', trigger: 'blur' }] }) const formRef = ref() // 表单 Ref -const spuData = ref<ProductSpuApi.Spu>({}) const skuData = ref({ id: -1, name: '', @@ -149,8 +139,7 @@ const resetForm = () => { userId: undefined, userNickname: undefined, userAvatar: undefined, - spuId: undefined, - spuName: undefined, + spuId: 0, skuId: undefined, descriptionScores: 5, benefitScores: 5, @@ -160,16 +149,6 @@ const resetForm = () => { formRef.value?.resetFields() } -/** SPU 表格选择 */ -const spuTableSelectRef = ref() -const handleSelectSpu = () => { - spuTableSelectRef.value.open() -} -const handleSpuChange = (spu: ProductSpuApi.Spu) => { - spuData.value = spu - formData.value.spuId = spu.id -} - /** SKU 表格选择 */ const skuTableSelectRef = ref() const handleSelectSku = () => { diff --git a/src/views/mall/product/comment/index.vue b/src/views/mall/product/comment/index.vue index b7398d84..ea2b2010 100644 --- a/src/views/mall/product/comment/index.vue +++ b/src/views/mall/product/comment/index.vue @@ -59,7 +59,7 @@ <!-- 列表 --> <ContentWrap> <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="false"> - <el-table-column label="评论编号" align="center" prop="id" min-width="50" /> + <el-table-column label="评论编号" align="center" prop="id" min-width="80" /> <el-table-column label="商品信息" align="center" min-width="400"> <template #default="scope"> <div class="row flex items-center gap-x-4px"> diff --git a/src/views/mall/product/spu/components/SpuShowcase.vue b/src/views/mall/product/spu/components/SpuShowcase.vue index 3d2caf9a..a56a3ef0 100644 --- a/src/views/mall/product/spu/components/SpuShowcase.vue +++ b/src/views/mall/product/spu/components/SpuShowcase.vue @@ -13,13 +13,8 @@ </div> </el-tooltip> </div> - <el-tooltip content="选择商品"> - <div - v-show="!disabled" - v-if="!limit || limit <= productSpus.length" - class="select-box" - @click="openSpuTableSelect" - > + <el-tooltip content="选择商品" v-if="canAdd"> + <div class="select-box" @click="openSpuTableSelect"> <Icon icon="ep:plus" /> </div> </el-tooltip> @@ -31,35 +26,52 @@ import * as ProductSpuApi from '@/api/mall/product/spu' import SpuTableSelect from '@/views/mall/product/spu/components/SpuTableSelect.vue' import { propTypes } from '@/utils/propTypes' -import { array } from 'vue-types' +import { oneOfType } from 'vue-types' +import { isArray } from "@/utils/is"; // 商品橱窗,一般用于与商品建立关系时使用 // 提供功能:展示商品列表、添加商品、移除商品 defineOptions({ name: 'SpuShowcase' }) const props = defineProps({ - modelValue: array<number>().def([]).isRequired, + modelValue: oneOfType<number | Array<number>>([Number, Array]).isRequired, // 限制数量:默认不限制 - limit: propTypes.number.def(0), + limit: propTypes.number.def(Number.MAX_VALUE), disabled: propTypes.bool.def(false) }) +// 计算是否可以添加 +const canAdd = computed(() => { + // 情况一:禁用时不可以添加 + if(props.disabled) return false + // 情况二:未指定限制数量时,可以添加 + if(!props.limit) return true + // 情况三:检查已添加数量是否小于限制数量 + return productSpus.value.length < props.limit +}) + // 商品列表 const productSpus = ref<ProductSpuApi.Spu[]>([]) watch( () => props.modelValue, async () => { - if (props.modelValue.length === 0) { + const ids = isArray(props.modelValue) + // 情况一:多选 + ? props.modelValue + // 情况二:单选 + : props.modelValue ? [props.modelValue]: [] + // 不需要返显 + if(ids.length === 0) { productSpus.value = [] return } // 只有商品发生变化之后,才去查询商品 if ( productSpus.value.length === 0 || - productSpus.value.some((spu) => !props.modelValue.includes(spu.id)) + productSpus.value.some((spu) => !ids.includes(spu.id!)) ) { - productSpus.value = await ProductSpuApi.getSpuDetailList(props.modelValue) + productSpus.value = await ProductSpuApi.getSpuDetailList(ids) } }, { immediate: true } @@ -91,11 +103,14 @@ const handleRemoveSpu = (index: number) => { } const emit = defineEmits(['update:modelValue', 'change']) const emitSpuChange = () => { - emit( - 'update:modelValue', - productSpus.value.map((spu) => spu.id) - ) - emit('change', productSpus.value) + if(props.limit === 1) { + const spu = productSpus.value.length > 0 ? productSpus.value[0] : null + emit('update:modelValue', spu?.id || 0) + emit('change', spu) + } else { + emit('update:modelValue', productSpus.value.map((spu) => spu.id)) + emit('change', productSpus.value) + } } </script> diff --git a/src/views/mall/promotion/coupon/template/CouponTemplateForm.vue b/src/views/mall/promotion/coupon/template/CouponTemplateForm.vue index ddf44eb3..fcb18337 100644 --- a/src/views/mall/promotion/coupon/template/CouponTemplateForm.vue +++ b/src/views/mall/promotion/coupon/template/CouponTemplateForm.vue @@ -26,15 +26,7 @@ label="商品" prop="productSpuIds" > - <div class="flex flex-wrap items-center gap-1"> - <div v-for="(spu, index) in productSpus" :key="spu.id" class="select-box spu-pic"> - <el-image :src="spu.picUrl" /> - <Icon class="del-icon" icon="ep:circle-close-filled" @click="handleRemoveSpu(index)" /> - </div> - <div class="select-box" @click="openSpuTableSelect"> - <Icon icon="ep:plus" /> - </div> - </div> + <SpuShowcase v-model="formData.productSpuIds" /> </el-form-item> <el-form-item v-if="formData.productScope === PromotionProductScopeEnum.CATEGORY.scope" @@ -186,18 +178,16 @@ <el-button @click="dialogVisible = false">取 消</el-button> </template> </Dialog> - <SpuTableSelect ref="spuTableSelectRef" multiple @change="handleSpuSelected" /> </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' -import SpuTableSelect from '@/views/mall/product/spu/components/SpuTableSelect.vue' +import SpuShowcase from "@/views/mall/product/spu/components/SpuShowcase.vue"; import ProductCategorySelect from '@/views/mall/product/category/components/ProductCategorySelect.vue' import { convertToInteger, formatToFraction } from '@/utils' @@ -251,7 +241,6 @@ const formRules = reactive({ productCategoryIds: [{ required: true, message: '分类不能为空', trigger: 'blur' }] }) const formRef = ref() // 表单 Ref -const productSpus = ref<ProductSpuApi.Spu[]>([]) // 商品列表 /** 打开弹窗 */ const open = async (type: string, id?: number) => { @@ -354,7 +343,6 @@ const resetForm = () => { productCategoryIds: [] } formRef.value?.resetFields() - productSpus.value = [] } /** 获得商品范围 */ @@ -363,8 +351,6 @@ const getProductScope = async () => { case PromotionProductScopeEnum.SPU.scope: // 设置商品编号 formData.value.productSpuIds = formData.value.productScopeValues - // 获得商品列表 - productSpus.value = await ProductSpuApi.getSpuDetailList(formData.value.productScopeValues) break case PromotionProductScopeEnum.CATEGORY.scope: await nextTick(() => { @@ -397,47 +383,7 @@ function setProductScopeValues(data: CouponTemplateApi.CouponTemplateVO) { break } } - -/** 活动商品的按钮 */ -const spuTableSelectRef = ref() -const openSpuTableSelect = () => { - spuTableSelectRef.value.open(productSpus.value) -} - -/** 选择商品后触发 */ -const handleSpuSelected = (spus: ProductSpuApi.Spu[]) => { - productSpus.value = spus - formData.value.productSpuIds = spus.map((spu) => spu.id) as [] -} - -/** 选择商品后触发 */ -const handleRemoveSpu = (index: number) => { - productSpus.value.splice(index, 1) - formData.value.productSpuIds.splice(index, 1) -} </script> <style lang="scss" scoped> -.select-box { - display: flex; - width: 60px; - height: 60px; - border: 1px dashed var(--el-border-color-darker); - border-radius: 8px; - align-items: center; - justify-content: center; -} - -.spu-pic { - position: relative; -} - -.del-icon { - position: absolute; - top: -10px; - right: -10px; - z-index: 1; - width: 20px !important; - height: 20px !important; -} </style> From 1bb9df7b3c6b7fd8b91195664a36afe151141f4c Mon Sep 17 00:00:00 2001 From: owen <owen@evolsun.com> Date: Tue, 21 Nov 2023 10:27:29 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E5=95=86=E5=93=81=E4=B8=AD=E5=BF=83?= =?UTF-8?q?=EF=BC=9A=E4=BF=AE=E5=A4=8D=E5=95=86=E5=93=81=E8=A1=A8=E6=A0=BC?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=AF=B9=E8=AF=9D=E6=A1=86=EF=BC=8C=E5=9C=A8?= =?UTF-8?q?=E5=A4=9A=E9=80=89=E6=97=B6=E8=A1=A8=E5=A4=B4=E5=86=85=E7=9A=84?= =?UTF-8?q?=E5=A4=9A=E9=80=89=E6=A1=86=E4=B8=8D=E6=98=BE=E7=A4=BA=E9=80=89?= =?UTF-8?q?=E4=B8=AD=E6=95=88=E6=9E=9C=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/spu/components/SpuShowcase.vue | 6 +- .../product/spu/components/SpuTableSelect.vue | 200 +++++++++++------- 2 files changed, 130 insertions(+), 76 deletions(-) diff --git a/src/views/mall/product/spu/components/SpuShowcase.vue b/src/views/mall/product/spu/components/SpuShowcase.vue index a56a3ef0..5d35238e 100644 --- a/src/views/mall/product/spu/components/SpuShowcase.vue +++ b/src/views/mall/product/spu/components/SpuShowcase.vue @@ -20,7 +20,7 @@ </el-tooltip> </div> <!-- 商品选择对话框(表格形式) --> - <SpuTableSelect ref="spuTableSelectRef" multiple @change="handleSpuSelected" /> + <SpuTableSelect ref="spuTableSelectRef" :multiple="limit != 1" @change="handleSpuSelected" /> </template> <script lang="ts" setup> import * as ProductSpuApi from '@/api/mall/product/spu' @@ -88,8 +88,8 @@ const openSpuTableSelect = () => { * 选择商品后触发 * @param spus 选中的商品列表 */ -const handleSpuSelected = (spus: ProductSpuApi.Spu[]) => { - productSpus.value = spus +const handleSpuSelected = (spus: ProductSpuApi.Spu | ProductSpuApi.Spu[]) => { + productSpus.value = isArray(spus) ? spus : [spus] emitSpuChange() } diff --git a/src/views/mall/product/spu/components/SpuTableSelect.vue b/src/views/mall/product/spu/components/SpuTableSelect.vue index e748b76d..8edf83d8 100644 --- a/src/views/mall/product/spu/components/SpuTableSelect.vue +++ b/src/views/mall/product/spu/components/SpuTableSelect.vue @@ -1,8 +1,14 @@ <template> <Dialog v-model="dialogVisible" :appendToBody="true" title="选择商品" width="70%"> <ContentWrap> - <el-row :gutter="20" class="mb-10px"> - <el-col :span="6"> + <el-form + ref="queryFormRef" + :inline="true" + :model="queryParams" + class="-mb-15px" + label-width="68px" + > + <el-form-item label="商品名称" prop="name"> <el-input v-model="queryParams.name" class="!w-240px" @@ -10,19 +16,19 @@ placeholder="请输入商品名称" @keyup.enter="handleQuery" /> - </el-col> - <el-col :span="6"> + </el-form-item> + <el-form-item label="商品分类" prop="categoryId"> <el-tree-select v-model="queryParams.categoryId" :data="categoryTreeList" :props="defaultProps" check-strictly - class="w-1/1" + class="!w-240px" node-key="id" placeholder="请选择商品分类" /> - </el-col> - <el-col :span="6"> + </el-form-item> + <el-form-item label="创建时间" prop="createTime"> <el-date-picker v-model="queryParams.createTime" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" @@ -32,8 +38,8 @@ type="daterange" value-format="YYYY-MM-DD HH:mm:ss" /> - </el-col> - <el-col :span="6"> + </el-form-item> + <el-form-item> <el-button @click="handleQuery"> <Icon class="mr-5px" icon="ep:search" /> 搜索 @@ -42,30 +48,32 @@ <Icon class="mr-5px" icon="ep:refresh" /> 重置 </el-button> - </el-col> - </el-row> + </el-form-item> + </el-form> <el-table v-loading="loading" :data="list" show-overflow-tooltip> - <!-- 多选模式 --> - <el-table-column key="2" type="selection" width="55" v-if="multiple"> + <!-- 1. 多选模式(不能使用type="selection",Element会忽略Header插槽) --> + <el-table-column width="55" v-if="multiple"> <template #header> <el-checkbox - :value="allChecked && checkedPageNos.indexOf(queryParams.pageNo) > -1" + v-model="isCheckAll" + :indeterminate="isIndeterminate" @change="handleCheckAll" /> </template> <template #default="{ row }"> <el-checkbox - :value="checkedSpuIds.indexOf(row.id) > -1" - @change="(checked: boolean) => handleCheckOne(checked, row)" + v-model="checkedStatus[row.id]" + @change="(checked: boolean) => handleCheckOne(checked, row, true)" /> </template> </el-table-column> - <!-- 单选模式 --> + <!-- 2. 单选模式 --> <el-table-column label="#" width="55" v-else> <template #default="{ row }"> - <el-radio :label="row.id" v-model="selectedSpuId" @change="handleSingleSelected(row)" - > </el-radio - > + <el-radio :label="row.id" v-model="selectedSpuId" @change="handleSingleSelected(row)"> + <!-- 空格不能省略,是为了让单选框不显示label,如果不指定label不会有选中的效果 --> + + </el-radio> </template> </el-table-column> <el-table-column key="id" align="center" label="商品编号" prop="id" min-width="60" /> @@ -102,54 +110,71 @@ </template> <script lang="ts" setup> -import { ElTable } from 'element-plus' import { defaultProps, handleTree } from '@/utils/tree' import * as ProductCategoryApi from '@/api/mall/product/category' import * as ProductSpuApi from '@/api/mall/product/spu' import { propTypes } from '@/utils/propTypes' +import { CHANGE_EVENT } from 'element-plus' type Spu = Required<ProductSpuApi.Spu> +/** + * 商品表格选择对话框 + * 1. 单选模式: + * 1.1 点击表格左侧的单选框时,结束选择,并关闭对话框 + * 1.2 再次打开时,保持选中状态 + * 2. 多选模式: + * 2.1 点击表格左侧的多选框时,记录选中的商品 + * 2.2 切换分页时,保持商品的选中的状态 + * 2.3 点击右下角的确定按钮时,结束选择,关闭对话框 + * 2.4 再次打开时,保持选中状态 + */ defineOptions({ name: 'SpuTableSelect' }) -const props = defineProps({ - // 多选 +defineProps({ + // 多选模式 multiple: propTypes.bool.def(false) }) -const total = ref(0) // 列表的总页数 -const list = ref<Spu[]>([]) // 列表的数据 -const loading = ref(false) // 列表的加载中 -const dialogVisible = ref(false) // 弹窗的是否展示 +// 列表的总页数 +const total = ref(0) +// 列表的数据 +const list = ref<Spu[]>([]) +// 列表的加载中 +const loading = ref(false) +// 弹窗的是否展示 +const dialogVisible = ref(false) +// 查询参数 const queryParams = ref({ pageNo: 1, pageSize: 10, - tabType: 0, // 默认获取上架的商品 + // 默认获取上架的商品 + tabType: 0, name: '', categoryId: null, createTime: [] -}) // 查询参数 - -const selectedSpuId = ref() // 选中的商品 spuId +}) /** 打开弹窗 */ -const open = (spus?: Spu[]) => { - if (spus && spus.length > 0) { - // todo check-box不显示选中? - checkedSpus.value = [...spus] - checkedSpuIds.value = spus.map((spu) => spu.id) - } else { - checkedSpus.value = [] - checkedSpuIds.value = [] +const open = (spuList?: Spu[]) => { + // 重置 + checkedSpus.value = [] + checkedStatus.value = {} + isCheckAll.value = false + isIndeterminate.value = false + + // 处理已选中 + if (spuList && spuList.length > 0) { + checkedSpus.value = [...spuList] + checkedStatus.value = Object.fromEntries(spuList.map(spu => [spu.id, true])) } - allChecked.value = false - checkedPageNos.value = [] dialogVisible.value = true resetQuery() } -defineExpose({ open }) // 提供 open 方法,用于打开弹窗 +// 提供 open 方法,用于打开弹窗 +defineExpose({ open }) /** 查询列表 */ const getList = async () => { @@ -158,6 +183,10 @@ const getList = async () => { const data = await ProductSpuApi.getSpuPage(queryParams.value) list.value = data.list total.value = data.total + // checkbox绑定undefined会有问题,需要给一个bool值 + list.value.forEach( spu => checkedStatus.value[spu.id] = checkedStatus.value[spu.id] || false) + // 计算全选框状态 + calculateIsCheckAll() } finally { loading.value = false } @@ -174,7 +203,8 @@ const resetQuery = () => { queryParams.value = { pageNo: 1, pageSize: 10, - tabType: 0, // 默认获取上架的商品 + // 默认获取上架的商品 + tabType: 0, name: '', categoryId: null, createTime: [] @@ -182,65 +212,89 @@ const resetQuery = () => { getList() } -const allChecked = ref(false) //是否全选 -const checkedPageNos = ref<number[]>([]) //选中的页码 -const checkedSpuIds = ref<number[]>([]) //选中的商品ID -const checkedSpus = ref<Spu[]>([]) //选中的商品 +// 是否全选 +const isCheckAll = ref(false) +// 全选框是否处于中间状态:不是全部选中 && 任意一个选中 +const isIndeterminate = ref(false) +// 选中的商品 +const checkedSpus = ref<Spu[]>([]) +// 选中状态:key为商品ID,value为是否选中 +const checkedStatus = ref<Record<string, boolean>>({}) +// 选中的商品 spuId +const selectedSpuId = ref() /** 单选中时触发 */ -const handleSingleSelected = (row: Spu) => { - emits('change', row) +const handleSingleSelected = (spu: Spu) => { + emits(CHANGE_EVENT, spu) // 关闭弹窗 dialogVisible.value = false // 记住上次选择的ID - selectedSpuId.value = row.id + selectedSpuId.value = spu.id } /** 多选完成 */ const handleEmitChange = () => { // 关闭弹窗 dialogVisible.value = false - emits('change', [...checkedSpus.value]) + emits(CHANGE_EVENT, [...checkedSpus.value]) } /** 确认选择时的触发事件 */ const emits = defineEmits<{ - (e: 'change', spu: Spu | Spu[] | any): void + change: [spu: Spu | Spu[] | any] }>() -/** 全选 */ +/** 全选/全不选 */ const handleCheckAll = (checked: boolean) => { - debugger - console.log('checkAll', checked) - allChecked.value = checked - const index = checkedPageNos.value.indexOf(queryParams.value.pageNo) - checkedPageNos.value.push(queryParams.value.pageNo) - if (index > -1) { - checkedPageNos.value.splice(index, 1) - } + isCheckAll.value = checked + isIndeterminate.value = false - list.value.forEach((item) => handleCheckOne(checked, item)) + list.value.forEach((spu) => handleCheckOne(checked, spu, false)) } -/** 选中一行 */ -const handleCheckOne = (checked: boolean, spu: Spu) => { +/** + * 选中一行 + * @param checked 是否选中 + * @param spu 商品 + * @param isCalcCheckAll 是否计算全选 + */ +const handleCheckOne = (checked: boolean, spu: Spu, isCalcCheckAll: boolean) => { if (checked) { - const index = checkedSpuIds.value.indexOf(spu.id) - if (index === -1) { - checkedSpuIds.value.push(spu.id) - checkedSpus.value.push(spu) - } + checkedSpus.value.push(spu) + checkedStatus.value[spu.id] = true } else { - const index = checkedSpuIds.value.indexOf(spu.id) + const index = findCheckedIndex(spu) if (index > -1) { - checkedSpuIds.value.splice(index, 1) checkedSpus.value.splice(index, 1) + checkedStatus.value[spu.id] = false + isCheckAll.value = false } } + + // 计算全选框状态 + if(isCalcCheckAll){ + calculateIsCheckAll() + } } -const categoryList = ref() // 分类列表 -const categoryTreeList = ref() // 分类树 +// 查找商品在已选中商品列表中的索引 +const findCheckedIndex = (spu: Spu) => checkedSpus.value.findIndex(item => item.id === spu.id) + +// 计算全选框状态 +const calculateIsCheckAll = () => { + isCheckAll.value = list.value.every(spu => { + let valueElement = checkedStatus.value[spu.id]; + debugger + return valueElement; + }); + // 计算中间状态:不是全部选中 && 任意一个选中 + isIndeterminate.value = !isCheckAll.value && list.value.some(spu => checkedStatus.value[spu.id]) +} + +// 分类列表 +const categoryList = ref() +// 分类树 +const categoryTreeList = ref() /** 初始化 **/ onMounted(async () => { await getList() From 19a0a83054f73c84a2d5600ef6a1c22e1c68e134 Mon Sep 17 00:00:00 2001 From: puhui999 <puhui999@163.com> Date: Wed, 22 Nov 2023 15:44:42 +0800 Subject: [PATCH 4/8] =?UTF-8?q?crm-=E6=95=B0=E6=8D=AE=E6=9D=83=E9=99=90?= =?UTF-8?q?=EF=BC=9A=E5=AE=8C=E5=96=84=E5=9B=A2=E9=98=9F=E6=88=90=E5=91=98?= =?UTF-8?q?=E5=85=AC=E5=85=B1=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/crm/permission/index.ts | 6 +- src/utils/dict.ts | 278 +++++++++--------- .../crm/components/CrmPermissionForm.vue | 14 +- ...{CrmTeamList.vue => CrmPermissionList.vue} | 80 ++--- src/views/crm/components/index.ts | 8 +- 5 files changed, 188 insertions(+), 198 deletions(-) rename src/views/crm/components/{CrmTeamList.vue => CrmPermissionList.vue} (63%) diff --git a/src/api/crm/permission/index.ts b/src/api/crm/permission/index.ts index 1292f29a..79f69451 100644 --- a/src/api/crm/permission/index.ts +++ b/src/api/crm/permission/index.ts @@ -6,9 +6,9 @@ export interface PermissionVO { bizType: number | undefined // Crm 类型 bizId: number | undefined // Crm 类型数据编号 level: number | undefined // 权限级别 - deptName?: string // 部门名称 // 岗位名称数组 TODO @puhui999:数组? + deptName?: string // 部门名称 nickname?: string // 用户昵称 - postNames?: string // 岗位名称数组 TODO @puhui999:数组? + postNames?: string[] // 岗位名称数组 createTime?: Date } @@ -19,7 +19,7 @@ export const getPermissionList = async (params) => { // 新增团队成员 export const createPermission = async (data: PermissionVO) => { - return await request.post({ url: `/crm/permission/add`, data }) + return await request.post({ url: `/crm/permission/create`, data }) } // 修改团队成员权限级别 diff --git a/src/utils/dict.ts b/src/utils/dict.ts index 9fe429e9..e551a5ef 100644 --- a/src/utils/dict.ts +++ b/src/utils/dict.ts @@ -1,8 +1,8 @@ /** * 数据字典工具类 */ -import { useDictStoreWithOut } from '@/store/modules/dict' -import { ElementPlusInfoType } from '@/types/elementPlus' +import {useDictStoreWithOut} from '@/store/modules/dict' +import {ElementPlusInfoType} from '@/types/elementPlus' const dictStore = useDictStoreWithOut() @@ -13,51 +13,51 @@ const dictStore = useDictStoreWithOut() * @returns {*|Array} 数据字典数组 */ export interface DictDataType { - dictType: string - label: string - value: string | number | boolean - colorType: ElementPlusInfoType | '' - cssClass: string + dictType: string + label: string + value: string | number | boolean + colorType: ElementPlusInfoType | '' + cssClass: string } export const getDictOptions = (dictType: string) => { - return dictStore.getDictByType(dictType) || [] + return dictStore.getDictByType(dictType) || [] } export const getIntDictOptions = (dictType: string): DictDataType[] => { - const dictOption: DictDataType[] = [] - const dictOptions: DictDataType[] = getDictOptions(dictType) - dictOptions.forEach((dict: DictDataType) => { - dictOption.push({ - ...dict, - value: parseInt(dict.value + '') + const dictOption: DictDataType[] = [] + const dictOptions: DictDataType[] = getDictOptions(dictType) + dictOptions.forEach((dict: DictDataType) => { + dictOption.push({ + ...dict, + value: parseInt(dict.value + '') + }) }) - }) - return dictOption + return dictOption } export const getStrDictOptions = (dictType: string) => { - const dictOption: DictDataType[] = [] - const dictOptions: DictDataType[] = getDictOptions(dictType) - dictOptions.forEach((dict: DictDataType) => { - dictOption.push({ - ...dict, - value: dict.value + '' + const dictOption: DictDataType[] = [] + const dictOptions: DictDataType[] = getDictOptions(dictType) + dictOptions.forEach((dict: DictDataType) => { + dictOption.push({ + ...dict, + value: dict.value + '' + }) }) - }) - return dictOption + return dictOption } export const getBoolDictOptions = (dictType: string) => { - const dictOption: DictDataType[] = [] - const dictOptions: DictDataType[] = getDictOptions(dictType) - dictOptions.forEach((dict: DictDataType) => { - dictOption.push({ - ...dict, - value: dict.value + '' === 'true' + const dictOption: DictDataType[] = [] + const dictOptions: DictDataType[] = getDictOptions(dictType) + dictOptions.forEach((dict: DictDataType) => { + dictOption.push({ + ...dict, + value: dict.value + '' === 'true' + }) }) - }) - return dictOption + return dictOption } /** @@ -67,12 +67,12 @@ export const getBoolDictOptions = (dictType: string) => { * @return DictDataType 字典对象 */ export const getDictObj = (dictType: string, value: any): DictDataType | undefined => { - const dictOptions: DictDataType[] = getDictOptions(dictType) - for (const dict of dictOptions) { - if (dict.value === value + '') { - return dict + const dictOptions: DictDataType[] = getDictOptions(dictType) + for (const dict of dictOptions) { + if (dict.value === value + '') { + return dict + } } - } } /** @@ -83,117 +83,121 @@ export const getDictObj = (dictType: string, value: any): DictDataType | undefin * @return 字典名称 */ export const getDictLabel = (dictType: string, value: any): string => { - const dictOptions: DictDataType[] = getDictOptions(dictType) - const dictLabel = ref('') - dictOptions.forEach((dict: DictDataType) => { - if (dict.value === value + '') { - dictLabel.value = dict.label - } - }) - return dictLabel.value + const dictOptions: DictDataType[] = getDictOptions(dictType) + const dictLabel = ref('') + dictOptions.forEach((dict: DictDataType) => { + if (dict.value === value + '') { + dictLabel.value = dict.label + } + }) + return dictLabel.value } export enum DICT_TYPE { - USER_TYPE = 'user_type', - COMMON_STATUS = 'common_status', - SYSTEM_TENANT_PACKAGE_ID = 'system_tenant_package_id', - TERMINAL = 'terminal', // 终端 + USER_TYPE = 'user_type', + COMMON_STATUS = 'common_status', + SYSTEM_TENANT_PACKAGE_ID = 'system_tenant_package_id', + TERMINAL = 'terminal', // 终端 - // ========== SYSTEM 模块 ========== - SYSTEM_USER_SEX = 'system_user_sex', - SYSTEM_MENU_TYPE = 'system_menu_type', - SYSTEM_ROLE_TYPE = 'system_role_type', - SYSTEM_DATA_SCOPE = 'system_data_scope', - SYSTEM_NOTICE_TYPE = 'system_notice_type', - SYSTEM_OPERATE_TYPE = 'system_operate_type', - SYSTEM_LOGIN_TYPE = 'system_login_type', - SYSTEM_LOGIN_RESULT = 'system_login_result', - SYSTEM_SMS_CHANNEL_CODE = 'system_sms_channel_code', - SYSTEM_SMS_TEMPLATE_TYPE = 'system_sms_template_type', - SYSTEM_SMS_SEND_STATUS = 'system_sms_send_status', - SYSTEM_SMS_RECEIVE_STATUS = 'system_sms_receive_status', - SYSTEM_ERROR_CODE_TYPE = 'system_error_code_type', - SYSTEM_OAUTH2_GRANT_TYPE = 'system_oauth2_grant_type', - SYSTEM_MAIL_SEND_STATUS = 'system_mail_send_status', - SYSTEM_NOTIFY_TEMPLATE_TYPE = 'system_notify_template_type', - SYSTEM_SOCIAL_TYPE = 'system_social_type', + // ========== SYSTEM 模块 ========== + SYSTEM_USER_SEX = 'system_user_sex', + SYSTEM_MENU_TYPE = 'system_menu_type', + SYSTEM_ROLE_TYPE = 'system_role_type', + SYSTEM_DATA_SCOPE = 'system_data_scope', + SYSTEM_NOTICE_TYPE = 'system_notice_type', + SYSTEM_OPERATE_TYPE = 'system_operate_type', + SYSTEM_LOGIN_TYPE = 'system_login_type', + SYSTEM_LOGIN_RESULT = 'system_login_result', + SYSTEM_SMS_CHANNEL_CODE = 'system_sms_channel_code', + SYSTEM_SMS_TEMPLATE_TYPE = 'system_sms_template_type', + SYSTEM_SMS_SEND_STATUS = 'system_sms_send_status', + SYSTEM_SMS_RECEIVE_STATUS = 'system_sms_receive_status', + SYSTEM_ERROR_CODE_TYPE = 'system_error_code_type', + SYSTEM_OAUTH2_GRANT_TYPE = 'system_oauth2_grant_type', + SYSTEM_MAIL_SEND_STATUS = 'system_mail_send_status', + SYSTEM_NOTIFY_TEMPLATE_TYPE = 'system_notify_template_type', + SYSTEM_SOCIAL_TYPE = 'system_social_type', - // ========== INFRA 模块 ========== - INFRA_BOOLEAN_STRING = 'infra_boolean_string', - INFRA_JOB_STATUS = 'infra_job_status', - INFRA_JOB_LOG_STATUS = 'infra_job_log_status', - INFRA_API_ERROR_LOG_PROCESS_STATUS = 'infra_api_error_log_process_status', - INFRA_CONFIG_TYPE = 'infra_config_type', - INFRA_CODEGEN_TEMPLATE_TYPE = 'infra_codegen_template_type', - INFRA_CODEGEN_FRONT_TYPE = 'infra_codegen_front_type', - INFRA_CODEGEN_SCENE = 'infra_codegen_scene', - INFRA_FILE_STORAGE = 'infra_file_storage', + // ========== INFRA 模块 ========== + INFRA_BOOLEAN_STRING = 'infra_boolean_string', + INFRA_JOB_STATUS = 'infra_job_status', + INFRA_JOB_LOG_STATUS = 'infra_job_log_status', + INFRA_API_ERROR_LOG_PROCESS_STATUS = 'infra_api_error_log_process_status', + INFRA_CONFIG_TYPE = 'infra_config_type', + INFRA_CODEGEN_TEMPLATE_TYPE = 'infra_codegen_template_type', + INFRA_CODEGEN_FRONT_TYPE = 'infra_codegen_front_type', + INFRA_CODEGEN_SCENE = 'infra_codegen_scene', + INFRA_FILE_STORAGE = 'infra_file_storage', - // ========== BPM 模块 ========== - BPM_MODEL_CATEGORY = 'bpm_model_category', - BPM_MODEL_FORM_TYPE = 'bpm_model_form_type', - BPM_TASK_ASSIGN_RULE_TYPE = 'bpm_task_assign_rule_type', - BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status', - BPM_PROCESS_INSTANCE_RESULT = 'bpm_process_instance_result', - BPM_TASK_ASSIGN_SCRIPT = 'bpm_task_assign_script', - BPM_OA_LEAVE_TYPE = 'bpm_oa_leave_type', + // ========== BPM 模块 ========== + BPM_MODEL_CATEGORY = 'bpm_model_category', + BPM_MODEL_FORM_TYPE = 'bpm_model_form_type', + BPM_TASK_ASSIGN_RULE_TYPE = 'bpm_task_assign_rule_type', + BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status', + BPM_PROCESS_INSTANCE_RESULT = 'bpm_process_instance_result', + BPM_TASK_ASSIGN_SCRIPT = 'bpm_task_assign_script', + BPM_OA_LEAVE_TYPE = 'bpm_oa_leave_type', - // ========== PAY 模块 ========== - PAY_CHANNEL_CODE = 'pay_channel_code', // 支付渠道编码类型 - PAY_ORDER_STATUS = 'pay_order_status', // 商户支付订单状态 - PAY_REFUND_STATUS = 'pay_refund_status', // 退款订单状态 - PAY_NOTIFY_STATUS = 'pay_notify_status', // 商户支付回调状态 - PAY_NOTIFY_TYPE = 'pay_notify_type', // 商户支付回调状态 - PAY_TRANSFER_STATUS = 'pay_transfer_status', // 转账订单状态 - PAY_TRANSFER_TYPE = 'pay_transfer_type', // 转账订单状态 + // ========== PAY 模块 ========== + PAY_CHANNEL_CODE = 'pay_channel_code', // 支付渠道编码类型 + PAY_ORDER_STATUS = 'pay_order_status', // 商户支付订单状态 + PAY_REFUND_STATUS = 'pay_refund_status', // 退款订单状态 + PAY_NOTIFY_STATUS = 'pay_notify_status', // 商户支付回调状态 + PAY_NOTIFY_TYPE = 'pay_notify_type', // 商户支付回调状态 + PAY_TRANSFER_STATUS = 'pay_transfer_status', // 转账订单状态 + PAY_TRANSFER_TYPE = 'pay_transfer_type', // 转账订单状态 - // ========== MP 模块 ========== - MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型 - MP_MESSAGE_TYPE = 'mp_message_type', // 消息类型 + // ========== MP 模块 ========== + MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型 + MP_MESSAGE_TYPE = 'mp_message_type', // 消息类型 - // ========== MALL - 会员模块 ========== - MEMBER_POINT_BIZ_TYPE = 'member_point_biz_type', // 积分的业务类型 - MEMBER_EXPERIENCE_BIZ_TYPE = 'member_experience_biz_type', // 会员经验业务类型 + // ========== MALL - 会员模块 ========== + MEMBER_POINT_BIZ_TYPE = 'member_point_biz_type', // 积分的业务类型 + MEMBER_EXPERIENCE_BIZ_TYPE = 'member_experience_biz_type', // 会员经验业务类型 - // ========== MALL - 商品模块 ========== - PRODUCT_UNIT = 'product_unit', // 商品单位 - PRODUCT_SPU_STATUS = 'product_spu_status', //商品状态 - PROMOTION_TYPE_ENUM = 'promotion_type_enum', // 营销类型枚举 + // ========== MALL - 商品模块 ========== + PRODUCT_UNIT = 'product_unit', // 商品单位 + PRODUCT_SPU_STATUS = 'product_spu_status', //商品状态 + PROMOTION_TYPE_ENUM = 'promotion_type_enum', // 营销类型枚举 - // ========== MALL - 交易模块 ========== - EXPRESS_CHARGE_MODE = 'trade_delivery_express_charge_mode', //快递的计费方式 - TRADE_AFTER_SALE_STATUS = 'trade_after_sale_status', // 售后 - 状态 - TRADE_AFTER_SALE_WAY = 'trade_after_sale_way', // 售后 - 方式 - TRADE_AFTER_SALE_TYPE = 'trade_after_sale_type', // 售后 - 类型 - TRADE_ORDER_TYPE = 'trade_order_type', // 订单 - 类型 - TRADE_ORDER_STATUS = 'trade_order_status', // 订单 - 状态 - TRADE_ORDER_ITEM_AFTER_SALE_STATUS = 'trade_order_item_after_sale_status', // 订单项 - 售后状态 - TRADE_DELIVERY_TYPE = 'trade_delivery_type', // 配送方式 - BROKERAGE_ENABLED_CONDITION = 'brokerage_enabled_condition', // 分佣模式 - BROKERAGE_BIND_MODE = 'brokerage_bind_mode', // 分销关系绑定模式 - BROKERAGE_BANK_NAME = 'brokerage_bank_name', // 佣金提现银行 - BROKERAGE_WITHDRAW_TYPE = 'brokerage_withdraw_type', // 佣金提现类型 - BROKERAGE_RECORD_BIZ_TYPE = 'brokerage_record_biz_type', // 佣金业务类型 - BROKERAGE_RECORD_STATUS = 'brokerage_record_status', // 佣金状态 - BROKERAGE_WITHDRAW_STATUS = 'brokerage_withdraw_status', // 佣金提现状态 + // ========== MALL - 交易模块 ========== + EXPRESS_CHARGE_MODE = 'trade_delivery_express_charge_mode', //快递的计费方式 + TRADE_AFTER_SALE_STATUS = 'trade_after_sale_status', // 售后 - 状态 + TRADE_AFTER_SALE_WAY = 'trade_after_sale_way', // 售后 - 方式 + TRADE_AFTER_SALE_TYPE = 'trade_after_sale_type', // 售后 - 类型 + TRADE_ORDER_TYPE = 'trade_order_type', // 订单 - 类型 + TRADE_ORDER_STATUS = 'trade_order_status', // 订单 - 状态 + TRADE_ORDER_ITEM_AFTER_SALE_STATUS = 'trade_order_item_after_sale_status', // 订单项 - 售后状态 + TRADE_DELIVERY_TYPE = 'trade_delivery_type', // 配送方式 + BROKERAGE_ENABLED_CONDITION = 'brokerage_enabled_condition', // 分佣模式 + BROKERAGE_BIND_MODE = 'brokerage_bind_mode', // 分销关系绑定模式 + BROKERAGE_BANK_NAME = 'brokerage_bank_name', // 佣金提现银行 + BROKERAGE_WITHDRAW_TYPE = 'brokerage_withdraw_type', // 佣金提现类型 + BROKERAGE_RECORD_BIZ_TYPE = 'brokerage_record_biz_type', // 佣金业务类型 + BROKERAGE_RECORD_STATUS = 'brokerage_record_status', // 佣金状态 + BROKERAGE_WITHDRAW_STATUS = 'brokerage_withdraw_status', // 佣金提现状态 - // ========== MALL - 营销模块 ========== - PROMOTION_DISCOUNT_TYPE = 'promotion_discount_type', // 优惠类型 - PROMOTION_PRODUCT_SCOPE = 'promotion_product_scope', // 营销的商品范围 - PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE = 'promotion_coupon_template_validity_type', // 优惠劵模板的有限期类型 - PROMOTION_COUPON_STATUS = 'promotion_coupon_status', // 优惠劵的状态 - PROMOTION_COUPON_TAKE_TYPE = 'promotion_coupon_take_type', // 优惠劵的领取方式 - PROMOTION_ACTIVITY_STATUS = 'promotion_activity_status', // 优惠活动的状态 - PROMOTION_CONDITION_TYPE = 'promotion_condition_type', // 营销的条件类型枚举 - PROMOTION_BARGAIN_RECORD_STATUS = 'promotion_bargain_record_status', // 砍价记录的状态 - PROMOTION_COMBINATION_RECORD_STATUS = 'promotion_combination_record_status', // 拼团记录的状态 - PROMOTION_BANNER_POSITION = 'promotion_banner_position', // banner 定位 + // ========== MALL - 营销模块 ========== + PROMOTION_DISCOUNT_TYPE = 'promotion_discount_type', // 优惠类型 + PROMOTION_PRODUCT_SCOPE = 'promotion_product_scope', // 营销的商品范围 + PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE = 'promotion_coupon_template_validity_type', // 优惠劵模板的有限期类型 + PROMOTION_COUPON_STATUS = 'promotion_coupon_status', // 优惠劵的状态 + PROMOTION_COUPON_TAKE_TYPE = 'promotion_coupon_take_type', // 优惠劵的领取方式 + PROMOTION_ACTIVITY_STATUS = 'promotion_activity_status', // 优惠活动的状态 + PROMOTION_CONDITION_TYPE = 'promotion_condition_type', // 营销的条件类型枚举 + PROMOTION_BARGAIN_RECORD_STATUS = 'promotion_bargain_record_status', // 砍价记录的状态 + PROMOTION_COMBINATION_RECORD_STATUS = 'promotion_combination_record_status', // 拼团记录的状态 + PROMOTION_BANNER_POSITION = 'promotion_banner_position', // banner 定位 - // ========== CRM - 客户管理模块 ========== - CRM_RECEIVABLE_CHECK_STATUS = 'crm_receivable_check_status', - CRM_RETURN_TYPE = 'crm_return_type', - CRM_CUSTOMER_INDUSTRY = 'crm_customer_industry', - CRM_CUSTOMER_LEVEL = 'crm_customer_level', - CRM_CUSTOMER_SOURCE = 'crm_customer_source', - CRM_PRODUCT_STATUS = 'crm_product_status' + // ========== CRM - 客户管理模块 ========== + CRM_RECEIVABLE_CHECK_STATUS = 'crm_receivable_check_status', + CRM_RETURN_TYPE = 'crm_return_type', + CRM_CUSTOMER_INDUSTRY = 'crm_customer_industry', + CRM_CUSTOMER_LEVEL = 'crm_customer_level', + CRM_CUSTOMER_SOURCE = 'crm_customer_source', + CRM_PRODUCT_STATUS = 'crm_product_status', + + // ========== CRM - 数据权限模块 ========== + CRM_BIZ_TYPE = 'crm_biz_type', // 数据模块类型 + CRM_PERMISSION_LEVEL = 'crm_permission_level' // 用户数据权限类型 } diff --git a/src/views/crm/components/CrmPermissionForm.vue b/src/views/crm/components/CrmPermissionForm.vue index 838aa68d..ad6ae588 100644 --- a/src/views/crm/components/CrmPermissionForm.vue +++ b/src/views/crm/components/CrmPermissionForm.vue @@ -19,9 +19,14 @@ </el-form-item> <el-form-item label="权限级别" prop="level"> <el-radio-group v-model="formData.level"> - <!-- TODO @puhui999:搞个字典配置?然后这里 remove 掉负责人 --> - <el-radio :label="CrmPermissionLevelEnum.READ">只读</el-radio> - <el-radio :label="CrmPermissionLevelEnum.WRITE">读写</el-radio> + <template + v-for="dict in getIntDictOptions(DICT_TYPE.CRM_PERMISSION_LEVEL)" + :key="dict.value" + > + <el-radio v-if="dict.value != CrmPermissionLevelEnum.OWNER" :label="dict.value"> + {{ dict.label }} + </el-radio> + </template> </el-radio-group> </el-form-item> </el-form> @@ -34,7 +39,8 @@ <script lang="ts" setup> import * as UserApi from '@/api/system/user' import * as PermissionApi from '@/api/crm/permission' -import { CrmPermissionLevelEnum } from './index' +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { CrmPermissionLevelEnum } from '@/views/crm/components/index' defineOptions({ name: 'CrmPermissionForm' }) diff --git a/src/views/crm/components/CrmTeamList.vue b/src/views/crm/components/CrmPermissionList.vue similarity index 63% rename from src/views/crm/components/CrmTeamList.vue rename to src/views/crm/components/CrmPermissionList.vue index 568777f6..cf50e0bc 100644 --- a/src/views/crm/components/CrmTeamList.vue +++ b/src/views/crm/components/CrmPermissionList.vue @@ -1,7 +1,7 @@ <template> <!-- 操作栏 --> <el-row justify="end"> - <el-button type="primary" @click="handleAdd"> + <el-button type="primary" @click="openForm"> <Icon class="mr-5px" icon="ep:plus" /> 新增 </el-button> @@ -9,7 +9,7 @@ <Icon class="mr-5px" icon="ep:edit" /> 编辑 </el-button> - <el-button @click="handleRemove"> + <el-button @click="handleDelete"> <Icon class="mr-5px" icon="ep:delete" /> 移除 </el-button> @@ -30,45 +30,32 @@ <el-table-column align="center" label="岗位" prop="postNames" /> <el-table-column align="center" label="权限级别" prop="level"> <template #default="{ row }"> - <el-tag>{{ getLevelName(row.level) }}</el-tag> + <dict-tag :type="DICT_TYPE.CRM_PERMISSION_LEVEL" :value="row.level" /> </template> </el-table-column> <el-table-column :formatter="dateFormatter" align="center" label="加入时间" prop="createTime" /> </el-table> - <CrmPermissionForm ref="crmPermissionFormRef" /> + <CrmPermissionForm ref="permissionFormRef" @success="getList" /> </template> <script lang="ts" setup> -// TODO @puhui999:改成 CrmPermissionList import { dateFormatter } from '@/utils/formatTime' import { ElTable } from 'element-plus' import * as PermissionApi from '@/api/crm/permission' import { useUserStoreWithOut } from '@/store/modules/user' import CrmPermissionForm from './CrmPermissionForm.vue' import { CrmPermissionLevelEnum } from './index' +import { DICT_TYPE } from '@/utils/dict' -defineOptions({ name: 'CrmTeam' }) +defineOptions({ name: 'CrmPermissionList' }) const message = useMessage() // 消息 const props = defineProps<{ - bizType: number - bizId: number + bizType: number // 模块类型 + bizId: number // 模块数据编号 }>() const loading = ref(true) // 列表的加载中 -const list = ref<PermissionApi.PermissionVO[]>([ - // TODO 测试数据 - { - id: 1, // 数据权限编号 - userId: 1, // 用户编号 - bizType: 1, // Crm 类型 - bizId: 1, // Crm 类型数据编号 - level: 1, // 权限级别 - deptName: '研发部门', // 部门名称 - nickname: '芋道源码', // 用户昵称 - postNames: '全栈开发工程师', // 岗位名称数组 - createTime: new Date() - } -]) // 列表的数据 +const list = ref<PermissionApi.PermissionVO[]>([]) // 列表的数据 /** 查询列表 */ const getList = async () => { @@ -83,40 +70,28 @@ const getList = async () => { loading.value = false } } - -// TODO @puhui999:字典格式化 -/** - * 获得权限级别名称 - * @param level 权限级别 - */ -const getLevelName = computed(() => (level: number) => { - switch (level) { - case CrmPermissionLevelEnum.OWNER: - return '负责人' - case CrmPermissionLevelEnum.READ: - return '只读' - case CrmPermissionLevelEnum.WRITE: - return '读写' - default: - break - } -}) -// TODO @puhui999:空行稍微注意下哈;一些注释补齐下; -const multipleSelection = ref<PermissionApi.PermissionVO[]>([]) +const multipleSelection = ref<PermissionApi.PermissionVO[]>([]) // 选择的团队成员 const handleSelectionChange = (val: PermissionApi.PermissionVO[]) => { multipleSelection.value = val } -// TODO @puhui999:一些变量命名,看看有没可能跟列表界面的 index.vue 保持他统一的风格; -const crmPermissionFormRef = ref<InstanceType<typeof CrmPermissionForm>>() + +const permissionFormRef = ref<InstanceType<typeof CrmPermissionForm>>() // 权限表单 Ref +/** + * 编辑团队成员 + */ const handleEdit = () => { if (multipleSelection.value?.length === 0) { message.warning('请先选择团队成员后操作!') return } const ids = multipleSelection.value?.map((item) => item.id) - crmPermissionFormRef.value?.open('update', props.bizType, props.bizId, ids) + permissionFormRef.value?.open('update', props.bizType, props.bizId, ids) } -const handleRemove = async () => { + +/** + * 移除团队成员 + */ +const handleDelete = async () => { if (multipleSelection.value?.length === 0) { message.warning('请先选择团队成员后操作!') return @@ -129,11 +104,18 @@ const handleRemove = async () => { ids }) } -const handleAdd = () => { - crmPermissionFormRef.value?.open('create', props.bizType, props.bizId) + +/** + * 添加团队成员 + */ +const openForm = () => { + permissionFormRef.value?.open('create', props.bizType, props.bizId) } -const userStore = useUserStoreWithOut() +const userStore = useUserStoreWithOut() // 用户信息缓存 +/** + * 退出团队 + */ const handleQuit = async () => { const permission = list.value.find( (item) => item.userId === userStore.getUser.id && item.level === CrmPermissionLevelEnum.OWNER diff --git a/src/views/crm/components/index.ts b/src/views/crm/components/index.ts index c25feef3..ea13634e 100644 --- a/src/views/crm/components/index.ts +++ b/src/views/crm/components/index.ts @@ -1,4 +1,4 @@ -import CrmTeam from './CrmTeamList.vue' +import CrmPermissionList from './CrmPermissionList.vue' enum CrmBizTypeEnum { CRM_LEADS = 1, // 线索 @@ -9,9 +9,7 @@ enum CrmBizTypeEnum { } enum CrmPermissionLevelEnum { - OWNER = 1, // 负责人 - READ = 2, // 读 - WRITE = 3 // 写 + OWNER = 1 // 负责人 } -export { CrmTeam, CrmBizTypeEnum, CrmPermissionLevelEnum } +export { CrmPermissionList, CrmBizTypeEnum, CrmPermissionLevelEnum } From 12b59200c1b98e5a5e9e9c6c7754ee42fe6730c5 Mon Sep 17 00:00:00 2001 From: puhui999 <puhui999@163.com> Date: Wed, 22 Nov 2023 15:45:10 +0800 Subject: [PATCH 5/8] =?UTF-8?q?crm-=E5=AE=A2=E6=88=B7=EF=BC=9A=E9=9B=86?= =?UTF-8?q?=E6=88=90=E5=9B=A2=E9=98=9F=E6=88=90=E5=91=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/crm/customer/detail/index.vue | 41 ++++++++++++++++--------- src/views/crm/customer/index.vue | 5 +-- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/views/crm/customer/detail/index.vue b/src/views/crm/customer/detail/index.vue index 392a5d35..c88137fa 100644 --- a/src/views/crm/customer/detail/index.vue +++ b/src/views/crm/customer/detail/index.vue @@ -8,7 +8,7 @@ </div> <div> <!-- 右上:按钮 --> - <el-button @click="openForm('update', customer.id)" v-hasPermi="['crm:customer:update']"> + <el-button v-hasPermi="['crm:customer:update']" @click="openForm('update', customer.id)"> 编辑 </el-button> <el-button>更改成交状态</el-button> @@ -16,31 +16,31 @@ </div> <el-row class="mt-10px"> <el-button> - <Icon icon="ph:calendar-fill" class="mr-5px" /> + <Icon class="mr-5px" icon="ph:calendar-fill" /> 创建任务 </el-button> <el-button> - <Icon icon="carbon:email" class="mr-5px" /> + <Icon class="mr-5px" icon="carbon:email" /> 发送邮件 </el-button> <el-button> - <Icon icon="system-uicons:contacts" class="mr-5px" /> + <Icon class="mr-5px" icon="system-uicons:contacts" /> 创建联系人 </el-button> <el-button> - <Icon icon="ep:opportunity" class="mr-5px" /> + <Icon class="mr-5px" icon="ep:opportunity" /> 创建商机 </el-button> <el-button> - <Icon icon="clarity:contract-line" class="mr-5px" /> + <Icon class="mr-5px" icon="clarity:contract-line" /> 创建合同 </el-button> <el-button> - <Icon icon="icon-park:income-one" class="mr-5px" /> + <Icon class="mr-5px" icon="icon-park:income-one" /> 创建回款 </el-button> <el-button> - <Icon icon="fluent:people-team-add-20-filled" class="mr-5px" /> + <Icon class="mr-5px" icon="fluent:people-team-add-20-filled" /> 添加团队成员 </el-button> </el-row> @@ -75,20 +75,32 @@ <el-tab-pane label="客户关系" lazy> 客户关系</el-tab-pane> <!-- TODO wanwan 以下标签上的数量需要接口统计返回 --> <el-tab-pane label="联系人" lazy> - <template #label> 联系人<el-badge class="item" type="primary" /> </template> + <template #label> + 联系人 + <el-badge class="item" type="primary" /> + </template> 联系人 </el-tab-pane> <el-tab-pane label="团队成员" lazy> - <template #label> 团队成员<el-badge class="item" type="primary" /> </template> - 团队成员 + <template #label> + 团队成员 + <el-badge class="item" type="primary" /> + </template> + <CrmPermissionList :biz-id="customer.id" :biz-type="CrmBizTypeEnum.CRM_CUSTOMER" /> </el-tab-pane> <el-tab-pane label="商机" lazy> 商机</el-tab-pane> <el-tab-pane label="合同" lazy> - <template #label> 合同<el-badge class="item" type="primary" /> </template> + <template #label> + 合同 + <el-badge class="item" type="primary" /> + </template> 合同 </el-tab-pane> <el-tab-pane label="回款" lazy> - <template #label> 回款<el-badge class="item" type="primary" /> </template> + <template #label> + 回款 + <el-badge class="item" type="primary" /> + </template> 回款 </el-tab-pane> <el-tab-pane label="回访" lazy> 回访</el-tab-pane> @@ -100,7 +112,7 @@ <CustomerForm ref="formRef" @success="getCustomerData(id)" /> </template> -<script setup lang="ts"> +<script lang="ts" setup> import { ElMessage } from 'element-plus' import { useTagsViewStore } from '@/store/modules/tagsView' import * as CustomerApi from '@/api/crm/customer' @@ -108,6 +120,7 @@ import CustomerBasicInfo from '@/views/crm/customer/detail/CustomerBasicInfo.vue import { DICT_TYPE } from '@/utils/dict' import CustomerDetails from '@/views/crm/customer/detail/CustomerDetails.vue' import CustomerForm from '@/views/crm/customer/CustomerForm.vue' +import { CrmBizTypeEnum, CrmPermissionList } from '@/views/crm/components' defineOptions({ name: 'CustomerDetail' }) diff --git a/src/views/crm/customer/index.vue b/src/views/crm/customer/index.vue index dea7e617..60bc8bea 100644 --- a/src/views/crm/customer/index.vue +++ b/src/views/crm/customer/index.vue @@ -121,7 +121,7 @@ <el-table-column align="center" label="手机" prop="mobile" width="120" /> <el-table-column align="center" label="详细地址" prop="detailAddress" width="200" /> <el-table-column align="center" label="负责人" prop="ownerUserName" /> - <el-table-column align="center" label="所属部门" prop="ownerUserDept" /> + <el-table-column align="center" label="所属部门" prop="ownerUserDeptName" /> <el-table-column align="center" label="创建人" prop="creatorName" /> <el-table-column :formatter="dateFormatter" @@ -185,8 +185,6 @@ @pagination="getList" /> </ContentWrap> - <!-- TODO 方便查看效果 TODO 芋艿:先注释了,避免演示环境报错 --> - <!-- <CrmTeam :biz-id="1" :biz-type="CrmBizTypeEnum.CRM_CUSTOMER" />--> <!-- 表单弹窗:添加/修改 --> <CustomerForm ref="formRef" @success="getList" /> @@ -198,7 +196,6 @@ import { dateFormatter } from '@/utils/formatTime' import download from '@/utils/download' import * as CustomerApi from '@/api/crm/customer' import CustomerForm from './CustomerForm.vue' -import { CrmBizTypeEnum, CrmTeam } from '@/views/crm/components' defineOptions({ name: 'CrmCustomer' }) From 253401ace3578ed3118b912b039304e40ee95399 Mon Sep 17 00:00:00 2001 From: owen <owen@evolsun.com> Date: Wed, 22 Nov 2023 16:58:00 +0800 Subject: [PATCH 6/8] =?UTF-8?q?=E8=90=A5=E9=94=80=EF=BC=9A=E9=80=82?= =?UTF-8?q?=E9=85=8D=E5=95=86=E5=9F=8E=E8=A3=85=E4=BF=AE=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E3=80=90=E4=BC=98=E6=83=A0=E5=88=B8=E3=80=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mall/promotion/coupon/couponTemplate.ts | 7 + .../mobile/CouponCard/component.tsx | 78 ++++++++++ .../components/mobile/CouponCard/config.ts | 47 ++++++ .../components/mobile/CouponCard/index.vue | 142 ++++++++++++++++++ .../components/mobile/CouponCard/property.vue | 104 +++++++++++++ .../components/mobile/ProductCard/config.ts | 2 +- .../components/mobile/ProductList/config.ts | 2 +- .../components/mobile/ProductList/index.vue | 37 ++--- src/components/DiyEditor/util.ts | 8 +- .../mall/product/comment/CommentForm.vue | 2 +- .../product/spu/components/SpuShowcase.vue | 30 ++-- .../product/spu/components/SpuTableSelect.vue | 18 +-- .../coupon/components/CouponSelect.vue | 10 +- .../coupon/template/CouponTemplateForm.vue | 5 +- 14 files changed, 436 insertions(+), 56 deletions(-) create mode 100644 src/components/DiyEditor/components/mobile/CouponCard/component.tsx create mode 100644 src/components/DiyEditor/components/mobile/CouponCard/config.ts create mode 100644 src/components/DiyEditor/components/mobile/CouponCard/index.vue create mode 100644 src/components/DiyEditor/components/mobile/CouponCard/property.vue diff --git a/src/api/mall/promotion/coupon/couponTemplate.ts b/src/api/mall/promotion/coupon/couponTemplate.ts index 243e22ee..50ae226c 100755 --- a/src/api/mall/promotion/coupon/couponTemplate.ts +++ b/src/api/mall/promotion/coupon/couponTemplate.ts @@ -73,6 +73,13 @@ export function getCouponTemplatePage(params: PageParam) { }) } +// 获得优惠劵模板分页 +export function getCouponTemplateList(ids: number[]) { + return request.get({ + url: `/promotion/coupon-template/list?ids=${ids}` + }) +} + // 导出优惠劵模板 Excel export function exportCouponTemplateExcel(params: PageParam) { return request.get({ diff --git a/src/components/DiyEditor/components/mobile/CouponCard/component.tsx b/src/components/DiyEditor/components/mobile/CouponCard/component.tsx new file mode 100644 index 00000000..1542cad6 --- /dev/null +++ b/src/components/DiyEditor/components/mobile/CouponCard/component.tsx @@ -0,0 +1,78 @@ +import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate' +import { CouponTemplateValidityTypeEnum, PromotionDiscountTypeEnum } from '@/utils/constants' +import { floatToFixed2 } from '@/utils' +import { formatDate } from '@/utils/formatTime' + +// 优惠值 +export const CouponDiscount = defineComponent({ + name: 'CouponDiscount', + props: { + coupon: { + type: CouponTemplateApi.CouponTemplateVO + } + }, + setup(props) { + const coupon = props.coupon as CouponTemplateApi.CouponTemplateVO + // 折扣 + let value = coupon.discountPercent + '' + let suffix = ' 折' + // 满减 + if (coupon.discountType === PromotionDiscountTypeEnum.PRICE.type) { + value = floatToFixed2(coupon.discountPrice) + suffix = ' 元' + } + return () => ( + <div> + <span class={'text-20px font-bold'}>{value}</span> + <span>{suffix}</span> + </div> + ) + } +}) + +// 优惠描述 +export const CouponDiscountDesc = defineComponent({ + name: 'CouponDiscountDesc', + props: { + coupon: { + type: CouponTemplateApi.CouponTemplateVO + } + }, + setup(props) { + const coupon = props.coupon as CouponTemplateApi.CouponTemplateVO + // 使用条件 + const useCondition = coupon.usePrice > 0 ? `满${floatToFixed2(coupon.usePrice)}元,` : '' + // 优惠描述 + const discountDesc = + coupon.discountType === PromotionDiscountTypeEnum.PRICE.type + ? `减${floatToFixed2(coupon.discountPrice)}元` + : `打${coupon.discountPercent}折` + return () => ( + <div> + <span>{useCondition}</span> + <span>{discountDesc}</span> + </div> + ) + } +}) + +// 有效期 +export const CouponValidTerm = defineComponent({ + name: 'CouponValidTerm', + props: { + coupon: { + type: CouponTemplateApi.CouponTemplateVO + } + }, + setup(props) { + const coupon = props.coupon as CouponTemplateApi.CouponTemplateVO + const text = + coupon.validityType === CouponTemplateValidityTypeEnum.DATE.type + ? `有效期:${formatDate(coupon.validStartTime, 'YYYY-MM-DD')} 至 ${formatDate( + coupon.validEndTime, + 'YYYY-MM-DD' + )}` + : `领取后第 ${coupon.fixedStartTerm} - ${coupon.fixedEndTerm} 天内可用` + return () => <div>{text}</div> + } +}) diff --git a/src/components/DiyEditor/components/mobile/CouponCard/config.ts b/src/components/DiyEditor/components/mobile/CouponCard/config.ts new file mode 100644 index 00000000..304533d1 --- /dev/null +++ b/src/components/DiyEditor/components/mobile/CouponCard/config.ts @@ -0,0 +1,47 @@ +import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util' + +/** 商品卡片属性 */ +export interface CouponCardProperty { + // 列数 + columns: number + // 背景图 + bgImg: string + // 文字颜色 + textColor: string + // 按钮样式 + button: { + // 颜色 + color: string + // 背景颜色 + bgColor: string + } + // 间距 + space: number + // 优惠券编号列表 + couponIds: number[] + // 组件样式 + style: ComponentStyle +} + +// 定义组件 +export const component = { + id: 'CouponCard', + name: '优惠券', + icon: 'ep:ticket', + property: { + columns: 1, + bgImg: '', + textColor: '#E9B461', + button: { + color: '#434343', + bgColor: '' + }, + space: 0, + couponIds: [], + style: { + bgType: 'color', + bgColor: '', + marginBottom: 8 + } as ComponentStyle + } +} as DiyComponent<CouponCardProperty> diff --git a/src/components/DiyEditor/components/mobile/CouponCard/index.vue b/src/components/DiyEditor/components/mobile/CouponCard/index.vue new file mode 100644 index 00000000..3e2302af --- /dev/null +++ b/src/components/DiyEditor/components/mobile/CouponCard/index.vue @@ -0,0 +1,142 @@ +<template> + <el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef"> + <div + class="flex flex-row text-12px" + :style="{ + gap: `${property.space}px`, + width: scrollbarWidth + }" + > + <div + class="box-content" + :style="{ + background: property.bgImg + ? `url(${property.bgImg}) 100% center / 100% 100% no-repeat` + : '#fff', + width: `${couponWidth}px`, + color: property.textColor + }" + v-for="(coupon, index) in couponList" + :key="index" + > + <!-- 布局1:1列--> + <div v-if="property.columns === 1" class="m-l-16px flex flex-row justify-between p-8px"> + <div class="flex flex-col justify-evenly gap-4px"> + <!-- 优惠值 --> + <CouponDiscount :coupon="coupon" /> + <!-- 优惠描述 --> + <CouponDiscountDesc :coupon="coupon" /> + <!-- 有效期 --> + <CouponValidTerm :coupon="coupon" /> + </div> + <div class="flex flex-col justify-evenly"> + <div + class="rounded-20px p-x-8px p-y-2px" + :style="{ + color: property.button.color, + background: property.button.bgColor + }" + > + 立即领取 + </div> + </div> + </div> + <!-- 布局2:2列--> + <div + v-else-if="property.columns === 2" + class="m-l-16px flex flex-row justify-between p-8px" + > + <div class="flex flex-col justify-evenly gap-4px"> + <!-- 优惠值 --> + <CouponDiscount :coupon="coupon" /> + <div>{{ coupon.name }}</div> + </div> + <div class="flex flex-col"> + <div + class="h-full w-20px rounded-20px p-x-2px p-y-8px text-center" + :style="{ + color: property.button.color, + background: property.button.bgColor + }" + > + 立即领取 + </div> + </div> + </div> + <!-- 布局3:3列--> + <div v-else class="flex flex-col items-center justify-around gap-4px p-4px"> + <!-- 优惠值 --> + <CouponDiscount :coupon="coupon" /> + <div>{{ coupon.name }}</div> + <div + class="rounded-20px p-x-8px p-y-2px" + :style="{ + color: property.button.color, + background: property.button.bgColor + }" + > + 立即领取 + </div> + </div> + </div> + </div> + </el-scrollbar> +</template> +<script setup lang="ts"> +import { CouponCardProperty } from './config' +import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate' +import { CouponDiscount } from './component' +import { + CouponDiscountDesc, + CouponValidTerm +} from '@/components/DiyEditor/components/mobile/CouponCard/component' + +/** 商品卡片 */ +defineOptions({ name: 'CouponCard' }) +// 定义属性 +const props = defineProps<{ property: CouponCardProperty }>() +// 商品列表 +const couponList = ref<CouponTemplateApi.CouponTemplateVO[]>([]) +watch( + () => props.property.couponIds, + async () => { + if (props.property.couponIds?.length > 0) { + couponList.value = await CouponTemplateApi.getCouponTemplateList(props.property.couponIds) + } + }, + { + immediate: true, + deep: true + } +) + +// 手机宽度 +const phoneWidth = ref(375) +// 容器 +const containerRef = ref() +// 滚动条宽度 +const scrollbarWidth = ref('100%') +// 优惠券的宽度 +const couponWidth = ref(375) +// 计算布局参数 +watch( + () => [props.property, phoneWidth, couponList.value.length], + () => { + // 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数 + couponWidth.value = + (phoneWidth.value * 0.95 - props.property.space * (props.property.columns - 1)) / + props.property.columns + // 显示滚动条 + scrollbarWidth.value = `${ + couponWidth.value * couponList.value.length + + props.property.space * (couponList.value.length - 1) + }px` + }, + { immediate: true, deep: true } +) +onMounted(() => { + // 提取手机宽度 + phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375 +}) +</script> +<style scoped lang="scss"></style> diff --git a/src/components/DiyEditor/components/mobile/CouponCard/property.vue b/src/components/DiyEditor/components/mobile/CouponCard/property.vue new file mode 100644 index 00000000..4f32c21e --- /dev/null +++ b/src/components/DiyEditor/components/mobile/CouponCard/property.vue @@ -0,0 +1,104 @@ +<template> + <ComponentContainerProperty v-model="formData.style"> + <el-form label-width="80px" :model="formData"> + <el-card header="优惠券列表" class="property-group" shadow="never"> + <div + v-for="(coupon, index) in couponList" + :key="index" + class="flex items-center justify-between" + > + <el-text size="large" truncated>{{ coupon.name }}</el-text> + <el-text type="info" truncated> + <span v-if="coupon.usePrice > 0">满{{ floatToFixed2(coupon.usePrice) }}元,</span> + <span v-if="coupon.discountType === PromotionDiscountTypeEnum.PRICE.type"> + 减{{ floatToFixed2(coupon.discountPrice) }}元 + </span> + <span v-else> 打{{ coupon.discountPercent }}折 </span> + </el-text> + </div> + <el-form-item label-width="0"> + <el-button @click="handleAddCoupon" type="primary" plain class="m-t-8px w-full"> + <Icon icon="ep:plus" class="mr-5px" /> 添加 + </el-button> + </el-form-item> + </el-card> + <el-card header="优惠券样式" class="property-group" shadow="never"> + <el-form-item label="列数" prop="type"> + <el-radio-group v-model="formData.columns"> + <el-tooltip class="item" content="一列" placement="bottom"> + <el-radio-button :label="1"> + <Icon icon="fluent:text-column-one-24-filled" /> + </el-radio-button> + </el-tooltip> + <el-tooltip class="item" content="二列" placement="bottom"> + <el-radio-button :label="2"> + <Icon icon="fluent:text-column-two-24-filled" /> + </el-radio-button> + </el-tooltip> + <el-tooltip class="item" content="三列" placement="bottom"> + <el-radio-button :label="3"> + <Icon icon="fluent:text-column-three-24-filled" /> + </el-radio-button> + </el-tooltip> + </el-radio-group> + </el-form-item> + <el-form-item label="背景图片" prop="bgImg"> + <UploadImg v-model="formData.bgImg" height="80px" width="100%" class="min-w-160px" /> + </el-form-item> + <el-form-item label="文字颜色" prop="textColor"> + <ColorInput v-model="formData.textColor" /> + </el-form-item> + <el-form-item label="按钮背景" prop="button.bgColor"> + <ColorInput v-model="formData.button.bgColor" /> + </el-form-item> + <el-form-item label="按钮文字" prop="button.color"> + <ColorInput v-model="formData.button.color" /> + </el-form-item> + <el-form-item label="间隔" prop="space"> + <el-slider + v-model="formData.space" + :max="100" + :min="0" + show-input + input-size="small" + :show-input-controls="false" + /> + </el-form-item> + </el-card> + </el-form> + </ComponentContainerProperty> + <!-- 优惠券选择 --> + <CouponSelect ref="couponSelectDialog" v-model:multiple-selection="couponList" /> +</template> + +<script setup lang="ts"> +import { CouponCardProperty } from './config' +import { usePropertyForm } from '@/components/DiyEditor/util' +import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate' +import { floatToFixed2 } from '@/utils' +import { PromotionDiscountTypeEnum } from '@/utils/constants' +import CouponSelect from '@/views/mall/promotion/coupon/components/CouponSelect.vue' + +// 优惠券卡片属性面板 +defineOptions({ name: 'CouponCardProperty' }) + +const props = defineProps<{ modelValue: CouponCardProperty }>() +const emit = defineEmits(['update:modelValue']) +const { formData } = usePropertyForm(props.modelValue, emit) + +// 优惠券列表 +const couponList = ref<CouponTemplateApi.CouponTemplateVO[]>([]) +const couponSelectDialog = ref() +// 添加优惠券 +const handleAddCoupon = () => { + couponSelectDialog.value.open() +} +watch( + () => couponList.value, + () => { + formData.value.couponIds = couponList.value.map((coupon) => coupon.id) + } +) +</script> + +<style scoped lang="scss"></style> diff --git a/src/components/DiyEditor/components/mobile/ProductCard/config.ts b/src/components/DiyEditor/components/mobile/ProductCard/config.ts index 49dd30d0..735b6ba0 100644 --- a/src/components/DiyEditor/components/mobile/ProductCard/config.ts +++ b/src/components/DiyEditor/components/mobile/ProductCard/config.ts @@ -62,7 +62,7 @@ export interface ProductCardFieldProperty { export const component = { id: 'ProductCard', name: '商品卡片', - icon: 'system-uicons:carousel', + icon: 'fluent:text-column-two-left-24-filled', property: { layoutType: 'oneColBigImg', fields: { diff --git a/src/components/DiyEditor/components/mobile/ProductList/config.ts b/src/components/DiyEditor/components/mobile/ProductList/config.ts index 019be74e..436de405 100644 --- a/src/components/DiyEditor/components/mobile/ProductList/config.ts +++ b/src/components/DiyEditor/components/mobile/ProductList/config.ts @@ -41,7 +41,7 @@ export interface ProductListFieldProperty { export const component = { id: 'ProductList', name: '商品栏', - icon: 'system-uicons:carousel', + icon: 'fluent:text-column-two-24-filled', property: { layoutType: 'twoCol', fields: { diff --git a/src/components/DiyEditor/components/mobile/ProductList/index.vue b/src/components/DiyEditor/components/mobile/ProductList/index.vue index cf644ea1..8a35628e 100644 --- a/src/components/DiyEditor/components/mobile/ProductList/index.vue +++ b/src/components/DiyEditor/components/mobile/ProductList/index.vue @@ -1,13 +1,13 @@ <template> <el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef"> - <!-- 商品网格 --> + <!-- 商品网格 --> <div class="grid overflow-x-auto" :style="{ gridGap: `${property.space}px`, gridTemplateColumns, - width: scrollbarWidth, - }" + width: scrollbarWidth + }" > <!-- 商品 --> <div @@ -63,11 +63,11 @@ </el-scrollbar> </template> <script setup lang="ts"> -import { ProductListProperty } from "./config" -import * as ProductSpuApi from "@/api/mall/product/spu" +import { ProductListProperty } from './config' +import * as ProductSpuApi from '@/api/mall/product/spu' /** 商品卡片 */ -defineOptions({ name: "ProductList" }) +defineOptions({ name: 'ProductList' }) // 定义属性 const props = defineProps<{ property: ProductListProperty }>() // 商品列表 @@ -89,39 +89,42 @@ const containerRef = ref() // 商品的列数 const columns = ref(2) // 滚动条宽度 -const scrollbarWidth = ref("100%") +const scrollbarWidth = ref('100%') // 商品图大小 -const imageSize = ref("0") +const imageSize = ref('0') // 商品网络列数 -const gridTemplateColumns = ref("") +const gridTemplateColumns = ref('') // 计算布局参数 watch( () => [props.property, phoneWidth, spuList.value.length], () => { // 计算列数 - columns.value = props.property.layoutType === "twoCol" ? 2 : 3 - // 提取手机宽度 + columns.value = props.property.layoutType === 'twoCol' ? 2 : 3 // 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数 - const productWidth = (phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value + const productWidth = + (phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value // 商品图布局:2列时,左右布局 3列时,上下布局 - imageSize.value = columns.value === 2 ? "64px" : `${productWidth}px` + imageSize.value = columns.value === 2 ? '64px' : `${productWidth}px` // 根据布局类型,计算行数、列数 - if (props.property.layoutType === "horizSwiper") { + if (props.property.layoutType === 'horizSwiper') { // 单行显示 gridTemplateColumns.value = `repeat(auto-fill, ${productWidth}px)` // 显示滚动条 - scrollbarWidth.value = `${productWidth * spuList.value.length + props.property.space * (spuList.value.length - 1)}px` + scrollbarWidth.value = `${ + productWidth * spuList.value.length + props.property.space * (spuList.value.length - 1) + }px` } else { // 指定列数 gridTemplateColumns.value = `repeat(${columns.value}, auto)` // 不滚动 - scrollbarWidth.value = "100%" + scrollbarWidth.value = '100%' } }, { immediate: true, deep: true } ) onMounted(() => { - phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375; + // 提取手机宽度 + phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375 }) </script> diff --git a/src/components/DiyEditor/util.ts b/src/components/DiyEditor/util.ts index 5dfcea9e..a8d0e095 100644 --- a/src/components/DiyEditor/util.ts +++ b/src/components/DiyEditor/util.ts @@ -111,7 +111,11 @@ export const PAGE_LIBS = [ { name: '会员组件', extended: true, - components: ['UserCard', 'OrderCard', 'WalletCard', 'CouponCard'] + components: ['UserCard', 'UserOrder', 'UserWallet', 'UserCoupon'] }, - { name: '营销组件', extended: true, components: ['Combination', 'Seckill', 'Point', 'Coupon'] } + { + name: '营销组件', + extended: true, + components: ['CombinationCard', 'SeckillCard', 'PointCard', 'CouponCard'] + } ] as DiyComponentLibrary[] diff --git a/src/views/mall/product/comment/CommentForm.vue b/src/views/mall/product/comment/CommentForm.vue index e9913ade..df6756a0 100644 --- a/src/views/mall/product/comment/CommentForm.vue +++ b/src/views/mall/product/comment/CommentForm.vue @@ -48,7 +48,7 @@ </template> <script setup lang="ts"> import * as CommentApi from '@/api/mall/product/comment' -import SpuShowcase from "@/views/mall/product/spu/components/SpuShowcase.vue"; +import SpuShowcase from '@/views/mall/product/spu/components/SpuShowcase.vue' import * as ProductSpuApi from '@/api/mall/product/spu' import SkuTableSelect from '@/views/mall/product/spu/components/SkuTableSelect.vue' diff --git a/src/views/mall/product/spu/components/SpuShowcase.vue b/src/views/mall/product/spu/components/SpuShowcase.vue index 5d35238e..8bee4004 100644 --- a/src/views/mall/product/spu/components/SpuShowcase.vue +++ b/src/views/mall/product/spu/components/SpuShowcase.vue @@ -27,7 +27,7 @@ import * as ProductSpuApi from '@/api/mall/product/spu' import SpuTableSelect from '@/views/mall/product/spu/components/SpuTableSelect.vue' import { propTypes } from '@/utils/propTypes' import { oneOfType } from 'vue-types' -import { isArray } from "@/utils/is"; +import { isArray } from '@/utils/is' // 商品橱窗,一般用于与商品建立关系时使用 // 提供功能:展示商品列表、添加商品、移除商品 @@ -43,9 +43,9 @@ const props = defineProps({ // 计算是否可以添加 const canAdd = computed(() => { // 情况一:禁用时不可以添加 - if(props.disabled) return false + if (props.disabled) return false // 情况二:未指定限制数量时,可以添加 - if(!props.limit) return true + if (!props.limit) return true // 情况三:检查已添加数量是否小于限制数量 return productSpus.value.length < props.limit }) @@ -57,20 +57,19 @@ watch( () => props.modelValue, async () => { const ids = isArray(props.modelValue) - // 情况一:多选 - ? props.modelValue - // 情况二:单选 - : props.modelValue ? [props.modelValue]: [] + ? // 情况一:多选 + props.modelValue + : // 情况二:单选 + props.modelValue + ? [props.modelValue] + : [] // 不需要返显 - if(ids.length === 0) { + if (ids.length === 0) { productSpus.value = [] return } // 只有商品发生变化之后,才去查询商品 - if ( - productSpus.value.length === 0 || - productSpus.value.some((spu) => !ids.includes(spu.id!)) - ) { + if (productSpus.value.length === 0 || productSpus.value.some((spu) => !ids.includes(spu.id!))) { productSpus.value = await ProductSpuApi.getSpuDetailList(ids) } }, @@ -103,12 +102,15 @@ const handleRemoveSpu = (index: number) => { } const emit = defineEmits(['update:modelValue', 'change']) const emitSpuChange = () => { - if(props.limit === 1) { + if (props.limit === 1) { const spu = productSpus.value.length > 0 ? productSpus.value[0] : null emit('update:modelValue', spu?.id || 0) emit('change', spu) } else { - emit('update:modelValue', productSpus.value.map((spu) => spu.id)) + emit( + 'update:modelValue', + productSpus.value.map((spu) => spu.id) + ) emit('change', productSpus.value) } } diff --git a/src/views/mall/product/spu/components/SpuTableSelect.vue b/src/views/mall/product/spu/components/SpuTableSelect.vue index 8edf83d8..8028f749 100644 --- a/src/views/mall/product/spu/components/SpuTableSelect.vue +++ b/src/views/mall/product/spu/components/SpuTableSelect.vue @@ -167,7 +167,7 @@ const open = (spuList?: Spu[]) => { // 处理已选中 if (spuList && spuList.length > 0) { checkedSpus.value = [...spuList] - checkedStatus.value = Object.fromEntries(spuList.map(spu => [spu.id, true])) + checkedStatus.value = Object.fromEntries(spuList.map((spu) => [spu.id, true])) } dialogVisible.value = true @@ -184,7 +184,9 @@ const getList = async () => { list.value = data.list total.value = data.total // checkbox绑定undefined会有问题,需要给一个bool值 - list.value.forEach( spu => checkedStatus.value[spu.id] = checkedStatus.value[spu.id] || false) + list.value.forEach( + (spu) => (checkedStatus.value[spu.id] = checkedStatus.value[spu.id] || false) + ) // 计算全选框状态 calculateIsCheckAll() } finally { @@ -272,23 +274,19 @@ const handleCheckOne = (checked: boolean, spu: Spu, isCalcCheckAll: boolean) => } // 计算全选框状态 - if(isCalcCheckAll){ + if (isCalcCheckAll) { calculateIsCheckAll() } } // 查找商品在已选中商品列表中的索引 -const findCheckedIndex = (spu: Spu) => checkedSpus.value.findIndex(item => item.id === spu.id) +const findCheckedIndex = (spu: Spu) => checkedSpus.value.findIndex((item) => item.id === spu.id) // 计算全选框状态 const calculateIsCheckAll = () => { - isCheckAll.value = list.value.every(spu => { - let valueElement = checkedStatus.value[spu.id]; - debugger - return valueElement; - }); + isCheckAll.value = list.value.every((spu) => checkedStatus.value[spu.id]) // 计算中间状态:不是全部选中 && 任意一个选中 - isIndeterminate.value = !isCheckAll.value && list.value.some(spu => checkedStatus.value[spu.id]) + isIndeterminate.value = !isCheckAll.value && list.value.some((spu) => checkedStatus.value[spu.id]) } // 分类列表 diff --git a/src/views/mall/promotion/coupon/components/CouponSelect.vue b/src/views/mall/promotion/coupon/components/CouponSelect.vue index 67e057b1..715dcb7d 100644 --- a/src/views/mall/promotion/coupon/components/CouponSelect.vue +++ b/src/views/mall/promotion/coupon/components/CouponSelect.vue @@ -150,15 +150,14 @@ import { } from '@/views/mall/promotion/coupon/formatter' import { dateFormatter } from '@/utils/formatTime' import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate' -import type { GiveCouponTemplate } from '@/api/mall/product/spu' defineOptions({ name: 'CouponSelect' }) defineProps<{ - multipleSelection: GiveCouponTemplate[] + multipleSelection: CouponTemplateApi.CouponTemplateVO[] }>() const emit = defineEmits<{ - (e: 'update:multipleSelection', v: GiveCouponTemplate[]) + (e: 'update:multipleSelection', v: CouponTemplateApi.CouponTemplateVO[]) }>() const dialogVisible = ref(false) // 弹窗的是否展示 const dialogTitle = ref('选择优惠卷') // 弹窗的标题 @@ -210,10 +209,7 @@ const open = async () => { defineExpose({ open }) // 提供 open 方法,用于打开弹窗 const handleSelectionChange = (val: CouponTemplateApi.CouponTemplateVO[]) => { - emit( - 'update:multipleSelection', - val.map((item) => ({ id: item.id, name: item.name })) - ) + emit('update:multipleSelection', val) } const submitForm = () => { diff --git a/src/views/mall/promotion/coupon/template/CouponTemplateForm.vue b/src/views/mall/promotion/coupon/template/CouponTemplateForm.vue index fcb18337..408f381a 100644 --- a/src/views/mall/promotion/coupon/template/CouponTemplateForm.vue +++ b/src/views/mall/promotion/coupon/template/CouponTemplateForm.vue @@ -187,7 +187,7 @@ import { PromotionDiscountTypeEnum, PromotionProductScopeEnum } from '@/utils/constants' -import SpuShowcase from "@/views/mall/product/spu/components/SpuShowcase.vue"; +import SpuShowcase from '@/views/mall/product/spu/components/SpuShowcase.vue' import ProductCategorySelect from '@/views/mall/product/category/components/ProductCategorySelect.vue' import { convertToInteger, formatToFraction } from '@/utils' @@ -385,5 +385,4 @@ function setProductScopeValues(data: CouponTemplateApi.CouponTemplateVO) { } </script> -<style lang="scss" scoped> -</style> +<style lang="scss" scoped></style> From f8b56a8c46f6d00acb807c5a5d6a464abefeb3ad Mon Sep 17 00:00:00 2001 From: puhui999 <puhui999@163.com> Date: Thu, 23 Nov 2023 09:35:59 +0800 Subject: [PATCH 7/8] =?UTF-8?q?crm-=E5=AE=A2=E6=88=B7=EF=BC=9A=E5=85=AC?= =?UTF-8?q?=E6=B5=B7=E6=95=B0=E6=8D=AE=E5=8C=BA=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/crm/customer/index.vue | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/views/crm/customer/index.vue b/src/views/crm/customer/index.vue index 60bc8bea..210691b9 100644 --- a/src/views/crm/customer/index.vue +++ b/src/views/crm/customer/index.vue @@ -208,11 +208,12 @@ const list = ref([]) // 列表的数据 const queryParams = reactive({ pageNo: 1, pageSize: 10, - name: null, - mobile: null, - industryId: null, - level: null, - source: null + pool: false, + name: '', + mobile: '', + industryId: undefined, + level: undefined, + source: undefined }) const queryFormRef = ref() // 搜索的表单 const exportLoading = ref(false) // 导出的加载中 @@ -238,6 +239,7 @@ const handleQuery = () => { /** 重置按钮操作 */ const resetQuery = () => { queryFormRef.value.resetFields() + queryParams.pool = false handleQuery() } From c3cad3f3ee4f23e5caabea83d08d42eaf955cc1d Mon Sep 17 00:00:00 2001 From: owen <owen@evolsun.com> Date: Fri, 24 Nov 2023 20:54:47 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E5=95=86=E5=9F=8E=EF=BC=9A=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=BC=9A=E5=91=98=E7=BB=9F=E8=AE=A1=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E8=8C=83=E5=9B=B4=E9=80=89=E6=8B=A9=E6=98=A8=E5=A4=A9=E6=97=B6?= =?UTF-8?q?,=20=E4=BC=9A=E5=BC=BA=E5=88=B6=E6=9F=A5=E8=AF=A2=E4=B8=A4?= =?UTF-8?q?=E5=A4=A9=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ShortcutDateRangePicker/index.vue | 5 ----- src/utils/formatTime.ts | 5 ++++- src/views/mall/statistics/trade/index.vue | 9 +++++++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/components/ShortcutDateRangePicker/index.vue b/src/components/ShortcutDateRangePicker/index.vue index 9f268a3f..117c079a 100644 --- a/src/components/ShortcutDateRangePicker/index.vue +++ b/src/components/ShortcutDateRangePicker/index.vue @@ -74,11 +74,6 @@ const emits = defineEmits<{ }>() /** 触发时间范围选中事件 */ const emitDateRangePicker = async () => { - // 开始与截止在同一天的, 折线图出不来, 需要延长一天 - if (DateUtil.isSameDay(times.value[0], times.value[1])) { - // 前天 - times.value[0] = DateUtil.formatDate(dayjs(times.value[0]).subtract(1, 'd')) - } emits('change', times.value) } diff --git a/src/utils/formatTime.ts b/src/utils/formatTime.ts index e2ffcadd..466f1e1f 100644 --- a/src/utils/formatTime.ts +++ b/src/utils/formatTime.ts @@ -335,5 +335,8 @@ export function getDateRange( beginDate: dayjs.ConfigType, endDate: dayjs.ConfigType ): [string, string] { - return [dayjs(beginDate).startOf('d').toString(), dayjs(endDate).endOf('d').toString()] + return [ + dayjs(beginDate).startOf('d').format('YYYY-MM-DD HH:mm:ss'), + dayjs(endDate).endOf('d').format('YYYY-MM-DD HH:mm:ss') + ] } diff --git a/src/views/mall/statistics/trade/index.vue b/src/views/mall/statistics/trade/index.vue index e89f0cc3..86deaa87 100644 --- a/src/views/mall/statistics/trade/index.vue +++ b/src/views/mall/statistics/trade/index.vue @@ -219,6 +219,8 @@ import { TradeSummaryRespVO, TradeTrendSummaryRespVO } from '@/api/mall/statisti import { calculateRelativeRate, fenToYuan } from '@/utils' import download from '@/utils/download' import { CardTitle } from '@/components/Card' +import * as DateUtil from '@/utils/formatTime' +import dayjs from 'dayjs' /** 交易统计 */ defineOptions({ name: 'TradeStatistics' }) @@ -289,6 +291,13 @@ const lineChartOptions = reactive<EChartsOption>({ /** 处理交易状况查询 */ const getTradeTrendData = async () => { trendLoading.value = true + // 1. 处理时间: 开始与截止在同一天的, 折线图出不来, 需要延长一天 + const times = shortcutDateRangePicker.value.times + if (DateUtil.isSameDay(times[0], times[1])) { + // 前天 + times[0] = DateUtil.formatDate(dayjs(times[0]).subtract(1, 'd')) + } + // 查询数据 await Promise.all([getTradeTrendSummary(), getTradeStatisticsList()]) trendLoading.value = false }