Vue3商品SKU多规格编辑组件

商品SKU多规格组件主要用于电商平台的商品详情页,帮助买家在选择商品时能够根据不同的规格(如颜色、尺码、容量等)进行筛选和购买。这种组件的设计旨在提升用户体验,简化购买流程,并确保买家能够准确地选择到自己需要的商品规格。以下是对商品SKU多规格组件的详细解析:

1. 定义与功能

  • SKU(Stock Keeping Unit)定义:SKU即库存量单位,是指宝贝的销售属性集合,供买家在下单时点选,如“规格”、“颜色分类”、“尺码”等。部分SKU的属性值可以由卖家自定义编辑,部分则不可编辑。
  • 功能:商品SKU多规格组件允许买家在商品详情页直接查看并选择商品的多种规格,同时展示每种规格对应的库存数量、价格等信息。这有助于买家快速了解商品的各种选项,并做出购买决策。

2. 组件特点

  • 数据驱动:现代的商品SKU多规格组件通常采用数据驱动的方式,即组件从服务器获取商品规格数据,并根据这些数据动态渲染出可供买家选择的规格选项。这种方式提高了组件的灵活性和可扩展性。
  • 用户友好:组件的设计注重用户体验,通过清晰的界面布局和交互设计,使买家能够轻松理解和选择商品的规格。
  • 实时更新:组件能够实时从服务器获取商品的库存信息和价格信息,确保买家在选择规格时能够看到最新的数据。

3. 实现方式

  • 前端开发:商品SKU多规格组件通常使用前端技术进行开发。这里使用了Vue3现代前端框架中,可以通过组件化的方式实现商品SKU多规格组件的复用和灵活配置。
  • 后端支持:组件需要后端服务的支持,以获取商品的规格数据、库存信息、价格信息等。后端服务可以通过API接口向前端组件提供这些数据。
  • 数据交互:前端组件与后端服务之间通过HTTP请求和响应进行数据交互。前端组件发送请求获取数据,后端服务处理请求并返回数据给前端组件进行展示。

4. 应用场景

  • 电商平台:商品SKU多规格组件广泛应用于各类电商平台,如淘宝、京东、天猫等。这些平台上的商品种类繁多,规格多样,使用SKU多规格组件可以大大提升买家的购物体验。
  • 零售店铺:对于拥有线上销售渠道的零售店铺来说,商品SKU多规格组件也是一个不可或缺的工具。它可以帮助店铺更好地展示商品信息,提高销售额。

5. 组件实现

<template>
	<div class="diygw-col-24">
		<div v-if="specs.length > 0">
			<VueDraggable v-bind="{ animation: 200, disabled: false, ghostClass: 'ghost' }" v-model="specs" class="list-group" ghost-class="ghost">
				<div v-for="(element, itemIndex) in specs" class="box sku-spec flex flex-direction-column margin-bottom">
					<div class="flex margin-bottom-xs align-center justify-between">
						<el-input style="width: 200px !important; flex: unset" placeholder="请输入规格名称" v-model="element.title"> </el-input>
						<div class="toolbar">
							<i class="diyicon handle diyicon-yidongxuanze" />
							<el-tooltip content="上移" placement="bottom">
								<i class="diy-icon-top" @click.stop="move(itemIndex, 'up')" />
							</el-tooltip>
							<el-tooltip content="下移" placement="bottom">
								<i class="diy-icon-down" @click.stop="move(itemIndex, 'down')" />
							</el-tooltip>
							<el-tooltip content="新增规格组" placement="bottom">
								<i class="diy-icon-add" @click.stop="addSpec()" />
							</el-tooltip>
							<el-tooltip content="删除规格组" placement="bottom">
								<i class="diy-icon-close" @click.stop="removeSpec(itemIndex)" />
							</el-tooltip>
						</div>
					</div>
					<div class="flex flex-wrap align-center">
						<VueDraggable
							v-bind="{ animation: 200, disabled: false, ghostClass: 'ghost' }"
							v-model="element.datas"
							class="flex flex-wrap align-center sku-spec-item"
							ghost-class="ghost"
						>
							<div v-for="(subitem, subindex) in element.datas" class="margin-right-xs spec-item margin-bottom-xs" style="width: 150px">
								<el-input v-model="subitem.title"> </el-input>
								<div class="spec-toolbar">
									<el-tooltip content="删除规格值" placement="bottom">
										<i class="diy-icon-close" @click.stop="removeSpecItem(itemIndex, subindex)" />
									</el-tooltip>
								</div>
							</div>
						</VueDraggable>
						<div class="margin-right-xs margin-bottom-xs text-xs" @click="addSpecIndex(itemIndex)">新增规格值</div>
					</div>
				</div>
			</VueDraggable>
		</div>
		<el-button class="diygw-col-24 margin-bottom-xs" @click="addSpec">新增规格</el-button>
		<DiySkutable v-model="skus" :specs="specs" :columns="columns" ref="skuref"></DiySkutable>
	</div>
