前言
最近一直忙于找工作,所以停更了一段时间,慢慢整理思绪,有招聘意向的可以联系本人,base北京,岗位前端开发
💡 更多技术分享,欢迎访问我的博客:叁木の小屋
最近刷视频时偶然看到关于电商 SKU 选择器的讨论,还有大佬纯手搓的代码实现,才发现这个看似简单的功能背后,其实藏着不少有趣的技术细节,真是让人大受震撼。
SKU(Stock Keeping Unit,库存量单位)选择器是电商系统中的经典功能。看似简单的规格选择按钮,背后却隐藏着一个精巧的算法设计,支撑着整个用户交互体验。
你是否遇到过这样的场景:
用户选择了「尺寸 = M」,然后发现「颜色」选项中只有「黑色」和「灰色」可以点击,「蓝色」和「红色」都是灰色的禁用状态?
这就是智能联动禁用------用户选择某个规格后,系统自动禁用其他维度中无库存的选项。
本文将带你从零开始,彻底理解并实现一个完整的 SKU 选择器。
一、为什么需要智能联动?
1.1 用户体验痛点
假设我们不实现智能联动,会发生什么?
场景:用户想买一个双肩包
- 用户先选了「M 码」
- 用户再选了「红色」
- 点击"加入购物车"时,系统提示:「M 码 + 红色」这个组合没库存
用户反应:😤 "那你一开始让我选红色干什么?"
这就是典型的无效交互,让用户产生了挫败感。
1.2 正确的交互方式
- 用户选择「M 码」后
- 系统立即计算:在「M 码」的前提下,哪些颜色有库存
- 没库存的颜色直接禁用,用户根本点不了
- 从源头避免用户选择无效组合
用户体验:✅ "我只能选有库存的选项,不会选错"
1.3 SKU 效果展示
二、核心算法:用一句话说清楚
2.1 算法的本质
问题:如何知道某个选项应该禁用?
答案:
objectivec
在当前已选择的规格下,查找所有匹配的 SKU,
然后看该维度的哪些值出现在这些 SKU 中,
没出现的值 → 禁用
2.2 举个例子
假设库存数据:
javascript
skus = [
{ size: "M", color: "黑色", material: "尼龙" },
{ size: "M", color: "灰色", material: "皮革" },
{ size: "L", color: "红色", material: "帆布" },
];
用户操作:选择了「尺寸 = M」
系统计算:
-
筛选出
size === 'M'的 SKUjavascript匹配的SKU = [ { size: "M", color: "黑色", material: "尼龙" }, { size: "M", color: "灰色", material: "皮革" }, ]; -
提取「颜色」维度下的有效值
javascript有效颜色 = ["黑色", "灰色"]; -
计算禁用的颜色
javascript所有颜色 = ['黑色', '灰色', '红色', '蓝色'] 禁用颜色 = 所有颜色 - 有效颜色 = ['红色', '蓝色']
结果:红色和蓝色的按钮被禁用 ✅
三、数据结构设计
3.1 后端返回的数据格式
javascript
{
// 规格维度定义
specs: [
{
name: "尺寸", // 显示名称
key: "size", // 唯一标识
values: ["S", "M", "L"] // 该维度所有可能的值
},
{
name: "颜色",
key: "color",
values: ["黑色", "灰色", "红色"]
},
{
name: "材质",
key: "material",
values: ["尼龙", "皮革", "帆布"]
}
],
// 有库存的 SKU 组合(注意:只返回有库存的)
skus: [
{
specs: { size: "M", color: "黑色", material: "尼龙" },
stock: 15,
price: 219
},
{
specs: { size: "M", color: "灰色", material: "皮革" },
stock: 8,
price: 299
}
// ... 其他有库存的组合
]
}
注意 :后端只需要返回有库存 的 SKU,无库存的不需要在 skus 数组中。
3.2 前端状态管理
javascript
const state = {
// 已选择的规格
selectedSpecs: {
size: "M",
color: "黑色",
},
// 各维度下禁用的值
disabledSpecs: {
size: [],
color: ["红色", "蓝色"],
material: ["帆布"],
},
// 当前匹配的完整 SKU(选择完整后才有值)
currentSKU: null,
};
四、核心算法实现
4.1 getDisabledOptions 函数
这是整个功能的核心,只需要这一个函数就能搞定禁用逻辑。
javascript
/**
* 计算某个维度下应该禁用的选项值
* @param {string} dimensionKey - 维度的 key(如 'size', 'color')
* @param {object} selectedSpecs - 当前已选择的规格
* @param {array} skus - 所有 SKU 数据
* @param {array} specs - 规格定义
* @returns {array} 应该禁用的值列表
*/
const getDisabledOptions = (dimensionKey, selectedSpecs, skus, specs) => {
// 步骤 1:获取该维度的定义
const dimension = specs.find(s => s.key === dimensionKey);
if (!dimension) return [];
// 步骤 2:获取已选择的维度 key(排除当前维度)
const selectedKeys = Object.keys(selectedSpecs).filter(k => k !== dimensionKey);
// 步骤 3:如果还没选择任何规格,所有选项都可用
if (selectedKeys.length === 0) return [];
// 步骤 4:根据已选择的规格筛选出匹配的 SKU
const matchedSKUs = skus.filter(sku =>
selectedKeys.every(key => sku.specs[key] === selectedSpecs[key])
);
// 步骤 5:从匹配的 SKU 中提取该维度下所有有效的值
const validValues = matchedSKUs.map(sku => sku.specs[dimensionKey]);
// 步骤 6:所有值减去有效值 = 禁用的值
return dimension.values.filter(value => !validValues.includes(value));
};
4.2 算法流程图
ini
┌─────────────────────────────────────┐
│ 用户选择了一个规格(如 size = M) │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 步骤 1:获取已选择的规格(排除当前) │
│ selectedSpecs = { size: 'M' } │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 步骤 2:筛选匹配的 SKU │
│ filter(sku.specs.size === 'M') │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 步骤 3:提取该维度下的有效值 │
│ matchedSKUs.map(sku => sku.color) │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 步骤 4:计算禁用的值 │
│ 所有值 - 有效值 = 禁用的值 │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 步骤 5:更新 UI,禁用对应按钮 │
└─────────────────────────────────────┘
五、完整实现
5.1 交互逻辑
javascript
// 当用户点击规格按钮时
const onSpecSelect = (specKey, specValue) => {
// 检查是否禁用
if (state.disabledSpecs[specKey]?.includes(specValue)) return;
// 更新已选规格(点击已选中的则取消选择)
if (state.selectedSpecs[specKey] === specValue) {
delete state.selectedSpecs[specKey];
} else {
state.selectedSpecs[specKey] = specValue;
}
// 重新计算所有维度的禁用状态
updateDisabledSpecs();
// 查找当前匹配的 SKU 并刷新界面
state.currentSKU = findMatchedSKU();
render();
};
// 更新所有维度的禁用状态
const updateDisabledSpecs = () => {
specs.forEach(spec => {
state.disabledSpecs[spec.key] = getDisabledOptions(
spec.key,
state.selectedSpecs,
skus,
specs,
);
});
};
5.2 价格显示逻辑
javascript
const renderPrice = () => {
if (state.currentSKU) {
// 已完整选择,显示精确价格
priceDisplay.textContent = state.currentSKU.price;
} else {
// 未完整选择,显示价格区间
const matchedSKUs = filterMatchedSKUs();
const prices = matchedSKUs.map(s => s.price);
priceDisplay.textContent = `${Math.min(...prices)} - ${Math.max(...prices)}`;
}
};
5.3 UI 渲染(伪代码)
javascript
const renderSpecButtons = () => {
specs.forEach(spec => {
spec.values.forEach(value => {
const button = createButton(value);
// 判断状态
const isSelected = state.selectedSpecs[spec.key] === value;
const isDisabled = state.disabledSpecs[spec.key]?.includes(value);
if (isSelected) button.classList.add('selected');
if (isDisabled) button.classList.add('disabled');
button.onClick = () => onSpecSelect(spec.key, value);
});
});
};
六、Vue 组件化封装
6.1 组件结构
vue
<template>
<div class="sku-selector">
<!-- 规格选择区域 -->
<div v-for="spec in specs" :key="spec.key" class="spec-group">
<div class="spec-label">{{ spec.name }}</div>
<div class="spec-buttons">
<button
v-for="value in spec.values"
:key="value"
:class="{
'spec-btn': true,
selected: selectedSpecs[spec.key] === value,
disabled: disabledSpecs[spec.key]?.includes(value),
}"
@click="onSelect(spec.key, value)"
>
{{ value }}
</button>
</div>
</div>
<!-- 价格显示 -->
<div class="price">
{{ currentSKU ? currentSKU.price : priceRangeText }}
</div>
</div>
</template>
<script setup>
import { reactive, computed, watch } from "vue";
const props = defineProps({
specs: Array,
skus: Array,
});
const emit = defineEmits(["change"]);
const state = reactive({
selectedSpecs: {},
disabledSpecs: {},
});
// 计算属性:当前匹配的 SKU
const currentSKU = computed(() => {
const selectedKeys = Object.keys(state.selectedSpecs);
if (selectedKeys.length !== props.specs.length) return null;
return props.skus.find((sku) =>
selectedKeys.every((key) => sku.specs[key] === state.selectedSpecs[key]),
);
});
// 计算属性:价格区间文本
const priceRangeText = computed(() => {
// ... 价格区间计算逻辑
});
// 核心函数:计算禁用选项
const getDisabledOptions = (dimensionKey, selectedSpecs) => {
// ... 算法实现(同上)
};
// 更新所有维度的禁用状态
const updateDisabledSpecs = () => {
props.specs.forEach((spec) => {
state.disabledSpecs[spec.key] = getDisabledOptions(
spec.key,
state.selectedSpecs,
);
});
};
// 处理规格选择
const onSelect = (key, value) => {
if (state.disabledSpecs[key]?.includes(value)) return;
state.selectedSpecs[key] =
state.selectedSpecs[key] === value ? undefined : value;
updateDisabledSpecs();
emit("change", state.selectedSpecs);
};
// 监听选择变化,自动更新禁用状态
watch(() => state.selectedSpecs, updateDisabledSpecs, { deep: true });
// 初始化
updateDisabledSpecs();
</script>
七、常见问题与优化
7.1 性能优化
问题:每次选择都要重新计算所有维度的禁用状态,数据量大时会不会卡?
优化方案:
- 缓存计算结果 :用
useMemo或缓存对象存储计算结果 - 防抖处理:如果规格数量特别多,可以加防抖
- 按需计算:只重新计算受影响的维度
javascript
// 缓存示例
const cache = new Map();
const getDisabledOptionsCached = (dimensionKey, selectedSpecs) => {
const cacheKey = JSON.stringify({ dimensionKey, selectedSpecs });
if (cache.has(cacheKey)) return cache.get(cacheKey);
const result = getDisabledOptions(dimensionKey, selectedSpecs);
cache.set(cacheKey, result);
return result;
};
7.2 边界情况处理
| 情况 | 处理方式 |
|---|---|
| 后端返回空 SKU 列表 | 显示"暂无库存",禁用所有按钮 |
| 某个维度只有一个值 | 直接默认选中,提升体验 |
| 用户选择后又取消 | 重新计算禁用状态,恢复可用选项 |
| 切换同一维度的选项 | 自动清除其他维度的冲突选择 |
7.3 扩展功能
- 规格图片关联:点击规格显示对应商品图
- 库存不足提示:低库存显示"仅剩 N 件"
- 价格日历:不同日期不同价格
- 规格组合推荐:热门组合高亮显示
八、总结
核心要点回顾
- 算法本质:筛选匹配的 SKU,提取有效值,剩下的就是禁用的值
- 数据结构:后端只返回有库存的 SKU,前端根据已选规格动态计算
- 交互设计:从源头禁用无效选项,避免用户产生挫败感
- 状态管理:维护已选规格和禁用状态,每次选择触发联动更新
学习收获
- ✅ 理解电商 SKU 选择器的交互逻辑
- ✅ 掌握智能联动禁用的核心算法
- ✅ 学会用筛选和映射解决实际问题
- ✅ 了解 Vue 组件化的最佳实践
一句话总结
SKU 选择器的核心就是:根据已选规格筛选出匹配的 SKU,然后找出每个维度下哪些值没有出现,把这些值禁用掉。
如果本文对你有帮助,欢迎点赞、收藏、分享!有问题欢迎在评论区讨论。
