feat:【antd】mall 发布界面的评审

pull/238/MERGE
YunaiV 2025-10-22 23:53:48 +08:00
parent 495a924d56
commit ebc7aba637
10 changed files with 176 additions and 191 deletions

View File

@ -4,7 +4,6 @@ import { DeliveryTypeEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks'; import { getDictOptions } from '@vben/hooks';
import { handleTree } from '@vben/utils'; import { handleTree } from '@vben/utils';
import { z } from '#/adapter/form';
import { getSimpleBrandList } from '#/api/mall/product/brand'; import { getSimpleBrandList } from '#/api/mall/product/brand';
import { getCategoryList } from '#/api/mall/product/category'; import { getCategoryList } from '#/api/mall/product/category';
import { getSimpleTemplateList } from '#/api/mall/trade/delivery/expressTemplate'; import { getSimpleTemplateList } from '#/api/mall/trade/delivery/expressTemplate';
@ -33,7 +32,6 @@ export function useInfoFormSchema(): VbenFormSchema[] {
{ {
fieldName: 'categoryId', fieldName: 'categoryId',
label: '分类名称', label: '分类名称',
// component: 'ApiCascader',
component: 'ApiTreeSelect', component: 'ApiTreeSelect',
componentProps: { componentProps: {
api: async () => { api: async () => {
@ -285,7 +283,7 @@ export function useOtherFormSchema(): VbenFormSchema[] {
componentProps: { componentProps: {
min: 0, min: 0,
}, },
rules: z.number().min(0).optional().default(0), rules: 'required',
}, },
{ {
fieldName: 'giveIntegral', fieldName: 'giveIntegral',
@ -294,7 +292,7 @@ export function useOtherFormSchema(): VbenFormSchema[] {
componentProps: { componentProps: {
min: 0, min: 0,
}, },
rules: z.number().min(0).optional().default(0), rules: 'required',
}, },
{ {
fieldName: 'virtualSalesCount', fieldName: 'virtualSalesCount',
@ -303,7 +301,7 @@ export function useOtherFormSchema(): VbenFormSchema[] {
componentProps: { componentProps: {
min: 0, min: 0,
}, },
rules: z.number().min(0).optional().default(0), rules: 'required',
}, },
]; ];
} }

View File

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { MallSpuApi } from '#/api/mall/product/spu'; import type { MallSpuApi } from '#/api/mall/product/spu';
// TODO @puhui999这个是不是 api 后端有定义类似的?如果是,是不是放到 api 哈?
export interface PropertyAndValues { export interface PropertyAndValues {
id: number; id: number;
name: string; name: string;
@ -23,12 +24,8 @@ export interface RuleConfig {
message: string; message: string;
} }
/** // TODO @puhui999这个是只有 index.ts 在用么?还是别的模块也会用
* - /** 获得商品的规格列表 - 商品相关的公共函数 */
*
* @param spu
* @return PropertyAndValues
*/
const getPropertyList = (spu: MallSpuApi.Spu): PropertyAndValues[] => { const getPropertyList = (spu: MallSpuApi.Spu): PropertyAndValues[] => {
// 直接拿返回的 skus 属性逆向生成出 propertyList // 直接拿返回的 skus 属性逆向生成出 propertyList
const properties: PropertyAndValues[] = []; const properties: PropertyAndValues[] = [];
@ -62,4 +59,5 @@ const getPropertyList = (spu: MallSpuApi.Spu): PropertyAndValues[] => {
export { getPropertyList }; export { getPropertyList };
// 导出组件 // 导出组件
// TODO @puhui999如果 sku-list.vue 要对外,可以考虑在 spu 下面,搞个 components 模块目前看别的模块应该会用到哈。modules 是当前模块用到的components 是跨模块要用到的。
export { default as SkuList } from './modules/sku-list.vue'; export { default as SkuList } from './modules/sku-list.vue';

View File

@ -8,7 +8,7 @@ import { useRoute } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { useTabs } from '@vben/hooks'; import { useTabs } from '@vben/hooks';
import { convertToInteger, floatToFixed2, formatToFraction } from '@vben/utils'; import { convertToInteger, formatToFraction } from '@vben/utils';
import { Button, Card, message } from 'ant-design-vue'; import { Button, Card, message } from 'ant-design-vue';
@ -31,11 +31,6 @@ const spuId = ref<number>();
const { params, name } = useRoute(); const { params, name } = useRoute();
const { closeCurrentTab } = useTabs(); const { closeCurrentTab } = useTabs();
const activeTabName = ref('info'); const activeTabName = ref('info');
function onTabChange(key: string) {
activeTabName.value = key;
}
const tabList = ref([ const tabList = ref([
{ {
key: 'info', key: 'info',
@ -58,44 +53,43 @@ const tabList = ref([
tab: '其它设置', tab: '其它设置',
}, },
]); ]);
// spu
const formData = ref<MallSpuApi.Spu>({ const formLoading = ref(false); // 12
name: '', // const isDetail = ref(name === 'ProductSpuDetail'); //
categoryId: undefined, //
keyword: '', //
picUrl: '', //
sliderPicUrls: [], //
introduction: '', //
deliveryTypes: [], //
deliveryTemplateId: undefined, //
brandId: undefined, //
specType: false, //
subCommissionType: false, //
skus: [
{
price: 0, //
marketPrice: 0, //
costPrice: 0, //
barCode: '', //
picUrl: '', //
stock: 0, //
weight: 0, //
volume: 0, //
firstBrokeragePrice: 0, //
secondBrokeragePrice: 0, //
},
],
description: '', //
sort: 0, //
giveIntegral: 0, //
virtualSalesCount: 0, //
});
const propertyList = ref<PropertyAndValues[]>([]); //
const formLoading = ref(true); // 12
const isDetail = ref(false); //
const skuListRef = ref(); // Ref const skuListRef = ref(); // Ref
// sku const formData = ref<MallSpuApi.Spu>({
name: '',
categoryId: undefined,
keyword: '',
picUrl: '',
sliderPicUrls: [],
introduction: '',
deliveryTypes: [],
deliveryTemplateId: undefined,
brandId: undefined,
specType: false,
subCommissionType: false,
skus: [
{
price: 0,
marketPrice: 0,
costPrice: 0,
barCode: '',
picUrl: '',
stock: 0,
weight: 0,
volume: 0,
firstBrokeragePrice: 0,
secondBrokeragePrice: 0,
},
],
description: '',
sort: 0,
giveIntegral: 0,
virtualSalesCount: 0,
}); // spu
const propertyList = ref<PropertyAndValues[]>([]); //
const ruleConfig: RuleConfig[] = [ const ruleConfig: RuleConfig[] = [
{ {
name: 'stock', name: 'stock',
@ -117,7 +111,7 @@ const ruleConfig: RuleConfig[] = [
rule: (arg) => arg >= 0.01, rule: (arg) => arg >= 0.01,
message: '商品成本价格必须大于等于 0.00 元!!!', message: '商品成本价格必须大于等于 0.00 元!!!',
}, },
]; ]; // sku
const [InfoForm, infoFormApi] = useVbenForm({ const [InfoForm, infoFormApi] = useVbenForm({
commonConfig: { commonConfig: {
@ -146,11 +140,11 @@ const [SkuForm, skuFormApi] = useVbenForm({
handleValuesChange: (values, fieldsChanged) => { handleValuesChange: (values, fieldsChanged) => {
if (fieldsChanged.includes('subCommissionType')) { if (fieldsChanged.includes('subCommissionType')) {
formData.value.subCommissionType = values.subCommissionType; formData.value.subCommissionType = values.subCommissionType;
changeSubCommissionType(); handleChangeSubCommissionType();
} }
if (fieldsChanged.includes('specType')) { if (fieldsChanged.includes('specType')) {
formData.value.specType = values.specType; formData.value.specType = values.specType;
onChangeSpec(); handleChangeSpec();
} }
}, },
}); });
@ -199,7 +193,13 @@ const [OtherForm, otherFormApi] = useVbenForm({
showDefaultActions: false, showDefaultActions: false,
}); });
async function onSubmit() { /** tab 切换 */
function handleTabChange(key: string) {
activeTabName.value = key;
}
/** 提交表单 */
async function handleSubmit() {
const values: MallSpuApi.Spu = await infoFormApi const values: MallSpuApi.Spu = await infoFormApi
.merge(skuFormApi) .merge(skuFormApi)
.merge(deliveryFormApi) .merge(deliveryFormApi)
@ -216,7 +216,7 @@ async function onSubmit() {
return; return;
} }
values.skus.forEach((item) => { values.skus.forEach((item) => {
// sku //
item.price = convertToInteger(item.price); item.price = convertToInteger(item.price);
item.marketPrice = convertToInteger(item.marketPrice); item.marketPrice = convertToInteger(item.marketPrice);
item.costPrice = convertToInteger(item.costPrice); item.costPrice = convertToInteger(item.costPrice);
@ -224,7 +224,7 @@ async function onSubmit() {
item.secondBrokeragePrice = convertToInteger(item.secondBrokeragePrice); item.secondBrokeragePrice = convertToInteger(item.secondBrokeragePrice);
}); });
} }
// // TODO @puhui999
const newSliderPicUrls: any[] = []; const newSliderPicUrls: any[] = [];
values.sliderPicUrls!.forEach((item: any) => { values.sliderPicUrls!.forEach((item: any) => {
// //
@ -234,12 +234,13 @@ async function onSubmit() {
}); });
values.sliderPicUrls = newSliderPicUrls; values.sliderPicUrls = newSliderPicUrls;
//
await (spuId.value ? updateSpu(values) : createSpu(values)); await (spuId.value ? updateSpu(values) : createSpu(values));
} }
/** 获得详情 */ /** 获得详情 */
async function getDetail() { async function getDetail() {
if (name === 'ProductSpuDetail') { if (isDetail.value) {
isDetail.value = true; isDetail.value = true;
infoFormApi.setDisabled(true); infoFormApi.setDisabled(true);
skuFormApi.setDisabled(true); skuFormApi.setDisabled(true);
@ -247,45 +248,36 @@ async function getDetail() {
descriptionFormApi.setDisabled(true); descriptionFormApi.setDisabled(true);
otherFormApi.setDisabled(true); otherFormApi.setDisabled(true);
} }
const id = params.id as unknown as number;
if (id) {
try {
const res = await getSpu(spuId.value!);
res.skus?.forEach((item) => {
if (isDetail.value) {
item.price = floatToFixed2(item.price);
item.marketPrice = floatToFixed2(item.marketPrice);
item.costPrice = floatToFixed2(item.costPrice);
item.firstBrokeragePrice = floatToFixed2(item.firstBrokeragePrice);
item.secondBrokeragePrice = floatToFixed2(item.secondBrokeragePrice);
} else {
//
item.price = formatToFraction(item.price);
item.marketPrice = formatToFraction(item.marketPrice);
item.costPrice = formatToFraction(item.costPrice);
item.firstBrokeragePrice = formatToFraction(item.firstBrokeragePrice);
item.secondBrokeragePrice = formatToFraction(
item.secondBrokeragePrice,
);
}
});
formData.value = res;
//
infoFormApi.setValues(res);
skuFormApi.setValues(res);
deliveryFormApi.setValues(res);
descriptionFormApi.setValues(res);
otherFormApi.setValues(res);
} finally {
formLoading.value = false;
}
}
// SKU PropertyAndValues // SKU PropertyAndValues
propertyList.value = getPropertyList(formData.value); propertyList.value = getPropertyList(formData.value);
formLoading.value = true;
try {
const res = await getSpu(spuId.value!);
//
res.skus?.forEach((item) => {
item.price = formatToFraction(item.price);
item.marketPrice = formatToFraction(item.marketPrice);
item.costPrice = formatToFraction(item.costPrice);
item.firstBrokeragePrice = formatToFraction(item.firstBrokeragePrice);
item.secondBrokeragePrice = formatToFraction(item.secondBrokeragePrice);
});
formData.value = res;
//
infoFormApi.setValues(res).then();
skuFormApi.setValues(res).then();
deliveryFormApi.setValues(res).then();
descriptionFormApi.setValues(res).then();
otherFormApi.setValues(res).then();
// SKU PropertyAndValues
propertyList.value = getPropertyList(formData.value);
} finally {
formLoading.value = false;
}
} }
// =========== sku form =========== // =========== sku form ===========
/** 打开属性添加表单 */
function openPropertyAddForm() { function openPropertyAddForm() {
productPropertyAddFormApi.open(); productPropertyAddFormApi.open();
} }
@ -296,7 +288,7 @@ function generateSkus(propertyList: any[]) {
} }
/** 分销类型 */ /** 分销类型 */
function changeSubCommissionType() { function handleChangeSubCommissionType() {
// //
for (const item of formData.value.skus!) { for (const item of formData.value.skus!) {
item.firstBrokeragePrice = 0; item.firstBrokeragePrice = 0;
@ -305,10 +297,10 @@ function changeSubCommissionType() {
} }
/** 选择规格 */ /** 选择规格 */
function onChangeSpec() { function handleChangeSpec() {
// //
propertyList.value = []; propertyList.value = [];
// sku // sku
formData.value.skus = [ formData.value.skus = [
{ {
price: 0, price: 0,
@ -325,7 +317,7 @@ function onChangeSpec() {
]; ];
} }
// sku form schema /** 监听 sku form schema 变化,更新表单 */
watch( watch(
propertyList, propertyList,
() => { () => {
@ -336,6 +328,7 @@ watch(
{ deep: true }, { deep: true },
); );
/** 初始化 */
onMounted(async () => { onMounted(async () => {
spuId.value = params.id as unknown as number; spuId.value = params.id as unknown as number;
if (!spuId.value) { if (!spuId.value) {
@ -355,18 +348,18 @@ onMounted(async () => {
:loading="formLoading" :loading="formLoading"
:tab-list="tabList" :tab-list="tabList"
:active-key="activeTabName" :active-key="activeTabName"
@tab-change="onTabChange" @tab-change="handleTabChange"
> >
<template #tabBarExtraContent> <template #tabBarExtraContent>
<Button type="primary" v-if="!isDetail" @click="onSubmit"> <Button type="primary" v-if="!isDetail" @click="handleSubmit">
保存 保存
</Button> </Button>
<Button type="default" v-else @click="() => closeCurrentTab()"> <Button type="default" v-else @click="() => closeCurrentTab()">
返回列表 返回列表
</Button> </Button>
</template> </template>
<InfoForm class="w-3/5" v-show="activeTabName === 'info'" />
<InfoForm class="w-3/5" v-show="activeTabName === 'info'" />
<SkuForm class="w-full" v-show="activeTabName === 'sku'"> <SkuForm class="w-full" v-show="activeTabName === 'sku'">
<template #singleSkuList> <template #singleSkuList>
<SkuList <SkuList
@ -418,6 +411,7 @@ onMounted(async () => {
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
// TODO @puhui999
:deep(.ant-tabs-tab-btn) { :deep(.ant-tabs-tab-btn) {
font-size: 14px !important; font-size: 14px !important;
} }

View File

@ -21,7 +21,6 @@ const props = withDefaults(defineProps<Props>(), {
isDetail: false, isDetail: false,
}); });
/** 输入框失去焦点或点击回车时触发 */
const emit = defineEmits(['success']); const emit = defineEmits(['success']);
interface Props { interface Props {
@ -30,12 +29,15 @@ interface Props {
} }
const inputValue = ref<string[]>([]); // tags 使 const inputValue = ref<string[]>([]); // tags 使
const attributeIndex = ref<null | number>(null); // index const attributeIndex = ref<null | number>(null); // index
//
const inputVisible = computed(() => (index: number) => { const inputVisible = computed(() => (index: number) => {
if (attributeIndex.value === null) return false; if (attributeIndex.value === null) {
if (attributeIndex.value === index) return true; return false;
}); }
if (attributeIndex.value === index) {
return true;
}
}); //
interface InputRefItem { interface InputRefItem {
inputRef?: { inputRef?: {
@ -46,7 +48,10 @@ interface InputRefItem {
focus: () => void; focus: () => void;
} }
const inputRef = ref<InputRefItem[]>([]); // Ref const inputRef = ref<InputRefItem[]>([]); // Ref
const attributeList = ref<PropertyAndValues[]>([]); //
const attributeOptions = ref<MallPropertyApi.PropertyValue[]>([]); //
/** 解决 ref 在 v-for 中的获取问题*/ /** 解决 ref 在 v-for 中的获取问题*/
function setInputRef(el: any) { function setInputRef(el: any) {
if (el === null || el === undefined) return; if (el === null || el === undefined) return;
@ -59,13 +64,13 @@ function setInputRef(el: any) {
inputRef.value.push(el); inputRef.value.push(el);
} }
} }
const attributeList = ref<PropertyAndValues[]>([]); //
const attributeOptions = ref<MallPropertyApi.PropertyValue[]>([]); //
watch( watch(
() => props.propertyList, () => props.propertyList,
(data) => { (data) => {
if (!data) return; if (!data) {
return;
}
attributeList.value = data; attributeList.value = data;
}, },
{ {
@ -74,12 +79,12 @@ watch(
}, },
); );
/** 删除属性值*/ /** 删除属性值 */
function handleCloseValue(index: number, valueIndex: number) { function handleCloseValue(index: number, valueIndex: number) {
attributeList.value?.[index]?.values?.splice(valueIndex, 1); attributeList.value?.[index]?.values?.splice(valueIndex, 1);
} }
/** 删除属性*/ /** 删除属性 */
function handleCloseProperty(index: number) { function handleCloseProperty(index: number) {
attributeList.value?.splice(index, 1); attributeList.value?.splice(index, 1);
emit('success', attributeList.value); emit('success', attributeList.value);
@ -93,7 +98,7 @@ async function showInput(index: number) {
await getAttributeOptions(attributeList.value?.[index]?.id!); await getAttributeOptions(attributeList.value?.[index]?.id!);
} }
// success /** 定义 success 事件,用于操作成功后的回调 */
async function handleInputConfirm(index: number, propertyId: number) { async function handleInputConfirm(index: number, propertyId: number) {
// tags inputValue // tags inputValue
const currentValue = inputValue.value?.[inputValue.value.length - 1]?.trim(); const currentValue = inputValue.value?.[inputValue.value.length - 1]?.trim();
@ -154,6 +159,7 @@ async function getAttributeOptions(propertyId: number) {
<template> <template>
<Col v-for="(item, index) in attributeList" :key="index"> <Col v-for="(item, index) in attributeList" :key="index">
<!-- TODO @puhui9991间隙可以看看2)vue3 + element-plus 添加属性这个按钮是和属性名在一排感觉更好看点 -->
<div> <div>
<span class="mx-1">属性名</span> <span class="mx-1">属性名</span>
<Tag <Tag
@ -174,6 +180,7 @@ async function getAttributeOptions(propertyId: number) {
class="mx-1" class="mx-1"
@close="handleCloseValue(index, valueIndex)" @close="handleCloseValue(index, valueIndex)"
> >
<!-- TODO @puhui999这里貌似爆红idea -->
{{ value.name }} {{ value.name }}
</Tag> </Tag>
<Select <Select

View File

@ -35,7 +35,9 @@ const attributeOptions = ref<MallPropertyApi.Property[]>([]); // 商品属性名
watch( watch(
() => props.propertyList, () => props.propertyList,
(data) => { (data) => {
if (!data) return; if (!data) {
return;
}
attributeList.value = data as any[]; attributeList.value = data as any[];
}, },
{ {
@ -44,7 +46,6 @@ watch(
}, },
); );
//
const formSchema: VbenFormSchema[] = [ const formSchema: VbenFormSchema[] = [
{ {
fieldName: 'name', fieldName: 'name',
@ -62,7 +63,6 @@ const formSchema: VbenFormSchema[] = [
showSearch: true, showSearch: true,
filterOption: true, filterOption: true,
placeholder: '请选择属性名称。如果不存在,可手动输入选择', placeholder: '请选择属性名称。如果不存在,可手动输入选择',
//
mode: 'tags', mode: 'tags',
maxTagCount: 1, maxTagCount: 1,
allowClear: true, allowClear: true,
@ -71,7 +71,6 @@ const formSchema: VbenFormSchema[] = [
}, },
]; ];
//
const [Form, formApi] = useVbenForm({ const [Form, formApi] = useVbenForm({
commonConfig: { commonConfig: {
componentProps: { componentProps: {
@ -85,16 +84,15 @@ const [Form, formApi] = useVbenForm({
showDefaultActions: false, showDefaultActions: false,
}); });
//
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
destroyOnClose: true, destroyOnClose: true,
async onConfirm() { async onConfirm() {
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) return; if (!valid) {
return;
}
const values = await formApi.getValues(); const values = await formApi.getValues();
const name = Array.isArray(values.name) ? values.name[0] : values.name; const name = Array.isArray(values.name) ? values.name[0] : values.name;
// //
for (const attrItem of attributeList.value) { for (const attrItem of attributeList.value) {
if (attrItem.name === name) { if (attrItem.name === name) {
@ -103,6 +101,8 @@ const [Modal, modalApi] = useVbenModal({
} }
} }
// TODO @puhui999modalApi.lock();
// 使 // 使
const existProperty = attributeOptions.value.find( const existProperty = attributeOptions.value.find(
(item: MallPropertyApi.Property) => item.name === name, (item: MallPropertyApi.Property) => item.name === name,
@ -113,6 +113,7 @@ const [Modal, modalApi] = useVbenModal({
name, name,
values: [], values: [],
}); });
// TODO @puhui999 if else await modalApi.close(); emit('success'); add existProperty
await modalApi.close(); await modalApi.close();
emit('success'); emit('success');
return; return;
@ -132,7 +133,6 @@ const [Modal, modalApi] = useVbenModal({
await modalApi.close(); await modalApi.close();
emit('success'); emit('success');
} catch (error) { } catch (error) {
//
console.error('添加属性失败:', error); console.error('添加属性失败:', error);
} }
}, },
@ -140,7 +140,6 @@ const [Modal, modalApi] = useVbenModal({
if (!isOpen) { if (!isOpen) {
return; return;
} }
//
await formApi.resetForm(); await formApi.resetForm();
}, },
}); });

View File

@ -46,16 +46,16 @@ const { isBatch, isDetail, isComponent, isActivityComponent } = props;
const formData: Ref<MallSpuApi.Spu | undefined> = ref<MallSpuApi.Spu>(); // const formData: Ref<MallSpuApi.Spu | undefined> = ref<MallSpuApi.Spu>(); //
const skuList = ref<MallSpuApi.Sku[]>([ const skuList = ref<MallSpuApi.Sku[]>([
{ {
price: 0, // price: 0,
marketPrice: 0, // marketPrice: 0,
costPrice: 0, // costPrice: 0,
barCode: '', // barCode: '',
picUrl: '', // picUrl: '',
stock: 0, // stock: 0,
weight: 0, // weight: 0,
volume: 0, // volume: 0,
firstBrokeragePrice: 0, // firstBrokeragePrice: 0,
secondBrokeragePrice: 0, // secondBrokeragePrice: 0,
}, },
]); // ]); //
@ -91,9 +91,7 @@ function deleteSku(row: MallSpuApi.Sku) {
const tableHeaders = ref<{ label: string; prop: string }[]>([]); // const tableHeaders = ref<{ label: string; prop: string }[]>([]); //
/** /** 保存时,每个商品规格的表单要校验下。例如说,销售金额最低是 0.01 这种 */
* 保存时每个商品规格的表单要校验下例如说,销售金额最低是 0.01 这种
*/
function validateSku() { function validateSku() {
validateProperty(); validateProperty();
let warningInfo = '请检查商品各行相关属性配置,'; let warningInfo = '请检查商品各行相关属性配置,';
@ -116,6 +114,7 @@ function validateSku() {
} }
} }
// TODO @puhui999 getNestedValue
function getValue(obj: any, arg: string): unknown { function getValue(obj: any, arg: string): unknown {
const keys = arg.split('.'); const keys = arg.split('.');
let value: any = obj; let value: any = obj;
@ -132,19 +131,20 @@ function getValue(obj: any, arg: string): unknown {
/** /**
* 选择时触发 * 选择时触发
*
* @param records 传递过来的选中的 sku 是一个数组 * @param records 传递过来的选中的 sku 是一个数组
*/ */
function handleSelectionChange({ records }: { records: MallSpuApi.Sku[] }) { function handleSelectionChange({ records }: { records: MallSpuApi.Sku[] }) {
emit('selectionChange', records); emit('selectionChange', records);
} }
/** /** 将传进来的值赋值给 skuList */
* 将传进来的值赋值给 skuList
*/
watch( watch(
() => props.propFormData, () => props.propFormData,
(data) => { (data) => {
if (!data) return; if (!data) {
return;
}
formData.value = data; formData.value = data;
}, },
{ {
@ -196,9 +196,7 @@ function generateTableData(propertyList: PropertyAndValues[]) {
} }
} }
/** /** 生成 skus 前置校验 */
* 生成 skus 前置校验
*/
function validateData(propertyList: PropertyAndValues[]): boolean { function validateData(propertyList: PropertyAndValues[]): boolean {
const skuPropertyIds: number[] = []; const skuPropertyIds: number[] = [];
formData.value!.skus!.forEach((sku: MallSpuApi.Sku) => formData.value!.skus!.forEach((sku: MallSpuApi.Sku) =>
@ -302,13 +300,13 @@ function getSkuTableRef() {
return activitySkuListRef.value; return activitySkuListRef.value;
} }
// sku
defineExpose({ generateTableData, validateSku, getSkuTableRef }); defineExpose({ generateTableData, validateSku, getSkuTableRef });
</script> </script>
<template> <template>
<div> <div>
<!-- 情况一添加/修改 --> <!-- 情况一添加/修改 -->
<!-- TODO @puhui999有可以通过 grid 来做么主要考虑这样不直接使用 vxe 标签抽象程度更高 -->
<VxeTable <VxeTable
v-if="!isDetail && !isActivityComponent" v-if="!isDetail && !isActivityComponent"
:data="isBatch ? skuList : formData?.skus || []" :data="isBatch ? skuList : formData?.skus || []"
@ -328,7 +326,7 @@ defineExpose({ generateTableData, validateSku, getSkuTableRef });
</template> </template>
</VxeColumn> </VxeColumn>
<template v-if="formData?.specType && !isBatch"> <template v-if="formData?.specType && !isBatch">
<!-- 根据商品属性动态添加 --> <!-- 根据商品属性动态添加 -->
<VxeColumn <VxeColumn
v-for="(item, index) in tableHeaders" v-for="(item, index) in tableHeaders"
:key="index" :key="index"
@ -481,7 +479,7 @@ defineExpose({ generateTableData, validateSku, getSkuTableRef });
</template> </template>
</VxeColumn> </VxeColumn>
<template v-if="formData?.specType && !isBatch"> <template v-if="formData?.specType && !isBatch">
<!-- 根据商品属性动态添加 --> <!-- 根据商品属性动态添加 -->
<VxeColumn <VxeColumn
v-for="(item, index) in tableHeaders" v-for="(item, index) in tableHeaders"
:key="index" :key="index"
@ -565,7 +563,7 @@ defineExpose({ generateTableData, validateSku, getSkuTableRef });
</template> </template>
</VxeColumn> </VxeColumn>
<template v-if="formData?.specType"> <template v-if="formData?.specType">
<!-- 根据商品属性动态添加 --> <!-- 根据商品属性动态添加 -->
<VxeColumn <VxeColumn
v-for="(item, index) in tableHeaders" v-for="(item, index) in tableHeaders"
:key="index" :key="index"
@ -605,7 +603,7 @@ defineExpose({ generateTableData, validateSku, getSkuTableRef });
{{ row.stock }} {{ row.stock }}
</template> </template>
</VxeColumn> </VxeColumn>
<!-- 方便扩展每个活动配置的属性不一样 --> <!-- 方便扩展每个活动配置的属性不一样 -->
<slot name="extension"></slot> <slot name="extension"></slot>
</VxeTable> </VxeTable>
</div> </div>

View File

@ -24,7 +24,8 @@ const emit = defineEmits<{
const selectedSkuId = ref<number>(); const selectedSkuId = ref<number>();
const spuId = ref<number>(); const spuId = ref<number>();
// /** 配置列 */
// TODO @puhui999
const gridColumns = computed<VxeGridProps['columns']>(() => [ const gridColumns = computed<VxeGridProps['columns']>(() => [
{ {
field: 'id', field: 'id',
@ -65,7 +66,6 @@ const gridColumns = computed<VxeGridProps['columns']>(() => [
}, },
]); ]);
//
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: { gridOptions: {
columns: gridColumns.value, columns: gridColumns.value,
@ -95,14 +95,13 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
}); });
// /** 处理选中 */
function handleSelected(row: MallSpuApi.Sku) { function handleSelected(row: MallSpuApi.Sku) {
emit('change', row); emit('change', row);
modalApi.close(); modalApi.close();
selectedSkuId.value = undefined; selectedSkuId.value = undefined;
} }
//
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
destroyOnClose: true, destroyOnClose: true,
onOpenChange: async (isOpen: boolean) => { onOpenChange: async (isOpen: boolean) => {
@ -111,8 +110,8 @@ const [Modal, modalApi] = useVbenModal({
spuId.value = undefined; spuId.value = undefined;
return; return;
} }
const data = modalApi.getData<SpuData>(); const data = modalApi.getData<SpuData>();
// TODO @puhui999 if return
if (data?.spuId) { if (data?.spuId) {
spuId.value = data.spuId; spuId.value = data.spuId;
// //
@ -125,7 +124,6 @@ const [Modal, modalApi] = useVbenModal({
<template> <template>
<Modal class="w-[700px]" title="选择规格"> <Modal class="w-[700px]" title="选择规格">
<Grid> <Grid>
<!-- 单选列 -->
<template #radio-column="{ row }"> <template #radio-column="{ row }">
<Input <Input
v-model="selectedSkuId" v-model="selectedSkuId"

View File

@ -1,5 +1,6 @@
<!-- SPU 商品选择弹窗组件 --> <!-- SPU 商品选择弹窗组件 -->
<script lang="ts" setup> <script lang="ts" setup>
// TODO @puhui999 components
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface'; import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface';
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
@ -30,33 +31,15 @@ const emit = defineEmits<{
change: [spu: MallSpuApi.Spu | MallSpuApi.Spu[]]; change: [spu: MallSpuApi.Spu | MallSpuApi.Spu[]];
}>(); }>();
// SPU ID const selectedSpuId = ref<number>(); // SPU ID
const selectedSpuId = ref<number>(); const checkedStatus = ref<Record<number, boolean>>({}); // map
// map const checkedSpus = ref<MallSpuApi.Spu[]>([]); // SPU
const checkedStatus = ref<Record<number, boolean>>({}); const isCheckAll = ref(false); //
// SPU const isIndeterminate = ref(false); //
const checkedSpus = ref<MallSpuApi.Spu[]>([]);
//
const isCheckAll = ref(false);
//
const isIndeterminate = ref(false);
// const categoryList = ref<any[]>([]); //
const categoryList = ref<any[]>([]); const categoryTreeList = ref<any[]>([]); //
//
const categoryTreeList = ref<any[]>([]);
//
onMounted(async () => {
try {
categoryList.value = await getCategoryList({});
categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId');
} catch (error) {
console.error('加载分类数据失败:', error);
}
});
//
const formSchema = computed<VbenFormSchema[]>(() => { const formSchema = computed<VbenFormSchema[]>(() => {
return [ return [
{ {
@ -97,7 +80,6 @@ const formSchema = computed<VbenFormSchema[]>(() => {
]; ];
}); });
//
const gridColumns = computed<VxeGridProps['columns']>(() => { const gridColumns = computed<VxeGridProps['columns']>(() => {
const columns: VxeGridProps['columns'] = []; const columns: VxeGridProps['columns'] = [];
@ -121,7 +103,7 @@ const gridColumns = computed<VxeGridProps['columns']>(() => {
}); });
} }
// //
columns.push( columns.push(
{ {
field: 'id', field: 'id',
@ -157,7 +139,6 @@ const gridColumns = computed<VxeGridProps['columns']>(() => {
return columns; return columns;
}); });
//
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { formOptions: {
schema: formSchema.value, schema: formSchema.value,
@ -172,6 +153,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
proxyConfig: { proxyConfig: {
ajax: { ajax: {
async query({ page }: any, formValues: any) { async query({ page }: any, formValues: any) {
// TODO @puhui999 try catch
try { try {
const params = { const params = {
pageNo: page.currentPage, pageNo: page.currentPage,
@ -182,6 +164,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
createTime: formValues.createTime || undefined, createTime: formValues.createTime || undefined,
}; };
// TODO @puhui999 params getSpuPage
const data = await getSpuPage(params); const data = await getSpuPage(params);
// //
@ -208,14 +191,15 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
}); });
// // TODO @puhui999 Grid
/** 单选:处理选中 */
function handleSingleSelected(row: MallSpuApi.Spu) { function handleSingleSelected(row: MallSpuApi.Spu) {
selectedSpuId.value = row.id; selectedSpuId.value = row.id;
emit('change', row); emit('change', row);
modalApi.close(); modalApi.close();
} }
// / /** 多选:全选/全不选 */
function handleCheckAll(e: CheckboxChangeEvent) { function handleCheckAll(e: CheckboxChangeEvent) {
const checked = e.target.checked; const checked = e.target.checked;
isCheckAll.value = checked; isCheckAll.value = checked;
@ -228,7 +212,7 @@ function handleCheckAll(e: CheckboxChangeEvent) {
calculateIsCheckAll(); calculateIsCheckAll();
} }
// /** 多选:选中单个 */
function handleCheckOne( function handleCheckOne(
checked: boolean, checked: boolean,
spu: MallSpuApi.Spu, spu: MallSpuApi.Spu,
@ -255,7 +239,7 @@ function handleCheckOne(
} }
} }
// /** 多选:计算全选状态 */
function calculateIsCheckAll() { function calculateIsCheckAll() {
const currentList = gridApi.grid.getData(); const currentList = gridApi.grid.getData();
if (currentList.length === 0) { if (currentList.length === 0) {
@ -272,7 +256,6 @@ function calculateIsCheckAll() {
isIndeterminate.value = checkedCount > 0 && checkedCount < currentList.length; isIndeterminate.value = checkedCount > 0 && checkedCount < currentList.length;
} }
//
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
destroyOnClose: true, destroyOnClose: true,
// //
@ -284,7 +267,7 @@ const [Modal, modalApi] = useVbenModal({
: undefined, : undefined,
async onOpenChange(isOpen: boolean) { async onOpenChange(isOpen: boolean) {
if (!isOpen) { if (!isOpen) {
// // TODO @puhui999 selectedSpuId.value
if (!props.multiple) { if (!props.multiple) {
selectedSpuId.value = undefined; selectedSpuId.value = undefined;
} }
@ -300,7 +283,6 @@ const [Modal, modalApi] = useVbenModal({
checkedStatus.value = {}; checkedStatus.value = {};
isCheckAll.value = false; isCheckAll.value = false;
isIndeterminate.value = false; isIndeterminate.value = false;
// //
if (Array.isArray(data) && data.length > 0) { if (Array.isArray(data) && data.length > 0) {
checkedSpus.value = [...data]; checkedSpus.value = [...data];
@ -316,9 +298,16 @@ const [Modal, modalApi] = useVbenModal({
} }
// //
// TODO @puhui999100%
await gridApi.query(); await gridApi.query();
}, },
}); });
/** 初始化分类数据 */
onMounted(async () => {
categoryList.value = await getCategoryList({});
categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId');
});
</script> </script>
<template> <template>

View File

@ -145,7 +145,9 @@ const [Modal, modalApi] = useVbenModal({
// "" // ""
async onConfirm() { async onConfirm() {
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) return; if (!valid) {
return;
}
modalApi.lock(); modalApi.lock();
try { try {

View File

@ -53,7 +53,9 @@ const rewardRuleRef = ref<InstanceType<typeof RewardRule>>();
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
async onConfirm() { async onConfirm() {
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) return; if (!valid) {
return;
}
modalApi.lock(); modalApi.lock();
try { try {