Figma 交互式商品属性矩阵与多维 SKU 规格选择器(E-commerce Multi-Attribute SKU Selector, Inventory Matrix & Price Calculator)组件架构实战:从笛卡尔积规格标签状态机(Normal/Selected/Disabled/Out-of-Stock)、跨维度互斥联动、价格与库存阶梯联动到 Variables 动态计算与 Dev Mode 前端 React 电商交付
在电子商务、跨境独立站、新零售自营商城以及 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 原型画布时,设计团队通常面临四大核心痛点:
- 规格组合爆炸与画板维护成本失控:若采用传统的静态穷举法,一个“4 款颜色 × 3 种容量 × 2 种网络”的商品需要绘制 24 张完整页面画板;一旦运营调整某个选项或修改通用边距,设计师需要逐个页面手动同步,维护成本不可接受;
- 跨维度互斥与无库存置灰逻辑脱节:例如用户先选了“极夜黑”,但该颜色下的“512GB”版本已售罄。静态原型无法表达“当选择黑色时,512GB 自动置灰划线;当切到银色时,512GB 恢复可选”的动态连通性图论逻辑;
- 价格与库存阶梯无法动态计算:高配规格与低配规格存在显著差价。原型中单价往往写死,用户无论怎么切换选项或增减购买数量,总价依然纹丝不动,严重削弱了可用性测试(Usability Testing)与用户走查的真实度;
- 设计规范与前端算法实现割裂:设计师交付的设计稿往往只体现了“选中”和“未选”两种简单视觉,而前端开发者在实现无向图(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 基础原子图层 #
- 新建一个 Frame,命名为
.spec-chip-base; - 按快捷键
Shift + A为其添加 Auto Layout 弹性布局:- 方向:水平排布(Horizontal);
- 内边距(Padding):水平
16px,垂直8px; - 子项间距(Item Spacing):
6px; - 容器对齐:居中对齐;
- 圆角(Corner Radius):
6px或全圆角999px(根据品牌设计语言确定);
- 容器内放置三个核心子元素:
- Prefix Icon / Swatch:尺寸
16×16px的圆形矢量 Frame(用于承载颜色色标或缩略图标),设置图层布尔属性showSwatch; - Label Text:文本图层(如
256GB),字号14px,行高20px,绑定文本属性chipLabel; - Stock Badge / Tag:右上角角标(如
热销、缺货或降价),设置图层布尔属性showBadge。
- Prefix Icon / Swatch:尺寸
步骤二:扩展四维状态组件集(Component Set) #
将 .spec-chip-base 转换为组件,并点击右侧属性面板创建变体(Add Variant),构建完整的状态矩阵:
- State 属性分支:
Default(默认未选):背景浅灰#F3F4F6,描边透明,文字深灰#374151;Hover(鼠标悬停):背景#E5E7EB,描边浅蓝,文字高亮;Selected(当前选中):背景品牌浅浅底色#EFF6FF,描边品牌主色1.5px solid #2563EB,文字深蓝#1D4ED8;右上角可叠加微小的选中勾选图标(Checkmark Indicator);Disabled(无货禁用 / 冲突互斥):背景#F9FAFB,描边细灰#E5E7EB,文字浅灰置灰#9CA3AF,文字图层添加删除线(Strikethrough),鼠标光标设为not-allowed。
- Type 属性分支:
TextOnly:常规文本按钮;WithColor:左侧显示对应颜色的圆形色标;WithPriceTag:文字下方显示附加增量价格(例如+¥800)。
步骤三:规格维度行(Attribute Row)的 Auto Layout Wrap 适配 #
在移动端或抽屉弹窗中,规格选项往往会因屏幕宽度受限而产生折行。
- 选中一行内的所有 Spec Chip 实例,按
Shift + A嵌套为 Frame; - 将 Auto Layout 的排布模式从
Horizontal调整为 Wrap(自动换行); - 水平间距设为
8px,垂直换行行距设为8px; - 将该 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 | 当前规格是否可售(用于控制加购按钮) |
步骤二:绑定文本图层与动态回显 #
- 选中规格行标题后方的选中值摘要文本,将其文本内容绑定至变量
selected_color与selected_storage,实现选择后标题即时显示“已选:皓月银,256GB”; - 选中价格区域的当前售价文本(如
¥6,799),将纯数字部分绑定至current_unit_price; - 选中总价展示文本(如
合计: ¥6,799),将其绑定至total_order_amount; - 选中数量步进器(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 选择器时,根据一线团队的交付经验,需警惕以下高频故障点:
- Auto Layout 容器嵌套导致标签被截断或异常挤压:
- 病因:规格行外层容器被误设为了
Fixed Width且内层开启了Clip Content,同时子标签被设为Fixed; - 修复:规格行必须设置为
Fill Container,子级 Spec Chip 设置为Hug Contents,并且开启 Auto Layout 的 Wrap 模式。
- 病因:规格行外层容器被误设为了
- 多语言出海(i18n)下标签文本溢出折行畸变:
- 病因:英语、德语等长语种下规格名称(如“Midnight Obsidian Titanium”)长度暴增,导致胶囊高度不一;
- 修复:为 Spec Chip 设定最小宽度
min-width: 64px,文本图层保持Auto Width,同时设置最大宽度限制max-width: 240px并开启单行文本截断或统一高度。相关规范参考 Design Tokens 标准命名对照手册。
- Variables 变量类型冲突导致价格计算输出为字符串拼接:
- 病因:单价或数量变量在初建时被误设为 String 类型,在表达式计算时执行了字符串连接(如
6799 + 1变成了"67991"); - 修复:必须确保所有涉及四则运算的变量(
price、stock、count)统一建为 Number 类型。详细故障诊断排查可查阅 Figma 常见报错与疑难故障诊断手册。
- 病因:单价或数量变量在初建时被误设为 String 类型,在表达式计算时执行了字符串连接(如
通过本套“原子 Spec Chip + Auto Layout Wrap + Variables 笛卡尔积状态机 + Dev Mode 前端图论算法”的四维工程架构,设计团队不仅能在 Figma 中还原逼近生产环境的真实电商交互体验,更能为前端开发提供零歧义、高度契合业务逻辑的代码交付标准。
核心手册与速查工具
高频手册与实用设计生产力工具直达常见问题与答疑 (FAQ)
高频疑难解析与避坑指南完全不需要。绝大多数核心功能在免费 Starter 计划中均可完整体验与实操练习,无需额外付费购买付费团队版。