Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vben into dev
This commit is contained in:
@@ -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>
|
||||
@@ -0,0 +1,166 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { ElImage, ElTooltip } from 'element-plus';
|
||||
|
||||
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"
|
||||
>
|
||||
<ElTooltip :content="spu.name">
|
||||
<div class="relative h-full w-full">
|
||||
<ElImage :src="spu.picUrl" class="h-full w-full" />
|
||||
<IconifyIcon
|
||||
v-show="!disabled"
|
||||
class="del-icon"
|
||||
icon="ep:circle-close-filled"
|
||||
@click="handleRemoveSpu(index)"
|
||||
/>
|
||||
</div>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
<ElTooltip content="选择商品" v-if="canAdd">
|
||||
<div class="select-box" @click="openSpuTableSelect">
|
||||
<IconifyIcon icon="ep:plus" />
|
||||
</div>
|
||||
</ElTooltip>
|
||||
</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>
|
||||
@@ -0,0 +1,359 @@
|
||||
<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,
|
||||
ElButton,
|
||||
ElCheckbox,
|
||||
ElDatePicker,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElImage,
|
||||
ElInput,
|
||||
ElRadio,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTreeSelect,
|
||||
} 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为商品ID,value为是否选中
|
||||
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>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:append-to-body="true"
|
||||
title="选择商品"
|
||||
width="70%"
|
||||
>
|
||||
<ContentWrap>
|
||||
<ElForm
|
||||
:inline="true"
|
||||
:model="queryParams"
|
||||
class="-mb-15px"
|
||||
label-width="68px"
|
||||
>
|
||||
<ElFormItem label="商品名称" prop="name">
|
||||
<ElInput
|
||||
v-model="queryParams.name"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请输入商品名称"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="商品分类" prop="categoryId">
|
||||
<ElTreeSelect
|
||||
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="请选择商品分类"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="创建时间" prop="createTime">
|
||||
<ElDatePicker
|
||||
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"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<ElButton @click="handleQuery">
|
||||
<Icon class="mr-5px" icon="ep:search" />
|
||||
搜索
|
||||
</ElButton>
|
||||
<ElButton @click="resetQuery">
|
||||
<Icon class="mr-5px" icon="ep:refresh" />
|
||||
重置
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<ElTable v-loading="loading" :data="list" show-overflow-tooltip>
|
||||
<!-- 1. 多选模式(不能使用type="selection",Element会忽略Header插槽) -->
|
||||
<ElTableColumn width="55" v-if="multiple">
|
||||
<template #header>
|
||||
<ElCheckbox
|
||||
v-model="isCheckAll"
|
||||
:indeterminate="isIndeterminate"
|
||||
@change="handleCheckAll"
|
||||
/>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<ElCheckbox
|
||||
v-model="checkedStatus[row.id]"
|
||||
@change="(checked: boolean) => handleCheckOne(checked, row, true)"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<!-- 2. 单选模式 -->
|
||||
<ElTableColumn label="#" width="55" v-else>
|
||||
<template #default="{ row }">
|
||||
<ElRadio
|
||||
:value="row.id"
|
||||
v-model="selectedSpuId"
|
||||
@change="handleSingleSelected(row)"
|
||||
>
|
||||
<!-- 空格不能省略,是为了让单选框不显示label,如果不指定label不会有选中的效果 -->
|
||||
|
||||
</ElRadio>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
key="id"
|
||||
align="center"
|
||||
label="商品编号"
|
||||
prop="id"
|
||||
min-width="60"
|
||||
/>
|
||||
<ElTableColumn label="商品图" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<ElImage
|
||||
:src="row.picUrl"
|
||||
class="h-30px w-30px"
|
||||
:preview-src-list="[row.picUrl]"
|
||||
preview-teleported
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="商品名称" min-width="200" prop="name" />
|
||||
<ElTableColumn label="商品分类" min-width="100" prop="categoryId">
|
||||
<template #default="{ row }">
|
||||
<span>{{
|
||||
categoryList?.find(
|
||||
(c: MallCategoryApi.Category) => c.id === row.categoryId,
|
||||
)?.name
|
||||
}}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
v-model:limit="queryParams.pageSize"
|
||||
v-model:page="queryParams.pageNo"
|
||||
:total="total"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
<template #footer v-if="multiple">
|
||||
<ElButton type="primary" @click="handleEmitChange">确 定</ElButton>
|
||||
<ElButton @click="dialogVisible = false">取 消</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
@@ -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>
|
||||
@@ -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为活动ID,value为是否选中
|
||||
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不会有选中的效果 -->
|
||||
|
||||
</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>
|
||||
@@ -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>
|
||||
@@ -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/diy-editor/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>
|
||||
@@ -0,0 +1,223 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MallDiyPageApi } from '#/api/mall/promotion/diy/page';
|
||||
import type { MallDiyTemplateApi } from '#/api/mall/promotion/diy/template';
|
||||
import type { DiyComponentLibrary } from '#/components/diy-editor/util'; // 商城的 DIY 组件,在 DiyEditor 目录下
|
||||
|
||||
import { onMounted, reactive, ref, unref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import * as DiyPageApi from '#/api/mall/promotion/diy/page';
|
||||
// TODO @疯狂:要不要建个 decorate 目录,然后挪进去,改成 index.vue,这样可以更明确看到是个独立界面哈,更好找
|
||||
import * as DiyTemplateApi from '#/api/mall/promotion/diy/template';
|
||||
import DiyEditor from '#/components/diy-editor/index.vue';
|
||||
import { PAGE_LIBS } from '#/components/diy-editor/util';
|
||||
|
||||
/** 装修模板表单 */
|
||||
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 formLoading = ref(false); // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const formData = ref<MallDiyTemplateApi.DiyTemplateProperty>();
|
||||
const formRef = ref(); // 表单 Ref
|
||||
// 当前编辑的属性
|
||||
const currentFormData = ref<
|
||||
MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty
|
||||
>({
|
||||
property: '',
|
||||
} as MallDiyPageApi.DiyPage);
|
||||
// templateItem 对应的缓存
|
||||
const currentFormDataMap = ref<
|
||||
Map<string, MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty>
|
||||
>(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
|
||||
| MallDiyPageApi.DiyPage
|
||||
| MallDiyTemplateApi.DiyTemplateProperty;
|
||||
return;
|
||||
}
|
||||
|
||||
// 编辑页面
|
||||
libs.value = PAGE_LIBS;
|
||||
currentFormData.value = (
|
||||
isEmpty(data)
|
||||
? formData.value!.pages.find(
|
||||
(page: MallDiyPageApi.DiyPage) =>
|
||||
page.name === templateItems[val]?.name,
|
||||
)
|
||||
: data
|
||||
) as MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty;
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const submitForm = async () => {
|
||||
// 校验表单
|
||||
if (!formRef.value) 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!);
|
||||
}
|
||||
}
|
||||
ElMessage.success('保存成功');
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
name: '',
|
||||
used: false,
|
||||
usedTime: undefined,
|
||||
remark: '',
|
||||
previewPicUrls: [],
|
||||
property: '',
|
||||
pages: [],
|
||||
} as MallDiyTemplateApi.DiyTemplateProperty;
|
||||
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 = Number(sessionStorage.getItem(DIY_PAGE_INDEX_KEY)) || 0;
|
||||
// 移除标记
|
||||
sessionStorage.removeItem(DIY_PAGE_INDEX_KEY);
|
||||
|
||||
// 重新初始化数据
|
||||
currentFormData.value = formData.value as
|
||||
| MallDiyPageApi.DiyPage
|
||||
| MallDiyTemplateApi.DiyTemplateProperty;
|
||||
currentFormDataMap.value = new Map<
|
||||
string,
|
||||
MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty
|
||||
>();
|
||||
// 切换页面
|
||||
if (pageIndex !== selectedTemplateItem.value) {
|
||||
handleTemplateItemChange(pageIndex);
|
||||
}
|
||||
};
|
||||
// #endregion
|
||||
|
||||
/** 初始化 */
|
||||
const { currentRoute } = useRouter(); // 路由
|
||||
onMounted(async () => {
|
||||
resetForm();
|
||||
if (!currentRoute.value.params.id) {
|
||||
ElMessage.warning('参数错误,页面编号不能为空!');
|
||||
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">
|
||||
<IconifyIcon :icon="item.icon" :size="24" />
|
||||
</el-radio-button>
|
||||
</el-tooltip>
|
||||
</el-radio-group>
|
||||
</template>
|
||||
</DiyEditor>
|
||||
</template>
|
||||
@@ -0,0 +1,178 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MallPointActivityApi } from '#/api/mall/promotion/point';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { ElImage, ElTooltip } from 'element-plus';
|
||||
|
||||
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"
|
||||
>
|
||||
<ElTooltip :content="pointActivity.spuName">
|
||||
<div class="relative h-full w-full">
|
||||
<ElImage :src="pointActivity.picUrl" class="h-full w-full" />
|
||||
<IconifyIcon
|
||||
v-show="!disabled"
|
||||
class="del-icon"
|
||||
icon="ep:circle-close-filled"
|
||||
@click="handleRemoveActivity(index)"
|
||||
/>
|
||||
</div>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
<ElTooltip v-if="canAdd" content="选择活动">
|
||||
<div class="select-box" @click="openSeckillActivityTableSelect">
|
||||
<IconifyIcon icon="ep:plus" />
|
||||
</div>
|
||||
</ElTooltip>
|
||||
</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>
|
||||
@@ -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为活动ID,value为是否选中
|
||||
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不会有选中的效果 -->
|
||||
|
||||
</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>
|
||||
@@ -0,0 +1,177 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MallSeckillActivityApi } from '#/api/mall/promotion/seckill/seckillActivity';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { ElImage, ElTooltip } from 'element-plus';
|
||||
|
||||
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"
|
||||
>
|
||||
<ElTooltip :content="seckillActivity.name">
|
||||
<div class="relative h-full w-full">
|
||||
<ElImage :src="seckillActivity.picUrl" class="h-full w-full" />
|
||||
<IconifyIcon
|
||||
v-show="!disabled"
|
||||
class="del-icon"
|
||||
icon="ep:circle-close-filled"
|
||||
@click="handleRemoveActivity(index)"
|
||||
/>
|
||||
</div>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
<ElTooltip content="选择活动" v-if="canAdd">
|
||||
<div class="select-box" @click="openSeckillActivityTableSelect">
|
||||
<IconifyIcon icon="ep:plus" />
|
||||
</div>
|
||||
</ElTooltip>
|
||||
</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>
|
||||
@@ -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为活动ID,value为是否选中
|
||||
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不会有选中的效果 -->
|
||||
|
||||
</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>
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user