feat:【ele】【mall】product/spu 的 components 迁移
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
export { default as SkuTableSelect } from './sku-table-select.vue';
|
||||||
|
export { default as SpuShowcase } from './spu-showcase.vue';
|
||||||
|
export { default as SpuTableSelect } from './spu-table-select.vue';
|
||||||
|
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<!-- SKU 选择弹窗组件 -->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import { fenToYuan } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { getSpu } from '#/api/mall/product/spu';
|
||||||
|
|
||||||
|
interface SpuData {
|
||||||
|
spuId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
change: [sku: MallSpuApi.Sku];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const spuId = ref<number>();
|
||||||
|
|
||||||
|
/** 表格列配置 */
|
||||||
|
const gridColumns = computed<VxeGridProps['columns']>(() => [
|
||||||
|
{
|
||||||
|
type: 'radio',
|
||||||
|
width: 55,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'picUrl',
|
||||||
|
title: '图片',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellImage',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'properties',
|
||||||
|
title: '规格',
|
||||||
|
minWidth: 120,
|
||||||
|
align: 'center',
|
||||||
|
formatter: ({ cellValue }) => {
|
||||||
|
return (
|
||||||
|
cellValue?.map((p: MallSpuApi.Property) => p.valueName)?.join(' ') ||
|
||||||
|
'-'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'price',
|
||||||
|
title: '销售价(元)',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
formatter: ({ cellValue }) => {
|
||||||
|
return fenToYuan(cellValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// TODO @芋艿:要不要直接非 pager?
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
gridOptions: {
|
||||||
|
columns: gridColumns.value,
|
||||||
|
height: 400,
|
||||||
|
border: true,
|
||||||
|
showOverflow: true,
|
||||||
|
radioConfig: {
|
||||||
|
reserve: true,
|
||||||
|
},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async () => {
|
||||||
|
if (!spuId.value) {
|
||||||
|
return { list: [], total: 0 };
|
||||||
|
}
|
||||||
|
const spu = await getSpu(spuId.value);
|
||||||
|
return {
|
||||||
|
list: spu.skus || [],
|
||||||
|
total: spu.skus?.length || 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gridEvents: {
|
||||||
|
radioChange: handleRadioChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 处理选中 */
|
||||||
|
function handleRadioChange() {
|
||||||
|
const selectedRow = gridApi.grid.getRadioRecord() as MallSpuApi.Sku;
|
||||||
|
if (selectedRow) {
|
||||||
|
emit('change', selectedRow);
|
||||||
|
modalApi.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
destroyOnClose: true,
|
||||||
|
onOpenChange: async (isOpen: boolean) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
gridApi.grid.clearRadioRow();
|
||||||
|
spuId.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = modalApi.getData<SpuData>();
|
||||||
|
if (!data?.spuId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
spuId.value = data.spuId;
|
||||||
|
await gridApi.query();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal class="w-[700px]" title="选择规格">
|
||||||
|
<Grid />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<!-- 商品橱窗组件:用于展示和选择商品 SPU -->
|
||||||
|
<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 { getSpuDetailList } from '#/api/mall/product/spu';
|
||||||
|
|
||||||
|
import SpuTableSelect from './spu-table-select.vue';
|
||||||
|
|
||||||
|
interface SpuShowcaseProps {
|
||||||
|
modelValue?: number | number[];
|
||||||
|
limit?: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<SpuShowcaseProps>(), {
|
||||||
|
modelValue: undefined,
|
||||||
|
limit: Number.MAX_VALUE,
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'change']);
|
||||||
|
|
||||||
|
const productSpus = ref<MallSpuApi.Spu[]>([]);
|
||||||
|
const spuTableSelectRef = ref<InstanceType<typeof SpuTableSelect>>();
|
||||||
|
const isMultiple = computed(() => props.limit !== 1); // 是否为多选模式
|
||||||
|
|
||||||
|
/** 计算是否可以添加 */
|
||||||
|
const canAdd = computed(() => {
|
||||||
|
if (props.disabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!props.limit) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return productSpus.value.length < props.limit;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 监听 modelValue 变化,加载商品详情 */
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
async (newValue) => {
|
||||||
|
// eslint-disable-next-line unicorn/no-nested-ternary
|
||||||
|
const ids = Array.isArray(newValue) ? newValue : newValue ? [newValue] : [];
|
||||||
|
if (ids.length === 0) {
|
||||||
|
productSpus.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 只有商品发生变化时才重新查询
|
||||||
|
if (
|
||||||
|
productSpus.value.length === 0 ||
|
||||||
|
productSpus.value.some((spu) => !ids.includes(spu.id!))
|
||||||
|
) {
|
||||||
|
productSpus.value = await getSpuDetailList(ids);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 打开商品选择对话框 */
|
||||||
|
function handleOpenSpuSelect() {
|
||||||
|
spuTableSelectRef.value?.open(productSpus.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选择商品后触发 */
|
||||||
|
function handleSpuSelected(spus: MallSpuApi.Spu | MallSpuApi.Spu[]) {
|
||||||
|
productSpus.value = Array.isArray(spus) ? spus : [spus];
|
||||||
|
emitSpuChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除商品 */
|
||||||
|
function handleRemoveSpu(index: number) {
|
||||||
|
productSpus.value.splice(index, 1);
|
||||||
|
emitSpuChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 触发变更事件 */
|
||||||
|
function 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="flex flex-wrap items-center gap-2">
|
||||||
|
<!-- 已选商品列表 -->
|
||||||
|
<div
|
||||||
|
v-for="(spu, index) in productSpus"
|
||||||
|
:key="spu.id"
|
||||||
|
class="spu-item group relative"
|
||||||
|
>
|
||||||
|
<ElTooltip :content="spu.name">
|
||||||
|
<div class="relative h-full w-full">
|
||||||
|
<ElImage
|
||||||
|
:src="spu.picUrl"
|
||||||
|
class="h-full w-full rounded-lg object-cover"
|
||||||
|
:preview-src-list="[spu.picUrl!]"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<!-- 删除按钮 -->
|
||||||
|
<IconifyIcon
|
||||||
|
v-if="!disabled"
|
||||||
|
icon="ep:circle-close-filled"
|
||||||
|
class="absolute -right-2 -top-2 cursor-pointer text-xl text-red-500 opacity-0 transition-opacity hover:text-red-600 group-hover:opacity-100"
|
||||||
|
@click="handleRemoveSpu(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ElTooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加商品按钮 -->
|
||||||
|
<ElTooltip v-if="canAdd" content="选择商品">
|
||||||
|
<div
|
||||||
|
class="spu-add-box hover:border-primary hover:bg-primary/5 flex cursor-pointer items-center justify-center rounded-lg border-2 border-dashed transition-colors"
|
||||||
|
@click="handleOpenSpuSelect"
|
||||||
|
>
|
||||||
|
<IconifyIcon icon="ep:plus" class="text-xl text-gray-400" />
|
||||||
|
</div>
|
||||||
|
</ElTooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 商品选择对话框 -->
|
||||||
|
<SpuTableSelect
|
||||||
|
ref="spuTableSelectRef"
|
||||||
|
:multiple="isMultiple"
|
||||||
|
@change="handleSpuSelected"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.spu-item {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spu-add-box {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
<!-- SPU 商品选择弹窗组件 -->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||||
|
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import { handleTree } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { getCategoryList } from '#/api/mall/product/category';
|
||||||
|
import { getSpuPage } from '#/api/mall/product/spu';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
interface SpuTableSelectProps {
|
||||||
|
multiple?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<SpuTableSelectProps>(), {
|
||||||
|
multiple: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
change: [spu: MallSpuApi.Spu | MallSpuApi.Spu[]];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// TODO @芋艿:要不要加类型;
|
||||||
|
const categoryList = ref<any[]>([]);
|
||||||
|
const categoryTreeList = ref<any[]>([]);
|
||||||
|
|
||||||
|
/** 搜索表单 Schema */
|
||||||
|
const formSchema = computed<VbenFormSchema[]>(() => [
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '商品名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入商品名称',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'categoryId',
|
||||||
|
label: '商品分类',
|
||||||
|
component: 'TreeSelect',
|
||||||
|
componentProps: {
|
||||||
|
treeData: categoryTreeList.value,
|
||||||
|
fieldNames: {
|
||||||
|
label: 'name',
|
||||||
|
value: 'id',
|
||||||
|
},
|
||||||
|
treeCheckStrictly: true,
|
||||||
|
placeholder: '请选择商品分类',
|
||||||
|
allowClear: true,
|
||||||
|
showSearch: true,
|
||||||
|
treeNodeFilterProp: 'name',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** 表格列配置 */
|
||||||
|
const gridColumns = computed<VxeGridProps['columns']>(() => {
|
||||||
|
const columns: VxeGridProps['columns'] = [];
|
||||||
|
if (props.multiple) {
|
||||||
|
columns.push({ type: 'checkbox', width: 55 });
|
||||||
|
} else {
|
||||||
|
columns.push({ type: 'radio', width: 55 });
|
||||||
|
}
|
||||||
|
columns.push(
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '商品编号',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'picUrl',
|
||||||
|
title: '商品图',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellImage',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '商品名称',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'categoryId',
|
||||||
|
title: '商品分类',
|
||||||
|
minWidth: 120,
|
||||||
|
formatter: ({ cellValue }) => {
|
||||||
|
const category = categoryList.value?.find((c) => c.id === cellValue);
|
||||||
|
return category?.name || '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return columns;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: formSchema.value,
|
||||||
|
layout: 'horizontal',
|
||||||
|
collapsed: false,
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: gridColumns.value,
|
||||||
|
height: 500,
|
||||||
|
border: true,
|
||||||
|
showOverflow: true,
|
||||||
|
checkboxConfig: props.multiple
|
||||||
|
? {
|
||||||
|
reserve: true,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
radioConfig: props.multiple
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
reserve: true,
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
async query({ page }: any, formValues: any) {
|
||||||
|
return await getSpuPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
tabType: 0,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gridEvents: props.multiple
|
||||||
|
? {
|
||||||
|
checkboxChange: handleCheckboxChange,
|
||||||
|
checkboxAll: handleCheckboxChange,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
radioChange: handleRadioChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 多选:处理选中变化 */
|
||||||
|
// TODO @芋艿:要不要清理掉?
|
||||||
|
function handleCheckboxChange() {
|
||||||
|
// vxe-table 自动管理选中状态,无需手动处理
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单选:处理选中变化 */
|
||||||
|
function handleRadioChange() {
|
||||||
|
const selectedRow = gridApi.grid.getRadioRecord() as MallSpuApi.Spu;
|
||||||
|
if (selectedRow) {
|
||||||
|
emit('change', selectedRow);
|
||||||
|
modalApi.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
destroyOnClose: true,
|
||||||
|
// TODO @芋艿:看看怎么简化
|
||||||
|
onConfirm: props.multiple
|
||||||
|
? () => {
|
||||||
|
const selectedRows =
|
||||||
|
gridApi.grid.getCheckboxRecords() as MallSpuApi.Spu[];
|
||||||
|
emit('change', selectedRows);
|
||||||
|
modalApi.close();
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
// TODO @芋艿:看看怎么简化?
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
gridApi.grid.clearCheckboxRow();
|
||||||
|
gridApi.grid.clearRadioRow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = modalApi.getData<MallSpuApi.Spu | MallSpuApi.Spu[]>();
|
||||||
|
|
||||||
|
// 先查询数据,确保表格已加载
|
||||||
|
await gridApi.query();
|
||||||
|
|
||||||
|
if (props.multiple && Array.isArray(data) && data.length > 0) {
|
||||||
|
// 等待数据加载完成后再设置选中状态
|
||||||
|
setTimeout(() => {
|
||||||
|
const tableData = gridApi.grid.getTableData().fullData;
|
||||||
|
data.forEach((spu) => {
|
||||||
|
// 在表格数据中查找匹配的行
|
||||||
|
const row = tableData.find((item: MallSpuApi.Spu) => item.id === spu.id);
|
||||||
|
if (row) {
|
||||||
|
gridApi.grid.setCheckboxRow(row, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
} else if (!props.multiple && data && !Array.isArray(data)) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const tableData = gridApi.grid.getTableData().fullData;
|
||||||
|
// 在表格数据中查找匹配的行
|
||||||
|
const row = tableData.find((item: MallSpuApi.Spu) => item.id === data.id);
|
||||||
|
if (row) {
|
||||||
|
gridApi.grid.setRadioRow(row);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 初始化分类数据 */
|
||||||
|
onMounted(async () => {
|
||||||
|
categoryList.value = await getCategoryList({});
|
||||||
|
categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId');
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 对外暴露的方法 */
|
||||||
|
defineExpose({
|
||||||
|
open: (data?: MallSpuApi.Spu | MallSpuApi.Spu[]) => {
|
||||||
|
modalApi.setData(data).open();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :class="props.multiple ? 'w-[900px]' : 'w-[800px]'" title="选择商品">
|
||||||
|
<Grid />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user