feat: 新增多个组件并优化优惠券相关功能

- 新增 AppLinkSelectDialog 组件,用于选择 APP 链接- 新增 NavigationBarCellProperty组件,用于导航栏单元格属性设置
- 新增 CombinationShowcase 和 CombinationTableSelect 组件,用于拼团活动展示和选择- 优化优惠券相关组件,导出所有优惠券相关组件
- 新增 ComponentContainer 组件,用于包裹和样式化 DIY 组件
This commit is contained in:
lrl
2025-08-04 09:09:39 +08:00
parent 38daaa2934
commit 2166ce3e4e
123 changed files with 11829 additions and 21 deletions

View File

@@ -0,0 +1,69 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { handleTree } from '@vben/utils';
import * as ProductCategoryApi from '#/api/mall/product/category';
/** 商品分类选择组件 */
defineOptions({ name: 'ProductCategorySelect' });
const props = defineProps({
// 选中的ID
modelValue: {
type: [Number, Array<Number>],
default: undefined,
},
// 是否多选
multiple: {
type: Boolean,
default: false,
},
// 上级品类的编号
parentId: {
type: Number,
default: undefined,
},
});
/** 分类选择 */
const emit = defineEmits(['update:modelValue']);
/** 选中的分类 ID */
const selectCategoryId = computed({
get: () => {
return props.modelValue;
},
set: (val: number | number[]) => {
emit('update:modelValue', val);
},
});
/** 初始化 */
const categoryList = ref<any[]>([]); // 分类树
onMounted(async () => {
// 获得分类树
const data = await ProductCategoryApi.getCategoryList({
parentId: props.parentId,
});
categoryList.value = handleTree(data, 'id', 'parentId');
});
</script>
<template>
<el-tree-select
v-model="selectCategoryId"
:data="categoryList"
:props="{
children: 'children',
label: 'name',
value: 'id',
isLeaf: 'leaf',
emitPath: false,
}"
:multiple="multiple"
:show-checkbox="multiple"
class="w-1/1"
node-key="id"
placeholder="请选择商品分类"
/>
</template>

View File