</template>

<script setup name="DiySku">
import { ElMessage } from 'element-plus';
import { useVModels } from '@vueuse/core';
import { VueDraggable } from 'vue-draggable-plus';
import DiySkutable from './skutable.vue';
const props = defineProps({
	skus: {
		type: Array,
		default: () => {
			return [];
		},
	},
	specs: {
		//规格配置
		type: Array,
		default: () => {
			return [];
		},
	},
	columns: {
		//自定义sku属性
		type: Array,
		default: () => {
			return [];
		},
	},
	filterable: {
		//是否开启sku搜索
		type: Boolean,
		default: true,
	},
});

const emit = defineEmits(['update:specs', 'update:skus']);
const { skus, specs } = useVModels(props, emit);
const removeSpec = (index) => {
	specs.value.splice(index, 1);
};

const removeSpecItem = (index, subindex) => {
	specs.value[index].datas.splice(subindex, 1);
};

const addSpecIndex = (index) => {
	let id = specs.value[index].datas.length + specs.value[index].id;
	specs.value[index].datas.push({
		title: '',
		id: id + '' + new Date().getTime(),
	});
};

const addSpec = () => {
	let id = (specs.value.length + 1) * 1000;
	specs.value.push({
		id: id,
		title: '',
		datas: [
			{
				title: '',
				id: id + '' + new Date().getTime(),
			},
		],
	});
};

