电商 SKU 选择器:用算法实现优雅的用户交互

前言

最近一直忙于找工作,所以停更了一段时间,慢慢整理思绪,有招聘意向的可以联系本人,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」

系统计算

  1. 筛选出 size === 'M' 的 SKU

    javascript 复制代码
    匹配的SKU = [
      { size: "M", color: "黑色", material: "尼龙" },
      { size: "M", color: "灰色", material: "皮革" },
    ];
  2. 提取「颜色」维度下的有效值

    javascript 复制代码
    有效颜色 = ["黑色", "灰色"];
  3. 计算禁用的颜色

    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 性能优化

问题:每次选择都要重新计算所有维度的禁用状态,数据量大时会不会卡?

优化方案

  1. 缓存计算结果 :用 useMemo 或缓存对象存储计算结果
  2. 防抖处理:如果规格数量特别多,可以加防抖
  3. 按需计算:只重新计算受影响的维度
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 扩展功能

  1. 规格图片关联:点击规格显示对应商品图
  2. 库存不足提示:低库存显示"仅剩 N 件"
  3. 价格日历:不同日期不同价格
  4. 规格组合推荐:热门组合高亮显示

八、总结

核心要点回顾

  1. 算法本质:筛选匹配的 SKU,提取有效值,剩下的就是禁用的值
  2. 数据结构:后端只返回有库存的 SKU,前端根据已选规格动态计算
  3. 交互设计:从源头禁用无效选项,避免用户产生挫败感
  4. 状态管理:维护已选规格和禁用状态,每次选择触发联动更新

学习收获

  • ✅ 理解电商 SKU 选择器的交互逻辑
  • ✅ 掌握智能联动禁用的核心算法
  • ✅ 学会用筛选和映射解决实际问题
  • ✅ 了解 Vue 组件化的最佳实践

一句话总结

SKU 选择器的核心就是:根据已选规格筛选出匹配的 SKU,然后找出每个维度下哪些值没有出现,把这些值禁用掉。


如果本文对你有帮助,欢迎点赞、收藏、分享!有问题欢迎在评论区讨论。

相关推荐
贵州数擎科技有限公司6 小时前
霓虹沙尘暴的 Three.js 实现
前端·webgl
代码中介商7 小时前
红黑树完全指南:从五条性质到完整插入删除实现
数据结构·算法
Aolith7 小时前
事件驱动设计:我如何为校园论坛实现消息通知功能
前端·vue.js
代码煮茶7 小时前
Vue3 Mock 数据实战 | 用 Mockjs + vite-plugin-mock 搭建前端独立开发环境
javascript·vue.js
JieE2127 小时前
反转链表:从双指针到递归,吃透链表反转的核心逻辑
javascript·算法
yingyima7 小时前
GitHub Actions 定时任务 schedule 踩坑实录:核心语法与实战技巧
前端
代码煮茶7 小时前
CSS 单位完全指南:px、em、rem、vw、vh、clamp 详解
前端·css
KaMeidebaby7 小时前
卡梅德生物技术快报|PROTAC 药物降解蛋白原理及数据库平台开发全流程
前端·数据库·其他·百度·新浪微博
玖釉-7 小时前
旋转图像:从矩阵转置、镜像到坐标变换的系统理解
c++·windows·算法·图形渲染