diff --git a/apps/web-antd/src/views/mall/product/spu/components/index.ts b/apps/web-antd/src/views/mall/product/spu/components/index.ts new file mode 100644 index 000000000..122cbcea0 --- /dev/null +++ b/apps/web-antd/src/views/mall/product/spu/components/index.ts @@ -0,0 +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'; diff --git a/apps/web-antd/src/views/mall/product/spu/form/modules/sku-table-select.vue b/apps/web-antd/src/views/mall/product/spu/components/sku-table-select.vue similarity index 71% rename from apps/web-antd/src/views/mall/product/spu/form/modules/sku-table-select.vue rename to apps/web-antd/src/views/mall/product/spu/components/sku-table-select.vue index 0900c360e..3ff82032f 100644 --- a/apps/web-antd/src/views/mall/product/spu/form/modules/sku-table-select.vue +++ b/apps/web-antd/src/views/mall/product/spu/components/sku-table-select.vue @@ -8,7 +8,7 @@ import { computed, ref } from 'vue'; import { useVbenModal } from '@vben/common-ui'; import { fenToYuan } from '@vben/utils'; -import { Input, message } from 'ant-design-vue'; +import { message } from 'ant-design-vue'; import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { getSpu } from '#/api/mall/product/spu'; @@ -21,18 +21,13 @@ const emit = defineEmits<{ change: [sku: MallSpuApi.Sku]; }>(); -const selectedSkuId = ref(); const spuId = ref(); -/** 配置列 */ -// TODO @puhui999:貌似列太宽了? +/** 表格列配置 */ const gridColumns = computed(() => [ { - field: 'id', - title: '#', - width: 60, - align: 'center', - slots: { default: 'radio-column' }, + type: 'radio', + width: 55, }, { field: 'picUrl', @@ -72,6 +67,9 @@ const [Grid, gridApi] = useVbenVxeGrid({ height: 400, border: true, showOverflow: true, + radioConfig: { + reserve: true, + }, proxyConfig: { ajax: { query: async () => { @@ -93,46 +91,42 @@ const [Grid, gridApi] = useVbenVxeGrid({ }, }, }, + gridEvents: { + radioChange: handleRadioChange, + }, }); /** 处理选中 */ -function handleSelected(row: MallSpuApi.Sku) { - emit('change', row); - modalApi.close(); - selectedSkuId.value = undefined; +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) { - selectedSkuId.value = undefined; + gridApi.grid.clearRadioRow(); spuId.value = undefined; return; } + const data = modalApi.getData(); - // TODO @puhui999:这里要不 if return,让括号的层级简单点。 - if (data?.spuId) { - spuId.value = data.spuId; - // 触发数据查询 - await gridApi.query(); + if (!data?.spuId) { + return; } + + spuId.value = data.spuId; + await gridApi.query(); }, }); diff --git a/apps/web-antd/src/views/mall/product/spu/components/spu-showcase.vue b/apps/web-antd/src/views/mall/product/spu/components/spu-showcase.vue new file mode 100644 index 000000000..1d75f8a8f --- /dev/null +++ b/apps/web-antd/src/views/mall/product/spu/components/spu-showcase.vue @@ -0,0 +1,154 @@ + + + + + + diff --git a/apps/web-antd/src/views/mall/product/spu/components/spu-table-select.vue b/apps/web-antd/src/views/mall/product/spu/components/spu-table-select.vue new file mode 100644 index 000000000..e503a95f8 --- /dev/null +++ b/apps/web-antd/src/views/mall/product/spu/components/spu-table-select.vue @@ -0,0 +1,232 @@ + + + + diff --git a/apps/web-antd/src/views/mall/product/spu/form/modules/sku-list.vue b/apps/web-antd/src/views/mall/product/spu/form/modules/sku-list.vue index b3df262c1..8c3b4a1f1 100644 --- a/apps/web-antd/src/views/mall/product/spu/form/modules/sku-list.vue +++ b/apps/web-antd/src/views/mall/product/spu/form/modules/sku-list.vue @@ -7,7 +7,12 @@ import type { MallSpuApi } from '#/api/mall/product/spu'; import { ref, watch } from 'vue'; -import { copyValueToTarget, formatToFraction, isEmpty } from '@vben/utils'; +import { + copyValueToTarget, + formatToFraction, + getNestedValue, + isEmpty, +} from '@vben/utils'; import { Button, Image, Input, InputNumber, message } from 'ant-design-vue'; @@ -43,9 +48,12 @@ const emit = defineEmits<{ const { isBatch, isDetail, isComponent, isActivityComponent } = props; -const formData: Ref = ref(); // 表单数据 -const skuList = ref([ - { +const formData: Ref = ref(); +const tableHeaders = ref<{ label: string; prop: string }[]>([]); + +/** 创建空 SKU 数据 */ +function createEmptySku(): MallSpuApi.Sku { + return { price: 0, marketPrice: 0, costPrice: 0, @@ -56,8 +64,10 @@ const skuList = ref([ volume: 0, firstBrokeragePrice: 0, secondBrokeragePrice: 0, - }, -]); // 批量添加时的临时数据 + }; +} + +const skuList = ref([createEmptySku()]); /** 批量添加 */ function batchAdd() { @@ -79,34 +89,33 @@ function validateProperty() { } } -/** 删除 sku */ +/** 删除 SKU */ function deleteSku(row: MallSpuApi.Sku) { const index = formData.value!.skus!.findIndex( - // 直接把列表转成字符串比较 (sku: MallSpuApi.Sku) => JSON.stringify(sku.properties) === JSON.stringify(row.properties), ); - formData.value!.skus!.splice(index, 1); + if (index !== -1) { + formData.value!.skus!.splice(index, 1); + } } -const tableHeaders = ref<{ label: string; prop: string }[]>([]); // 多属性表头 - -/** 保存时,每个商品规格的表单要校验下。例如说,销售金额最低是 0.01 这种 */ +/** 校验 SKU 数据:保存时,每个商品规格的表单要校验。例如:销售金额最低是 0.01 */ function validateSku() { validateProperty(); let warningInfo = '请检查商品各行相关属性配置,'; - let validate = true; // 默认通过 + let validate = true; + for (const sku of formData.value!.skus!) { - // 作为活动组件的校验 for (const rule of props?.ruleConfig as RuleConfig[]) { - const arg = getValue(sku, rule.name); - if (!rule.rule(arg)) { - validate = false; // 只要有一个不通过则直接不通过 + const value = getNestedValue(sku, rule.name); + if (!rule.rule(value)) { + validate = false; warningInfo += rule.message; break; } } - // 只要有一个不通过则结束后续的校验 + if (!validate) { message.warning(warningInfo); throw new Error(warningInfo); @@ -114,21 +123,6 @@ function validateSku() { } } -// TODO @puhui999:是不是可以通过 getNestedValue 简化? -function getValue(obj: any, arg: string): unknown { - const keys = arg.split('.'); - let value: any = obj; - for (const key of keys) { - if (value && typeof value === 'object' && key in value) { - value = value[key]; - } else { - value = undefined; - break; - } - } - return value; -} - /** * 选择时触发 * @@ -155,7 +149,6 @@ watch( /** 生成表数据 */ function generateTableData(propertyList: PropertyAndValues[]) { - // 构建数据结构 const propertyValues = propertyList.map((item: PropertyAndValues) => (item.values || []).map((v: { id: number; name: string }) => ({ propertyId: item.id, @@ -164,35 +157,30 @@ function generateTableData(propertyList: PropertyAndValues[]) { valueName: v.name, })), ); + const buildSkuList = build(propertyValues); + // 如果回显的 sku 属性和添加的属性不一致则重置 skus 列表 if (!validateData(propertyList)) { - // 如果不一致则重置表数据,默认添加新的属性重新生成 sku 列表 formData.value!.skus = []; } + for (const item of buildSkuList) { + const properties = Array.isArray(item) ? item : [item]; const row = { - properties: Array.isArray(item) ? item : [item], // 如果只有一个属性的话返回的是一个 property 对象 - price: 0, - marketPrice: 0, - costPrice: 0, - barCode: '', - picUrl: '', - stock: 0, - weight: 0, - volume: 0, - firstBrokeragePrice: 0, - secondBrokeragePrice: 0, + ...createEmptySku(), + properties, }; + // 如果存在属性相同的 sku 则不做处理 - const index = formData.value!.skus!.findIndex( + const exists = formData.value!.skus!.some( (sku: MallSpuApi.Sku) => JSON.stringify(sku.properties) === JSON.stringify(row.properties), ); - if (index !== -1) { - continue; + + if (!exists) { + formData.value!.skus!.push(row); } - formData.value!.skus!.push(row); } } @@ -248,43 +236,33 @@ watch( if (!formData.value!.specType) { return; } + // 如果当前组件作为批量添加数据使用,则重置表数据 if (props.isBatch) { - skuList.value = [ - { - price: 0, - marketPrice: 0, - costPrice: 0, - barCode: '', - picUrl: '', - stock: 0, - weight: 0, - volume: 0, - firstBrokeragePrice: 0, - secondBrokeragePrice: 0, - }, - ]; + skuList.value = [createEmptySku()]; } // 判断代理对象是否为空 if (JSON.stringify(propertyList) === '[]') { return; } - // 重置表头 - tableHeaders.value = []; - // 生成表头 - propertyList.forEach((item, index) => { - // name加属性项index区分属性值 - tableHeaders.value.push({ prop: `name${index}`, label: item.name }); - }); + + // 重置并生成表头 + tableHeaders.value = propertyList.map((item, index) => ({ + prop: `name${index}`, + label: item.name, + })); + // 如果回显的 sku 属性和添加的属性一致则不处理 if (validateData(propertyList)) { return; } + // 添加新属性没有属性值也不做处理 if (propertyList.some((item) => !item.values || isEmpty(item.values))) { return; } + // 生成 table 数据,即 sku 列表 generateTableData(propertyList); }, @@ -296,17 +274,23 @@ watch( const activitySkuListRef = ref(); +/** 获取 SKU 表格引用 */ function getSkuTableRef() { return activitySkuListRef.value; } -defineExpose({ generateTableData, validateSku, getSkuTableRef }); +defineExpose({ + generateTableData, + validateSku, + getSkuTableRef, +});