const move = (itemIndex, dir) => {
	let comps = specs.value;
	let item = comps[itemIndex];
	if (dir == 'up') {
		if (itemIndex == 0) {
			ElMessage.error('已经是第一个了!');
			return;
		}
		const swap = comps[itemIndex - 1];
		const tmp = swap;
		comps[itemIndex - 1] = item;
		comps[itemIndex] = tmp;
	} else {
		if (itemIndex == comps.length - 1) {
			ElMessage.error('已经是最一个了!');
			return;
		} else {
			const swap = comps[itemIndex + 1];
			const tmp = swap;
			comps[itemIndex + 1] = item;
			comps[itemIndex] = tmp;
		}
	}
};
</script>
<style lang="scss">
.sku-spec {
	padding: 6px;
	border-radius: 5px;
	border: 1px solid #ebeef5;

	.sku-spec-item {
		padding-left: 30px;
		padding-right: 5px;
		.spec-item {
			position: relative;

			.spec-toolbar {
				top: -2px;
				right: -3px;
				position: absolute;
				display: none;
			}

			&:hover {
				.spec-toolbar {
					display: block;
				}
			}
		}
	}
}
</style>
<template>
	<div class="sku-table" v-if="specs.length > 0">
		<table class="diy-table diy-skutable">
			<tr>
				<th v-for="(item, colIndex) in specs" :key="colIndex">
					{{ item.title }}
				</th>
				<th v-for="(item, colIndex) in columns" :style="{ width: item.type == 'number' ? '100px' : 'auto' }" :key="colIndex + specs.length + 1">
					{{ item.title }}
				</th>
			</tr>
			<tr>
				<td v-for="(item, colIndex) in specs" :key="colIndex">
					<el-select v-if="filterable" @change="resetTable" v-model="headerFilterParams[item.title]" placeholder="过滤查询" clearable>
						<el-option label="全部" value=""></el-option>
						<el-option v-for="(child, index2) in item.datas" :key="index2" :label="child.title" :value="child.title"></el-option>
					</el-select>
					<span v-else class="spec-title">
						{{ item.title }}
					</span>
				</td>
				<td v-for="(item, colIndex) in columns" :key="colIndex + specs.length + 1">
					<span v-if="item.readOnly">{{ item.title }}</span>
					<diy-uploadinput
						class="diygw-col-24"
						v-else-if="item.type == 'img'"
						@blur="
							() => {
								onBatchEdit(item.id);
							}
						"
						placeholder="批量修改"
						v-model="columnsValue[item.id]"
					/>
					<el-input-number
						:min="0"
						:precision="item.precision ? item.precision : 0"
						:step="item.precision ? 0.1 : 1"
						v-else-if="item.type == 'number'"
						v-model="columnsValue[item.id]"
						controls-position="right"
						:controls="false"
						class="sku-input-number"
						@blur="
							() => {
								onBatchEdit(item.id);
							}
						"
						@keyup.native.enter="
							() => {
								onBatchEdit(item.id);
							}
						"
						placeholder="批量修改"
					/>
					<el-input
						v-else
						class="diygw-col-24"
						v-model="columnsValue[item.id]"
						@blur="
							() => {
								onBatchEdit(item.id);
							}
						"
						@keyup.native.enter="
							() => {
								onBatchEdit(item.id);
							}
						"
						placeholder="批量修改"
					/>
				</td>
			</tr>
			<tr v-for="(row, rowIndex) in renderTableRows" :key="row.id">
				<td
					v-for="(child, colIndex) in row.columns"
					:class="[
						child.shouldSetRowSpan ? '' : 'hide',
						rowIndex === rowLastCanSpan[colIndex] ? 'col-last-rowspan' : '',
						colIndex < specs.length - 1 ? 'row-span-style' : '',
					]"
					:rowspan="child.shouldSetRowSpan ? assignRule[colIndex] : ''"
					:key="colIndex"
				>
					<span>{{ child.showValue }}</span>
				</td>
				<td v-for="(child, colIndex) in columns" :key="colIndex + columns.length + 1">
					<span v-if="child.readOnly">{{ row[child.id] }}</span>
					<diy-uploadinput
						v-else-if="child.type == 'img'"
						@change="
							(value) => {
								checkValue(value, child, row);
							}
						"
						:placeholder="child.title"
						v-model="row[child.id]"
					></diy-uploadinput>
					<el-input-number
						:min="0"
						:precision="child.precision ? child.precision : 0"
						:step="child.precision ? 0.1 : 1"
						v-else-if="child.type == 'number'"
						v-model="row[child.id]"
						controls-position="right"
						:controls="false"
						class="sku-input-number"
						@change="
							(value) => {
								checkValue(value, child, row);
							}
						"
						:placeholder="child.title"
					/>
					<el-input
						v-else
						v-model="row[child.id]"
						@change="
							(value) => {
								checkValue(value, child, row);
							}
						"
						:placeholder="child.title"
					/>
				</td>
			</tr>
		</table>
	</div>
</template>

<script setup name="DiySkutable">
import { uuid } from '@/utils';
import { ElMessageBox } from 'element-plus';
import { onMounted, ref, watch, computed } from 'vue';
import DiyUploadinput from '@/components/upload/uploadinput.vue';
import { useVModel } from '@vueuse/core';

const props = defineProps({
	modelValue: {
		type: Array,
		default: () => {
			return [];
		},
	},
	specs: {
		//规格配置
		type: Array,
		default: () => {
			return [];
		},
	},
	columns: {
		//自定义sku属性
		type: Array,
		default: () => {
			return [];
		},
	},
	filterable: {
		//是否开启sku搜索
		type: Boolean,
		default: true,
	},
});

const emit = defineEmits(['update:modelValue']);
const modeldata = useVModel(props, 'modelValue', emit);

const headerFilterParams = ref({});
const columnsValue = ref({});
const originTableRows = ref([]);
const renderTableRows = ref([]);

onMounted(() => {
	render();
});

function render() {
	originTableRows.value = createTable();
	renderTableRows.value = originTableRows.value;
	getData();
}

