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

This commit is contained in:
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 { handleTree } from '@vben/utils';
import { z } from '#/adapter/form';
import { getSimpleBrandList } from '#/api/mall/product/brand';
import { getCategoryList } from '#/api/mall/product/category';
import { getSimpleTemplateList } from '#/api/mall/trade/delivery/expressTemplate';
@@ -33,7 +32,6 @@ export function useInfoFormSchema(): VbenFormSchema[] {
{
fieldName: 'categoryId',
label: '分类名称',
// component: 'ApiCascader',
component: 'ApiTreeSelect',
componentProps: {
api: async () => {
@@ -285,7 +283,7 @@ export function useOtherFormSchema(): VbenFormSchema[] {
componentProps: {
min: 0,
},
rules: z.number().min(0).optional().default(0),
rules: 'required',
},
{
fieldName: 'giveIntegral',
@@ -294,7 +292,7 @@ export function useOtherFormSchema(): VbenFormSchema[] {
componentProps: {
min: 0,
},
rules: z.number().min(0).optional().default(0),
rules: 'required',
},
{
fieldName: 'virtualSalesCount',
@@ -303,7 +301,7 @@ export function useOtherFormSchema(): VbenFormSchema[] {
componentProps: {
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 */
import type { MallSpuApi } from '#/api/mall/product/spu';
// TODO @puhui999这个是不是 api 后端有定义类似的?如果是,是不是放到 api 哈?
export interface PropertyAndValues {
id: number;
name: string;
@@ -23,12 +24,8 @@ export interface RuleConfig {
message: string;
}
/**
* 获得商品的规格列表 - 商品相关的公共函数
*
* @param spu
* @return PropertyAndValues 规格列表
*/
// TODO @puhui999这个是只有 index.ts 在用么?还是别的模块也会用
/** 获得商品的规格列表 - 商品相关的公共函数 */
const getPropertyList = (spu: MallSpuApi.Spu): PropertyAndValues[] => {
// 直接拿返回的 skus 属性逆向生成出 propertyList
const properties: PropertyAndValues[] = [];
@@ -62,4 +59,5 @@ const getPropertyList = (spu: MallSpuApi.Spu): PropertyAndValues[] => {
export { getPropertyList };
// 导出组件
// TODO @puhui999如果 sku-list.vue 要对外,可以考虑在 spu 下面,搞个 components 模块目前看别的模块应该会用到哈。modules 是当前模块用到的components 是跨模块要用到的。
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 { 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';
@@ -31,11 +31,6 @@ const spuId = ref<number>();
const { params, name } = useRoute();
const { closeCurrentTab } = useTabs();
const activeTabName = ref('info');
function onTabChange(key: string) {
activeTabName.value = key;
}
const tabList = ref([
{
key: 'info',
@@ -58,44 +53,43 @@ const tabList = ref([
tab: '其它设置',
},
]);
// spu 表单数据
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, // 虚拟销量
});
const propertyList = ref<PropertyAndValues[]>([]); // 商品属性列表
const formLoading = ref(true); // 表单的加载中1修改时的数据加载2提交的按钮禁用
const isDetail = ref(false); // 是否查看详情
const formLoading = ref(false); // 表单的加载中1修改时的数据加载2提交的按钮禁用
const isDetail = ref(name === 'ProductSpuDetail'); // 是否查看详情
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[] = [
{
name: 'stock',
@@ -117,7 +111,7 @@ const ruleConfig: RuleConfig[] = [
rule: (arg) => arg >= 0.01,
message: '商品成本价格必须大于等于 0.00 元!!!',
},
];
]; // sku 相关属性校验规则
const [InfoForm, infoFormApi] = useVbenForm({
commonConfig: {
@@ -146,11 +140,11 @@ const [SkuForm, skuFormApi] = useVbenForm({
handleValuesChange: (values, fieldsChanged) => {
if (fieldsChanged.includes('subCommissionType')) {
formData.value.subCommissionType = values.subCommissionType;
changeSubCommissionType();
handleChangeSubCommissionType();
}
if (fieldsChanged.includes('specType')) {
formData.value.specType = values.specType;
onChangeSpec();
handleChangeSpec();
}
},
});
@@ -199,7 +193,13 @@ const [OtherForm, otherFormApi] = useVbenForm({
showDefaultActions: false,
});
async function onSubmit() {
/** tab 切换 */
function handleTabChange(key: string) {
activeTabName.value = key;
}
/** 提交表单 */
async function handleSubmit() {
const values: MallSpuApi.Spu = await infoFormApi
.merge(skuFormApi)
.merge(deliveryFormApi)
@@ -216,7 +216,7 @@ async function onSubmit() {
return;
}
values.skus.forEach((item) => {
// sku相关价格元转分
// 金额转换:元转分
item.price = convertToInteger(item.price);
item.marketPrice = convertToInteger(item.marketPrice);
item.costPrice = convertToInteger(item.costPrice);
@@ -224,7 +224,7 @@ async function onSubmit() {
item.secondBrokeragePrice = convertToInteger(item.secondBrokeragePrice);
});
}
// 处理轮播图列表
// 处理轮播图列表 TODO @puhui999这个是必须的哇
const newSliderPicUrls: any[] = [];
values.sliderPicUrls!.forEach((item: any) => {
// 如果是前端选的图
@@ -234,12 +234,13 @@ async function onSubmit() {
});
values.sliderPicUrls = newSliderPicUrls;
// 提交数据
await (spuId.value ? updateSpu(values) : createSpu(values));
}
/** 获得详情 */
async function getDetail() {
if (name === 'ProductSpuDetail') {
if (isDetail.value) {
isDetail.value = true;
infoFormApi.setDisabled(true);
skuFormApi.setDisabled(true);
@@ -247,45 +248,36 @@ async function getDetail() {
descriptionFormApi.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 数组
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 逻辑 ===========
/** 打开属性添加表单 */
function openPropertyAddForm() {
productPropertyAddFormApi.open();
}
@@ -296,7 +288,7 @@ function generateSkus(propertyList: any[]) {
}
/** 分销类型 */
function changeSubCommissionType() {
function handleChangeSubCommissionType() {
// 默认为零,类型切换后也要重置为零
for (const item of formData.value.skus!) {
item.firstBrokeragePrice = 0;
@@ -305,10 +297,10 @@ function changeSubCommissionType() {
}
/** 选择规格 */
function onChangeSpec() {
function handleChangeSpec() {
// 重置商品属性列表
propertyList.value = [];
// 重置sku列表
// 重置 sku 列表
formData.value.skus = [
{
price: 0,
@@ -325,7 +317,7 @@ function onChangeSpec() {
];
}
// 监听 sku form schema 变化,更新表单
/** 监听 sku form schema 变化,更新表单 */
watch(
propertyList,
() => {
@@ -336,6 +328,7 @@ watch(
{ deep: true },
);
/** 初始化 */
onMounted(async () => {
spuId.value = params.id as unknown as number;
if (!spuId.value) {
@@ -355,18 +348,18 @@ onMounted(async () => {
:loading="formLoading"
:tab-list="tabList"
:active-key="activeTabName"
@tab-change="onTabChange"
@tab-change="handleTabChange"
>
<template #tabBarExtraContent>
<Button type="primary" v-if="!isDetail" @click="onSubmit">
<Button type="primary" v-if="!isDetail" @click="handleSubmit">
保存
</Button>
<Button type="default" v-else @click="() => closeCurrentTab()">
返回列表
</Button>
</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'">
<template #singleSkuList>
<SkuList
@@ -418,6 +411,7 @@ onMounted(async () => {
</div>
</template>
<style lang="scss" scoped>
// TODO @puhui999这个样式是必须的哇
:deep(.ant-tabs-tab-btn) {
font-size: 14px !important;
}

View File

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

View File

@@ -35,7 +35,9 @@ const attributeOptions = ref<MallPropertyApi.Property[]>([]); // 商品属性名
watch(
() => props.propertyList,
(data) => {
if (!data) return;
if (!data) {
return;
}
attributeList.value = data as any[];
},
{
@@ -44,7 +46,6 @@ watch(
},
);
// 表单配置
const formSchema: VbenFormSchema[] = [
{
fieldName: 'name',
@@ -62,7 +63,6 @@ const formSchema: VbenFormSchema[] = [
showSearch: true,
filterOption: true,
placeholder: '请选择属性名称。如果不存在,可手动输入选择',
// 支持手动输入新选项
mode: 'tags',
maxTagCount: 1,
allowClear: true,
@@ -71,7 +71,6 @@ const formSchema: VbenFormSchema[] = [
},
];
// 初始化表单
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
@@ -85,16 +84,15 @@ const [Form, formApi] = useVbenForm({
showDefaultActions: false,
});
// 初始化弹窗
const [Modal, modalApi] = useVbenModal({
destroyOnClose: true,
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) return;
if (!valid) {
return;
}
const values = await formApi.getValues();
const name = Array.isArray(values.name) ? values.name[0] : values.name;
// 重复添加校验
for (const attrItem of attributeList.value) {
if (attrItem.name === name) {
@@ -103,6 +101,8 @@ const [Modal, modalApi] = useVbenModal({
}
}
// TODO @puhui999modalApi.lock(); 这种写法;
// 情况一:属性名已存在,则直接使用
const existProperty = attributeOptions.value.find(
(item: MallPropertyApi.Property) => item.name === name,
@@ -113,6 +113,7 @@ const [Modal, modalApi] = useVbenModal({
name,
values: [],
});
// TODO @puhui999这里要不 if else这样 await modalApi.close(); emit('success'); 可以复用另外感觉甚至可以情况二add 后,成为 existProperty可以进一步简化
await modalApi.close();
emit('success');
return;
@@ -132,7 +133,6 @@ const [Modal, modalApi] = useVbenModal({
await modalApi.close();
emit('success');
} catch (error) {
// 发生错误时不关闭弹窗
console.error('添加属性失败:', error);
}
},
@@ -140,7 +140,6 @@ const [Modal, modalApi] = useVbenModal({
if (!isOpen) {
return;
}
// 重置表单
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 skuList = ref<MallSpuApi.Sku[]>([
{
price: 0, // 商品价格
marketPrice: 0, // 市场价
costPrice: 0, // 成本价
barCode: '', // 商品条码
picUrl: '', // 图片地址
stock: 0, // 库存
weight: 0, // 商品重量
volume: 0, // 商品体积
firstBrokeragePrice: 0, // 一级分销的佣金
secondBrokeragePrice: 0, // 二级分销的佣金
price: 0,
marketPrice: 0,
costPrice: 0,
barCode: '',
picUrl: '',
stock: 0,
weight: 0,
volume: 0,
firstBrokeragePrice: 0,
secondBrokeragePrice: 0,
},
]); // 批量添加时的临时数据
@@ -91,9 +91,7 @@ function deleteSku(row: MallSpuApi.Sku) {
const tableHeaders = ref<{ label: string; prop: string }[]>([]); // 多属性表头
/**
* 保存时,每个商品规格的表单要校验下。例如说,销售金额最低是 0.01 这种。
*/
/** 保存时,每个商品规格的表单要校验下。例如说,销售金额最低是 0.01 这种 */
function validateSku() {
validateProperty();
let warningInfo = '请检查商品各行相关属性配置,';
@@ -116,6 +114,7 @@ function validateSku() {
}
}
// TODO @puhui999是不是可以通过 getNestedValue 简化?
function getValue(obj: any, arg: string): unknown {
const keys = arg.split('.');
let value: any = obj;
@@ -132,19 +131,20 @@ function getValue(obj: any, arg: string): unknown {
/**
* 选择时触发
*
* @param records 传递过来的选中的 sku 是一个数组
*/
function handleSelectionChange({ records }: { records: MallSpuApi.Sku[] }) {
emit('selectionChange', records);
}
/**
* 将传进来的值赋值给 skuList
*/
/** 将传进来的值赋值给 skuList */
watch(
() => props.propFormData,
(data) => {
if (!data) return;
if (!data) {
return;
}
formData.value = data;
},
{
@@ -196,9 +196,7 @@ function generateTableData(propertyList: PropertyAndValues[]) {
}
}
/**
* 生成 skus 前置校验
*/
/** 生成 skus 前置校验 */
function validateData(propertyList: PropertyAndValues[]): boolean {
const skuPropertyIds: number[] = [];
formData.value!.skus!.forEach((sku: MallSpuApi.Sku) =>
@@ -302,13 +300,13 @@ function getSkuTableRef() {
return activitySkuListRef.value;
}
// 暴露出生成 sku 方法,给添加属性成功时调用
defineExpose({ generateTableData, validateSku, getSkuTableRef });
</script>
<template>
<div>
<!-- 情况一添加/修改 -->
<!-- TODO @puhui999有可以通过 grid 来做么主要考虑这样不直接使用 vxe 标签抽象程度更高 -->
<VxeTable
v-if="!isDetail && !isActivityComponent"
:data="isBatch ? skuList : formData?.skus || []"
@@ -328,7 +326,7 @@ defineExpose({ generateTableData, validateSku, getSkuTableRef });
</template>
</VxeColumn>
<template v-if="formData?.specType && !isBatch">
<!-- 根据商品属性动态添加 -->
<!-- 根据商品属性动态添加 -->
<VxeColumn
v-for="(item, index) in tableHeaders"
:key="index"
@@ -481,7 +479,7 @@ defineExpose({ generateTableData, validateSku, getSkuTableRef });
</template>
</VxeColumn>
<template v-if="formData?.specType && !isBatch">
<!-- 根据商品属性动态添加 -->
<!-- 根据商品属性动态添加 -->
<VxeColumn
v-for="(item, index) in tableHeaders"
:key="index"
@@ -565,7 +563,7 @@ defineExpose({ generateTableData, validateSku, getSkuTableRef });
</template>
</VxeColumn>
<template v-if="formData?.specType">
<!-- 根据商品属性动态添加 -->
<!-- 根据商品属性动态添加 -->
<VxeColumn
v-for="(item, index) in tableHeaders"
:key="index"
@@ -605,7 +603,7 @@ defineExpose({ generateTableData, validateSku, getSkuTableRef });
{{ row.stock }}
</template>
</VxeColumn>
<!-- 方便扩展每个活动配置的属性不一样 -->
<!-- 方便扩展每个活动配置的属性不一样 -->
<slot name="extension"></slot>
</VxeTable>
</div>

View File

@@ -24,7 +24,8 @@ const emit = defineEmits<{
const selectedSkuId = ref<number>();
const spuId = ref<number>();
// 配置列
/** 配置列 */
// TODO @puhui999貌似列太宽了
const gridColumns = computed<VxeGridProps['columns']>(() => [
{
field: 'id',
@@ -65,7 +66,6 @@ const gridColumns = computed<VxeGridProps['columns']>(() => [
},
]);
// 初始化表格
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: gridColumns.value,
@@ -95,14 +95,13 @@ const [Grid, gridApi] = useVbenVxeGrid({
},
});
// 处理选中
/** 处理选中 */
function handleSelected(row: MallSpuApi.Sku) {
emit('change', row);
modalApi.close();
selectedSkuId.value = undefined;
}
// 初始化弹窗
const [Modal, modalApi] = useVbenModal({
destroyOnClose: true,
onOpenChange: async (isOpen: boolean) => {
@@ -111,8 +110,8 @@ const [Modal, modalApi] = useVbenModal({
spuId.value = undefined;
return;
}
const data = modalApi.getData<SpuData>();
// TODO @puhui999这里要不 if return让括号的层级简单点。
if (data?.spuId) {
spuId.value = data.spuId;
// 触发数据查询
@@ -125,7 +124,6 @@ const [Modal, modalApi] = useVbenModal({
<template>
<Modal class="w-[700px]" title="选择规格">
<Grid>
<!-- 单选列 -->
<template #radio-column="{ row }">
<Input
v-model="selectedSkuId"

View File

@@ -1,5 +1,6 @@
<!-- SPU 商品选择弹窗组件 -->
<script lang="ts" setup>
// TODO @puhui999这个是不是可以放到 components 里?,和商品发布,关系不大
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface';
import type { VbenFormSchema } from '#/adapter/form';
@@ -30,33 +31,15 @@ const emit = defineEmits<{
change: [spu: MallSpuApi.Spu | MallSpuApi.Spu[]];
}>();
// 单选:选中的 SPU ID
const selectedSpuId = ref<number>();
// 多选:选中状态 map
const checkedStatus = ref<Record<number, boolean>>({});
// 多选:选中的 SPU 列表
const checkedSpus = ref<MallSpuApi.Spu[]>([]);
// 多选:全选状态
const isCheckAll = ref(false);
// 多选:半选状态
const isIndeterminate = ref(false);
const selectedSpuId = ref<number>(); // 单选:选中的 SPU ID
const checkedStatus = ref<Record<number, boolean>>({}); // 多选:选中状态 map
const checkedSpus = ref<MallSpuApi.Spu[]>([]); // 多选:选中的 SPU 列表
const isCheckAll = ref(false); // 多选:全选状态
const isIndeterminate = ref(false); // 多选:半选状态
// 分类列表(扁平)
const categoryList = ref<any[]>([]);
// 分类树
const categoryTreeList = ref<any[]>([]);
const categoryList = 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[]>(() => {
return [
{
@@ -97,7 +80,6 @@ const formSchema = computed<VbenFormSchema[]>(() => {
];
});
// 列配置
const gridColumns = computed<VxeGridProps['columns']>(() => {
const columns: VxeGridProps['columns'] = [];
@@ -121,7 +103,7 @@ const gridColumns = computed<VxeGridProps['columns']>(() => {
});
}
// 其
// 其
columns.push(
{
field: 'id',
@@ -157,7 +139,6 @@ const gridColumns = computed<VxeGridProps['columns']>(() => {
return columns;
});
// 初始化表格
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: formSchema.value,
@@ -172,6 +153,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
proxyConfig: {
ajax: {
async query({ page }: any, formValues: any) {
// TODO @puhui999这里是不是不 try catch
try {
const params = {
pageNo: page.currentPage,
@@ -182,6 +164,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
createTime: formValues.createTime || undefined,
};
// TODO @puhui999一次性的是不是不声明 params直接放到 getSpuPage 里?
const data = await getSpuPage(params);
// 初始化多选状态
@@ -208,14 +191,15 @@ const [Grid, gridApi] = useVbenVxeGrid({
},
});
// 单选:处理选中
// TODO @puhui999如下的选中方法可以因为 Grid 做简化么?
/** 单选:处理选中 */
function handleSingleSelected(row: MallSpuApi.Spu) {
selectedSpuId.value = row.id;
emit('change', row);
modalApi.close();
}
// 多选:全选/全不选
/** 多选:全选/全不选 */
function handleCheckAll(e: CheckboxChangeEvent) {
const checked = e.target.checked;
isCheckAll.value = checked;
@@ -228,7 +212,7 @@ function handleCheckAll(e: CheckboxChangeEvent) {
calculateIsCheckAll();
}
// 多选:选中单个
/** 多选:选中单个 */
function handleCheckOne(
checked: boolean,
spu: MallSpuApi.Spu,
@@ -255,7 +239,7 @@ function handleCheckOne(
}
}
// 多选:计算全选状态
/** 多选:计算全选状态 */
function calculateIsCheckAll() {
const currentList = gridApi.grid.getData();
if (currentList.length === 0) {
@@ -272,7 +256,6 @@ function calculateIsCheckAll() {
isIndeterminate.value = checkedCount > 0 && checkedCount < currentList.length;
}
// 初始化弹窗
const [Modal, modalApi] = useVbenModal({
destroyOnClose: true,
// 多选模式时显示确认按钮
@@ -284,7 +267,7 @@ const [Modal, modalApi] = useVbenModal({
: undefined,
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
// 关闭时清理状态
// TODO @puhui999是不是直接清理不要判断 selectedSpuId.value
if (!props.multiple) {
selectedSpuId.value = undefined;
}
@@ -300,7 +283,6 @@ const [Modal, modalApi] = useVbenModal({
checkedStatus.value = {};
isCheckAll.value = false;
isIndeterminate.value = false;
// 恢复已选中的数据
if (Array.isArray(data) && data.length > 0) {
checkedSpus.value = [...data];
@@ -316,9 +298,16 @@ const [Modal, modalApi] = useVbenModal({
}
// 触发查询
// TODO @puhui999貌似不用这里再查询一次100% 会查询的,记忆中是;
await gridApi.query();
},
});
/** 初始化分类数据 */
onMounted(async () => {
categoryList.value = await getCategoryList({});
categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId');
});
</script>
<template>

View File

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

View File

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