@@ -0,0 +1,162 @@
<script lang="ts" setup>
import type { MallSpuApi } from '#/api/mall/product/spu';
import { computed, ref, watch } from 'vue';
import * as ProductSpuApi from '#/api/mall/product/spu';
import SpuTableSelect from '#/views/mall/product/spu/components/spu-table-select.vue';
// 商品橱窗,一般用于与商品建立关系时使用
// 提供功能:展示商品列表、添加商品、移除商品
defineOptions({ name: 'SpuShowcase' });
const props = defineProps({
modelValue: {
type: [Number, Array],
required: true,
},
// 限制数量:默认不限制
limit: {
type: Number,
default: Number.MAX_VALUE,
},
disabled: Boolean,
});
const emit = defineEmits(['update:modelValue', 'change']);
// 计算是否可以添加
const canAdd = computed(() => {
// 情况一:禁用时不可以添加
if (props.disabled) return false;
// 情况二:未指定限制数量时,可以添加
if (!props.limit) return true;
// 情况三:检查已添加数量是否小于限制数量
return productSpus.value.length < props.limit;
});
// 商品列表
const productSpus = ref<MallSpuApi.Spu[]>([]);
watch(
() => props.modelValue,
async () => {
let ids: number[] = [];
if (Array.isArray(props.modelValue)) {
ids = props.modelValue as number[];
} else {
ids = props.modelValue ? [props.modelValue] : [];
}
// 不需要返显
if (ids.length === 0) {
productSpus.value = [];
return;
}
// 只有商品发生变化之后,才去查询商品
if (
productSpus.value.length === 0 ||
productSpus.value.some((spu) => !ids.includes(spu.id!))
) {
productSpus.value = await ProductSpuApi.getSpuDetailList(ids);
}
},
{ immediate: true },
);
/** 商品表格选择对话框 */
const spuTableSelectRef = ref();
// 打开对话框
const openSpuTableSelect = () => {
spuTableSelectRef.value.open(productSpus.value);
};
/**
* 选择商品后触发
*
* @param spus 选中的商品列表
*/
const handleSpuSelected = (spus: MallSpuApi.Spu | MallSpuApi.Spu[]) => {
productSpus.value = Array.isArray(spus) ? spus : [spus];
emitSpuChange();
};
/**
* 删除商品
*
* @param index 商品索引
*/
const handleRemoveSpu = (index: number) => {
productSpus.value.splice(index, 1);
emitSpuChange();
};
const emitSpuChange = () => {
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>
<template>
<div class="gap-8px flex flex-wrap items-center">
<div
v-for="(spu, index) in productSpus"
:key="spu.id"
class="select-box spu-pic"
>
<el-tooltip :content="spu.name">
<div class="relative h-full w-full">
<el-image :src="spu.picUrl" class="h-full w-full" />
<Icon
v-show="!disabled"
class="del-icon"
icon="ep:circle-close-filled"
@click="handleRemoveSpu(index)"
/>
</div>
</el-tooltip>
</div>
<el-tooltip content="选择商品" v-if="canAdd">
<div class="select-box" @click="openSpuTableSelect">
<Icon icon="ep:plus" />
</div>
</el-tooltip>
</div>
<!-- 商品选择对话框表格形式 -->
<SpuTableSelect
ref="spuTableSelectRef"
:multiple="limit !== 1"
@change="handleSpuSelected"
/>
</template>
<style lang="scss" scoped>
.select-box {
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
border: 1px dashed var(--el-border-color-darker);
border-radius: 8px;
}
.spu-pic {
position: relative;
}
.del-icon {
position: absolute;
top: -10px;
right: -10px;
z-index: 1;
width: 20px !important;
height: 20px !important;
}
</style>

View File

@@ -0,0 +1,345 @@
<script lang="ts" setup>
import type { MallCategoryApi } from '#/api/mall/product/category';
import type { MallSpuApi } from '#/api/mall/product/spu';
import { onMounted, ref } from 'vue';
import { handleTree } from '@vben/utils';
import { CHANGE_EVENT } from 'element-plus';
import * as ProductCategoryApi from '#/api/mall/product/category';
import * as ProductSpuApi from '#/api/mall/product/spu';
/**
* 商品表格选择对话框
* 1. 单选模式:
* 1.1 点击表格左侧的单选框时,结束选择,并关闭对话框
* 1.2 再次打开时,保持选中状态
* 2. 多选模式:
* 2.1 点击表格左侧的多选框时,记录选中的商品
* 2.2 切换分页时,保持商品的选中的状态
* 2.3 点击右下角的确定按钮时,结束选择,关闭对话框
* 2.4 再次打开时,保持选中状态
*/
defineOptions({ name: 'SpuTableSelect' });
defineProps({
// 多选模式
multiple: {
type: Boolean,
default: false,
},
});
/** 确认选择时的触发事件 */
const emits = defineEmits<{
change: [spu: any | MallSpuApi.Spu | MallSpuApi.Spu[]];
}>();
// 列表的总页数
const total = ref(0);
// 列表的数据
const list = ref<MallSpuApi.Spu[]>([]);
// 列表的加载中
const loading = ref(false);
// 弹窗的是否展示
const dialogVisible = ref(false);
// 查询参数
const queryParams = ref({
pageNo: 1,
pageSize: 10,
// 默认获取上架的商品
tabType: 0,
name: '',
categoryId: null,
createTime: [],
});
/** 打开弹窗 */
const open = (spuList?: MallSpuApi.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]),
);
}
dialogVisible.value = true;
resetQuery();
};
// 提供 open 方法,用于打开弹窗
defineExpose({ open });
/** 查询列表 */
const getList = async () => {
loading.value = true;
try {
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 || 0] =
checkedStatus.value[spu.id || 0] || false),
);
// 计算全选框状态
calculateIsCheckAll();
} finally {
loading.value = false;
}
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNo = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryParams.value = {
pageNo: 1,
pageSize: 10,
// 默认获取上架的商品
tabType: 0,
name: '',
categoryId: null,
createTime: [],
};
getList();
};
// 是否全选
const isCheckAll = ref(false);
// 全选框是否处于中间状态:不是全部选中 && 任意一个选中
const isIndeterminate = ref(false);
// 选中的商品
const checkedSpus = ref<MallSpuApi.Spu[]>([]);
// 选中状态key为商品IDvalue为是否选中
const checkedStatus = ref<Record<string, boolean>>({});
// 选中的商品 spuId
const selectedSpuId = ref();
/** 单选中时触发 */
const handleSingleSelected = (spu: MallSpuApi.Spu) => {
emits(CHANGE_EVENT, spu);
// 关闭弹窗
dialogVisible.value = false;
// 记住上次选择的ID
selectedSpuId.value = spu.id;
};
/** 多选完成 */
const handleEmitChange = () => {
// 关闭弹窗
dialogVisible.value = false;
emits(CHANGE_EVENT, [...checkedSpus.value]);
};
/** 全选/全不选 */
const handleCheckAll = (checked: boolean) => {
isCheckAll.value = checked;
isIndeterminate.value = false;
list.value.forEach((spu) => handleCheckOne(checked, spu, false));
};
/**
* 选中一行
* @param checked 是否选中
* @param spu 商品
* @param isCalcCheckAll 是否计算全选
*/
const handleCheckOne = (
checked: boolean,
spu: MallSpuApi.Spu,
isCalcCheckAll: boolean,
) => {
if (checked) {
checkedSpus.value.push(spu);
checkedStatus.value[spu.id || 0] = true;
} else {
const index = findCheckedIndex(spu);
if (index > -1) {
checkedSpus.value.splice(index, 1);
checkedStatus.value[spu.id || 0] = false;
isCheckAll.value = false;
}
}
// 计算全选框状态
if (isCalcCheckAll) {
calculateIsCheckAll();
}
};
// 查找商品在已选中商品列表中的索引
const findCheckedIndex = (spu: MallSpuApi.Spu) =>
checkedSpus.value.findIndex((item) => item.id === spu.id);
// 计算全选框状态
const calculateIsCheckAll = () => {
isCheckAll.value = list.value.every(
(spu) => checkedStatus.value[spu.id || 0],
);
// 计算中间状态:不是全部选中 && 任意一个选中
isIndeterminate.value =
!isCheckAll.value &&
list.value.some((spu) => checkedStatus.value[spu.id || 0]);
};
// 分类列表
const categoryList = ref<MallCategoryApi.Category[]>();
// 分类树
const categoryTreeList = ref();
/** 初始化 */
onMounted(async () => {
await getList();
// 获得分类树
categoryList.value = await ProductCategoryApi.getCategoryList({});
categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId');
});
</script>
<template>
<Dialog
v-model="dialogVisible"
:append-to-body="true"
title="选择商品"
width="70%"
>
<ContentWrap>
<el-form
: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"
clearable
placeholder="请输入商品名称"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="商品分类" prop="categoryId">
<el-tree-select
v-model="queryParams.categoryId"
:data="categoryTreeList"
:props="{
children: 'children',
label: 'name',
value: 'id',
isLeaf: 'leaf',
emitPath: false,
}"
check-strictly
class="!w-240px"
node-key="id"
placeholder="请选择商品分类"
/>
</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')]"
class="!w-240px"
end-placeholder="结束日期"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="list" show-overflow-tooltip>
<!-- 1. 多选模式不能使用type="selection"Element会忽略Header插槽 -->
<el-table-column width="55" v-if="multiple">
<template #header>
<el-checkbox
v-model="isCheckAll"
:indeterminate="isIndeterminate"
@change="handleCheckAll"
/>
</template>
<template #default="{ row }">
<el-checkbox
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
:value="row.id"
v-model="selectedSpuId"
@change="handleSingleSelected(row)"
>
<!-- 空格不能省略是为了让单选框不显示label如果不指定label不会有选中的效果 -->
&nbsp;
</el-radio>
</template>
</el-table-column>
<el-table-column
key="id"
align="center"
label="商品编号"
prop="id"
min-width="60"
/>
<el-table-column label="商品图" min-width="80">
<template #default="{ row }">
<el-image
:src="row.picUrl"
class="h-30px w-30px"
:preview-src-list="[row.picUrl]"
preview-teleported
/>
</template>
</el-table-column>
<el-table-column label="商品名称" min-width="200" prop="name" />
<el-table-column label="商品分类" min-width="100" prop="categoryId">
<template #default="{ row }">
<span>{{
categoryList?.find(
(c: MallCategoryApi.Category) => c.id === row.categoryId,
)?.name
}}</span>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<template #footer v-if="multiple">
<el-button type="primary" @click="handleEmitChange"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,175 @@
<script lang="ts" setup>
import type { MallCombinationActivityApi } from '#/api/mall/promotion/combination/combinationActivity';
import { computed, ref, watch } from 'vue';
import * as CombinationActivityApi from '#/api/mall/promotion/combination/combinationActivity';
import CombinationTableSelect from '#/views/mall/promotion/combination/components/combination-table-select.vue';
// 活动橱窗,一般用于装修时使用
// 提供功能:展示活动列表、添加活动、删除活动
defineOptions({ name: 'CombinationShowcase' });
const props = defineProps({
modelValue: {
type: [Array, Number],
default: () => [],
},
// 限制数量:默认不限制
limit: {
type: Number,
default: Number.MAX_VALUE,
},
disabled: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['update:modelValue', 'change']);
// 计算是否可以添加
const canAdd = computed(() => {
// 情况一:禁用时不可以添加
if (props.disabled) return false;
// 情况二:未指定限制数量时,可以添加
if (!props.limit) return true;
// 情况三:检查已添加数量是否小于限制数量
return Activitys.value.length < props.limit;
});
// 拼团活动列表
const Activitys = ref<MallCombinationActivityApi.CombinationActivity[]>([]);
watch(
() => props.modelValue,
async () => {
let ids;
if (Array.isArray(props.modelValue)) {
ids = props.modelValue;
} else {
ids = props.modelValue ? [props.modelValue] : [];
}
// 不需要返显
if (ids.length === 0) {
Activitys.value = [];
return;
}
// 只有活动发生变化之后,才会查询活动
if (
Activitys.value.length === 0 ||
Activitys.value.some(
(combinationActivity) => !ids.includes(combinationActivity.id!),
)
) {
Activitys.value =
await CombinationActivityApi.getCombinationActivityListByIds(
ids as number[],
);
}
},
{ immediate: true },
);
/** 活动表格选择对话框 */
const combinationActivityTableSelectRef = ref();
// 打开对话框
const openCombinationActivityTableSelect = () => {
combinationActivityTableSelectRef.value.open(Activitys.value);
};
/**
* 选择活动后触发
* @param activityVOs 选中的活动列表
*/
const handleActivitySelected = (
activityVOs:
| MallCombinationActivityApi.CombinationActivity
| MallCombinationActivityApi.CombinationActivity[],
) => {
Activitys.value = Array.isArray(activityVOs) ? activityVOs : [activityVOs];
emitActivityChange();
};
/**
* 删除活动
* @param index 活动索引
*/
const handleRemoveActivity = (index: number) => {
Activitys.value.splice(index, 1);
emitActivityChange();
};
const emitActivityChange = () => {
if (props.limit === 1) {
const combinationActivity =
Activitys.value.length > 0 ? Activitys.value[0] : null;
emit('update:modelValue', combinationActivity?.id || 0);
emit('change', combinationActivity);
} else {
emit(
'update:modelValue',
Activitys.value.map((combinationActivity) => combinationActivity.id),
);
emit('change', Activitys.value);
}
};
</script>
<template>
<div class="gap-8px flex flex-wrap items-center">
<div
v-for="(combinationActivity, index) in Activitys"
:key="combinationActivity.id"
class="select-box spu-pic"
>
<el-tooltip :content="combinationActivity.name">
<div class="relative h-full w-full">
<el-image :src="combinationActivity.picUrl" class="h-full w-full" />
<Icon
v-show="!disabled"
class="del-icon"
icon="ep:circle-close-filled"
@click="handleRemoveActivity(index)"
/>
</div>
</el-tooltip>
</div>
<el-tooltip content="选择活动" v-if="canAdd">
<div class="select-box" @click="openCombinationActivityTableSelect">
<Icon icon="ep:plus" />
</div>
</el-tooltip>
</div>
<!-- 拼团活动选择对话框表格形式 -->
<CombinationTableSelect
ref="combinationActivityTableSelectRef"
:multiple="limit !== 1"
@change="handleActivitySelected"
/>
</template>
<style lang="scss" scoped>
.select-box {
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
cursor: pointer;
border: 1px dashed var(--el-border-color-darker);
border-radius: 8px;
}
.spu-pic {
position: relative;
}
.del-icon {
position: absolute;
top: -10px;
right: -10px;
z-index: 1;
width: 20px !important;
height: 20px !important;
}
</style>

View File

@@ -0,0 +1,392 @@
<script lang="ts" setup>
import type { MallCombinationActivityApi } from '#/api/mall/promotion/combination/combinationActivity';
import { onMounted, ref } from 'vue';
import {
dateFormatter,
fenToYuan,
fenToYuanFormat,
formatDate,
handleTree,
} from '@vben/utils';
import { CHANGE_EVENT } from 'element-plus';
import * as ProductCategoryApi from '#/api/mall/product/category';
import * as CombinationActivityApi from '#/api/mall/promotion/combination/combinationActivity';
import { DICT_TYPE, getIntDictOptions } from '#/utils/dict';
/**
* 活动表格选择对话框
* 1. 单选模式:
* 1.1 点击表格左侧的单选框时,结束选择,并关闭对话框
* 1.2 再次打开时,保持选中状态
* 2. 多选模式:
* 2.1 点击表格左侧的多选框时,记录选中的活动
* 2.2 切换分页时,保持活动的选中状态
* 2.3 点击右下角的确定按钮时,结束选择,关闭对话框
* 2.4 再次打开时,保持选中状态
*/
defineOptions({ name: 'CombinationTableSelect' });
defineProps({
// 多选模式
multiple: {
type: Boolean,
default: false,
},
});
/** 确认选择时的触发事件 */
const emits = defineEmits<{
change: [
CombinationActivityApi:
| any
| MallCombinationActivityApi.CombinationActivity
| MallCombinationActivityApi.CombinationActivity[],
];
}>();
// 列表的总页数
const total = ref(0);
// 列表的数据
const list = ref<MallCombinationActivityApi.CombinationActivity[]>([]);
// 列表的加载中
const loading = ref(false);
// 弹窗的是否展示
const dialogVisible = ref(false);
// 查询参数
const queryParams = ref({
pageNo: 1,
pageSize: 10,
name: undefined,
status: undefined,
});
/** 打开弹窗 */
const open = (
CombinationList?: MallCombinationActivityApi.CombinationActivity[],
) => {
// 重置
checkedActivitys.value = [];
checkedStatus.value = {};
isCheckAll.value = false;
isIndeterminate.value = false;
// 处理已选中
if (CombinationList && CombinationList.length > 0) {
checkedActivitys.value = [...CombinationList];
checkedStatus.value = Object.fromEntries(
CombinationList.map((activityVO) => [activityVO.id, true]),
);
}
dialogVisible.value = true;
resetQuery();
};
// 提供 open 方法,用于打开弹窗
defineExpose({ open });
/** 查询列表 */
const getList = async () => {
loading.value = true;
try {
const data = await CombinationActivityApi.getCombinationActivityPage(
queryParams.value,
);
list.value = data.list;
total.value = data.total;
// checkbox绑定undefined会有问题需要给一个bool值
list.value.forEach(
(activityVO) =>
(checkedStatus.value[activityVO.id || ''] =
checkedStatus.value[activityVO.id || ''] || false),
);
// 计算全选框状态
calculateIsCheckAll();
} finally {
loading.value = false;
}
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNo = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryParams.value = {
pageNo: 1,
pageSize: 10,
name: undefined,
status: undefined,
};
getList();
};
/**
* 格式化拼团价格
* @param products
*/
const formatCombinationPrice = (
products: MallCombinationActivityApi.CombinationActivity[],
) => {
const combinationPrice = Math.min(
...products.map((item) => item.combinationPrice || 0),
);
return `${fenToYuan(combinationPrice)}`;
};
// 是否全选
const isCheckAll = ref(false);
// 全选框是否处于中间状态:不是全部选中 && 任意一个选中
const isIndeterminate = ref(false);
// 选中的活动
const checkedActivitys = ref<MallCombinationActivityApi.CombinationActivity[]>(
[],
);
// 选中状态key为活动IDvalue为是否选中
const checkedStatus = ref<Record<string, boolean>>({});
// 选中的活动 activityId
const selectedActivityId = ref();
/** 单选中时触发 */
const handleSingleSelected = (
combinationActivityVO: MallCombinationActivityApi.CombinationActivity,
) => {
emits(CHANGE_EVENT, combinationActivityVO);
// 关闭弹窗
dialogVisible.value = false;
// 记住上次选择的ID
selectedActivityId.value = combinationActivityVO.id;
};
/** 多选完成 */
const handleEmitChange = () => {
// 关闭弹窗
dialogVisible.value = false;
emits(CHANGE_EVENT, [...checkedActivitys.value]);
};
/** 全选/全不选 */
const handleCheckAll = (checked: boolean) => {
isCheckAll.value = checked;
isIndeterminate.value = false;
list.value.forEach((combinationActivity) =>
handleCheckOne(checked, combinationActivity, false),
);
};
/**
* 选中一行
* @param checked 是否选中
* @param combinationActivity 活动
* @param isCalcCheckAll 是否计算全选
*/
const handleCheckOne = (
checked: boolean,
combinationActivity: MallCombinationActivityApi.CombinationActivity,
isCalcCheckAll: boolean,
) => {
if (checked) {
checkedActivitys.value.push(combinationActivity);
checkedStatus.value[combinationActivity.id || ''] = true;
} else {
const index = findCheckedIndex(combinationActivity);
if (index > -1) {
checkedActivitys.value.splice(index, 1);
checkedStatus.value[combinationActivity.id || ''] = false;
isCheckAll.value = false;
}
}
// 计算全选框状态
if (isCalcCheckAll) {
calculateIsCheckAll();
}
};
// 查找活动在已选中活动列表中的索引
const findCheckedIndex = (
activityVO: MallCombinationActivityApi.CombinationActivity,
) => checkedActivitys.value.findIndex((item) => item.id === activityVO.id);
// 计算全选框状态
const calculateIsCheckAll = () => {
isCheckAll.value = list.value.every(
(activityVO) => checkedStatus.value[activityVO.id || ''],
);
// 计算中间状态:不是全部选中 && 任意一个选中
isIndeterminate.value =
!isCheckAll.value &&
list.value.some((activityVO) => checkedStatus.value[activityVO.id || '']);
};
// 分类列表
const categoryList = ref();
// 分类树
const categoryTreeList = ref();
/** 初始化 */
onMounted(async () => {
await getList();
// 获得分类树
categoryList.value = await ProductCategoryApi.getCategoryList({});
categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId');
});
</script>
<template>
<Dialog
v-model="dialogVisible"
:append-to-body="true"
title="选择活动"
width="70%"
>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
:inline="true"
:model="queryParams"
class="-mb-15px"
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>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="list" show-overflow-tooltip>
<!-- 1. 多选模式不能使用type="selection"Element会忽略Header插槽 -->
<el-table-column width="55" v-if="multiple">
<template #header>
<el-checkbox
v-model="isCheckAll"
:indeterminate="isIndeterminate"
@change="handleCheckAll"
/>
</template>
<template #default="{ row }">
<el-checkbox
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
:value="row.id"
v-model="selectedActivityId"
@change="handleSingleSelected(row)"
>
<!-- 空格不能省略是为了让单选框不显示label如果不指定label不会有选中的效果 -->
&nbsp;
</el-radio>
</template>
</el-table-column>
<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="原价"
prop="marketPrice"
min-width="100"
:formatter="fenToYuanFormat"
/>
<el-table-column label="拼团价" prop="seckillPrice" min-width="100">
<template #default="scope">
{{ formatCombinationPrice(scope.row.products) }}
</template>
</el-table-column>
<el-table-column label="开团组数" prop="groupCount" min-width="100" />
<el-table-column
label="成团组数"
prop="groupSuccessCount"
min-width="100"
/>
<el-table-column label="购买次数" prop="recordCount" min-width="100" />
<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>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<template #footer v-if="multiple">
<el-button type="primary" @click="handleEmitChange"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,219 @@
<script lang="ts" setup>
import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTemplate';
import { reactive, ref } from 'vue';
import * as CouponTemplateApi from '#/api/mall/promotion/coupon/couponTemplate';
import { CouponTemplateTakeTypeEnum } from '#/utils/constants';
import { DICT_TYPE, getIntDictOptions } from '#/utils/dict';
import {
discountFormat,
remainedCountFormat,
takeLimitCountFormat,
validityTypeFormat,
} from '#/views/mall/promotion/coupon/formatter';
defineOptions({ name: 'CouponSelect' });
const props = defineProps<{
multipleSelection?: MallCouponTemplateApi.CouponTemplate[];
takeType: number; // 领取方式
}>();
const emit = defineEmits<{
(
e: 'update:multipleSelection',
v: MallCouponTemplateApi.CouponTemplate[],
): void;
(e: 'change', v: MallCouponTemplateApi.CouponTemplate[]): void;
}>();
const dialogVisible = ref(false); // 弹窗的是否展示
const dialogTitle = ref('选择优惠劵'); // 弹窗的标题
const formLoading = ref(false); // 表单的加载中1修改时的数据加载2提交的按钮禁用
const loading = ref(true); // 列表的加载中
const total = ref(0); // 列表的总页数
const list = ref<MallCouponTemplateApi.CouponTemplate[]>([]); // 字典表格数据
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: null,
discountType: null,
canTakeTypes: [CouponTemplateTakeTypeEnum.USER.type], // 只获得直接领取的券
});
const queryFormRef = ref(); // 搜索的表单
const selectedCouponList = ref<MallCouponTemplateApi.CouponTemplate[]>([]); // 选择的数据
/** 查询列表 */
const getList = async () => {
loading.value = true;
try {
// 执行查询
queryParams.canTakeTypes = [props.takeType] as any;
const data = await CouponTemplateApi.getCouponTemplatePage(queryParams);
list.value = data.list;
total.value = data.total;
} finally {
loading.value = false;
}
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef?.value?.resetFields();
handleQuery();
};
/** 打开弹窗 */
const open = async () => {
dialogVisible.value = true;
resetQuery();
};
defineExpose({ open }); // 提供 open 方法,用于打开弹窗
const handleSelectionChange = (val: MallCouponTemplateApi.CouponTemplate[]) => {
if (props.multipleSelection) {
emit('update:multipleSelection', val);
return;
}
selectedCouponList.value = val;
};
const submitForm = () => {
dialogVisible.value = false;
emit('change', selectedCouponList.value);
};
</script>
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="65%">
<!-- 搜索工作栏 -->
<ContentWrap>
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="82px"
>
<el-form-item label="优惠券名称" prop="name">
<el-input
v-model="queryParams.name"
class="!w-240px"
clearable
placeholder="请输入优惠劵名"
@keyup="handleQuery"
/>
</el-form-item>
<el-form-item label="优惠类型" prop="discountType">
<el-select
v-model="queryParams.discountType"
class="!w-240px"
clearable
placeholder="请选择优惠券类型"
>
<el-option
v-for="dict in getIntDictOptions(
DICT_TYPE.PROMOTION_DISCOUNT_TYPE,
)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="优惠券名称" min-width="140" prop="name" />
<el-table-column label="类型" min-width="80" prop="productScope">
<template #default="scope">
<dict-tag
:type="DICT_TYPE.PROMOTION_PRODUCT_SCOPE"
:value="scope.row.productScope"
/>
</template>
</el-table-column>
<el-table-column label="优惠" min-width="100" prop="discount">
<template #default="scope">
<dict-tag
:type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE"
:value="scope.row.discountType"
/>
{{ discountFormat(scope.row) }}
</template>
</el-table-column>
<el-table-column label="领取方式" min-width="100" prop="takeType">
<template #default="scope">
<dict-tag
:type="DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE"
:value="scope.row.takeType"
/>
</template>
</el-table-column>
<el-table-column
:formatter="validityTypeFormat"
align="center"
label="使用时间"
prop="validityType"
width="185"
/>
<el-table-column align="center" label="发放数量" prop="totalCount" />
<el-table-column
:formatter="remainedCountFormat"
align="center"
label="剩余数量"
prop="totalCount"
/>
<el-table-column
:formatter="takeLimitCountFormat"
align="center"
label="领取上限"
prop="takeLimitCount"
/>
<el-table-column align="center" label="状态" prop="status">
<template #default="scope">
<dict-tag
:type="DICT_TYPE.COMMON_STATUS"
:value="scope.row.status"
/>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm">
确 定
</el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,75 @@
<script setup lang="ts">
import type { MallDiyPageApi } from '#/api/mall/promotion/diy/page';
import { onMounted, ref, unref } from 'vue';
import { useRoute } from 'vue-router';
import { ElMessage } from 'element-plus';
import * as DiyPageApi from '#/api/mall/promotion/diy/page';
import { PAGE_LIBS } from '#/components/DiyEditor/util';
/** 装修页面表单 */
defineOptions({ name: 'DiyPageDecorate' });
const formLoading = ref(false); // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formData = ref<MallDiyPageApi.DiyPage>();
const formRef = ref(); // 表单 Ref
// 获取详情
const getPageDetail = async (id: any) => {
formLoading.value = true;
try {
formData.value = await DiyPageApi.getDiyPageProperty(id);
} finally {
formLoading.value = false;
}
};
// 提交表单
const submitForm = async () => {
// 校验表单
if (!formRef.value) return;
// 提交请求
formLoading.value = true;
try {
await DiyPageApi.updateDiyPageProperty(unref(formData)!);
ElMessage.success('保存成功');
} finally {
formLoading.value = false;
}
};
// 重置表单
const resetForm = () => {
formData.value = {
id: undefined,
templateId: undefined,
name: '',
remark: '',
previewPicUrls: [],
property: '',
} as MallDiyPageApi.DiyPage;
formRef.value?.resetFields();
};
/** 初始化 */
const route = useRoute();
onMounted(() => {
resetForm();
if (!route.params.id) {
ElMessage.warning('参数错误,页面编号不能为空!');
return;
}
getPageDetail(route.params.id);
});
</script>
<template>
<DiyEditor
v-if="formData && !formLoading"
v-model="formData.property"
:title="formData.name"
:libs="PAGE_LIBS"
@save="submitForm"
/>
</template>

