---
title: 开发文档
order: 1
category: plus
---

# SKU Options Selection 组件开发文档

## 1. 组件的命名和存放位置

**组件名称**: `SKUOptionsSelection`  
**存放位置**: `private-materials/src/plus/skuOptionsSelection`  
**组件编号**: `pcl_skuOptionsSelection`

## 2. 版本

**当前版本**: `1.0.0`  
**最后更新**: 2026-08-12

## 3. 相关文档

### 设计文档

- **Figma**: [SKU Options Selection 组件设计稿](https://www.figma.com/design/pM8Ho6d7kCMv9vIBvFHUlj/Pisell-2.0-Component-Library?node-id=11491-149085&m=dev)
- **飞书**: [SKU Options Selection 组件需求文档](https://project.feishu.cn/v2qint/projectm/detail/6454740529)

## 4. 组件规划

### 4.1 组件架构

#### SKUOptionsSelection

```
SKUOptionsSelection
  ├── store (逻辑层)
  │     ├── store (数据源) 
  │     ├── actions (状态操作、校验方法)
  |     └── createStore (store构建方法)
  ├── content (内容渲染)
  │     └── Selector Group (选择组)
  │           └── SKUCard (sku卡片)
  │                 └── modal (单规格选择弹窗)
  │                       └── Selector Group (选择组)
  └── components (基础组件)
  
```

## 5. 组件设计

### 5.1 主要接口

```typescript
export interface DataSource {
  /** 商品id */
  id?: number;
  /** 生产编码 */
  production_code?: string;
  /** 单规格组数据 */
  option_group?: Array<OptionGroupItem>;
  /** 组合规格组装后的数据 */
  variant?: Array<VariantProductItem>;
  /** 组合规格组数据 */
  variant_group?: Array<VariantGroupItem>;
  /** 套餐商品组数据 */
  bundle_group?: Array<BundleGroupItem>;
}

export interface OptionGroupMetadata {
  advanced_settings_enabled?: 0 | 1;
  option_types_config?: {
    min?: number;
    max?: number;
    [key: string]: unknown;
  };
  [key: string]: unknown;
}

/**
 * 单规格组数据
 */
export interface OptionGroupItem {
  /** 创建者id */
  create_account_id?: number;
  /** 创建时间 */
  created_at?: string;
  /** 删除时间 */
  deleted_at?: string | null;
  /** 模板id */
  template_id?: number;
  /** 更新者id */
  update_account_id?: number;
  /** 更新时间 */
  updated_at?: string;

  /** 单规格组id */
  id?: number;
  /** 是否启用 */
  is_enabled?: 0 | 1;
  /** 是否多选 */
  is_multiple?: 0 | 1;
  /** 最大选择数量 */
  max_quantity?: number;
  /** 最小选择数量 */
  min_quantity?: number;
  /** 高级设置元数据 */
  metadata?: OptionGroupMetadata;
  /** 单规格组名称 */
  name?: string;
  /** 单规格组下规格数据 */
  option_item?: Array<OptionItem>;
  /** 商品id */
  product_id?: number;
  /** 是否必选 */
  required?: 0 | 1;
  /** 店铺id */
  shop_id?: number;
  /** 排序 */
  sort?: number;
  /** 状态 */
  status?: 0 | 1;
}

/**
 * 单规格组下选项数据
 */
export interface OptionItem {
  /** 创建者id */
  create_account_id?: number;
  /** 创建时间 */
  created_at?: string;
  /** 删除时间 */
  deleted_at?: string | null;
  /** 模板id */
  template_id?: number;
  /** 更新者id */
  update_account_id?: number;
  /** 更新时间 */
  updated_at?: string;

  /** 加价 */
  add_price?: string;
  /** 封面 */
  cover?: string;
  /** 描述 */
  description?: string;
  /** 规格id */
  id?: number;
  /** 是否启用 */
  is_enabled?: 0 | 1;
  /** 最大选择数量 */
  max_quantity?: number;
  /** 最小选择数量 */
  min_quantity?: number;
  /** 规格名称 */
  name?: string;
  /** 规格组id */
  option_group_id?: number;
  /** 原价 */
  original_price?: string;
  /** 商品id */
  product_id?: number;
  /** 生产编码 */
  production_code?: string;
  /** 店铺id */
  shop_id?: number;
  /** 排序 */
  sort?: number;
  /** 状态 */
  status?: 0 | 1;
}

/**
 * 组合规格组数据
 */
export interface VariantGroupItem {
  /** 创建者id */
  create_account_id?: number;
  /** 创建时间 */
  created_at?: string;
  /** 删除时间 */
  deleted_at?: string | null;
  /** 更新者id */
  update_account_id?: number;
  /** 更新时间 */
  updated_at?: string;

  /** 组合规格组id */
  id?: number;
  /** 组合规格组名称 */
  name?: string;
  /** 位置id */
  position_id?: number;
  /** 商品id */
  product_id?: number;
  /** 店铺id */
  shop_id?: number;
  /** 排序 */
  sort?: number;
  /** 状态 */
  status?: 0 | 1;
  /** 组合规格组下选项数据 */
  variant_item?: Array<VariantItem>;
}

/**
 * 组合规格组下选项数据
 */
export interface VariantItem {
  /** 创建者id */
  create_account_id?: number;
  /** 创建时间 */
  created_at?: string;
  /** 删除时间 */
  deleted_at?: string | null;
  /** 更新者id */
  update_account_id?: number;
  /** 更新时间 */
  updated_at?: string;
  /** 店铺id */
  shop_id?: number;
  /** 排序 */
  sort?: number;

  /** 规格id */
  id?: number;
  /** 是否默认 */
  is_default?: 0 | 1;
  /** 变体名称 */
  name?: string;
  /** 位置id */
  position_id?: number;
  /** 商品id */
  product_id?: number;
  /** 状态 */
  status?: 0 | 1;
  /** 组合规格组id */
  variant_group_id?: number;
}

/**
 * 组合规格商品数据
 */
export interface VariantProductItem {
  /** 创建者id */
  create_account_id?: number;
  /** 创建时间 */
  created_at?: string;
  /** 更新者id */
  update_account_id?: number;
  /** 更新时间 */
  updated_at?: string;

  /** 条形码 */
  barcode?: string;
  /** 基础价格 */
  base_price?: string;
  /** 箱规数量 */
  box_specification_quantity?: number;
  /** 编码 */
  code?: string;
  /** 成本价格 */
  cost_price?: string;
  /** 封面 */
  cover?: string;
  /** 描述 */
  description?: string | null;
  /** 是否禁用 */
  disabled?: 0 | 1;
  /** 组合规格商品id */
  id?: number;
  /** 是否收税 */
  is_charge_tax?: 0 | 1;
  /** 是否发货 */
  is_delivery?: 0 | 1;
  /** 是否收税 */
  is_gst?: 0 | 1;
  /** 是否跟踪库存 */
  is_track?: 0 | 1;
  /** 名称 */
  name?: Record<string, string>;
  /** 开售重量 */
  open_sold_weight?: number;
  /** 原价 */
  original_price?: string;
  /** 是否超卖 */
  over_sold?: 0 | 1;
  /** 价格 */
  price?: string;
  /** 商品id */
  product_id?: number;
  /** 生产编码 */
  production_code?: string;
  /** 折扣类型 */
  rebate_type?: string;
  /** 折扣值 */
  rebate_value?: number;
  /** 标题 */
  title?: Record<string, string>;
  /** 单位 */
  unit?: string;
  /** 单位值 */
  unit_value?: number;
  /** 重量单位 */
  weight_unit?: string;
  /** 重量值 */
  weight_value?: string;
}

/**
 * 套餐商品组数据
 */
export interface BundleGroupItem {
  /** 店铺id */
  shop_id?: number;
  /** 排序 */
  sort?: number;

  /** 套餐id */
  id?: number;
  /** 最大选择数量 */
  max_quantity?: number;
  /** 最小选择数量 */
  min_quantity?: number;
  /** 套餐名称 */
  name?: string;
  /** 商品id */
  product_id?: number;
  /** 套餐商品组数据 */
  bundle_item?: Array<BundleItem>;
}

/**
 * 套餐商品组下选项数据
 */
export interface BundleItem {
  /** 店铺id */
  shop_id?: number;
  /** 排序 */
  sort?: number;

  /** 条形码 */
  barcode?: string;
  /** 套餐商品id */
  bundle_product_id?: number;
  /** 套餐商品变体id */
  bundle_variant_id?: number;
  /** 编码 */
  code?: string;
  /** 封面 */
  cover?: string;
  /** 扩展id */
  extension_id?: number;
  /** 扩展类型 */
  extension_type?: string;
  /** 套餐商品组id */
  group_id?: number;
  /** 套餐商品id */
  id?: number;
  /** 是否跟踪库存 */
  is_track?: 0 | 1;
  /** 最大选择数量 */
  max_quantity?: number;
  /** 单规格组数据 */
  option_group?: Array<OptionGroupItem>;
  /** 是否允许超卖 */
  over_sold?: 0 | 1;
  /** 价格 */
  price?: string;
  /** 价格类型: mark_up==>加价 | mark_down==>减价 */
  price_type?: 'mark_up' | 'mark_down';
  /** 价格类型扩展: product_price==>商品价格 */
  price_type_ext?: '' | 'product_price';
  /** 商品id */
  product_id?: number;
  /** 生产编码 */
  production_code?: string;
  /** 商品价格 */
  product_price?: string;
  /** 商品副标题 */
  product_subtitle?: string;
  /** 库存数量 */
  stock_quantity?: number;
  /** 标题 */
  title?: string;
}

/**
 * sku value
 */
export interface SkuValue {
  /** 商品id */
  product_id: number;
  /** 多规格商品id */
  product_variant_id: number;
  /** 多规格商品选项字符串 */
  product_option_string?: string;
  /** 当前商品或组合规格商品的生产编码 */
  production_code?: string;
  /** 套餐商品组数据 */
  bundle: Array<any>;
  /** 单规格组数据 */
  option: Array<
    OptionItem & { num: number; price: string; product_option_item_id: number }
  >;
}
```

### 5.2 Props 详细说明

#### 基本属性

| 属性 | 类型 | 必填 | 说明 |
| :--- | :--- | :--- | :--- |
| `dataSource` | `DataSource` | 是 | 数据源，详见定义：DataSource |
| `value` | `SkuValue` | 是 | 受控值，详见定义：SkuValue |
| `onChange` | `(value: SkuValue) => void` | 是 | 选中变更回调 |
| `config` | `{ overSold?: 0 \| 1 }` | 否 | 配置项，overSold: 是否允许超卖 |
| `skuCardConfig` | `SkuCardConfig` | 否 | SKU 卡片样式配置，详见定义：SkuCardConfig |
| `customScrollParent` | `HTMLElement \| string` | 否 | 自定义滚动容器 |
| `style` | `React.CSSProperties` | 否 | 自定义样式 |
| `customConfig` | `{ modalWidth?: number \| string }` | 否 | 自定义配置，modalWidth: 弹窗宽度 |

#### SkuCardConfig 配置说明

```typescript
/**
 * SKU Card 文本样式配置
 */
export interface SkuCardTextConfig {
  /** 是否显示 */
  visible?: boolean;
  /** 字体大小 */
  fontSize?: number;
  /** 颜色 */
  color?: string;
  /** 字体粗细 */
  fontWeight?: 'normal' | 'bold' | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
  /** 展示行数 */
  lineClamp?: number;
}

/**
 * SKU Card 图片配置
 */
export interface SkuCardImageConfig {
  /** 是否显示 */
  visible?: boolean;
  /** 宽度 */
  width?: number;
  /** 高度 */
  height?: number;
  /** 圆角 */
  borderRadius?: number | string;
  /** 边框 */
  border?: string;
}

/**
 * SKU Card 配置
 */
export interface SkuCardConfig {
  /** 主标题配置 */
  title?: SkuCardTextConfig;
  /** 副标题配置 */
  subTitle?: SkuCardTextConfig;
  /** 价格配置 */
  price?: Omit<SkuCardTextConfig, 'lineClamp'>;
  /** 图片配置 */
  image?: SkuCardImageConfig;
}

/** SKU Card 默认配置 */
export const defaultSkuCardConfig: SkuCardConfig = {
  title: {
    visible: true,
  },
  subTitle: {
    visible: true,
    lineClamp: 2,
  },
  price: {
    visible: true,
    color: '#DC6803',
  },
  image: {
    visible: true,
  },
};
```

### 5.3 SKU Options Selection 组件设计

#### 5.3.1 概述

SKUOptionsSelection 组件是一个复杂的 SKU 选择组件，支持多种规格类型：

1. **组合规格（Variant）**：多规格组合商品的选择
2. **单规格（Option）**：单个规格选项的选择，支持单选和多选
3. **套餐商品（Bundle）**：套餐商品的选择

组件采用三层架构设计：

- **数据层**：由 store 提供数据，action 提供驱动数据变更的事件
- **数据转化层**：将原始数据转换成 Selector 组件能够识别的数据格式
- **渲染层**：消费转化后的数据，负责具体 UI 呈现

#### 5.3.2 Store / Actions

**状态管理**：
- 自定义状态管理工具，提供响应式数据和最新数据访问
- 解决了直接使用 useState 的闭包问题
- 支持通过 `store.getState()` 获取最新状态

**Actions 方法**：
- `updateDataSource`: 更新数据源
- `updateValue`: 更新原始值
- `updateConfig`: 更新配置项
- `updateSymbol`: 更新货币符号
- `updateSelectorDataSource`: 更新 Selector 数据源
- `updateSelectorValue`: 更新 Selector 识别的值
- `updateSkuCardConfig`: 更新 SKU Card 配置
- `updateCustomConfig`: 更新自定义配置

#### 5.3.3 数据转换工具

组件内部提供了多个数据转换工具函数：

- `transformVariantGroup`: 将组合规格组数据转换为 Selector 识别的格式
- `transformOptionGroup`: 将单规格组数据转换为 Selector 识别的格式
- `transformBundleGroup`: 将套餐商品组数据转换为 Selector 识别的格式
- `transformValueToSelectorValue`: 将原始值转换为 Selector 值
- `transformSelectorValueToValue`: 将 Selector 值转换为原始值

#### 5.3.4 组件 Ref 方法

组件通过 `forwardRef` 暴露以下方法：

| 方法 | 返回值 | 说明 |
| :--- | :--- | :--- |
| `validate()` | `Promise<boolean>` | 验证当前选择是否符合规则 |
| `getValues()` | `SelectorValue` | 获取 Selector 格式的值 |
| `getTransformValues()` | `SkuValue` | 获取转换后的原始格式值 |

#### 5.3.5 事件流与受控模式

组件采用完全受控模式设计：

- **受控模式**：优先以外部传入的 `value` 为准
- **变更流程**：用户操作 → 触发 `onChange` 回调 → 父组件更新 `value` → 组件重新渲染
- **数据流向**：props.value → transformValueToSelectorValue → Selector → onChange → transformSelectorValueToValue → props.onChange

## 6. 如何使用

### 6.1 基础用法

```
import React, { useState } from 'react';
import { SKUOptionsSelection } from '@pisell/private-materials';

const App = () => {
  const [value, setValue] = useState({
    product_id: 123,
    product_variant_id: 456,
    product_option_string: '',
    bundle: [],
    option: [],
  });

  return (
    <SKUOptionsSelection
      dataSource={{
        variant_group: [],
        option_group: [],
        bundle_group: [],
      }}
      value={value}
      onChange={(newValue) => {
        console.log('选择变更:', newValue);
        setValue(newValue);
      }}
    />
  );
};
```

### 6.2 配置 SKU 卡片样式

```
<SKUOptionsSelection
  dataSource={dataSource}
  value={value}
  onChange={setValue}
  skuCardConfig={{
    title: {
      visible: true,
      fontSize: 14,
      fontWeight: 500,
      color: '#333',
    },
    subTitle: {
      visible: true,
      fontSize: 12,
      color: '#999',
      lineClamp: 2,
    },
    price: {
      visible: true,
      fontSize: 16,
      fontWeight: 'bold',
      color: '#DC6803',
    },
    image: {
      visible: true,
      width: 80,
      height: 80,
      borderRadius: 8,
    },
  }}
/>
```

### 6.3 自定义弹窗宽度

```
<SKUOptionsSelection
  dataSource={dataSource}
  value={value}
  onChange={setValue}
  customConfig={{
    modalWidth: 800, // 或 '80%'
  }}
/>
```

### 6.4 使用 Ref 方法

```
import React, { useRef } from 'react';
import { SKUOptionsSelection } from '@pisell/private-materials';

const App = () => {
  const skuRef = useRef(null);

  const handleSubmit = async () => {
    // 验证选择
    const isValid = await skuRef.current?.validate();
    if (!isValid) {
      console.log('验证失败');
      return;
    }

    // 获取转换后的值
    const values = skuRef.current?.getTransformValues();
    console.log('提交的值:', values);
  };

  return (
    <>
      <SKUOptionsSelection
        ref={skuRef}
        dataSource={dataSource}
        value={value}
        onChange={setValue}
      />
      <button onClick={handleSubmit}>提交</button>
    </>
  );
};
```

### 6.5 允许超卖配置

```
<SKUOptionsSelection
  dataSource={dataSource}
  value={value}
  onChange={setValue}
  config={{
    overSold: 1, // 1: 允许超卖, 0: 不允许超卖
  }}
/>
```

### 6.6 自定义滚动容器

```
<SKUOptionsSelection
  dataSource={dataSource}
  value={value}
  onChange={setValue}
  customScrollParent="#custom-scroll-container"
/>
```

### 6.7 Option 高级限制与 Production Code

多选 Option Group 在 `metadata.advanced_settings_enabled=1` 时支持种类数限制：

```typescript
{
  is_multiple: 1,
  min_quantity: 1, // 总份数下限
  max_quantity: 4, // 总份数上限，0 表示不限
  metadata: {
    advanced_settings_enabled: 1,
    option_types_config: {
      min: 1, // 最少种类数，0 表示不限
      max: 2, // 最多种类数，0 表示不限
    },
  },
  option_item: [
    {
      id: 1,
      min_quantity: 2, // 选择该项后的最少份数
      max_quantity: 0, // 单项份数不限
      production_code: 'OPTION-1',
    },
  ],
}
```

达到最多种类数后，组件只禁用尚未选择的 Option Item；已选项仍可在总份数和单项规则允许的范围内调整。Item `min_quantity` 仅对已选项生效，首次选择会从该最小数量开始。

`getTransformValues()` 和 `onChange` 输出会明确携带以下编码：

- `production_code`：Product 或当前 Variant 编码。
- `option[*].production_code`：Option Item 编码。
- `bundle[*].production_code`：Bundle Item 编码。
- `bundle[*].option[*].production_code`：套餐嵌套 Option Item 编码。

显式空字符串表示“无编码”，不会回退到 Product 或其他节点编码。

## 7. 注意事项

1. **数据格式**：确保传入的 `dataSource` 和 `value` 格式正确，否则可能导致组件渲染异常
2. **受控模式**：组件采用完全受控模式，必须通过 `onChange` 更新外部 `value` 才能生效
3. **性能优化**：当数据量较大时，建议使用 `React.memo` 或其他优化手段避免不必要的重渲染
4. **类型定义**：建议使用 TypeScript 并导入相应的类型定义以获得更好的开发体验
5. **多规格组合**：组合规格（variant）、单规格（option）、套餐商品（bundle）可以同时使用
6. **SKU Card 配置**：可以通过 `skuCardConfig` 精细控制每个卡片元素的显示和样式
