Figma 交互式商品属性矩阵与多维 SKU 规格选择器(E-commerce Multi-Attribute SKU Selector, Inventory Matrix & Price Calculator)组件架构实战:从笛卡尔积规格标签状态机(Normal/Selected/Disabled/Out-of-Stock)、跨维度互斥联动、价格与库存阶梯联动到 Variables 动态计算与 Dev Mode 前端 React 电商交付

2026 年 11 月 15 日

在电子商务、跨境独立站、新零售自营商城以及 DTC 消费数码品牌的产品详情页(PDP, Product Detail Page)中,商品多维规格与 SKU 属性选择器(Multi-Attribute SKU Selector & Spec Matrix) 是承载用户购买决策、决定最终转化率(Conversion Rate)的最核心交互中枢。

然而,在设计系统工程化落地过程中,规格选择器历来是交互设计师与前端工程师沟通成本最高的重灾区。一方面,商品规格通常包含多个正交维度(例如“机身颜色”ד存储容量”ד网络制式”ד售后延保套餐”),多个维度通过笛卡尔积(Cartesian Product)交织成庞大而复杂的库存单元(SKU)组合;另一方面,不同规格组合之间存在强耦合的业务规则——某一属性选中后,与之互斥或无库存(Out of Stock)的规格选项必须即时置灰禁用,商品主图与缩略图需同步切换,同时商品单价、划线原价、可用库存计数与加购结算状态必须毫秒级响应联动。

本教程将依托 Auto Layout 弹性布局、组件集与变体架构、Figma Variables 变量系统 以及 Dev Mode 生产级交付,深度拆解一套高扩展性、具备严密数学状态机并无缝对接代码工程的电商多维 SKU 选择器组件架构。


1. 电商多维 SKU 选择器在设计系统与工程落地中的痛点与挑战 #

将真实商业世界的电商库存规则映射到 Figma 原型画布时,设计团队通常面临四大核心痛点:

  1. 规格组合爆炸与画板维护成本失控:若采用传统的静态穷举法,一个“4 款颜色 × 3 种容量 × 2 种网络”的商品需要绘制 24 张完整页面画板;一旦运营调整某个选项或修改通用边距,设计师需要逐个页面手动同步,维护成本不可接受;
  2. 跨维度互斥与无库存置灰逻辑脱节:例如用户先选了“极夜黑”,但该颜色下的“512GB”版本已售罄。静态原型无法表达“当选择黑色时,512GB 自动置灰划线;当切到银色时,512GB 恢复可选”的动态连通性图论逻辑;
  3. 价格与库存阶梯无法动态计算:高配规格与低配规格存在显著差价。原型中单价往往写死,用户无论怎么切换选项或增减购买数量,总价依然纹丝不动,严重削弱了可用性测试(Usability Testing)与用户走查的真实度;
  4. 设计规范与前端算法实现割裂:设计师交付的设计稿往往只体现了“选中”和“未选”两种简单视觉,而前端开发者在实现无向图(Undirected Graph)连通算法或字典树(Trie)判别时,缺乏关于禁用态提示文案、预选默认规则与断货预警的边界规范。

技术架构方案对比矩阵 #

在规划电商设计系统中的规格选择器时,团队应明确不同建模方式的成本与产出比:

评估维度 传统静态画板穷举法 纯 Component Variants 多态组合 原子 Spec Chip + Variables 状态机矩阵(推荐)
画布维护成本 极高(画板数量随属性相乘呈指数爆炸) 中等(变体矩阵庞大,易超出变体性能阈值) 低(单组通用组件 + 集中式变量集合驱动)
跨维度互斥表达 无法交互(仅能展示孤立预设路径) 需大量复杂交互连线,容易死循环或连线错乱 原生支持条件判断(Conditional Logic)与动态禁用
价格与库存联动 静态数值,无法根据选型与数量联动计算 需为每种价格组合单设变体,无法数学运算 支持 Number 变量表达式:总价 = 单价 × 数量 实时刷新
研发交付友好度 差(开发需自行推敲规格数据结构) 一般(开发难以直接提取数据流向) 极佳(变量名与数据结构直接对齐前端 Props 与 API)
适用业务阶段 快速概念草图阶段 简单单维度商品(仅颜色或仅尺码) 复杂多规格电商详情页、3C 数码/服装/定制类商城