View File

@@ -0,0 +1,217 @@
<script lang="ts" setup>
import * as DiyPageApi from '@/api/mall/promotion/diy/page';
// TODO @疯狂:要不要建个 decorate 目录,然后挪进去,改成 index.vue这样可以更明确看到是个独立界面哈更好找
import * as DiyTemplateApi from '@/api/mall/promotion/diy/template';
import { DiyComponentLibrary, PAGE_LIBS } from '@/components/DiyEditor/util'; // 商城的 DIY 组件,在 DiyEditor 目录下
import { useTagsViewStore } from '@/store/modules/tagsView';
import { isEmpty } from '@/utils/is';
import { toNumber } from 'lodash-es';
/** 装修模板表单 */
defineOptions({ name: 'DiyTemplateDecorate' });
// 左上角工具栏操作按钮
const selectedTemplateItem = ref(0);
const templateItems = reactive([
{ name: '基础设置', icon: 'ep:iphone' },
{ name: '首页', icon: 'ep:home-filled' },
{ name: '我的', icon: 'ep:user-filled' },
]);
const message = useMessage(); // 消息弹窗
const formLoading = ref(false); // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formData = ref<DiyTemplateApi.DiyTemplatePropertyVO>();
const formRef = ref(); // 表单 Ref
// 当前编辑的属性
const currentFormData = ref<
DiyPageApi.DiyPageVO | DiyTemplateApi.DiyTemplatePropertyVO
>({
property: '',
} as DiyPageApi.DiyPageVO);
// templateItem 对应的缓存
const currentFormDataMap = ref<
Map<string, DiyPageApi.DiyPageVO | DiyTemplateApi.DiyTemplatePropertyVO>
>(new Map());
// 商城 H5 预览地址
const previewUrl = ref('');
// 获取详情
const getPageDetail = async (id: any) => {
formLoading.value = true;
try {
formData.value = await DiyTemplateApi.getDiyTemplateProperty(id);
// 拼接手机预览链接
const domain = import.meta.env.VITE_MALL_H5_DOMAIN;
previewUrl.value = `${domain}/#/pages/index/index?templateId=${formData.value.id}`;
} finally {
formLoading.value = false;
}
};
// 模板组件库
const templateLibs = [] as DiyComponentLibrary[];
// 当前组件库
const libs = ref<DiyComponentLibrary[]>(templateLibs);
// 模板选项切换
const handleTemplateItemChange = (val: number) => {
// 缓存模版编辑数据
currentFormDataMap.value.set(
templateItems[selectedTemplateItem.value].name,
currentFormData.value!,
);
// 读取模版缓存
const data = currentFormDataMap.value.get(templateItems[val].name);
// 切换模版
selectedTemplateItem.value = val;
// 编辑模板
if (val === 0) {
libs.value = templateLibs;
currentFormData.value = (isEmpty(data) ? formData.value : data) as
| DiyPageApi.DiyPageVO
| DiyTemplateApi.DiyTemplatePropertyVO;
return;
}
// 编辑页面
libs.value = PAGE_LIBS;
currentFormData.value = (
isEmpty(data)
? formData.value!.pages.find(
(page: DiyPageApi.DiyPageVO) => page.name === templateItems[val].name,
)
: data
) as DiyPageApi.DiyPageVO | DiyTemplateApi.DiyTemplatePropertyVO;
};
// 提交表单
const submitForm = async () => {
// 校验表单
if (!formRef) return;
// 提交请求
formLoading.value = true;
try {
// 对所有的 templateItems 都进行保存,有缓存则保存缓存,解决都有修改时只保存了当前所编辑的 templateItem导致装修效果存在差异
for (const [i, templateItem] of templateItems.entries()) {
const data = currentFormDataMap.value.get(templateItem.name) as any;
// 情况一:基础设置
if (i === 0) {
// 提交模板属性
await DiyTemplateApi.updateDiyTemplateProperty(
isEmpty(data) ? unref(formData)! : data,
);
continue;
}
// 提交页面属性
// 情况二:提交当前正在编辑的页面
if (currentFormData.value?.name.includes(templateItem.name)) {
await DiyPageApi.updateDiyPageProperty(unref(currentFormData)!);
continue;
}
// 情况三:提交页面编辑缓存
if (!isEmpty(data)) {
await DiyPageApi.updateDiyPageProperty(data!);
}
}
message.success('保存成功');
} finally {
formLoading.value = false;
}
};
// 重置表单
const resetForm = () => {
formData.value = {
id: undefined,
name: '',
used: false,
usedTime: undefined,
remark: '',
previewPicUrls: [],
property: '',
pages: [],
} as DiyTemplateApi.DiyTemplatePropertyVO;
formRef.value?.resetFields();
};
// 重置时记录当前编辑的页面
const handleEditorReset = () => storePageIndex();
// #region 无感刷新
// 记录标识
const DIY_PAGE_INDEX_KEY = 'diy_page_index';
// 1. 记录
const storePageIndex = () =>
sessionStorage.setItem(DIY_PAGE_INDEX_KEY, `${selectedTemplateItem.value}`);
// 2. 恢复
const recoverPageIndex = () => {
// 恢复重置前的页面,默认是第一个页面
const pageIndex = toNumber(sessionStorage.getItem(DIY_PAGE_INDEX_KEY)) || 0;
// 移除标记
sessionStorage.removeItem(DIY_PAGE_INDEX_KEY);
// 重新初始化数据
currentFormData.value = formData.value as
| DiyPageApi.DiyPageVO
| DiyTemplateApi.DiyTemplatePropertyVO;
currentFormDataMap.value = new Map<
string,
DiyPageApi.DiyPageVO | DiyTemplateApi.DiyTemplatePropertyVO
>();
// 切换页面
if (pageIndex !== selectedTemplateItem.value) {
handleTemplateItemChange(pageIndex);
}
};
// #endregion
/** 初始化 */
const { currentRoute } = useRouter(); // 路由
const { delView } = useTagsViewStore(); // 视图操作
onMounted(async () => {
resetForm();
if (!currentRoute.value.params.id) {
message.warning('参数错误,页面编号不能为空!');
delView(unref(currentRoute));
return;
}
// 查询详情
await getPageDetail(currentRoute.value.params.id);
// 恢复重置前的页面
recoverPageIndex();
});
</script>
<template>
<DiyEditor
v-if="formData && !formLoading"
v-model="currentFormData!.property"
:libs="libs"
:preview-url="previewUrl"
:show-navigation-bar="selectedTemplateItem !== 0"
:show-page-config="selectedTemplateItem !== 0"
:show-tab-bar="selectedTemplateItem === 0"
:title="templateItems[selectedTemplateItem].name"
@reset="handleEditorReset"
@save="submitForm"
>
<template #toolBarLeft>
<el-radio-group
:model-value="selectedTemplateItem"
class="h-full!"
@change="handleTemplateItemChange"
>
<el-tooltip
v-for="(item, index) in templateItems"
:key="index"
:content="item.name"
>
<el-radio-button :value="index">
<Icon :icon="item.icon" :size="24" />
</el-radio-button>
</el-tooltip>
</el-radio-group>
</template>
</DiyEditor>
</template>

