> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aftersell.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 组合来自其他应用的套装行

> 使用 setInternalProperties 告诉 Aftersell Cart 哪些行属于同一个套装，让它们渲染为一个商品而不是几个不相关的行。

大多数套装应用构建套装的方式是把**每个组件作为单独的购物车行**添加，然后用它们自己设计的行项目属性把这些行关联起来。Shopify 的 Ajax API 把这些行交给购物车时不带任何"它们属于一起"的标识，所以默认情况下抽屉会把一个三件套装显示为三个不相关的商品，各自带有自己的价格和数量调节器。

`setInternalProperties` 就是你告诉购物车它们是一体的方式。

<div id="how-grouping-works">
  ## 组合的工作原理
</div>

购物车根据两个**规范属性**来组合行。它不知道你的套装应用的属性名称，所以由你来翻译：读取应用写入的内容，然后用[行转换](/zh/aftersell/cart/sdk-hooks#registerlinetransform)把规范属性对盖章到每一行上。

| 属性                            | 必需 | 值                           |
| ----------------------------- | -- | --------------------------- |
| `_aftersell_cart_bundle_id`   | 是  | 一个共享 ID。携带相同 ID 的每一行同属一个套装。 |
| `_aftersell_cart_bundle_role` | 否  | 在套装应显示为的那一行上设为 `parent`。    |

这些通过 `setInternalProperties` 传递，而不是通过 Shopify。它们是**仅用于渲染的覆盖层**：永远不会进入 `properties`，永远不会持久化到 Shopify，也永远不会出现在订单上。

<div id="step-1-find-out-what-your-app-writes">
  ## 第 1 步：查明你的应用写入了什么
</div>

每个套装应用给属性起的名字都不同，所以先看一个真实购物车。在你的店面上添加一个套装，然后在浏览器控制台运行：

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.actions.getCart().items.forEach((line) => {
  console.log(line.title, line.properties);
});
```

你要找的是套装各行共享的一个属性。它通常是一个隐藏属性（名称以 `_` 开头），持有一个 ID、一个引用或套装的名称。典型的有 `_bundle_id`、`_bundle_ref` 或 `_parent_id`。记下确切的键名，以及是否有某一行被标记为主产品。

<div id="step-2-map-it-onto-the-canonical-properties">
  ## 第 2 步：映射到规范属性
</div>

粘贴到 **Cart settings → Custom script → Initialization**，把属性名替换为你找到的名称：

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.hooks.registerLineTransform((line) => {
  const props = line.properties;
  if (!props) return;

  const bundleId = props._bundle_id;
  if (!bundleId) return;

  line.setInternalProperties({
    _aftersell_cart_bundle_id: bundleId,
    // Mark the main product so the bundle renders under it.
    _aftersell_cart_bundle_role: props._bundle_role === 'main' ? 'parent' : 'child',
  });
});
```

这就是整个集成。一旦两行或更多行共享一个 ID，购物车就会把它们折叠成一个套装。

<Note>
  如果你的应用没有标记主产品，就完全省略 `_aftersell_cart_bundle_role`。购物车会为你挑选一个主行。
</Note>

<div id="what-you-get">
  ## 你会得到什么
</div>

行被组合后，主行会携带一个 [`bundle` 对象](/zh/aftersell/cart/sdk-cart-object#bundles)，抽屉将套装渲染为单个商品：

* **子项嵌套在主行之下**，而不是显示为单独的行。
* \*\*数量是原子的。\*\*更改套装的数量会按每个子项的 `perAnchorQty` 比例一起缩放所有成员，所以某组件数量为二的套装会保持这个二比一的关系。
* \*\*移除是原子的。\*\*移除套装会在一次请求中移除所有成员行，而不会留下孤立的组件。
* **只有一个价格行。**显示什么遵循 [Cart items](/zh/aftersell/cart/cart-items-block) 区块上的**套装价格**设置：所有成员的总价，或仅主产品的价格。

<div id="how-the-anchor-is-chosen">
  ## 主行如何选定
</div>

主行是套装显示为的那一行。购物车按此顺序挑选：

1. `_aftersell_cart_bundle_role` 设为 `parent` 的行。
2. 否则，**价格最高**的成员。
3. 否则，购物车中的第一个成员。

价格回退通常是正确的，因为套装应用往往把折扣放在主产品上。当不是这样时，请显式设置角色，例如当主产品是最便宜的商品或是免费的时候。

<div id="rules-worth-knowing">
  ## 值得了解的规则
</div>

* \*\*套装至少需要两行。\*\*只有一行携带套装 ID 时不会被处理，正常渲染。
* \*\*Shopify 原生套装已经被处理。\*\*Shopify 本身标记为组件化的行会被此组合跳过并自动适配。你只需要为添加独立行的应用使用它。
* \*\*转换在每次渲染时运行。\*\*保持它开销小且无副作用。不要在其内部调用操作或发起请求。
* \*\*合并是累加式的。\*\*你的属性会与其他转换设置的属性合并。对同一个键的真正冲突，由最后注册的转换胜出。
* \*\*组合在隐藏和重命名之后、排序之前运行。\*\*所以你用 `setHidden` 隐藏的行永远不会成为套装的一部分，而[比较器](/zh/aftersell/cart/sdk-hooks#registerlinecomparator)看到的是主行，而不是子项。

<Warning>
  \*\*组合后的子项会离开 `state.items`。\*\*一旦行被折叠进套装，只有主行出现在 `getCart().items` 和事件载荷中；子项移动到 `anchor.bundle.children`。它们也不再计入 `itemCount`。

  **购物车总额不受影响**，因为总额直接来自 Shopify。组合只改变展示，永远不改变购物者支付的金额。
</Warning>

<div id="reading-a-bundle-back">
  ## 读取套装
</div>

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('cart_updated', (state) => {
  state.items.forEach((line) => {
    if (!line.bundle) return;
    console.log(line.title, 'is a bundle of', line.bundle.children.length, 'items:');
    line.bundle.children.forEach((child) => {
      console.log('  ', child.quantity, 'x', child.title);
    });
  });
});
```

要对套装的行进行操作，请使用 `bundle.memberKeys`，它持有包括主行在内的每个成员的 `key`。

<div id="using-it-for-other-things">
  ## 用于其他用途
</div>

套装组合是 `setInternalProperties` 的设计初衷，但这个覆盖层是一个通用通道，用于**你从行派生出的仅用于渲染的数据**。你放在那里的任何东西都可以在 `line.internalProperties` 和[自定义代码区块](/zh/aftersell/cart/custom-code-blocks)中读取，而不触及真实购物车：

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.hooks.registerLineTransform((line) => {
  if (line.properties?._preorder_ship_date) {
    line.setInternalProperties({ _badge: `Ships ${line.properties._preorder_ship_date}` });
  }
});
```

当值是**派生的**且仅用于显示时使用它。如果数据需要保留到订单，它应该是一个真正的行项目属性，在产品表单上通过一个隐藏的 `properties[...]` 输入设置，这样无论由谁执行添加，值都能到达 Shopify。

<div id="where-to-go-next">
  ## 后续阅读
</div>

* **[`registerLineTransform`](/zh/aftersell/cart/sdk-hooks#registerlinetransform)**：这个方案所依赖的 hook。
* **[购物车对象](/zh/aftersell/cart/sdk-cart-object#bundles)**：`bundle` 及其子项的结构。
* **[Cart items 区块](/zh/aftersell/cart/cart-items-block)**：套装价格设置。