2. 多维 SKU 选择器组件体系:四层解耦拓扑模型 #

为了保证组件在千万级 SKU 电商平台中的可维护性与扩展性,我们将选择器系统自底向上解耦为四层物理拓扑:

┌─────────────────────────────────────────────────────────────┐
│ Layer 4: 商品规格控制器总装(PDP SKU Controller Container)    │
├─────────────────────────────────────────────────────────────┤
│ ├─ Layer 3: 规格维度分组行(Spec Attribute Row: Auto Layout Wrap)│
│ │   ├─ 维度标题与选中回显(Dimension Label & Current Value) │
│ │   └─ 胶囊标签排列流(Chips Flex Flow: Gap 8px)           │
│ ├─ Layer 2: 规格值原子胶囊(Spec Chip: Component Set)      │
│ │   ├─ 文本标签型(Text Chip: "256GB")                    │
│ │   ├─ 色块缩略型(Color Swatch Chip: 16px 圆形色标+文字)   │
│ │   └─ 图标套餐型(Bundle Chip: 包含套餐赠品微缩 Badge)    │
│ └─ Layer 1: 辅助操作组件(Quantity Stepper & Cart CTA Button)│
└─────────────────────────────────────────────────────────────┘
                               ▲
                               │ 驱动与状态联动
┌──────────────────────────────┴──────────────────────────────┐
│ Data Layer: Figma Variables 电商状态机与数据字典              │
│ ├─ String: selectedColor, selectedStorage, selectedNetwork   │
│ ├─ Number: currentPrice, originPrice, availableStock, count │
│ └─ Boolean: isOutOfStock, isAddToCartEnabled                 │
└─────────────────────────────────────────────────────────────┘

各层级职责清晰:底层 Spec Chip 仅关注自身的选中、悬停、禁用等视觉渲染;中层 Attribute Row 负责标签在不同屏幕宽度下的自适应折行换行;顶层控制器汇聚所有维度的状态,并由数据字典中的变量统一分配渲染管线。


3. 原子规格胶囊标签(Spec Chip)与四态组件集搭建实操 #

步骤一:构建 Spec Chip 基础原子图层 #

  1. 新建一个 Frame,命名为 .spec-chip-base;
  2. 按快捷键 Shift + A 为其添加 Auto Layout 弹性布局:
    • 方向:水平排布(Horizontal);
    • 内边距(Padding):水平 16px,垂直 8px;
    • 子项间距(Item Spacing):6px;
    • 容器对齐:居中对齐;
    • 圆角(Corner Radius):6px 或全圆角 999px(根据品牌设计语言确定);
  3. 容器内放置三个核心子元素:
    • Prefix Icon / Swatch:尺寸 16×16px 的圆形矢量 Frame(用于承载颜色色标或缩略图标),设置图层布尔属性 showSwatch;
    • Label Text:文本图层(如 256GB),字号 14px,行高 20px,绑定文本属性 chipLabel;
    • Stock Badge / Tag:右上角角标(如 热销、缺货 或 降价),设置图层布尔属性 showBadge。

步骤二:扩展四维状态组件集(Component Set) #

将 .spec-chip-base 转换为组件,并点击右侧属性面板创建变体(Add Variant),构建完整的状态矩阵:

  1. State 属性分支:
    • Default(默认未选):背景浅灰 #F3F4F6,描边透明,文字深灰 #374151;
    • Hover(鼠标悬停):背景 #E5E7EB,描边浅蓝,文字高亮;
    • Selected(当前选中):背景品牌浅浅底色 #EFF6FF,描边品牌主色 1.5px solid #2563EB,文字深蓝 #1D4ED8;右上角可叠加微小的选中勾选图标(Checkmark Indicator);
    • Disabled(无货禁用 / 冲突互斥):背景 #F9FAFB,描边细灰 #E5E7EB,文字浅灰置灰 #9CA3AF,文字图层添加删除线(Strikethrough),鼠标光标设为 not-allowed。
  2. Type 属性分支:
    • TextOnly:常规文本按钮;
    • WithColor:左侧显示对应颜色的圆形色标;
    • WithPriceTag:文字下方显示附加增量价格(例如 +¥800)。