View File

@@ -0,0 +1,176 @@
<script lang="ts" setup>
import type { MallPointActivityApi } from '#/api/mall/promotion/point';
import { computed, ref, watch } from 'vue';
import * as PointActivityApi from '#/api/mall/promotion/point';
import PointTableSelect from './point-table-select.vue';
// 活动橱窗,一般用于装修时使用
// 提供功能:展示活动列表、添加活动、删除活动
defineOptions({ name: 'PointShowcase' });
const props = defineProps({
modelValue: {
type: [Number, Array],
required: true,
},
// 限制数量:默认不限制
limit: {
type: Number,
default: Number.MAX_VALUE,
},
disabled: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['update:modelValue', 'change']);
// 计算是否可以添加
const canAdd = computed(() => {
// 情况一:禁用时不可以添加
if (props.disabled) return false;
// 情况二:未指定限制数量时,可以添加
if (!props.limit) return true;
// 情况三:检查已添加数量是否小于限制数量
return pointActivityList.value.length < props.limit;
});
// 拼团活动列表
const pointActivityList = ref<MallPointActivityApi.PointActivity[]>([]);
watch(
() => props.modelValue,
async () => {
let ids;
if (Array.isArray(props.modelValue)) {
ids = props.modelValue;
} else {
ids = props.modelValue ? [props.modelValue] : [];
}
// 不需要返显
if (ids.length === 0) {
pointActivityList.value = [];
return;
}
// 只有活动发生变化之后,才会查询活动
if (
pointActivityList.value.length === 0 ||
pointActivityList.value.some(
(pointActivity) => !ids.includes(pointActivity.id!),
)
) {
pointActivityList.value =
await PointActivityApi.getPointActivityListByIds(ids as number[]);
}
},
{ immediate: true },
);
/** 活动表格选择对话框 */
const pointActivityTableSelectRef = ref();
// 打开对话框
const openSeckillActivityTableSelect = () => {
pointActivityTableSelectRef.value.open(pointActivityList.value);
};
/**
* 选择活动后触发
* @param activityList 选中的活动列表
*/
const handleActivitySelected = (
activityList:
| MallPointActivityApi.PointActivity
| MallPointActivityApi.PointActivity[],
) => {
pointActivityList.value = Array.isArray(activityList)
? activityList
: [activityList];
emitActivityChange();
};
/**
* 删除活动
* @param index 活动索引
*/
const handleRemoveActivity = (index: number) => {
pointActivityList.value.splice(index, 1);
emitActivityChange();
};
const emitActivityChange = () => {
if (props.limit === 1) {
const pointActivity =
pointActivityList.value.length > 0 ? pointActivityList.value[0] : null;
emit('update:modelValue', pointActivity?.id || 0);
emit('change', pointActivity);
} else {
emit(
'update:modelValue',
pointActivityList.value.map((pointActivity) => pointActivity.id),
);
emit('change', pointActivityList.value);
}
};
</script>
<template>
<div class="gap-8px flex flex-wrap items-center">
<div
v-for="(pointActivity, index) in pointActivityList"
:key="pointActivity.id"
class="select-box spu-pic"
>
<el-tooltip :content="pointActivity.spuName">
<div class="relative h-full w-full">
<el-image :src="pointActivity.picUrl" class="h-full w-full" />
<Icon
v-show="!disabled"
class="del-icon"
icon="ep:circle-close-filled"
@click="handleRemoveActivity(index)"
/>
</div>
</el-tooltip>
</div>
<el-tooltip v-if="canAdd" content="选择活动">
<div class="select-box" @click="openSeckillActivityTableSelect">
<Icon icon="ep:plus" />
</div>
</el-tooltip>
</div>
<!-- 拼团活动选择对话框表格形式 -->
<PointTableSelect
ref="pointActivityTableSelectRef"
:multiple="limit !== 1"
@change="handleActivitySelected"
/>
</template>
<style lang="scss" scoped>
.select-box {
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
cursor: pointer;
border: 1px dashed var(--el-border-color-darker);
border-radius: 8px;
}
.spu-pic {
position: relative;
}
.del-icon {
position: absolute;
top: -10px;
right: -10px;
z-index: 1;
width: 20px !important;
height: 20px !important;
}
</style>

View File

@@ -0,0 +1,351 @@
<script lang="ts" setup>
import type { MallPointActivityApi } from '#/api/mall/promotion/point';
import { computed, ref } from 'vue';
import { dateFormatter, fenToYuanFormat } from '@vben/utils';
import { CHANGE_EVENT } from 'element-plus';
import * as PointActivityApi from '#/api/mall/promotion/point';
import { DICT_TYPE, getIntDictOptions } from '#/utils/dict';
/**
* 活动表格选择对话框
* 1. 单选模式:
* 1.1 点击表格左侧的单选框时,结束选择,并关闭对话框
* 1.2 再次打开时,保持选中状态
* 2. 多选模式:
* 2.1 点击表格左侧的多选框时,记录选中的活动
* 2.2 切换分页时,保持活动的选中状态
* 2.3 点击右下角的确定按钮时,结束选择,关闭对话框
* 2.4 再次打开时,保持选中状态
*/
defineOptions({ name: 'PointTableSelect' });
defineProps({
// 多选模式
multiple: {
type: Boolean,
default: false,
},
});
/** 确认选择时的触发事件 */
const emits = defineEmits<{
(
e: 'change',
v:
| any
| MallPointActivityApi.PointActivity
| MallPointActivityApi.PointActivity[],
): void;
}>();
// 列表的总页数
const total = ref(0);
// 列表的数据
const list = ref<MallPointActivityApi.PointActivity[]>([]);
// 列表的加载中
const loading = ref(false);
// 弹窗的是否展示
const dialogVisible = ref(false);
// 查询参数
const queryParams = ref({
pageNo: 1,
pageSize: 10,
name: null,
status: undefined,
});
const getRedeemedQuantity = computed(
() => (row: any) => (row.totalStock || 0) - (row.stock || 0),
); // 获得商品已兑换数量
/** 打开弹窗 */
const open = (pointList?: MallPointActivityApi.PointActivity[]) => {
// 重置
checkedActivities.value = [];
checkedStatus.value = {};
isCheckAll.value = false;
isIndeterminate.value = false;
// 处理已选中
if (pointList && pointList.length > 0) {
checkedActivities.value = [...pointList];
checkedStatus.value = Object.fromEntries(
pointList.map((activityVO) => [activityVO.id, true]),
);
}
dialogVisible.value = true;
resetQuery();
};
// 提供 open 方法,用于打开弹窗
defineExpose({ open });
/** 查询列表 */
const getList = async () => {
loading.value = true;
try {
const data = await PointActivityApi.getPointActivityPage(queryParams.value);
list.value = data.list;
total.value = data.total;
// checkbox绑定undefined会有问题需要给一个bool值
list.value.forEach(
(activityVO) =>
(checkedStatus.value[activityVO.id] =
checkedStatus.value[activityVO.id] || false),
);
// 计算全选框状态
calculateIsCheckAll();
} finally {
loading.value = false;
}
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNo = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryParams.value = {
pageNo: 1,
pageSize: 10,
name: null,
status: undefined,
};
getList();
};
// 是否全选
const isCheckAll = ref(false);
// 全选框是否处于中间状态:不是全部选中 && 任意一个选中
const isIndeterminate = ref(false);
// 选中的活动
const checkedActivities = ref<MallPointActivityApi.PointActivity[]>([]);
// 选中状态key为活动IDvalue为是否选中
const checkedStatus = ref<Record<string, boolean>>({});
// 选中的活动 activityId
const selectedActivityId = ref();
/** 单选中时触发 */
const handleSingleSelected = (
pointActivityVO: MallPointActivityApi.PointActivity,
) => {
emits(CHANGE_EVENT, pointActivityVO);
// 关闭弹窗
dialogVisible.value = false;
// 记住上次选择的ID
selectedActivityId.value = pointActivityVO.id;
};
/** 多选完成 */
const handleEmitChange = () => {
// 关闭弹窗
dialogVisible.value = false;
emits(CHANGE_EVENT, [...checkedActivities.value]);
};
/** 全选/全不选 */
const handleCheckAll = (checked: boolean) => {
isCheckAll.value = checked;
isIndeterminate.value = false;
list.value.forEach((pointActivity) =>
handleCheckOne(checked, pointActivity, false),
);
};
/**
* 选中一行
* @param checked 是否选中
* @param pointActivity 活动
* @param isCalcCheckAll 是否计算全选
*/
const handleCheckOne = (
checked: boolean,
pointActivity: MallPointActivityApi.PointActivity,
isCalcCheckAll: boolean,
) => {
if (checked) {
checkedActivities.value.push(pointActivity);
checkedStatus.value[pointActivity.id] = true;
} else {
const index = findCheckedIndex(pointActivity);
if (index > -1) {
checkedActivities.value.splice(index, 1);
checkedStatus.value[pointActivity.id] = false;
isCheckAll.value = false;
}
}
// 计算全选框状态
if (isCalcCheckAll) {
calculateIsCheckAll();
}
};
// 查找活动在已选中活动列表中的索引
const findCheckedIndex = (activityVO: MallPointActivityApi.PointActivity) =>
checkedActivities.value.findIndex((item) => item.id === activityVO.id);
// 计算全选框状态
const calculateIsCheckAll = () => {
isCheckAll.value = list.value.every(
(activityVO) => checkedStatus.value[activityVO.id],
);
// 计算中间状态:不是全部选中 && 任意一个选中
isIndeterminate.value =
!isCheckAll.value &&
list.value.some((activityVO) => checkedStatus.value[activityVO.id]);
};
</script>
<template>
<Dialog
v-model="dialogVisible"
:append-to-body="true"
title="选择活动"
width="70%"
>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="活动状态" prop="status">
<el-select
v-model="queryParams.status"
class="!w-240px"
clearable
placeholder="请选择活动状态"
>
<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>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="list" show-overflow-tooltip>
<!-- 1. 多选模式不能使用type="selection"Element会忽略Header插槽 -->
<el-table-column v-if="multiple" width="55">
<template #header>
<el-checkbox
v-model="isCheckAll"
:indeterminate="isIndeterminate"
@change="handleCheckAll"
/>
</template>
<template #default="{ row }">
<el-checkbox
v-model="checkedStatus[row.id]"
@change="(checked: boolean) => handleCheckOne(checked, row, true)"
/>
</template>
</el-table-column>
<!-- 2. 单选模式 -->
<el-table-column v-else label="#" width="55">
<template #default="{ row }">
<el-radio
v-model="selectedActivityId"
:value="row.id"
@change="handleSingleSelected(row)"
>
<!-- 空格不能省略是为了让单选框不显示label如果不指定label不会有选中的效果 -->
&nbsp;
</el-radio>
</template>
</el-table-column>
<el-table-column label="活动编号" min-width="80" prop="id" />
<el-table-column label="商品图片" min-width="80" prop="spuName">
<template #default="scope">
<el-image
:preview-src-list="[scope.row.picUrl]"
:src="scope.row.picUrl"
class="h-40px w-40px"
preview-teleported
/>
</template>
</el-table-column>
<el-table-column label="商品标题" min-width="300" prop="spuName" />
<el-table-column
:formatter="fenToYuanFormat"
label="原价"
min-width="100"
prop="marketPrice"
/>
<el-table-column label="原价" min-width="100" prop="marketPrice" />
<el-table-column
align="center"
label="活动状态"
min-width="100"
prop="status"
>
<template #default="scope">
<dict-tag
:type="DICT_TYPE.COMMON_STATUS"
:value="scope.row.status"
/>
</template>
</el-table-column>
<el-table-column
align="center"
label="库存"
min-width="80"
prop="stock"
/>
<el-table-column
align="center"
label="总库存"
min-width="80"
prop="totalStock"
/>
<el-table-column
align="center"
label="已兑换数量"
min-width="100"
prop="redeemedQuantity"
>
<template #default="{ row }">
{{ getRedeemedQuantity(row) }}
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
width="180px"
/>
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<template v-if="multiple" #footer>
<el-button type="primary" @click="handleEmitChange"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,173 @@
<script lang="ts" setup>
import type { MallSeckillActivityApi } from '#/api/mall/promotion/seckill/seckillActivity';
import { computed, ref, watch } from 'vue';
import * as SeckillActivityApi from '#/api/mall/promotion/seckill/seckillActivity';
import SeckillTableSelect from '#/views/mall/promotion/seckill/components/seckill-table-select.vue';
// 活动橱窗,一般用于装修时使用
// 提供功能:展示活动列表、添加活动、删除活动
defineOptions({ name: 'SeckillShowcase' });
const props = defineProps({
modelValue: {
type: [Number, Array],
required: true,
},
// 限制数量:默认不限制
limit: {
type: Number,
default: Number.MAX_VALUE,
},
disabled: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['update:modelValue', 'change']);
// 计算是否可以添加
const canAdd = computed(() => {
// 情况一:禁用时不可以添加
if (props.disabled) return false;
// 情况二:未指定限制数量时,可以添加
if (!props.limit) return true;
// 情况三:检查已添加数量是否小于限制数量
return Activitys.value.length < props.limit;
});
// 拼团活动列表
const Activitys = ref<MallSeckillActivityApi.SeckillActivity[]>([]);
watch(
() => props.modelValue,
async () => {
let ids;
if (Array.isArray(props.modelValue)) {
ids = props.modelValue;
} else {
ids = props.modelValue ? [props.modelValue] : [];
}
// 不需要返显
if (ids.length === 0) {
Activitys.value = [];
return;
}
// 只有活动发生变化之后,才会查询活动
if (
Activitys.value.length === 0 ||
Activitys.value.some(
(seckillActivity) => !ids.includes(seckillActivity.id!),
)
) {
Activitys.value = await SeckillActivityApi.getSeckillActivityListByIds(
ids as number[],
);
}
},
{ immediate: true },
);
/** 活动表格选择对话框 */
const seckillActivityTableSelectRef = ref();
// 打开对话框
const openSeckillActivityTableSelect = () => {
seckillActivityTableSelectRef.value.open(Activitys.value);
};
/**
* 选择活动后触发
* @param activityVOs 选中的活动列表
*/
const handleActivitySelected = (
activityVOs:
| MallSeckillActivityApi.SeckillActivity
| MallSeckillActivityApi.SeckillActivity[],
) => {
Activitys.value = Array.isArray(activityVOs) ? activityVOs : [activityVOs];
emitActivityChange();
};
/**
* 删除活动
* @param index 活动索引
*/
const handleRemoveActivity = (index: number) => {
Activitys.value.splice(index, 1);
emitActivityChange();
};
const emitActivityChange = () => {
if (props.limit === 1) {
const seckillActivity =
Activitys.value.length > 0 ? Activitys.value[0] : null;
emit('update:modelValue', seckillActivity?.id || 0);
emit('change', seckillActivity);
} else {
emit(
'update:modelValue',
Activitys.value.map((seckillActivity) => seckillActivity.id),
);
emit('change', Activitys.value);
}
};
</script>
<template>
<div class="gap-8px flex flex-wrap items-center">
<div
v-for="(seckillActivity, index) in Activitys"
:key="seckillActivity.id"
class="select-box spu-pic"
>
<el-tooltip :content="seckillActivity.name">
<div class="relative h-full w-full">
<el-image :src="seckillActivity.picUrl" class="h-full w-full" />
<Icon
v-show="!disabled"
class="del-icon"
icon="ep:circle-close-filled"
@click="handleRemoveActivity(index)"
/>
</div>
</el-tooltip>
</div>
<el-tooltip content="选择活动" v-if="canAdd">
<div class="select-box" @click="openSeckillActivityTableSelect">
<Icon icon="ep:plus" />
</div>
</el-tooltip>
</div>
<!-- 拼团活动选择对话框表格形式 -->
<SeckillTableSelect
ref="seckillActivityTableSelectRef"
:multiple="limit !== 1"
@change="handleActivitySelected"
/>
</template>
<style lang="scss" scoped>
.select-box {
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
cursor: pointer;
border: 1px dashed var(--el-border-color-darker);
border-radius: 8px;
}
.spu-pic {
position: relative;
}
.del-icon {
position: absolute;
top: -10px;
right: -10px;
z-index: 1;
width: 20px !important;
height: 20px !important;
}
</style>

View File

@@ -0,0 +1,385 @@
<script lang="ts" setup>
import type { MallSeckillActivityApi } from '#/api/mall/promotion/seckill/seckillActivity';
import { onMounted, ref } from 'vue';
import {
dateFormatter,
fenToYuan,
fenToYuanFormat,
formatDate,
handleTree,
} from '@vben/utils';
import { CHANGE_EVENT } from 'element-plus';
import * as ProductCategoryApi from '#/api/mall/product/category';
import * as SeckillActivityApi from '#/api/mall/promotion/seckill/seckillActivity';
import { DICT_TYPE, getIntDictOptions } from '#/utils/dict';
/**
* 活动表格选择对话框
* 1. 单选模式:
* 1.1 点击表格左侧的单选框时,结束选择,并关闭对话框
* 1.2 再次打开时,保持选中状态
* 2. 多选模式:
* 2.1 点击表格左侧的多选框时,记录选中的活动
* 2.2 切换分页时,保持活动的选中状态
* 2.3 点击右下角的确定按钮时,结束选择,关闭对话框
* 2.4 再次打开时,保持选中状态
*/
defineOptions({ name: 'SeckillTableSelect' });
defineProps({
// 多选模式
multiple: {
type: Boolean,
default: false,
},
});
/** 确认选择时的触发事件 */
const emits = defineEmits<{
change: [
SeckillActivityApi:
| any
| MallSeckillActivityApi.SeckillActivity
| MallSeckillActivityApi.SeckillActivity[],
];
}>();
// 列表的总页数
const total = ref(0);
// 列表的数据
const list = ref<MallSeckillActivityApi.SeckillActivity[]>([]);
// 列表的加载中
const loading = ref(false);
// 弹窗的是否展示
const dialogVisible = ref(false);
// 查询参数
const queryParams = ref({
pageNo: 1,
pageSize: 10,
name: undefined,
status: undefined,
});
/** 打开弹窗 */
const open = (SeckillList?: MallSeckillActivityApi.SeckillActivity[]) => {
// 重置
checkedActivitys.value = [];
checkedStatus.value = {};
isCheckAll.value = false;
isIndeterminate.value = false;
// 处理已选中
if (SeckillList && SeckillList.length > 0) {
checkedActivitys.value = [...SeckillList];
checkedStatus.value = Object.fromEntries(
SeckillList.map((activityVO) => [activityVO.id, true]),
);
}
dialogVisible.value = true;
resetQuery();
};
// 提供 open 方法,用于打开弹窗
defineExpose({ open });
/** 查询列表 */
const getList = async () => {
loading.value = true;
try {
const data = await SeckillActivityApi.getSeckillActivityPage(
queryParams.value,
);
list.value = data.list;
total.value = data.total;
// checkbox绑定undefined会有问题需要给一个bool值
list.value.forEach(
(activityVO) =>
(checkedStatus.value[activityVO.id || ''] =
checkedStatus.value[activityVO.id || ''] || false),
);
// 计算全选框状态
calculateIsCheckAll();
} finally {
loading.value = false;
}
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNo = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryParams.value = {
pageNo: 1,
pageSize: 10,
name: undefined,
status: undefined,
};
getList();
};
/**
* 格式化拼团价格
* @param products
*/
const formatSeckillPrice = (
products: MallSeckillActivityApi.SeckillProduct[],
) => {
const seckillPrice = Math.min(...products.map((item) => item.seckillPrice));
return `${fenToYuan(seckillPrice)}`;
};
// 是否全选
const isCheckAll = ref(false);
// 全选框是否处于中间状态:不是全部选中 && 任意一个选中
const isIndeterminate = ref(false);
// 选中的活动
const checkedActivitys = ref<MallSeckillActivityApi.SeckillActivity[]>([]);
// 选中状态key为活动IDvalue为是否选中
const checkedStatus = ref<Record<string, boolean>>({});
// 选中的活动 activityId
const selectedActivityId = ref();
/** 单选中时触发 */
const handleSingleSelected = (
seckillActivityVO: MallSeckillActivityApi.SeckillActivity,
) => {
emits(CHANGE_EVENT, seckillActivityVO);
// 关闭弹窗
dialogVisible.value = false;
// 记住上次选择的ID
selectedActivityId.value = seckillActivityVO.id;
};
/** 多选完成 */
const handleEmitChange = () => {
// 关闭弹窗
dialogVisible.value = false;
emits(CHANGE_EVENT, [...checkedActivitys.value]);
};
/** 全选/全不选 */
const handleCheckAll = (checked: boolean) => {
isCheckAll.value = checked;
isIndeterminate.value = false;
list.value.forEach((seckillActivity) =>
handleCheckOne(checked, seckillActivity, false),
);
};
/**
* 选中一行
* @param checked 是否选中
* @param seckillActivity 活动
* @param isCalcCheckAll 是否计算全选
*/
const handleCheckOne = (
checked: boolean,
seckillActivity: MallSeckillActivityApi.SeckillActivity,
isCalcCheckAll: boolean,
) => {
if (checked) {
checkedActivitys.value.push(seckillActivity);
checkedStatus.value[seckillActivity.id || ''] = true;
} else {
const index = findCheckedIndex(seckillActivity);
if (index > -1) {
checkedActivitys.value.splice(index, 1);
checkedStatus.value[seckillActivity.id || ''] = false;
isCheckAll.value = false;
}
}
// 计算全选框状态
if (isCalcCheckAll) {
calculateIsCheckAll();
}
};
// 查找活动在已选中活动列表中的索引
const findCheckedIndex = (activityVO: MallSeckillActivityApi.SeckillActivity) =>
checkedActivitys.value.findIndex((item) => item.id === activityVO.id);
// 计算全选框状态
const calculateIsCheckAll = () => {
isCheckAll.value = list.value.every(
(activityVO) => checkedStatus.value[activityVO.id || ''],
);
// 计算中间状态:不是全部选中 && 任意一个选中
isIndeterminate.value =
!isCheckAll.value &&
list.value.some((activityVO) => checkedStatus.value[activityVO.id || '']);
};
// 分类列表
const categoryList = ref();
// 分类树
const categoryTreeList = ref();
/** 初始化 */
onMounted(async () => {
await getList();
// 获得分类树
categoryList.value = await ProductCategoryApi.getCategoryList({});
categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId');
});
</script>
<template>
<Dialog
v-model="dialogVisible"
:append-to-body="true"
title="选择活动"
width="70%"
>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
:inline="true"
:model="queryParams"
class="-mb-15px"
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>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="list" show-overflow-tooltip>
<!-- 1. 多选模式不能使用type="selection"Element会忽略Header插槽 -->
<el-table-column width="55" v-if="multiple">
<template #header>
<el-checkbox
v-model="isCheckAll"
:indeterminate="isIndeterminate"
@change="handleCheckAll"
/>
</template>
<template #default="{ row }">
<el-checkbox
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
:value="row.id"
v-model="selectedActivityId"
@change="handleSingleSelected(row)"
>
<!-- 空格不能省略是为了让单选框不显示label如果不指定label不会有选中的效果 -->
&nbsp;
</el-radio>
</template>
</el-table-column>
<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="原价"
prop="marketPrice"
min-width="100"
:formatter="fenToYuanFormat"
/>
<el-table-column label="拼团价" prop="seckillPrice" min-width="100">
<template #default="scope">
{{ formatSeckillPrice(scope.row.products) }}
</template>
</el-table-column>
<el-table-column label="开团组数" prop="groupCount" min-width="100" />
<el-table-column
label="成团组数"
prop="groupSuccessCount"
min-width="100"
/>
<el-table-column label="购买次数" prop="recordCount" min-width="100" />
<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>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<template #footer v-if="multiple">
<el-button type="primary" @click="handleEmitChange"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>

View File

@@ -7,7 +7,7 @@ import { onMounted, reactive, ref } from 'vue';
import { AnalysisChartCard } from '@vben/common-ui';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { fenToYuan } from '@vben/utils';
import { fenToYuan, fenToYuanFormat } from '@vben/utils';
import { ElRow } from 'element-plus';
@@ -70,11 +70,6 @@ const getMemberAreaStatisticsList = async () => {
areaChartOptions.visualMap.max = max;
};
// 格式化为元
const fenToYuanFormat = (row: any, column: any, cellValue: any) => {
return fenToYuan(cellValue);
};
onMounted(async () => {
await getMemberAreaStatisticsList();
renderEcharts(areaChartOptions);

View File

@@ -4,7 +4,7 @@ import type { MallProductStatisticsApi } from '#/api/mall/statistics/product';
import { onMounted, reactive, ref } from 'vue';
import { AnalysisChartCard } from '@vben/common-ui';
import { buildSortingField, floatToFixed2 } from '@vben/utils';
import { buildSortingField, fenToYuanFormat } from '@vben/utils';
import * as ProductStatisticsApi from '#/api/mall/statistics/product';
import ShortcutDateRangePicker from '#/views/mall/home/components/shortcut-date-range-picker.vue';
@@ -52,12 +52,6 @@ const getSpuList = async () => {
}
};
// 格式化金额【分转元】
// @ts-ignore
const fenToYuanFormat = (_, __, cellValue: any, ___) => {
return `${floatToFixed2(cellValue)}`;
};
/** 初始化 */
onMounted(async () => {
await getSpuList();