feat:【ele】【mall】product/spu 的 components 迁移

This commit is contained in:
YunaiV
2025-11-01 21:09:29 +08:00
parent 4bb631fb24
commit b3e1dab487
8 changed files with 134 additions and 200 deletions

View File

@@ -26,8 +26,8 @@ const props = withDefaults(defineProps<SpuShowcaseProps>(), {
const emit = defineEmits(['update:modelValue', 'change']);
const productSpus = ref<MallSpuApi.Spu[]>([]);
const spuTableSelectRef = ref<InstanceType<typeof SpuTableSelect>>();
const productSpus = ref<MallSpuApi.Spu[]>([]); // 已选择的商品列表
const spuTableSelectRef = ref<InstanceType<typeof SpuTableSelect>>(); // 商品选择表格组件引用
const isMultiple = computed(() => props.limit !== 1); // 是否为多选模式
/** 计算是否可以添加 */
@@ -47,12 +47,10 @@ watch(
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 ||
@@ -103,16 +101,16 @@ function emitSpuChange() {
<div
v-for="(spu, index) in productSpus"
:key="spu.id"
class="spu-item group relative"
class="group relative h-[60px] w-[60px] overflow-hidden rounded-lg"
>
<Tooltip :title="spu.name">
<div class="relative h-full w-full">
<Image
:src="spu.picUrl"
class="h-full w-full rounded-lg object-cover"
:preview="false"
/>
<!-- 删除按钮 -->
<!-- TODO @AI还是使用 IconifyIcon使用自己的 + 图标 -->
<CloseCircleFilled
v-if="!disabled"
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"
@@ -125,9 +123,10 @@ function emitSpuChange() {
<!-- 添加商品按钮 -->
<Tooltip v-if="canAdd" title="选择商品">
<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"
class="hover:border-primary hover:bg-primary/5 flex h-[60px] w-[60px] cursor-pointer items-center justify-center rounded-lg border-2 border-dashed transition-colors"
@click="handleOpenSpuSelect"
>
<!-- TODO @AI还是使用 IconifyIcon使用自己的 + 图标 -->
<PlusOutlined class="text-xl text-gray-400" />
</div>
</Tooltip>
@@ -140,17 +139,3 @@ function emitSpuChange() {
@change="handleSpuSelected"
/>
</template>
<style scoped>
.spu-item {
width: 60px;
height: 60px;
border-radius: 8px;
overflow: hidden;
}
.spu-add-box {
width: 60px;
height: 60px;
}
</style>

View File

@@ -2,6 +2,7 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { MallCategoryApi } from '#/api/mall/product/category';
import type { MallSpuApi } from '#/api/mall/product/spu';
import { computed, onMounted, ref } from 'vue';
@@ -15,7 +16,7 @@ import { getSpuPage } from '#/api/mall/product/spu';
import { getRangePickerDefaultProps } from '#/utils';
interface SpuTableSelectProps {
multiple?: boolean;
multiple?: boolean; // 是否单选true - checkboxfalse - radio
}
const props = withDefaults(defineProps<SpuTableSelectProps>(), {
@@ -26,9 +27,17 @@ const emit = defineEmits<{
change: [spu: MallSpuApi.Spu | MallSpuApi.Spu[]];
}>();
// TODO @芋艿:要不要加类型;
const categoryList = ref<any[]>([]);
const categoryTreeList = ref<any[]>([]);
const categoryList = ref<MallCategoryApi.Category[]>([]); // 分类列表
const categoryTreeList = ref<any[]>([]); // 分类树
/** 单选:处理选中变化 */
function handleRadioChange() {
const selectedRow = gridApi.grid.getRadioRecord() as MallSpuApi.Spu;
if (selectedRow) {
emit('change', selectedRow);
modalApi.close();
}
}
/** 搜索表单 Schema */
const formSchema = computed<VbenFormSchema[]>(() => [
@@ -45,8 +54,9 @@ const formSchema = computed<VbenFormSchema[]>(() => [
fieldName: 'categoryId',
label: '商品分类',
component: 'TreeSelect',
// TODO @芋艿:可能要测试下;
componentProps: {
treeData: categoryTreeList.value,
treeData: categoryTreeList,
fieldNames: {
label: 'name',
value: 'id',
@@ -121,74 +131,42 @@ const [Grid, gridApi] = useVbenVxeGrid({
columns: gridColumns.value,
height: 500,
border: true,
showOverflow: true,
checkboxConfig: props.multiple
? {
reserve: true,
}
: undefined,
radioConfig: props.multiple
? undefined
: {
reserve: true,
},
checkboxConfig: {
reserve: true,
},
radioConfig: {
reserve: true,
},
rowConfig: {
keyField: 'id',
isHover: true,
},
proxyConfig: {
ajax: {
async query({ page }: any, formValues: any) {
// TODO @芋艿:怎么简化下。
const data = await getSpuPage({
return await getSpuPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
tabType: 0,
name: formValues.name || undefined,
categoryId: formValues.categoryId || undefined,
createTime: formValues.createTime || undefined,
...formValues,
});
return {
items: data.list || [],
total: data.total || 0,
};
},
},
},
},
gridEvents: props.multiple
? {
checkboxChange: handleCheckboxChange,
checkboxAll: handleCheckboxChange,
}
: {
radioChange: handleRadioChange,
},
gridEvents: {
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 @芋艿:看看怎么简化?
showConfirmButton: props.multiple, // 特殊radio 单选情况下,走 handleRadioChange 处理。
onConfirm: () => {
const selectedRows = gridApi.grid.getCheckboxRecords() as MallSpuApi.Spu[];
emit('change', selectedRows);
modalApi.close();
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
gridApi.grid.clearCheckboxRow();
@@ -196,39 +174,52 @@ const [Modal, modalApi] = useVbenModal({
return;
}
// 1. 先查询数据
await gridApi.query();
// 2. 设置已选中行
const data = modalApi.getData<MallSpuApi.Spu | MallSpuApi.Spu[]>();
if (props.multiple && Array.isArray(data)) {
// 等待数据加载完成后再设置选中状态
if (props.multiple && Array.isArray(data) && data.length > 0) {
setTimeout(() => {
const tableData = gridApi.grid.getTableData().fullData;
data.forEach((spu) => {
gridApi.grid.setCheckboxRow(spu, true);
const row = tableData.find(
(item: MallSpuApi.Spu) => item.id === spu.id,
);
if (row) {
gridApi.grid.setCheckboxRow(row, true);
}
});
}, 100);
}, 300);
} else if (!props.multiple && data && !Array.isArray(data)) {
setTimeout(() => {
gridApi.grid.setRadioRow(data);
}, 100);
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();
},
});
/** 初始化分类数据 */
onMounted(async () => {
categoryList.value = await getCategoryList({});
categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId');
});
</script>
<template>
<Modal :class="props.multiple ? 'w-[900px]' : 'w-[800px]'" title="选择商品">
<Modal title="选择商品" class="w-[950px]">
<Grid />
</Modal>
</template>

View File

@@ -50,7 +50,7 @@ export function getCategoryList(params: any) {
);
}
// 获得商品分类列表
/** 获得商品分类列表 */
export function getCategorySimpleList() {
return requestClient.get<MallCategoryApi.Category[]>(
'/product/category/list',

View File

@@ -1,4 +1,3 @@
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';

View File

@@ -59,6 +59,15 @@ const gridColumns = computed<VxeGridProps['columns']>(() => [
},
]);
/** 处理选中 */
function handleRadioChange() {
const selectedRow = gridApi.grid.getRadioRecord() as MallSpuApi.Sku;
if (selectedRow) {
emit('change', selectedRow);
modalApi.close();
}
}
// TODO @芋艿:要不要直接非 pager
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
@@ -89,15 +98,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
},
});
/** 处理选中 */
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) => {

View File

@@ -26,8 +26,8 @@ const props = withDefaults(defineProps<SpuShowcaseProps>(), {
const emit = defineEmits(['update:modelValue', 'change']);
const productSpus = ref<MallSpuApi.Spu[]>([]);
const spuTableSelectRef = ref<InstanceType<typeof SpuTableSelect>>();
const productSpus = ref<MallSpuApi.Spu[]>([]); // 已选择的商品列表
const spuTableSelectRef = ref<InstanceType<typeof SpuTableSelect>>(); // 商品选择表格组件引用
const isMultiple = computed(() => props.limit !== 1); // 是否为多选模式
/** 计算是否可以添加 */
@@ -101,7 +101,7 @@ function emitSpuChange() {
<div
v-for="(spu, index) in productSpus"
:key="spu.id"
class="spu-item group relative"
class="group relative h-[60px] w-[60px] overflow-hidden rounded-lg"
>
<ElTooltip :content="spu.name">
<div class="relative h-full w-full">
@@ -125,7 +125,7 @@ function emitSpuChange() {
<!-- 添加商品按钮 -->
<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"
class="hover:border-primary hover:bg-primary/5 flex h-[60px] w-[60px] 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" />
@@ -140,17 +140,3 @@ function emitSpuChange() {
@change="handleSpuSelected"
/>
</template>
<style scoped>
.spu-item {
width: 60px;
height: 60px;
border-radius: 8px;
overflow: hidden;
}
.spu-add-box {
width: 60px;
height: 60px;
}
</style>

View File

@@ -2,6 +2,7 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { MallCategoryApi } from '#/api/mall/product/category';
import type { MallSpuApi } from '#/api/mall/product/spu';
import { computed, onMounted, ref } from 'vue';
@@ -15,7 +16,7 @@ import { getSpuPage } from '#/api/mall/product/spu';
import { getRangePickerDefaultProps } from '#/utils';
interface SpuTableSelectProps {
multiple?: boolean;
multiple?: boolean; // 是否单选true - checkboxfalse - radio
}
const props = withDefaults(defineProps<SpuTableSelectProps>(), {
@@ -26,9 +27,17 @@ const emit = defineEmits<{
change: [spu: MallSpuApi.Spu | MallSpuApi.Spu[]];
}>();
// TODO @芋艿:要不要加类型;
const categoryList = ref<any[]>([]);
const categoryTreeList = ref<any[]>([]);
const categoryList = ref<MallCategoryApi.Category[]>([]); // 分类列表
const categoryTreeList = ref<any[]>([]); // 分类树
/** 单选:处理选中变化 */
function handleRadioChange() {
const selectedRow = gridApi.grid.getRadioRecord() as MallSpuApi.Spu;
if (selectedRow) {
emit('change', selectedRow);
modalApi.close();
}
}
/** 搜索表单 Schema */
const formSchema = computed<VbenFormSchema[]>(() => [
@@ -38,24 +47,19 @@ const formSchema = computed<VbenFormSchema[]>(() => [
component: 'Input',
componentProps: {
placeholder: '请输入商品名称',
allowClear: true,
clearable: true,
},
},
{
fieldName: 'categoryId',
label: '商品分类',
component: 'TreeSelect',
component: 'ApiTreeSelect',
componentProps: {
treeData: categoryTreeList.value,
fieldNames: {
label: 'name',
value: 'id',
},
treeCheckStrictly: true,
options: categoryTreeList,
props: { label: 'name', children: 'children' },
nodeKey: 'id',
placeholder: '请选择商品分类',
allowClear: true,
showSearch: true,
treeNodeFilterProp: 'name',
clearable: true,
},
},
{
@@ -64,7 +68,7 @@ const formSchema = computed<VbenFormSchema[]>(() => [
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
clearable: true,
},
},
]);
@@ -121,17 +125,12 @@ const [Grid, gridApi] = useVbenVxeGrid({
columns: gridColumns.value,
height: 500,
border: true,
showOverflow: true,
checkboxConfig: props.multiple
? {
reserve: true,
}
: undefined,
radioConfig: props.multiple
? undefined
: {
reserve: true,
},
checkboxConfig: {
reserve: true,
},
radioConfig: {
reserve: true,
},
rowConfig: {
keyField: 'id',
isHover: true,
@@ -149,43 +148,19 @@ const [Grid, gridApi] = useVbenVxeGrid({
},
},
},
gridEvents: props.multiple
? {
checkboxChange: handleCheckboxChange,
checkboxAll: handleCheckboxChange,
}
: {
radioChange: handleRadioChange,
},
gridEvents: {
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 @芋艿:看看怎么简化?
showConfirmButton: props.multiple, // 特殊radio 单选情况下,走 handleRadioChange 处理。
onConfirm: () => {
const selectedRows = gridApi.grid.getCheckboxRecords() as MallSpuApi.Spu[];
emit('change', selectedRows);
modalApi.close();
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
gridApi.grid.clearCheckboxRow();
@@ -193,18 +168,17 @@ const [Modal, modalApi] = useVbenModal({
return;
}
const data = modalApi.getData<MallSpuApi.Spu | MallSpuApi.Spu[]>();
// 先查询数据,确保表格已加载
// 1. 先查询数据
await gridApi.query();
// 2. 设置已选中行
const data = modalApi.getData<MallSpuApi.Spu | MallSpuApi.Spu[]>();
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);
const row = tableData.find(
(item: MallSpuApi.Spu) => item.id === spu.id,
);
if (row) {
gridApi.grid.setCheckboxRow(row, true);
}
@@ -213,8 +187,9 @@ const [Modal, modalApi] = useVbenModal({
} 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);
const row = tableData.find(
(item: MallSpuApi.Spu) => item.id === data.id,
);
if (row) {
gridApi.grid.setRadioRow(row);
}
@@ -223,22 +198,22 @@ const [Modal, modalApi] = useVbenModal({
},
});
/** 初始化分类数据 */
onMounted(async () => {
categoryList.value = await getCategoryList({});
categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId');
});
/** 对外暴露的方法 */
defineExpose({
open: (data?: MallSpuApi.Spu | MallSpuApi.Spu[]) => {
modalApi.setData(data).open();
},
});
/** 初始化分类数据 */
onMounted(async () => {
categoryList.value = await getCategoryList({});
categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId');
});
</script>
<template>
<Modal :class="props.multiple ? 'w-[900px]' : 'w-[800px]'" title="选择商品">
<Modal title="选择商品" class="w-[950px]">
<Grid />
</Modal>
</template>

View File

@@ -7,8 +7,6 @@ import { handleTree } from '@vben/utils';
import { getCategoryList } from '#/api/mall/product/category';
import { getRangePickerDefaultProps } from '#/utils';
// TODO @霖:所有 mall 的 search 少了,请输入 xxx表单也是类似
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [