feat: ele spu form 保持一致
This commit is contained in:
@@ -1,43 +0,0 @@
|
||||
import type { PropertyAndValues } from './model';
|
||||
|
||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||
|
||||
/**
|
||||
* 获得商品的规格列表 - 商品相关的公共函数
|
||||
*
|
||||
* @param spu
|
||||
* @return PropertyAndValues 规格列表
|
||||
*/
|
||||
const getPropertyList = (spu: MallSpuApi.Spu): PropertyAndValues[] => {
|
||||
// 直接拿返回的 skus 属性逆向生成出 propertyList
|
||||
const properties: PropertyAndValues[] = [];
|
||||
// 只有是多规格才处理
|
||||
if (spu.specType) {
|
||||
spu.skus?.forEach((sku) => {
|
||||
sku.properties?.forEach(
|
||||
({ propertyId, propertyName, valueId, valueName }) => {
|
||||
// 添加属性
|
||||
if (!properties?.some((item) => item.id === propertyId)) {
|
||||
properties.push({
|
||||
id: propertyId!,
|
||||
name: propertyName!,
|
||||
values: [],
|
||||
});
|
||||
}
|
||||
// 添加属性值
|
||||
const index = properties?.findIndex((item) => item.id === propertyId);
|
||||
if (
|
||||
index !== undefined &&
|
||||
index >= 0 &&
|
||||
!properties[index]!.values?.some((value) => value.id === valueId)
|
||||
) {
|
||||
properties[index]!.values?.push({ id: valueId!, name: valueName! });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
return properties;
|
||||
};
|
||||
|
||||
export { getPropertyList };
|
||||
@@ -1,86 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { DeliveryTypeEnum, DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import * as ExpressTemplateApi from '#/api/mall/trade/delivery/expressTemplate';
|
||||
|
||||
const props = defineProps<{
|
||||
propFormData: Object;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:activeName']);
|
||||
|
||||
/** 将传进来的值赋值给 formData */
|
||||
watch(
|
||||
() => props.propFormData,
|
||||
(data) => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
formApi.setValues(data);
|
||||
},
|
||||
);
|
||||
|
||||
const validate = async () => {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 校验通过更新数据
|
||||
Object.assign(props.propFormData, formApi.getValues());
|
||||
} catch (error) {
|
||||
ElMessage.error('【物流设置】不完善,请填写相关信息');
|
||||
emit('update:activeName', 'delivery');
|
||||
throw error; // 目的截断之后的校验
|
||||
}
|
||||
};
|
||||
defineExpose({ validate });
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: '!w-1/6',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'deliveryTypes',
|
||||
label: '配送方式',
|
||||
component: 'CheckboxGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.TRADE_DELIVERY_TYPE, 'number'),
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'deliveryTemplateId',
|
||||
label: '运费模板',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: ExpressTemplateApi.getSimpleTemplateList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['deliveryTypes'],
|
||||
show: (values) =>
|
||||
values.deliveryTypes.includes(DeliveryTypeEnum.EXPRESS.type),
|
||||
},
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Form />
|
||||
</template>
|
||||
@@ -1,67 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
const props = defineProps<{
|
||||
propFormData: Object;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:activeName']);
|
||||
|
||||
/** 将传进来的值赋值给 formData */
|
||||
watch(
|
||||
() => props.propFormData,
|
||||
(data) => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
formApi.setValues(data);
|
||||
},
|
||||
);
|
||||
|
||||
const validate = async () => {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 校验通过更新数据
|
||||
Object.assign(props.propFormData, formApi.getValues());
|
||||
} catch (error) {
|
||||
ElMessage.error('【商品详情】不完善,请填写相关信息');
|
||||
emit('update:activeName', 'description');
|
||||
throw error; // 目的截断之后的校验
|
||||
}
|
||||
};
|
||||
defineExpose({ validate });
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: '!w-1/6',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'description',
|
||||
label: '商品详情',
|
||||
component: 'RichTextarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入商品详情',
|
||||
height: 1000,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Form />
|
||||
</template>
|
||||
@@ -1,140 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import * as ProductBrandApi from '#/api/mall/product/brand';
|
||||
import * as ProductCategoryApi from '#/api/mall/product/category';
|
||||
|
||||
const props = defineProps<{
|
||||
propFormData: Object;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:activeName']);
|
||||
|
||||
const getCategoryList = async () => {
|
||||
const data = await ProductCategoryApi.getCategorySimpleList();
|
||||
return handleTree(data, 'id');
|
||||
};
|
||||
|
||||
/** 将传进来的值赋值给 formData */
|
||||
watch(
|
||||
() => props.propFormData,
|
||||
(data) => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
formApi.setValues(data);
|
||||
},
|
||||
);
|
||||
|
||||
const validate = async () => {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 校验通过更新数据
|
||||
Object.assign(props.propFormData, formApi.getValues());
|
||||
} catch (error) {
|
||||
ElMessage.error('【基础设置】不完善,请填写相关信息');
|
||||
emit('update:activeName', 'info');
|
||||
throw error; // 目的截断之后的校验
|
||||
}
|
||||
};
|
||||
defineExpose({ validate });
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: '!w-1/6',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '商品名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入商品名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'categoryId',
|
||||
label: '商品分类',
|
||||
component: 'ApiCascader',
|
||||
componentProps: {
|
||||
api: getCategoryList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'brandId',
|
||||
label: '商品品牌',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: ProductBrandApi.getSimpleBrandList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'keyword',
|
||||
label: '商品关键字',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入商品关键字',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'introduction',
|
||||
label: '商品简介',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
placeholder: '请输入商品简介',
|
||||
maxlength: 128,
|
||||
showWordLimit: true,
|
||||
autosize: {
|
||||
minRows: 4,
|
||||
maxRows: 4,
|
||||
},
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'picUrl',
|
||||
label: '商品封面图',
|
||||
component: 'ImageUpload',
|
||||
componentProps: {
|
||||
max: 1,
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'sliderPicUrls',
|
||||
label: '商品轮播图',
|
||||
component: 'ImageUpload',
|
||||
componentProps: {
|
||||
max: 10,
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Form />
|
||||
</template>
|
||||
@@ -1,25 +0,0 @@
|
||||
interface PropertyAndValues {
|
||||
id: number;
|
||||
name: string;
|
||||
values?: PropertyAndValues[];
|
||||
}
|
||||
|
||||
interface RuleConfig {
|
||||
// 需要校验的字段
|
||||
// 例:name: 'name' 则表示校验 sku.name 的值
|
||||
// 例:name: 'productConfig.stock' 则表示校验 sku.productConfig.name 的值,此处 productConfig 表示我在 Sku 上扩展的属性
|
||||
name: string;
|
||||
// 校验规格为一个毁掉函数,其中 arg 为需要校验的字段的值。
|
||||
// 例:需要校验价格必须大于0.01
|
||||
// {
|
||||
// name:'price',
|
||||
// rule:(arg: number) => arg > 0.01
|
||||
// }
|
||||
rule: (arg: any) => boolean;
|
||||
// 校验不通过时的消息提示
|
||||
message: string;
|
||||
}
|
||||
|
||||
export { getPropertyList, PropertyAndValues, RuleConfig };
|
||||
|
||||
export { default as SkuList } from './SkuList.vue';
|
||||
@@ -1,87 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
const props = defineProps<{
|
||||
propFormData: Object;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:activeName']);
|
||||
|
||||
/** 将传进来的值赋值给 formData */
|
||||
watch(
|
||||
() => props.propFormData,
|
||||
(data) => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
formApi.setValues(data);
|
||||
},
|
||||
);
|
||||
|
||||
const validate = async () => {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 校验通过更新数据
|
||||
Object.assign(props.propFormData, formApi.getValues());
|
||||
} catch (error) {
|
||||
ElMessage.error('【其它设置】不完善,请填写相关信息');
|
||||
emit('update:activeName', 'other');
|
||||
throw error; // 目的截断之后的校验
|
||||
}
|
||||
};
|
||||
defineExpose({ validate });
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: '!w-1/6',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'sort',
|
||||
label: '商品排序',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
step: 1,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'giveIntegral',
|
||||
label: '赠送积分',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
step: 1,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'virtualSalesCount',
|
||||
label: '虚拟销量',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
step: 1,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Form />
|
||||
</template>
|
||||
@@ -1,200 +0,0 @@
|
||||
<!-- 商品发布 - 库存价格 - 属性列表 -->
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import type { PropertyAndValues } from './model';
|
||||
|
||||
import type { MallPropertyApi } from '#/api/mall/product/property';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElCol,
|
||||
ElDivider,
|
||||
ElMessage,
|
||||
ElSpace,
|
||||
ElTag,
|
||||
ElText,
|
||||
} from 'element-plus';
|
||||
|
||||
import * as PropertyApi from '#/api/mall/product/property';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
defineOptions({ name: 'ProductAttributes' });
|
||||
|
||||
// 商品属性名称下拉框
|
||||
const props = defineProps({
|
||||
propertyList: {
|
||||
type: Array as PropType<PropertyAndValues[]>,
|
||||
default: () => [],
|
||||
},
|
||||
}); /** 输入框失去焦点或点击回车时触发 */
|
||||
const emit = defineEmits(['success']);
|
||||
const inputValue = ref(''); // 输入框值
|
||||
const attributeIndex = ref<null | number>(null); // 获取焦点时记录当前属性项的index
|
||||
// 输入框显隐控制
|
||||
const inputVisible = computed(() => (index: number) => {
|
||||
if (attributeIndex.value === null) return false;
|
||||
if (attributeIndex.value === index) return true;
|
||||
});
|
||||
const inputRef = ref<any[]>([]); // 标签输入框Ref
|
||||
/** 解决 ref 在 v-for 中的获取问题*/
|
||||
const setInputRef = (el: any) => {
|
||||
if (el === null || el === undefined) return;
|
||||
// 如果不存在 id 相同的元素才添加
|
||||
if (
|
||||
!inputRef.value.some(
|
||||
(item) => item.inputRef?.attributes.id === el.inputRef?.attributes.id,
|
||||
)
|
||||
) {
|
||||
inputRef.value.push(el);
|
||||
}
|
||||
};
|
||||
const attributeList = ref<PropertyAndValues[]>([]); // 商品属性列表
|
||||
const attributeOptions = ref([] as MallPropertyApi.PropertyValue[]);
|
||||
watch(
|
||||
() => props.propertyList,
|
||||
(data) => {
|
||||
if (!data) return;
|
||||
attributeList.value = data as any;
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
/** 删除属性值*/
|
||||
const handleCloseValue = (index: number, valueIndex: number) => {
|
||||
if (index < attributeList.value.length) {
|
||||
const values = attributeList.value[index]!.values as any[];
|
||||
values.splice(valueIndex, 1);
|
||||
}
|
||||
};
|
||||
|
||||
/** 删除属性*/
|
||||
const handleCloseProperty = (index: number) => {
|
||||
if (index < attributeList.value.length) {
|
||||
attributeList.value.splice(index, 1);
|
||||
emit('success', attributeList.value);
|
||||
}
|
||||
};
|
||||
|
||||
/** 显示输入框并获取焦点 */
|
||||
const showInput = async (index: number) => {
|
||||
if (index < attributeList.value.length) {
|
||||
attributeIndex.value = index;
|
||||
inputRef.value[index].focus();
|
||||
// 获取属性下拉选项
|
||||
await getAttributeOptions(attributeList.value[index]!.id);
|
||||
}
|
||||
};
|
||||
|
||||
// 定义 success 事件,用于操作成功后的回调
|
||||
const handleInputConfirm = async (index: number, propertyId: number) => {
|
||||
if (inputValue.value && index < attributeList.value.length) {
|
||||
// 1. 重复添加校验
|
||||
const values = attributeList.value[index]!.values as any[];
|
||||
if (values.find((item) => item.name === inputValue.value)) {
|
||||
ElMessage.warning('已存在相同属性值,请重试');
|
||||
attributeIndex.value = null;
|
||||
inputValue.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.1 情况一:属性值已存在,则直接使用并结束
|
||||
const existValue = attributeOptions.value.find(
|
||||
(item) => item.name === inputValue.value,
|
||||
);
|
||||
if (existValue) {
|
||||
attributeIndex.value = null;
|
||||
inputValue.value = '';
|
||||
const values = attributeList.value[index]!.values as any[];
|
||||
values.push({
|
||||
id: existValue.id!,
|
||||
name: existValue.name,
|
||||
});
|
||||
emit('success', attributeList.value);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.2 情况二:新属性值,则进行保存
|
||||
try {
|
||||
const id = await PropertyApi.createPropertyValue({
|
||||
propertyId,
|
||||
name: inputValue.value,
|
||||
});
|
||||
const values = attributeList.value[index]!.values as any[];
|
||||
values.push({ id, name: inputValue.value });
|
||||
ElMessage.success($t('common.createSuccess'));
|
||||
emit('success', attributeList.value);
|
||||
} catch {
|
||||
ElMessage.error('添加失败,请重试');
|
||||
}
|
||||
}
|
||||
attributeIndex.value = null;
|
||||
inputValue.value = '';
|
||||
};
|
||||
|
||||
/** 获取商品属性下拉选项 */
|
||||
const getAttributeOptions = async (propertyId: number) => {
|
||||
attributeOptions.value =
|
||||
await PropertyApi.getPropertyValueSimpleList(propertyId);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElCol v-for="(item, index) in attributeList" :key="index">
|
||||
<div>
|
||||
<ElText class="mx-1">属性名:</ElText>
|
||||
<ElTag class="mx-1" type="success" @close="handleCloseProperty(index)">
|
||||
{{ item.name }}
|
||||
</ElTag>
|
||||
</div>
|
||||
<div>
|
||||
<ElSpace :size="1">
|
||||
<ElText class="mx-1">属性值:</ElText>
|
||||
<ElTag
|
||||
v-for="(value, valueIndex) in item.values"
|
||||
:key="value.id"
|
||||
class="mx-1"
|
||||
@close="handleCloseValue(index, valueIndex)"
|
||||
>
|
||||
{{ value.name }}
|
||||
</ElTag>
|
||||
<ElSelect
|
||||
v-show="inputVisible(index)"
|
||||
:id="`input${index}`"
|
||||
:ref="setInputRef"
|
||||
v-model="inputValue"
|
||||
:reserve-keyword="false"
|
||||
allow-create
|
||||
class="!w-30"
|
||||
default-first-option
|
||||
filterable
|
||||
size="small"
|
||||
@blur="handleInputConfirm(index, item.id)"
|
||||
@change="handleInputConfirm(index, item.id)"
|
||||
@keyup.enter="handleInputConfirm(index, item.id)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item2 in attributeOptions"
|
||||
:key="item2.id"
|
||||
:label="item2.name"
|
||||
:value="item2.name"
|
||||
/>
|
||||
</ElSelect>
|
||||
<ElButton
|
||||
v-show="!inputVisible(index)"
|
||||
class="button-new-tag ml-1"
|
||||
size="small"
|
||||
@click="showInput(index)"
|
||||
>
|
||||
+ 添加
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</div>
|
||||
<ElDivider class="my-10px" />
|
||||
</ElCol>
|
||||
</template>
|
||||
@@ -1,133 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import type { MallPropertyApi } from '#/api/mall/product/property';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getPropertySimpleList } from '#/api/mall/product/property';
|
||||
import * as PropertyApi from '#/api/mall/product/property';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
// 扩展Property接口,添加values属性
|
||||
interface ExtendedProperty extends MallPropertyApi.Property {
|
||||
values?: any[];
|
||||
}
|
||||
|
||||
// 商品属性名称下拉框
|
||||
|
||||
const props = defineProps({
|
||||
propertyList: {
|
||||
type: Array as PropType<ExtendedProperty[]>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const attributeList = ref<ExtendedProperty[]>([]); // 商品属性列表
|
||||
const attributeOptions = ref([] as MallPropertyApi.Property[]);
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '属性名称',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getPropertySimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
defaultFirstOption: true,
|
||||
filterable: true,
|
||||
allowCreate: true,
|
||||
placeholder: '请选择属性名称。如果不存在,可手动输入选择',
|
||||
},
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
modalApi.lock();
|
||||
const { name } = await formApi.getValues();
|
||||
// 1.1 重复添加校验
|
||||
for (const attrItem of attributeList.value) {
|
||||
if (attrItem.name === name) {
|
||||
return ElMessage.error('该属性已存在,请勿重复添加');
|
||||
}
|
||||
}
|
||||
// 1.2 校验表单
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.1 情况一:属性名已存在,则直接使用并结束
|
||||
const existProperty = attributeOptions.value.find(
|
||||
(item) => item.name === name,
|
||||
);
|
||||
if (existProperty) {
|
||||
// 添加到属性列表
|
||||
attributeList.value.push({
|
||||
id: existProperty.id,
|
||||
...(await formApi.getValues()),
|
||||
values: [],
|
||||
});
|
||||
// 关闭弹窗
|
||||
modalApi.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.2 情况二:如果是不存在的属性,则需要执行新增
|
||||
// 提交请求
|
||||
modalApi.lock();
|
||||
try {
|
||||
const data = (await formApi.getValues()) as MallPropertyApi.Property;
|
||||
const propertyId = await PropertyApi.createProperty(data);
|
||||
// 添加到属性列表
|
||||
attributeList.value.push({
|
||||
id: propertyId,
|
||||
...(await formApi.getValues()),
|
||||
values: [],
|
||||
});
|
||||
// 关闭弹窗
|
||||
ElMessage.success($t('common.createSuccess'));
|
||||
modalApi.close();
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.propertyList, // 解决 props 无法直接修改父组件的问题
|
||||
(data) => {
|
||||
if (!data) return;
|
||||
attributeList.value = data;
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-2/5" title="添加商品属性">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -1,248 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage, ElSpace } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
import { getPropertyList } from './data';
|
||||
import ProductAttributes from './product-attributes.vue';
|
||||
import ProductPropertyAddForm from './product-property-add-form.vue';
|
||||
import SkuList from './sku-list.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
propFormData: Object;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:activeName']);
|
||||
|
||||
interface PropertyAndValues {
|
||||
id: number;
|
||||
name: string;
|
||||
values?: PropertyAndValues[];
|
||||
}
|
||||
|
||||
interface RuleConfig {
|
||||
// 需要校验的字段
|
||||
// 例:name: 'name' 则表示校验 sku.name 的值
|
||||
// 例:name: 'productConfig.stock' 则表示校验 sku.productConfig.name 的值,此处 productConfig 表示我在 Sku 上扩展的属性
|
||||
name: string;
|
||||
// 校验规格为一个毁掉函数,其中 arg 为需要校验的字段的值。
|
||||
// 例:需要校验价格必须大于0.01
|
||||
// {
|
||||
// name:'price',
|
||||
// rule:(arg: number) => arg > 0.01
|
||||
// }
|
||||
rule: (arg: any) => boolean;
|
||||
// 校验不通过时的消息提示
|
||||
message: string;
|
||||
}
|
||||
|
||||
const propertyList = ref<PropertyAndValues[]>([]); // 商品属性列表
|
||||
|
||||
// sku 相关属性校验规则
|
||||
const ruleConfig: RuleConfig[] = [
|
||||
{
|
||||
name: 'stock',
|
||||
rule: (arg) => arg >= 0,
|
||||
message: '商品库存必须大于等于 1 !!!',
|
||||
},
|
||||
{
|
||||
name: 'price',
|
||||
rule: (arg) => arg >= 0.01,
|
||||
message: '商品销售价格必须大于等于 0.01 元!!!',
|
||||
},
|
||||
{
|
||||
name: 'marketPrice',
|
||||
rule: (arg) => arg >= 0.01,
|
||||
message: '商品市场价格必须大于等于 0.01 元!!!',
|
||||
},
|
||||
{
|
||||
name: 'costPrice',
|
||||
rule: (arg) => arg >= 0.01,
|
||||
message: '商品成本价格必须大于等于 0.00 元!!!',
|
||||
},
|
||||
];
|
||||
|
||||
/** 将传进来的值赋值给 formData */
|
||||
watch(
|
||||
() => props.propFormData,
|
||||
(data) => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
formApi.setValues(data);
|
||||
},
|
||||
);
|
||||
|
||||
const validate = async () => {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 校验通过更新数据
|
||||
Object.assign(props.propFormData, formApi.getValues());
|
||||
} catch (error) {
|
||||
ElMessage.error('【库存价格】不完善,请填写相关信息');
|
||||
emit('update:activeName', 'sku');
|
||||
throw error; // 目的截断之后的校验
|
||||
}
|
||||
};
|
||||
defineExpose({ validate });
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: '!w-1/6',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'subCommissionType',
|
||||
label: '分销类型',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{
|
||||
label: '默认设置',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
label: '单独设置',
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultValue: false,
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'specType',
|
||||
label: '商品规格',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{
|
||||
label: '单规格',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
label: '多规格',
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultValue: false,
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'skuList',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: ['specType'],
|
||||
show: (values) => !values.specType,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'specTypeItem',
|
||||
label: '商品属性',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: ['specType'],
|
||||
show: (values) => values.specType,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'batchSettings',
|
||||
label: '批量设置',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: ['specType'],
|
||||
show: (values) => values.specType && propertyList.value.length > 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'specTypeItemList',
|
||||
label: '规格列表',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: ['specType'],
|
||||
show: (values) => values.specType && propertyList.value.length > 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [ProductPropertyAddFormModal, productPropertyAddFormApi] = useVbenModal({
|
||||
connectedComponent: ProductPropertyAddForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 调用 SkuList generateTableData 方法*/
|
||||
const skuListRef = ref();
|
||||
const generateSkus = (propertyList: any[]) => {
|
||||
skuListRef.value.generateTableData(propertyList);
|
||||
};
|
||||
|
||||
/** 将传进来的值赋值给 formData */
|
||||
watch(
|
||||
() => props.propFormData,
|
||||
(data) => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
// 将 SKU 的属性,整理成 PropertyAndValues 数组
|
||||
propertyList.value = getPropertyList(data);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<Form>
|
||||
<template #skuList>
|
||||
<SkuList
|
||||
ref="skuListRef"
|
||||
:prop-form-data="props.propFormData"
|
||||
:property-list="propertyList"
|
||||
:rule-config="ruleConfig"
|
||||
/>
|
||||
</template>
|
||||
<template #specTypeItem>
|
||||
<ElSpace direction="vertical" alignment="flex-start">
|
||||
<ElButton type="primary" @click="productPropertyAddFormApi.open()">
|
||||
添加属性
|
||||
</ElButton>
|
||||
<ProductAttributes
|
||||
:property-list="propertyList"
|
||||
@success="generateSkus"
|
||||
/>
|
||||
</ElSpace>
|
||||
</template>
|
||||
<template #batchSettings>
|
||||
<SkuList
|
||||
:is-batch="true"
|
||||
:prop-form-data="props.propFormData"
|
||||
:property-list="propertyList"
|
||||
/>
|
||||
</template>
|
||||
<template #specTypeItemList>
|
||||
<SkuList
|
||||
:prop-form-data="props.propFormData"
|
||||
:property-list="propertyList"
|
||||
:rule-config="ruleConfig"
|
||||
/>
|
||||
</template>
|
||||
</Form>
|
||||
<ProductPropertyAddFormModal :property-list="propertyList" />
|
||||
</Page>
|
||||
</template>
|
||||
@@ -1,533 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import type { PropertyAndValues, RuleConfig } from './model';
|
||||
|
||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { formatToFraction, isEmpty } from '@vben/utils';
|
||||
|
||||
import { ElInput, ElMessage, ElTable } from 'element-plus';
|
||||
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
import { copyValueToTarget } from '#/utils';
|
||||
|
||||
defineOptions({ name: 'SkuList' });
|
||||
|
||||
const props = defineProps({
|
||||
propFormData: {
|
||||
type: Object as PropType<MallSpuApi.Spu>,
|
||||
default: () => ({}),
|
||||
},
|
||||
propertyList: {
|
||||
type: Array as PropType<PropertyAndValues[]>,
|
||||
default: () => [],
|
||||
},
|
||||
ruleConfig: {
|
||||
type: Array as PropType<RuleConfig[]>,
|
||||
default: () => [],
|
||||
},
|
||||
isBatch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}, // 是否作为批量操作组件
|
||||
isComponent: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}, // 是否作为组件
|
||||
isActivityComponent: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}, // 是否作为活动组件
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'selectionChange', value: MallSpuApi.Sku[]): void;
|
||||
}>();
|
||||
const formData = 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, // 二级分销的佣金
|
||||
},
|
||||
]); // 批量添加时的临时数据
|
||||
|
||||
/** 批量添加 */
|
||||
const batchAdd = () => {
|
||||
validateProperty();
|
||||
formData.value!.skus!.forEach((item: MallSpuApi.Sku) => {
|
||||
copyValueToTarget(item, skuList.value[0]);
|
||||
});
|
||||
};
|
||||
/** 校验商品属性属性值 */
|
||||
const validateProperty = () => {
|
||||
// 校验商品属性属性值是否为空,有一个为空都不给过
|
||||
const warningInfo = '存在属性属性值为空,请先检查完善属性值后重试!!!';
|
||||
for (const item of props.propertyList) {
|
||||
if (!item.values || isEmpty(item.values)) {
|
||||
ElMessage.warning(warningInfo);
|
||||
throw new Error(warningInfo);
|
||||
}
|
||||
}
|
||||
};
|
||||
/** 删除 sku */
|
||||
const 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);
|
||||
};
|
||||
const tableHeaders = ref<{ label: string; prop: string }[]>([]); // 多属性表头
|
||||
/**
|
||||
* 保存时,每个商品规格的表单要校验下。例如说,销售金额最低是 0.01 这种。
|
||||
*/
|
||||
const validateSku = () => {
|
||||
validateProperty();
|
||||
let warningInfo = '请检查商品各行相关属性配置,';
|
||||
let validate = true; // 默认通过
|
||||
for (const sku of formData.value!.skus!) {
|
||||
// 作为活动组件的校验
|
||||
for (const rule of props?.ruleConfig) {
|
||||
const arg = getValue(sku, rule.name);
|
||||
if (!rule.rule(arg)) {
|
||||
validate = false; // 只要有一个不通过则直接不通过
|
||||
warningInfo += rule.message;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 只要有一个不通过则结束后续的校验
|
||||
if (!validate) {
|
||||
ElMessage.warning(warningInfo);
|
||||
throw new Error(warningInfo);
|
||||
}
|
||||
}
|
||||
};
|
||||
const getValue = (obj: any, arg: string) => {
|
||||
const keys = arg.split('.');
|
||||
let value = obj;
|
||||
for (const key of keys) {
|
||||
if (value && typeof value === 'object' && key in value) {
|
||||
value = value[key];
|
||||
} else {
|
||||
value = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* 选择时触发
|
||||
* @param Sku 传递过来的选中的 sku 是一个数组
|
||||
*/
|
||||
const handleSelectionChange = (val: MallSpuApi.Sku[]) => {
|
||||
emit('selectionChange', val);
|
||||
};
|
||||
|
||||
/**
|
||||
* 将传进来的值赋值给 skuList
|
||||
*/
|
||||
watch(
|
||||
() => props.propFormData,
|
||||
(data) => {
|
||||
if (!data) return;
|
||||
formData.value = data;
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
/** 生成表数据 */
|
||||
const generateTableData = (propertyList: any[]) => {
|
||||
// 构建数据结构
|
||||
const propertyValues = propertyList.map((item) =>
|
||||
item.values.map((v: any) => ({
|
||||
propertyId: item.id,
|
||||
propertyName: item.name,
|
||||
valueId: v.id,
|
||||
valueName: v.name,
|
||||
})),
|
||||
);
|
||||
const buildSkuList = build(propertyValues);
|
||||
// 如果回显的 sku 属性和添加的属性不一致则重置 skus 列表
|
||||
if (!validateData(propertyList)) {
|
||||
// 如果不一致则重置表数据,默认添加新的属性重新生成 sku 列表
|
||||
formData.value!.skus = [];
|
||||
}
|
||||
if (buildSkuList && buildSkuList.length > 0) {
|
||||
for (const item of buildSkuList) {
|
||||
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,
|
||||
};
|
||||
// 如果存在属性相同的 sku 则不做处理
|
||||
const index = formData.value!.skus!.findIndex(
|
||||
(sku: MallSpuApi.Sku) =>
|
||||
JSON.stringify(sku.properties) === JSON.stringify(row.properties),
|
||||
);
|
||||
if (index !== -1) {
|
||||
continue;
|
||||
}
|
||||
formData.value!.skus!.push(row);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成 skus 前置校验
|
||||
*/
|
||||
const validateData = (propertyList: any[]) => {
|
||||
const skuPropertyIds: number[] = [];
|
||||
formData.value!.skus!.forEach((sku: MallSpuApi.Sku) =>
|
||||
sku.properties
|
||||
?.map((property: any) => property.propertyId)
|
||||
?.forEach((propertyId: number) => {
|
||||
if (!skuPropertyIds.includes(propertyId!)) {
|
||||
skuPropertyIds.push(propertyId!);
|
||||
}
|
||||
}),
|
||||
);
|
||||
const propertyIds = propertyList.map((item) => item.id);
|
||||
return skuPropertyIds.length === propertyIds.length;
|
||||
};
|
||||
|
||||
/** 构建所有排列组合 */
|
||||
const build = (
|
||||
propertyValuesList: MallSpuApi.Property[][],
|
||||
): MallSpuApi.Property[] | MallSpuApi.Property[][] => {
|
||||
if (!propertyValuesList || propertyValuesList.length === 0) {
|
||||
return [];
|
||||
} else if (propertyValuesList.length === 1) {
|
||||
return propertyValuesList[0] || [];
|
||||
} else {
|
||||
const result: MallSpuApi.Property[][] = [];
|
||||
const rest = build(propertyValuesList.slice(1));
|
||||
if (propertyValuesList[0] && Array.isArray(rest)) {
|
||||
for (let i = 0; i < propertyValuesList[0].length; i++) {
|
||||
for (const restItem of rest) {
|
||||
const currentItem = propertyValuesList[0][i];
|
||||
// 第一次不是数组结构,后面的都是数组结构
|
||||
if (Array.isArray(restItem)) {
|
||||
result.push([currentItem!, ...restItem]);
|
||||
} else if (restItem) {
|
||||
// 确保restItem不是undefined,并进行类型断言
|
||||
result.push([currentItem!, restItem as MallSpuApi.Property]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
/** 监听属性列表,生成相关参数和表头 */
|
||||
watch(
|
||||
() => props.propertyList,
|
||||
(propertyList: PropertyAndValues[]) => {
|
||||
// 如果不是多规格则结束
|
||||
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,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// 判断代理对象是否为空
|
||||
if (JSON.stringify(propertyList) === '[]') {
|
||||
return;
|
||||
}
|
||||
// 重置表头
|
||||
tableHeaders.value = [];
|
||||
// 生成表头
|
||||
propertyList.forEach((item, index) => {
|
||||
// name加属性项index区分属性值
|
||||
tableHeaders.value.push({ 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);
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
const activitySkuListRef = ref<InstanceType<typeof ElTable>>();
|
||||
|
||||
const getSkuTableRef = () => {
|
||||
return activitySkuListRef.value;
|
||||
};
|
||||
// 暴露出生成 sku 方法,给添加属性成功时调用
|
||||
defineExpose({ generateTableData, validateSku, getSkuTableRef });
|
||||
</script>
|
||||
<template>
|
||||
<!-- 情况一:添加/修改 -->
|
||||
<ElTable
|
||||
v-if="!isActivityComponent"
|
||||
:data="isBatch ? skuList : formData!.skus!"
|
||||
border
|
||||
class="tabNumWidth"
|
||||
max-height="500"
|
||||
size="small"
|
||||
>
|
||||
<el-table-column align="center" label="图片" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<UploadImg
|
||||
v-model="row.picUrl"
|
||||
height="50px"
|
||||
width="50px"
|
||||
:show-description="false"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template v-if="formData!.specType && !isBatch">
|
||||
<!-- 根据商品属性动态添加 -->
|
||||
<el-table-column
|
||||
v-for="(item, index) in tableHeaders"
|
||||
:key="index"
|
||||
:label="item.label"
|
||||
align="center"
|
||||
min-width="120"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span style="font-weight: bold; color: #40aaff">
|
||||
{{ row.properties?.[index]?.valueName }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
<el-table-column align="center" label="商品条码" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<ElInput v-model="row.barCode" class="w-100%" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="销售价" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.price"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-100%"
|
||||
controls-position="right"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="市场价" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.marketPrice"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-100%"
|
||||
controls-position="right"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="成本价" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.costPrice"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-100%"
|
||||
controls-position="right"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="库存" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.stock"
|
||||
:min="0"
|
||||
class="w-100%"
|
||||
controls-position="right"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="重量(kg)" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.weight"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-100%"
|
||||
controls-position="right"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="体积(m^3)" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.volume"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-100%"
|
||||
controls-position="right"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template v-if="formData!.subCommissionType">
|
||||
<el-table-column align="center" label="一级返佣(元)" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.firstBrokeragePrice"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-100%"
|
||||
controls-position="right"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="二级返佣(元)" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.secondBrokeragePrice"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-100%"
|
||||
controls-position="right"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
<el-table-column
|
||||
v-if="formData?.specType"
|
||||
align="center"
|
||||
fixed="right"
|
||||
label="操作"
|
||||
width="80"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="isBatch"
|
||||
link
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="batchAdd"
|
||||
>
|
||||
批量添加
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
link
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="deleteSku(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ElTable>
|
||||
|
||||
<!-- 情况二:作为活动组件 -->
|
||||
<ElTable
|
||||
v-if="isActivityComponent"
|
||||
:data="formData!.skus!"
|
||||
border
|
||||
max-height="500"
|
||||
size="small"
|
||||
style="width: 99%"
|
||||
>
|
||||
<el-table-column v-if="isComponent" type="selection" width="45" />
|
||||
<el-table-column align="center" label="图片" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<el-image :src="row.picUrl" class="h-60px w-60px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template v-if="formData!.specType">
|
||||
<!-- 根据商品属性动态添加 -->
|
||||
<el-table-column
|
||||
v-for="(item, index) in tableHeaders"
|
||||
:key="index"
|
||||
:label="item.label"
|
||||
align="center"
|
||||
min-width="80"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span style="font-weight: bold; color: #40aaff">
|
||||
{{ row.properties?.[index]?.valueName }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
<el-table-column align="center" label="商品条码" min-width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.barCode }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="销售价(元)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ formatToFraction(row.price) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="市场价(元)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ formatToFraction(row.marketPrice) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="成本价(元)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ formatToFraction(row.costPrice) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="库存" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.stock }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 方便扩展每个活动配置的属性不一样 -->
|
||||
<slot name="extension"></slot>
|
||||
</ElTable>
|
||||
</template>
|
||||
@@ -1,166 +0,0 @@
|
||||
<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 * as ProductSpuApi from '#/api/mall/product/spu';
|
||||
import SpuTableSelect from '#/views/mall/product/spu/components/spu-table-select.vue';
|
||||
|
||||
// 商品橱窗,一般用于与商品建立关系时使用
|
||||
// 提供功能:展示商品列表、添加商品、移除商品
|
||||
defineOptions({ name: 'SpuShowcase' });
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [Number, Array],
|
||||
required: true,
|
||||
},
|
||||
// 限制数量:默认不限制
|
||||
limit: {
|
||||
type: Number,
|
||||
default: Number.MAX_VALUE,
|
||||
},
|
||||
disabled: Boolean,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
// 计算是否可以添加
|
||||
const canAdd = computed(() => {
|
||||
// 情况一:禁用时不可以添加
|
||||
if (props.disabled) return false;
|
||||
// 情况二:未指定限制数量时,可以添加
|
||||
if (!props.limit) return true;
|
||||
// 情况三:检查已添加数量是否小于限制数量
|
||||
return productSpus.value.length < props.limit;
|
||||
});
|
||||
|
||||
// 商品列表
|
||||
const productSpus = ref<MallSpuApi.Spu[]>([]);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async () => {
|
||||
let ids: number[] = [];
|
||||
if (Array.isArray(props.modelValue)) {
|
||||
ids = props.modelValue as number[];
|
||||
} else {
|
||||
ids = props.modelValue ? [props.modelValue] : [];
|
||||
}
|
||||
// 不需要返显
|
||||
if (ids.length === 0) {
|
||||
productSpus.value = [];
|
||||
return;
|
||||
}
|
||||
// 只有商品发生变化之后,才去查询商品
|
||||
if (
|
||||
productSpus.value.length === 0 ||
|
||||
productSpus.value.some((spu) => !ids.includes(spu.id!))
|
||||
) {
|
||||
productSpus.value = await ProductSpuApi.getSpuDetailList(ids);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/** 商品表格选择对话框 */
|
||||
const spuTableSelectRef = ref();
|
||||
// 打开对话框
|
||||
const openSpuTableSelect = () => {
|
||||
spuTableSelectRef.value.open(productSpus.value);
|
||||
};
|
||||
|
||||
/**
|
||||
* 选择商品后触发
|
||||
*
|
||||
* @param spus 选中的商品列表
|
||||
*/
|
||||
const handleSpuSelected = (spus: MallSpuApi.Spu | MallSpuApi.Spu[]) => {
|
||||
productSpus.value = Array.isArray(spus) ? spus : [spus];
|
||||
emitSpuChange();
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除商品
|
||||
*
|
||||
* @param index 商品索引
|
||||
*/
|
||||
const handleRemoveSpu = (index: number) => {
|
||||
productSpus.value.splice(index, 1);
|
||||
emitSpuChange();
|
||||
};
|
||||
const 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="gap-8px flex flex-wrap items-center">
|
||||
<div
|
||||
v-for="(spu, index) in productSpus"
|
||||
:key="spu.id"
|
||||
class="select-box spu-pic"
|
||||
>
|
||||
<ElTooltip :content="spu.name">
|
||||
<div class="relative h-full w-full">
|
||||
<ElImage :src="spu.picUrl" class="h-full w-full" />
|
||||
<IconifyIcon
|
||||
v-show="!disabled"
|
||||
class="del-icon"
|
||||
icon="ep:circle-close-filled"
|
||||
@click="handleRemoveSpu(index)"
|
||||
/>
|
||||
</div>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
<ElTooltip content="选择商品" v-if="canAdd">
|
||||
<div class="select-box" @click="openSpuTableSelect">
|
||||
<IconifyIcon icon="ep:plus" />
|
||||
</div>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
<!-- 商品选择对话框(表格形式) -->
|
||||
<SpuTableSelect
|
||||
ref="spuTableSelectRef"
|
||||
:multiple="limit !== 1"
|
||||
@change="handleSpuSelected"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.select-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border: 1px dashed var(--el-border-color-darker);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.spu-pic {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.del-icon {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
z-index: 1;
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,359 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MallCategoryApi } from '#/api/mall/product/category';
|
||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { ContentWrap } from '@vben/common-ui';
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import {
|
||||
CHANGE_EVENT,
|
||||
ElButton,
|
||||
ElCheckbox,
|
||||
ElDatePicker,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElImage,
|
||||
ElInput,
|
||||
ElRadio,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTreeSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import * as ProductCategoryApi from '#/api/mall/product/category';
|
||||
import * as ProductSpuApi from '#/api/mall/product/spu';
|
||||
/**
|
||||
* 商品表格选择对话框
|
||||
* 1. 单选模式:
|
||||
* 1.1 点击表格左侧的单选框时,结束选择,并关闭对话框
|
||||
* 1.2 再次打开时,保持选中状态
|
||||
* 2. 多选模式:
|
||||
* 2.1 点击表格左侧的多选框时,记录选中的商品
|
||||
* 2.2 切换分页时,保持商品的选中的状态
|
||||
* 2.3 点击右下角的确定按钮时,结束选择,关闭对话框
|
||||
* 2.4 再次打开时,保持选中状态
|
||||
*/
|
||||
defineOptions({ name: 'SpuTableSelect' });
|
||||
|
||||
defineProps({
|
||||
// 多选模式
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
/** 确认选择时的触发事件 */
|
||||
const emits = defineEmits<{
|
||||
change: [spu: any | MallSpuApi.Spu | MallSpuApi.Spu[]];
|
||||
}>();
|
||||
// 列表的总页数
|
||||
const total = ref(0);
|
||||
// 列表的数据
|
||||
const list = ref<MallSpuApi.Spu[]>([]);
|
||||
// 列表的加载中
|
||||
const loading = ref(false);
|
||||
// 弹窗的是否展示
|
||||
const dialogVisible = ref(false);
|
||||
// 查询参数
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
// 默认获取上架的商品
|
||||
tabType: 0,
|
||||
name: '',
|
||||
categoryId: null,
|
||||
createTime: [],
|
||||
});
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = (spuList?: MallSpuApi.Spu[]) => {
|
||||
// 重置
|
||||
checkedSpus.value = [];
|
||||
checkedStatus.value = {};
|
||||
isCheckAll.value = false;
|
||||
isIndeterminate.value = false;
|
||||
|
||||
// 处理已选中
|
||||
if (spuList && spuList.length > 0) {
|
||||
checkedSpus.value = [...spuList];
|
||||
checkedStatus.value = Object.fromEntries(
|
||||
spuList.map((spu) => [spu.id, true]),
|
||||
);
|
||||
}
|
||||
|
||||
dialogVisible.value = true;
|
||||
resetQuery();
|
||||
};
|
||||
// 提供 open 方法,用于打开弹窗
|
||||
defineExpose({ open });
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await ProductSpuApi.getSpuPage(queryParams.value);
|
||||
list.value = data.list;
|
||||
total.value = data.total;
|
||||
// checkbox绑定undefined会有问题,需要给一个bool值
|
||||
list.value.forEach(
|
||||
(spu) =>
|
||||
(checkedStatus.value[spu.id || 0] =
|
||||
checkedStatus.value[spu.id || 0] || false),
|
||||
);
|
||||
// 计算全选框状态
|
||||
calculateIsCheckAll();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNo = 1;
|
||||
getList();
|
||||
};
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryParams.value = {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
// 默认获取上架的商品
|
||||
tabType: 0,
|
||||
name: '',
|
||||
categoryId: null,
|
||||
createTime: [],
|
||||
};
|
||||
getList();
|
||||
};
|
||||
|
||||
// 是否全选
|
||||
const isCheckAll = ref(false);
|
||||
// 全选框是否处于中间状态:不是全部选中 && 任意一个选中
|
||||
const isIndeterminate = ref(false);
|
||||
// 选中的商品
|
||||
const checkedSpus = ref<MallSpuApi.Spu[]>([]);
|
||||
// 选中状态:key为商品ID,value为是否选中
|
||||
const checkedStatus = ref<Record<string, boolean>>({});
|
||||
|
||||
// 选中的商品 spuId
|
||||
const selectedSpuId = ref();
|
||||
/** 单选中时触发 */
|
||||
const handleSingleSelected = (spu: MallSpuApi.Spu) => {
|
||||
emits(CHANGE_EVENT, spu);
|
||||
// 关闭弹窗
|
||||
dialogVisible.value = false;
|
||||
// 记住上次选择的ID
|
||||
selectedSpuId.value = spu.id;
|
||||
};
|
||||
|
||||
/** 多选完成 */
|
||||
const handleEmitChange = () => {
|
||||
// 关闭弹窗
|
||||
dialogVisible.value = false;
|
||||
emits(CHANGE_EVENT, [...checkedSpus.value]);
|
||||
};
|
||||
|
||||
/** 全选/全不选 */
|
||||
const handleCheckAll = (checked: boolean) => {
|
||||
isCheckAll.value = checked;
|
||||
isIndeterminate.value = false;
|
||||
|
||||
list.value.forEach((spu) => handleCheckOne(checked, spu, false));
|
||||
};
|
||||
|
||||
/**
|
||||
* 选中一行
|
||||
* @param checked 是否选中
|
||||
* @param spu 商品
|
||||
* @param isCalcCheckAll 是否计算全选
|
||||
*/
|
||||
const handleCheckOne = (
|
||||
checked: boolean,
|
||||
spu: MallSpuApi.Spu,
|
||||
isCalcCheckAll: boolean,
|
||||
) => {
|
||||
if (checked) {
|
||||
checkedSpus.value.push(spu);
|
||||
checkedStatus.value[spu.id || 0] = true;
|
||||
} else {
|
||||
const index = findCheckedIndex(spu);
|
||||
if (index > -1) {
|
||||
checkedSpus.value.splice(index, 1);
|
||||
checkedStatus.value[spu.id || 0] = false;
|
||||
isCheckAll.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算全选框状态
|
||||
if (isCalcCheckAll) {
|
||||
calculateIsCheckAll();
|
||||
}
|
||||
};
|
||||
|
||||
// 查找商品在已选中商品列表中的索引
|
||||
const findCheckedIndex = (spu: MallSpuApi.Spu) =>
|
||||
checkedSpus.value.findIndex((item) => item.id === spu.id);
|
||||
|
||||
// 计算全选框状态
|
||||
const calculateIsCheckAll = () => {
|
||||
isCheckAll.value = list.value.every(
|
||||
(spu) => checkedStatus.value[spu.id || 0],
|
||||
);
|
||||
// 计算中间状态:不是全部选中 && 任意一个选中
|
||||
isIndeterminate.value =
|
||||
!isCheckAll.value &&
|
||||
list.value.some((spu) => checkedStatus.value[spu.id || 0]);
|
||||
};
|
||||
|
||||
// 分类列表
|
||||
const categoryList = ref<MallCategoryApi.Category[]>();
|
||||
// 分类树
|
||||
const categoryTreeList = ref();
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await getList();
|
||||
// 获得分类树
|
||||
categoryList.value = await ProductCategoryApi.getCategoryList({});
|
||||
categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:append-to-body="true"
|
||||
title="选择商品"
|
||||
width="70%"
|
||||
>
|
||||
<ContentWrap>
|
||||
<ElForm
|
||||
:inline="true"
|
||||
:model="queryParams"
|
||||
class="-mb-15px"
|
||||
label-width="68px"
|
||||
>
|
||||
<ElFormItem label="商品名称" prop="name">
|
||||
<ElInput
|
||||
v-model="queryParams.name"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请输入商品名称"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="商品分类" prop="categoryId">
|
||||
<ElTreeSelect
|
||||
v-model="queryParams.categoryId"
|
||||
:data="categoryTreeList"
|
||||
:props="{
|
||||
children: 'children',
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
isLeaf: 'leaf',
|
||||
emitPath: false,
|
||||
}"
|
||||
check-strictly
|
||||
class="!w-240px"
|
||||
node-key="id"
|
||||
placeholder="请选择商品分类"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="创建时间" prop="createTime">
|
||||
<ElDatePicker
|
||||
v-model="queryParams.createTime"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-240px"
|
||||
end-placeholder="结束日期"
|
||||
start-placeholder="开始日期"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<ElButton @click="handleQuery">
|
||||
<Icon class="mr-5px" icon="ep:search" />
|
||||
搜索
|
||||
</ElButton>
|
||||
<ElButton @click="resetQuery">
|
||||
<Icon class="mr-5px" icon="ep:refresh" />
|
||||
重置
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<ElTable v-loading="loading" :data="list" show-overflow-tooltip>
|
||||
<!-- 1. 多选模式(不能使用type="selection",Element会忽略Header插槽) -->
|
||||
<ElTableColumn width="55" v-if="multiple">
|
||||
<template #header>
|
||||
<ElCheckbox
|
||||
v-model="isCheckAll"
|
||||
:indeterminate="isIndeterminate"
|
||||
@change="handleCheckAll"
|
||||
/>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<ElCheckbox
|
||||
v-model="checkedStatus[row.id]"
|
||||
@change="(checked: boolean) => handleCheckOne(checked, row, true)"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<!-- 2. 单选模式 -->
|
||||
<ElTableColumn label="#" width="55" v-else>
|
||||
<template #default="{ row }">
|
||||
<ElRadio
|
||||
:value="row.id"
|
||||
v-model="selectedSpuId"
|
||||
@change="handleSingleSelected(row)"
|
||||
>
|
||||
<!-- 空格不能省略,是为了让单选框不显示label,如果不指定label不会有选中的效果 -->
|
||||
|
||||
</ElRadio>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
key="id"
|
||||
align="center"
|
||||
label="商品编号"
|
||||
prop="id"
|
||||
min-width="60"
|
||||
/>
|
||||
<ElTableColumn label="商品图" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<ElImage
|
||||
:src="row.picUrl"
|
||||
class="h-30px w-30px"
|
||||
:preview-src-list="[row.picUrl]"
|
||||
preview-teleported
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="商品名称" min-width="200" prop="name" />
|
||||
<ElTableColumn label="商品分类" min-width="100" prop="categoryId">
|
||||
<template #default="{ row }">
|
||||
<span>{{
|
||||
categoryList?.find(
|
||||
(c: MallCategoryApi.Category) => c.id === row.categoryId,
|
||||
)?.name
|
||||
}}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
v-model:limit="queryParams.pageSize"
|
||||
v-model:page="queryParams.pageNo"
|
||||
:total="total"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
<template #footer v-if="multiple">
|
||||
<ElButton type="primary" @click="handleEmitChange">确 定</ElButton>
|
||||
<ElButton @click="dialogVisible = false">取 消</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
Reference in New Issue
Block a user