步骤三:规格维度行(Attribute Row)的 Auto Layout Wrap 适配 #

在移动端或抽屉弹窗中,规格选项往往会因屏幕宽度受限而产生折行。

  1. 选中一行内的所有 Spec Chip 实例,按 Shift + A 嵌套为 Frame;
  2. 将 Auto Layout 的排布模式从 Horizontal 调整为 Wrap(自动换行);
  3. 水平间距设为 8px,垂直换行行距设为 8px;
  4. 将该 Frame 的宽度属性设置为 Fill Container(充满父级容器)。这样无论外层父级卡片宽度是 375px(移动端)、480px(侧边栏抽屉)还是 1200px(桌面端),规格标签均会自动流动折行,杜绝标签被溢出裁切或重叠破损。相关排查可查阅 Auto Layout 容器尺寸失控排查指南。

4. Figma Variables 驱动的规格联动状态机与价格计算实操 #

传统的静态原型无法响应真实商业运算,借助 Figma Variables 变量系统,我们可以将真实的数据计算逻辑植入原型中。

步骤一:创建电商数据字典(Variables Collection) #

在左侧 Assets 面板或 Local Variables 中新建集合 PDP_SKU_State,建立如下变量:

变量名称 类型 (Type) 初始默认值 (Default) 业务作用说明
selected_color String "皓月银" 记录用户当前选中的机身颜色
selected_storage String "256GB" 记录用户当前选中的存储规格
base_price Number 5999 基础款起售价(人民币元)
storage_price_offset Number 800 存储升级附加差价
current_unit_price Number 6799 最终计算得到的单品价格
buy_quantity Number 1 用户在步进器中加减的购买数量
total_order_amount Number 6799 结算总额(单价 × 数量)
sku_stock Number 12 当前组合的实时剩余库存数
is_sku_available Boolean true 当前规格是否可售(用于控制加购按钮)

步骤二:绑定文本图层与动态回显 #

  1. 选中规格行标题后方的选中值摘要文本,将其文本内容绑定至变量 selected_color 与 selected_storage,实现选择后标题即时显示“已选:皓月银,256GB”;
  2. 选中价格区域的当前售价文本(如 ¥6,799),将纯数字部分绑定至 current_unit_price;
  3. 选中总价展示文本(如 合计: ¥6,799),将其绑定至 total_order_amount;
  4. 选中数量步进器(Quantity Stepper)中间的数字文本,绑定至 buy_quantity。步进器的原子级搭建规范可参考 数字输入框与数值步进器系统架构实战。

步骤三:编写点击交互动作与表达式(Conditional Actions) #

在 Figma 交互面板(Interaction Details)中,为各个 Spec Chip 绑定 On Click 事件:

1. 点击“512GB”存储标签时: #

Set variable: selected_storage = "512GB"
Set variable: storage_price_offset = 1600
Set variable: current_unit_price = base_price + storage_price_offset
Set variable: total_order_amount = current_unit_price * buy_quantity

2. 点击“深空灰”颜色标签时(加入缺货模拟条件分支): #

Set variable: selected_color = "深空灰"
// 条件判断:若此时选中的是 512GB,模拟该颜色高配已售罄
Conditional:
If selected_storage == "512GB" Then:
   Set variable: sku_stock = 0
   Set variable: is_sku_available = false
Else:
   Set variable: sku_stock = 15
   Set variable: is_sku_available = true

3. 加购主按钮(Add to Cart CTA)的禁用状态联动: #

选中底部加购按钮,在其图层可见性(Layer Visibility)或组件变体属性中绑定 is_sku_available。当 is_sku_available == false 时,按钮自动切换为“已售罄 / 到货通知”的置灰禁用状态;当为 true 时,恢复“立即购买 / 加入购物车”的高亮主色。关于购物车抽屉结算的完整流程可扩展阅读 购物车与滑出式结算抽屉系统组件架构实战。


5. Dev Mode 生产级交付:React + TypeScript 电商 SKU 状态机与算法实现 #