function _resetRowSpan(table) {
	table.forEach((row, rowIndex) => {
		row.columns.forEach((column, columnIndex) => {
			column.shouldSetRowSpan = shouldSetRowSpan(rowIndex, columnIndex);
		});
	});
}
function createTable() {
	let tableData = [];
	let details = props.modelValue;
	let theaderFilterParams = {};
	props.specs.forEach((item) => {
		theaderFilterParams[item.title] = '';
	});
	headerFilterParams.value = theaderFilterParams;
	for (let i = 0; i < skuTotal.value; i++) {
		let columns = props.specs.map((t, j) => {
			let { title, id } = getShowValue(i, j);
			return {
				shouldSetRowSpan: shouldSetRowSpan(i, j),
				showValue: title,
				valueId: id,
				columnName: t.title,
				columnId: t.id,
				precision: t.precision,
			};
		});
		//获取当前组合
		let pattern = columns
			.map((t) => t.valueId)
			.sort()
			.toString();
		//从详情中找回同一个组合的sku数据
		let rowDetails = details.find((t) => t.specIds === pattern);
		//当数据长度不为0,并新增了大的规格
		if (details.length > 0 && details.length >= i && !rowDetails && details[0].specIds.split(',').length != columns.length) {
			rowDetails = details[i];
		}
		tableData.push({
			id: uuid(),
			...createSkuPropertyFields(columns, i, rowDetails),
			columns,
		});
	}
	return tableData;
}
function createSkuPropertyFields(columns, rowIndex, row) {
	return props.columns.reduce((res, item) => {
		if (row && row[item.id]) {
			res[item.id] = row[item.id] || '';
		} else {
			if (item.defaultValue) {
				// 设置默认值,可以为string或function,fuction时会传入行的索引和列的信息
				if (typeof item.defaultValue === 'string') {
					res[item.id] = item.defaultValue;
				} else if (typeof item.defaultValue === 'function') {
					res[item.id] = item.defaultValue({ columns, rowIndex });
				}
			} else if (columnsValue.value[item.id]) {
				res[item.id] = columnsValue.value[item.id];
			} else if (res.type == 'number') {
				res[item.id] = 0;
			} else {
				res[item.id] = '';
			}
		}
		return res;
	}, {});
}
function shouldSetRowSpan(rowIndex, colIndex) {
	return rowIndex % assignRule.value[colIndex] === 0;
}
function getShowValue(rowIndex, colIndex) {
	let datas = props.specs[colIndex].datas;
	let index;
	if (colIndex === props.specs.length - 1) {
		index = rowIndex % datas.length;
	} else {
		let step = assignRule.value[colIndex];
		index = Math.floor(rowIndex / step);
		if (index >= datas.length) {
			index = index % datas.length;
		}
	}
	return datas[index];
}

function onBatchEdit(id) {
	ElMessageBox.confirm(`确认批量修改吗?`, '提示', {
		customClass: 'diygw-messagebox',
		confirmButtonText: '确认',
		cancelButtonText: '取消',
		type: 'warning',
	})
		.then(() => {
			let value = columnsValue.value[id];
			renderTableRows.value.forEach((item) => {
				item[id] = value;
			});
			getData();
		})
		.catch(() => {});
}
function checkValue(value, columnInfo, row) {
	// let { id, pattern } = columnInfo;
	// if (pattern) {
	// 	(row || columnsValue.value)[id] = value.replace(pattern, '');
	// }
	getData();
}
function getData() {
	let data = originTableRows.value.map((t) => {
		let columnObj = props.columns.reduce((res, item) => {
			res[item.id] = item.format ? item.format(t[item.id]) : t[item.id];
			return res;
		}, {});
		return {
			specIds: t.columns.map((t) => t.valueId).join(','),
			...columnObj,
		};
	});
	modeldata.value = data;
}

const hasFilter = computed(() => {
	return Object.values(headerFilterParams.value).filter((t) => !!t).length > 0;
});

const renderSpecs = computed(() => {
	return props.specs
		.map((t) => t.datas.length)
		.map((t, index) => {
			return hasFilter.value ? (headerFilterParams.value[props.specs[index].title] ? 1 : t) : t;
		});
});