在进入 Dev Mode 前端代码交付 阶段时,前端工程师最需要的不仅是 CSS 属性,更是一套成熟的 SKU 状态判定算法。现代电商前端通常基于无向图连通性算法(Graph Adjacency Matrix)或前缀字典树来动态计算各规格选项的可选状态。

以下为基于 React 18 + TypeScript 的生产级电商 SKU 选择器组件参考实现:

import React, { useState, useMemo } from 'react';

// 1. 数据结构定义
export interface SpecItem {
  id: string;
  name: string;
  disabled?: boolean;
}

export interface SpecDimension {
  title: string;
  key: string;
  items: SpecItem[];
}

export interface SKUProduct {
  skuId: string;
  specs: Record<string, string>; // e.g. { color: 'silver', storage: '256GB' }
  price: number;
  stock: number;
}

interface SKUSelectorProps {
  dimensions: SpecDimension[];
  skuList: SKUProduct[];
  onSKUChange?: (selectedSku: SKUProduct | null, totalAmount: number) => void;
}

export const SKUSelector: React.FC<SKUSelectorProps> = ({
  dimensions,
  skuList,
  onSKUChange
}) => {
  // 当前各维度选中状态
  const [selectedSpecs, setSelectedSpecs] = useState<Record<string, string>>({
    color: '皓月银',
    storage: '256GB'
  });
  const [quantity, setQuantity] = useState<number>(1);

  // 匹配当前完整命中的 SKU
  const currentSKU = useMemo(() => {
    return skuList.find(sku => {
      return Object.entries(selectedSpecs).every(
        ([dimKey, val]) => sku.specs[dimKey] === val
      );
    }) || null;
  }, [selectedSpecs, skuList]);

  // 动态判别某个规格值当前是否可选(库存大于0)
  const isOptionAvailable = (dimensionKey: string, optionValue: string): boolean => {
    // 假设选中该选项后的测试组合
    const testSpecs = { ...selectedSpecs, [dimensionKey]: optionValue };
    // 检查是否存在包含该测试组合且库存 > 0 的有效 SKU
    return skuList.some(sku => {
      const matchSpecs = Object.entries(testSpecs).every(
        ([k, v]) => sku.specs[k] === v
      );
      return matchSpecs && sku.stock > 0;
    });
  };

  const handleSelect = (dimKey: string, val: string) => {
    if (!isOptionAvailable(dimKey, val)) return;
    const next = { ...selectedSpecs, [dimKey]: val };
    setSelectedSpecs(next);
    
    const matched = skuList.find(sku => 
      Object.entries(next).every(([k, v]) => sku.specs[k] === v)
    ) || null;

    if (matched && onSKUChange) {
      onSKUChange(matched, matched.price * quantity);
    }
  };

  const unitPrice = currentSKU ? currentSKU.price : 0;
  const isStockEmpty = !currentSKU || currentSKU.stock <= 0;

  return (
    <div className="p-4 max-w-lg bg-white rounded-xl shadow-sm border border-gray-100 flex flex-col gap-5">
      {/* 价格与库存看板 */}
      <div className="flex items-baseline justify-between border-b pb-3 border-gray-100">
        <div>
          <span className="text-sm text-red-500 font-semibold">¥</span>
          <span className="text-3xl font-bold text-red-600 tracking-tight ml-0.5">
            {unitPrice.toLocaleString()}
          </span>
          <span className="text-xs text-gray-400 ml-2">
            剩余库存: {currentSKU ? currentSKU.stock : 0} 件
          </span>
        </div>
        <div className="text-xs text-gray-500">
          已选:{Object.values(selectedSpecs).join(' · ')}
        </div>
      </div>

      {/* 规格维度列表 */}
      {dimensions.map(dim => (
        <div key={dim.key} className="flex flex-col gap-2">
          <label className="text-xs font-semibold text-gray-700 tracking-wide uppercase">
            {dim.title}
          </label>
          <div className="flex flex-wrap gap-2.5" role="radiogroup" aria-label={dim.title}>
            {dim.items.map(item => {
              const isSelected = selectedSpecs[dim.key] === item.name;
              const available = isOptionAvailable(dim.key, item.name);

              return (
                <button
                  key={item.id}
                  type="button"
                  role="radio"
                  aria-checked={isSelected}
                  aria-disabled={!available}
                  disabled={!available}
                  onClick={() => handleSelect(dim.key, item.name)}
                  className={`px-4 py-2 rounded-lg text-sm font-medium transition-all duration-150 border ${
                    !available
                      ? 'bg-gray-50 border-gray-200 text-gray-300 cursor-not-allowed line-through'
                      : isSelected
                      ? 'bg-blue-50 border-blue-600 text-blue-700 shadow-sm'
                      : 'bg-gray-50 border-transparent text-gray-800 hover:bg-gray-100'
                  }`}
                >
                  {item.name}
                </button>
              );
            })}
          </div>
        </div>
      ))}

      {/* 购买数量与加购操作底栏 */}
      <div className="flex items-center justify-between pt-2">
        <span className="text-sm font-medium text-gray-700">购买数量</span>
        <div className="flex items-center border border-gray-200 rounded-lg overflow-hidden">
          <button
            type="button"
            disabled={quantity <= 1}
            onClick={() => setQuantity(q => Math.max(1, q - 1))}
            className="px-3 py-1 bg-gray-50 hover:bg-gray-100 disabled:opacity-40 text-gray-600"
          >
            -
          </button>
          <span className="px-4 py-1 text-sm font-semibold text-gray-800">{quantity}</span>
          <button
            type="button"
            disabled={currentSKU ? quantity >= currentSKU.stock : true}
            onClick={() => setQuantity(q => q + 1)}
            className="px-3 py-1 bg-gray-50 hover:bg-gray-100 disabled:opacity-40 text-gray-600"
          >
            +
          </button>
        </div>
      </div>

      <button
        type="button"
        disabled={isStockEmpty}
        className={`w-full py-3 rounded-xl font-semibold text-white shadow-md transition-all ${
          isStockEmpty
            ? 'bg-gray-300 cursor-not-allowed shadow-none'
            : 'bg-blue-600 hover:bg-blue-700 active:scale-[0.99]'
        }`}
      >
        {isStockEmpty ? '该规格暂时缺货' : `立即加购 (合计: ¥${(unitPrice * quantity).toLocaleString()})`}
      </button>
    </div>
  );
};

6. 生产落地常见避坑与排错决策树 #

在搭建大型电商多维 SKU 选择器时,根据一线团队的交付经验,需警惕以下高频故障点:

  1. Auto Layout 容器嵌套导致标签被截断或异常挤压:
    • 病因:规格行外层容器被误设为了 Fixed Width 且内层开启了 Clip Content,同时子标签被设为 Fixed;
    • 修复:规格行必须设置为 Fill Container,子级 Spec Chip 设置为 Hug Contents,并且开启 Auto Layout 的 Wrap 模式。
  2. 多语言出海(i18n)下标签文本溢出折行畸变:
    • 病因:英语、德语等长语种下规格名称(如“Midnight Obsidian Titanium”)长度暴增,导致胶囊高度不一;
    • 修复:为 Spec Chip 设定最小宽度 min-width: 64px,文本图层保持 Auto Width,同时设置最大宽度限制 max-width: 240px 并开启单行文本截断或统一高度。相关规范参考 Design Tokens 标准命名对照手册。
  3. Variables 变量类型冲突导致价格计算输出为字符串拼接:
    • 病因:单价或数量变量在初建时被误设为 String 类型,在表达式计算时执行了字符串连接(如 6799 + 1 变成了 "67991");
    • 修复:必须确保所有涉及四则运算的变量(price、stock、count)统一建为 Number 类型。详细故障诊断排查可查阅 Figma 常见报错与疑难故障诊断手册。

通过本套“原子 Spec Chip + Auto Layout Wrap + Variables 笛卡尔积状态机 + Dev Mode 前端图论算法”的四维工程架构,设计团队不仅能在 Figma 中还原逼近生产环境的真实电商交互体验,更能为前端开发提供零歧义、高度契合业务逻辑的代码交付标准。

❓ 常见问题与答疑 (FAQ)

高频疑难解析与避坑指南
A

完全不需要。绝大多数核心功能在免费 Starter 计划中均可完整体验与实操练习,无需额外付费购买付费团队版。