const skuTotal = computed(() => {
	return renderSpecs.value.reduce((result, item) => result * item, renderSpecs.value.length ? 1 : 0);
});

const assignRule = computed(() => {
	return renderSpecs.value.reduce((result, item, index) => {
		let preValue = result[index - 1];
		if (preValue) {
			result.push(preValue / item);
		} else {
			result.push(skuTotal.value / item);
		}
		return result;
	}, []);
});

const rowLastCanSpan = computed(() => {
	let indexArr = Array.from(new Array(skuTotal.value).keys()); //生成行的索引数组
	//每列可以合并的最后一行的行索引数组,为了设置样式
	return assignRule.value.map((t, index, array) => {
		return index === array.length - 1 ? null : indexArr.filter((row) => row % t === 0).pop();
	});
});

watch(
	() => props.specs,
	() => {
		render();
	},
	{
		deep: true,
		immediate: true,
	}
);
function resetTable() {
	if (hasFilter.value) {
		let trenderTableRows = originTableRows.value.filter((t) => {
			return t.columns.reduce((res, item) => {
				let filterValue = headerFilterParams.value[item.columnName];
				return filterValue ? res && item.showValue === filterValue : res;
			}, true);
		});
		_resetRowSpan(trenderTableRows);
		renderTableRows.value = trenderTableRows;
	} else {
		_resetRowSpan(originTableRows.value);
		renderTableRows.value = originTableRows.value;
	}
}
// watch(
// 	headerFilterParams,
// 	() => {
// 		resetTable();
// 	},
// 	{
// 		deep: true,
// 		immediate: true,
// 	}
// );
</script>

组件调用

<template>
	<div class="container">
		<div class="el-card is-always-shadow diygw-col-24">
			<div class="el-card__body">
				<div class="mb8">
					<el-button type="primary" plain @click="onOpenAddModule"> <SvgIcon name="ele-Plus" />新增 </el-button>
					<el-button type="danger" plain :disabled="state.multiple" @click="onTabelRowDel"> <SvgIcon name="ele-Delete" />删除 </el-button>
				</div>
				<el-table
					@selection-change="handleSelectionChange"
					v-loading="state.loading"
					:data="state.tableData"
					stripe
					border
					current-row-key="id"
					empty-text="没有数据"
					style="width: 100%"
				>
					<el-table-column type="selection" width="55" align="center"></el-table-column>
					<el-table-column label="姓名" prop="name" align="center"> </el-table-column>
					<el-table-column label="性别" prop="sex" align="center"> </el-table-column>
					<el-table-column label="邮箱" prop="email" align="center"> </el-table-column>
					<el-table-column label="操作" align="center" fixed="right" width="180">
						<template #default="scope">
							<el-button type="primary" plain size="small" @click="onOpenEditModule(scope.row)"> <SvgIcon name="ele-Edit" />修改 </el-button>
							<el-button v-if="scope.row.id != 0" type="danger" plain size="small" @click="onTabelRowDel(scope.row)">
								<SvgIcon name="ele-Delete" />删除
							</el-button>
						</template>
					</el-table-column>
				</el-table>
				<!-- 分页设置-->
				<div v-show="state.total > 0">
					<el-divider></el-divider>
					<el-pagination
						background
						:total="state.total"
						:current-page="state.queryParams.pageNum"
						:page-size="state.queryParams.pageSize"
						layout="total, sizes, prev, pager, next, jumper"
						@size-change="handleSizeChange"
						@current-change="handleCurrentChange"
					/>
				</div>
			</div>
			<!-- 添加或修改参数配置对话框 -->
			<el-dialog :fullscreen="isFullscreen" width="800px" v-model="isShowDialog" destroy-on-close title="CRUD表格" draggable center>
				<template #header="{ close, titleId, titleClass }">
					<h4 :id="titleId" :class="titleClass">CRUD表格</h4>
					<el-tooltip v-if="!isFullscreen" content="最大化" placement="bottom">
						<el-button class="diygw-max-icon el-dialog__headerbtn">
							<el-icon @click="isFullscreen = !isFullscreen">
								<FullScreen />
							</el-icon>
						</el-button>
					</el-tooltip>
					<el-tooltip v-if="isFullscreen" content="返回" placement="bottom">
						<el-button class="diygw-max-icon el-dialog__headerbtn">
							<el-icon @click="isFullscreen = !isFullscreen">
								<Remove />
							</el-icon>
						</el-button>
					</el-tooltip>
				</template>
				<el-form class="flex flex-direction-row flex-wrap" :model="state.editForm" :rules="state.editFormRules" ref="editFormRef" label-width="80px">
					<div class="flex diygw-col-24">
						<el-form-item prop="sku" label="多规格">
							<diy-sku :columns="editFormData.skuColumns" v-model:skus="editForm.sku.skus" v-model:specs="editForm.sku.specs"></diy-sku>
						</el-form-item>
					</div>
				</el-form>
				<template #footer>
					<div class="dialog-footer flex justify-center">
						<el-button @click="closeDialog"> 取 消 </el-button>
						<el-button type="primary" @click="onSubmit" :loading="state.saveloading"> 保 存 </el-button>
					</div>
				</template>
			</el-dialog>
		</div>
		<div class="clearfix"></div>
	</div>
</template>

<script setup name="index">
import { ElMessageBox, ElMessage } from 'element-plus';
import { ref, toRefs, reactive, onMounted, getCurrentInstance, onUnmounted, unref } from 'vue';
import { deepClone, changeRowToForm } from '@/utils/other';
import { addData, updateData, delData, listData } from '@/api';

import { useRouter, useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useUserInfo } from '@/stores/userInfo';

import DiySku from '@/components/sku/index.vue';

const stores = useUserInfo();
const { userInfos } = storeToRefs(stores);
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const globalOption = ref(route.query);
const getParamData = (e, row) => {
	row = row || {};
	let dataset = e.currentTarget ? e.currentTarget.dataset : e;
	if (row) {
		dataset = Object.assign(dataset, row);
	}
	return dataset;
};

const navigateTo = (e, row) => {
	let dataset = getParamData(e, row);
	let { type } = dataset;
	if ((type == 'page' || type == 'inner' || type == 'href') && dataset.url) {
		router.push({
			path: (dataset.url.startsWith('/') ? '' : '/') + dataset.url,
			query: dataset,
		});
	} else {
		ElMessage.error('待实现点击事件');
	}
};

const state = reactive({
	userInfo: userInfos.value,
	// 遮罩层
	loading: true,
	// 选中数组
	ids: [],
	// 非单个禁用
	single: true,
	// 非多个禁用
	multiple: true,
	// 弹出层标题
	title: '',
	// 总条数
	total: 0,
	tableData: [],
	// 是否显示弹出层
	isFullscreen: false,
	isShowDialog: false,
	saveloading: false,

	editFormData: {
		skuColumns: [
			{ title: '图片地址', id: 'thumb', type: 'img' },
			{ title: '价格', id: 'price', type: 'number' },
			{ title: '划线价格', id: 'linePrice', type: 'number' },
			{ title: '数量', id: 'amount', type: 'number' },
			{ title: '备注', id: 'sku', type: 'text' },
		],
	},

	queryParams: {
		pageNum: 1,
		pageSize: 10,
	},

	queryParamsRules: {},

	editForm: {
		id: undefined,
		sku: {
			skus: [],
			specs: [],
		},
	},

	editFormRules: {},
});
const {
	userInfo,
	editFormData,
	queryParams,
	multiple,
	tableData,
	loading,
	title,
	single,
	total,
	isShowDialog,
	editForm,
	ids,
	saveloading,
	isFullscreen,
} = toRefs(state);
const editFormRef = ref(null);

const editFormInit = deepClone(state.editForm);

// 打开弹窗
const openDialog = (row) => {
	if (row.id && row.id != undefined && row.id != 0) {
		state.editForm = changeRowToForm(row, state.editForm);
	} else {
		initForm();
	}
	state.isShowDialog = true;
	state.saveloading = false;
};

// 关闭弹窗
const closeDialog = (row) => {
	proxy.mittBus.emit('onEditAdmintableModule', row);
	state.isShowDialog = false;
};

// 保存
const onSubmit = () => {
	const formWrap = unref(editFormRef);
	if (!formWrap) return;
	formWrap.validate((valid) => {
		if (valid) {
			state.saveloading = true;
			if (state.editForm.id != undefined && state.editForm.id != 0) {
				updateData('/sys/user', state.editForm)
					.then(() => {
						ElMessage.success('修改成功');
						state.saveloading = false;
						closeDialog(state.editForm); // 关闭弹窗
					})
					.finally(() => {
						state.saveloading = false;
					});
			} else {
				addData('/sys/user', state.editForm)
					.then(() => {
						ElMessage.success('新增成功');
						state.saveloading = false;
						closeDialog(state.editForm); // 关闭弹窗
					})
					.finally(() => {
						state.saveloading = false;
					});
			}
		} else {
			ElMessage.error('验证未通过!');
		}
	});
};
// 表单初始化,方法:`resetFields()` 无法使用
const initForm = () => {
	state.editForm = deepClone(editFormInit);
};
const queryParamsInit = deepClone(state.queryParams);
/** 查询CRUD表格列表 */
const handleQuery = () => {
	state.loading = true;
	listData('/sys/user', state.queryParams).then((response) => {
		state.tableData = response.rows;
		state.total = response.total;
		state.loading = false;
	});
};
/** 重置按钮操作 */
const resetQuery = () => {
	state.queryParams = deepClone(queryParamsInit);
	handleQuery();
};

// 打开新增CRUD表格弹窗
const onOpenAddModule = (row) => {
	row = [];
	state.title = '添加CRUD表格';
	initForm();
	openDialog(row);
};
// 打开编辑CRUD表格弹窗
const onOpenEditModule = (row) => {
	state.title = '修改CRUD表格';
	openDialog(row);
};
/** 删除按钮操作 */
const onTabelRowDel = (row) => {
	const id = row.id || state.ids;

	ElMessageBox({
		message: '是否确认删除选中的CRUD表格?',
		title: '警告',
		showCancelButton: true,
		confirmButtonText: '确定',
		cancelButtonText: '取消',
		type: 'warning',
	}).then(function () {
		return delData('/sys/user', id).then(() => {
			handleQuery();
			ElMessage.success('删除成功');
		});
	});
};
// 多选框选中数据
const handleSelectionChange = (selection) => {
	state.ids = selection.map((item) => item.id);
	state.single = selection.length != 1;
	state.multiple = !selection.length;
};

//分页页面大小发生变化
const handleSizeChange = (val) => {
	state.queryParams.pageSize = val;
	handleQuery();
};
//当前页码发生变化
const handleCurrentChange = (val) => {
	state.queryParams.pageNum = val;
	handleQuery();
};
const init = async () => {};
// 页面加载时
onMounted(async () => {
	await init();
	handleQuery();
	proxy.mittBus.on('onEditAdmintableModule', () => {
		handleQuery();
	});
});

// 页面卸载时
onUnmounted(() => {
	proxy.mittBus.off('onEditAdmintableModule');
});
</script>

<style lang="scss" scoped>
.container {
	font-size: 12px;
}
</style>

已集成进开源项目组件:diygw-ui-admin: 基于vite、vue3.x 、router、pinia、Typescript、Element plus等,适配手机、平板、pc 的后台开源免费模板库

  • 19
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
根据引用内容,可以得知在Vue3中处理商品规格SKU的问题时,首先需要考虑将商品的所有规格渲染上去,并且可以根据数据得知该商品有三种规格:颜色、运行内存和存储。 在判断商品规格SKU的库存时,需要注意每点击一个规格属性,都要去判断其他规格属性是否有库存。这可以通过选中完整的规格信息来向父组件传递有效的数据,包括SKU的ID、价格、原价格、库存以及商品的说明等。 因此,在Vue3中处理商品规格SKU的方式可以是通过渲染所有规格属性,并在选中完整的规格信息后,通过传递有效数据来判断SKU的库存情况。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [基于Vue3 Sku的设计](https://blog.csdn.net/qq_43817005/article/details/121889677)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [vue电商项目sku 规格 详细步骤](https://blog.csdn.net/m0_46846526/article/details/119142417)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值