> ## 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.

# 自定义模板

> 用你自己的 JSX 覆盖任意 Aftersell Cart 区块的渲染方式：模板替换什么、作用域内有什么、如何设置样式，以及在哪里查找每个区块的 props。

**自定义模板**让你可以覆盖单个区块的渲染方式。购物车不再渲染区块的内置 UI，而是渲染你自己的 JSX，使用的仍是该区块通常会使用的相同数据。它是一项横切能力，而不是一个独立的区块：大多数区块都在其 **Code** 标签页中提供此功能。

本页介绍适用于**所有**区块的内容。若要了解特定区块提供给你的 props，请跳转到[该区块自己的参考](#props-for-each-block)。

<div id="custom-template-vs-custom-code-block">
  ## 自定义模板与自定义代码区块的区别
</div>

两者听起来相似，但作用不同：

* **自定义模板**用你自己的标记*替换现有区块的渲染*，并把该区块自身的数据交给你（Header 的标题和商品数量、Summary 的总额等等）。它不会添加任何新内容；它只是为一个区块重新设计样式。
* \*\*[自定义代码](/zh/aftersell/cart/custom-code-blocks)\*\*区块则是在购物车的任意位置*添加一个新区块*，内容为任意 HTML 或 React。

当内置区块基本符合需求，但你需要不同的布局或标记时，选择自定义模板。当你想添加内置区块未涵盖的内容时，选择自定义代码区块。

<div id="using-a-custom-template">
  ## 使用自定义模板
</div>

1. 在编辑器中选择一个区块并打开其 **Code** 标签页。
2. 编辑默认模板。自定义模板**仅支持 JSX**（HTML 或 JSX 的选择仅限于自定义代码区块）。
3. 点击 **Compile**。编译会去除类型并转译 JSX，因此它能捕获**语法**错误。类型错误不会阻止编译——编辑器会在你输入时以内联方式标记它们，并提供可自动补全区块 props 的同款 IntelliSense。
4. 启用模板，让购物车使用它替代内置渲染。
5. **Reset to default** 可随时恢复该区块的原始模板。

<div id="writing-a-template-with-ai">
  ## 用 AI 编写模板
</div>

Code 标签页包含一个 **Copy AI prompt** 按钮（✦ 魔杖图标）。点击它会将一份自包含的简报复制到剪贴板，你可以直接粘贴到 AI 对话中（Claude、ChatGPT 或类似工具）。

该提示词包含 AI 为该特定区块编写有效模板所需的一切：

* 编译规则（单一表达式、无 `export default`、无导入）
* 该区块接收的确切 props，与编辑器 IntelliSense 显示的一致
* 编辑器强制执行的锁定函数签名
* 区块特定规则（货币格式、需要接线的处理函数、无障碍要求）
* 一个填空部分，供你粘贴当前模板并描述想要的改动

复制后，打开 AI 会话，粘贴提示词，填写底部的两个空白处（你当前的模板和想要的改动），然后发送。AI 会返回一个完整的模板，你可以将其粘贴回编辑器并编译。

<Tip>
  请将你现有的模板粘贴到填空部分，而不要留空。AI 会以它为起点，这样你已经做过的所有自定义都会得到保留，而不会被默认模板取代。
</Tip>

<Note>
  提示词是针对每个区块定制的。**Copy AI prompt** 按钮只出现在支持自定义模板的区块上。
</Note>

<Tip>
  你的起点默认模板是**该区块内置标记的可运行副本**，所以你始终拥有一个正确、可渲染的参考来修改，而不是从空白页开始。任何时候想找回这个参考，就使用 **Reset to default**。

  它并不总是逐字节一致。Header 的默认模板还会渲染 `logoUrl`，而内置标记没有为它安排位置，所以启用该模板正是让上传的页眉图片首次显示的方式。
</Tip>

<div id="what-your-template-replaces">
  ## 你的模板替换了什么
</div>

模板会**完全**替换区块的渲染。你的 JSX 外面不会保留任何包装器，在开始删除内容之前，有些后果值得了解：

| 你失去的                 | 意味着什么                                                                 |
| -------------------- | --------------------------------------------------------------------- |
| 区块的包装元素              | 没有任何东西包裹你的标记。区块原本提供的内边距、对齐或布局现在都由你来提供。                                |
| **区块 Design 标签页的设置** | 设计设置以内联样式应用在内置包装器上，而那个包装器已经不存在了。在 Design 标签页中设置的颜色、间距和圆角**不再应用**于此区块。 |
| 内置的无障碍支持             | `aria-label`、焦点处理和语义化元素只有在你的 JSX 包含它们时才存在。                            |

<Warning>
  \*\*Design 标签页是最容易让人措手不及的一项。\*\*当自定义模板处于启用状态时，Design 标签页的字段会被禁用，"Design" 标题旁会出现警告图标。将鼠标悬停在图标上可查看原因。请改为从模板中为区块设置样式，[使用内联样式或你自己的 CSS](#styling-a-custom-template)。关闭自定义模板后，这些字段会立即重新启用。
</Warning>

你保留的内容：区块在购物车中的位置、其可见性开关、其设置（这些设置仍会作为你接收的 props 的数据来源）、购物车的[自定义 CSS](/zh/aftersell/cart/custom-css) 面板，以及**内置的加载骨架屏**。

最后一项常让人意外。区块会在到达你的模板*之前*检查购物车是否仍在加载，所以内置骨架屏会在加载期间渲染，而你的模板只在购物车就绪后运行。你不必构建加载状态。

<div id="whats-available-inside-a-template">
  ## 模板内部可用的内容
</div>

你的模板是一个单一的函数组件。它从 **TSX** 编译而来，所以允许使用类型注解，并在编译时被去除。这就是默认模板带有类型注解的原因：

```jsx theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
function Header(props: HeaderProps) {
  return <div>{/* … */}</div>;
}
```

**签名行和结束花括号是锁定的**——编辑器不允许你编辑它们中的任何一个，悬停时会显示 "Locked — this line can't be edited."。你在它们之间编写函数主体。只有 **Reset to default** 能替换它们。

其他要点：

* **你有五个 hooks 可用：**`useState`、`useEffect`、`useMemo`、`useRef` 和 `useCallback`。外加 `Fragment`，用于 `<>…</>`。
* \*\*没有导入。\*\*你不能 `import` 任何东西，作用域内也没有 `React` 对象，所以没有 `React.useReducer`，也没有 `React.Children`。如果某个 hook 不在上面的列表中，它就不可用。
* \*\*props 是只读的。\*\*修改 prop 不会有任何用处。要更改购物车，请使用区块提供的处理函数 props（`onClose`、`increment`、`selectPlan` 等），而不要直接写入 props。
* **可以访问 `window`**，所以当区块的 props 无法满足需求时，模板可以通过 `window.aftersell.cart` 调用 [Cart SDK](/zh/aftersell/cart/sdk-overview)。

<div id="conventions-across-every-block">
  ## 所有区块通用的约定
</div>

有三条规则处处适用，了解它们能消除大部分猜测：

* \*\*`*Html` props 是已消毒的富文本。\*\*用 `dangerouslySetInnerHTML` 渲染它们。它们已经过购物车的消毒器处理，`{{total_price}}` 之类的商家令牌也已被解析。
* \*\*以 `string` 形式到达的价格已经按商店的货币格式格式化好了。\*\*以 `number` 形式的价格以分为单位。每个区块只会给你其中一种，各区块的表格会说明是哪一种。
* \*\*在模板内部 `isLoading` 始终为 `false`。\*\*区块会渲染其内置骨架屏，并且只在购物车加载完成后才调用你的模板，所以传入这个 prop 是为了完整性，而不是让你基于它做分支。

<Note>
  少数区块在某些状态下完全不渲染任何内容，所以你的模板永远不会收到空数据。Rewards 模板永远不会看到空的 `milestones`，Subscription upgrade 模板永远不会看到为 null 的 `view`。每个区块的参考都会注明适用之处，这样你就可以跳过空状态分支。
</Note>

<div id="styling-a-custom-template">
  ## 为自定义模板设置样式
</div>

你的起点默认模板带有该区块的类名。如何为你的修改设置样式，取决于你离这个起点走了多远。

<div id="the-two-class-families">
  ### 两个类名家族
</div>

默认模板中的每个元素都带有一对类名，它们的作用截然不同：

| 家族                | 作用                                                         | 可以针对它写 CSS 吗？                           |
| ----------------- | ---------------------------------------------------------- | --------------------------------------- |
| `cart-internal-*` | \*\*承载区块的内置样式。\*\*购物车样式表中的每条规则都针对这个家族。                     | 不可以。它是购物车自身的内部机制，自定义 CSS 编辑器会标记针对它的选择器。 |
| `cart-external-*` | \*\*一个自身没有任何样式的挂钩。\*\*购物车样式表中没有任何规则针对它；它存在的目的就是供你的 CSS 使用。 | 可以。这是重新设计区块样式的受支持方式。                    |

所以 `cart-internal-header__title` 是让标题*看起来*像内置标题的原因，而 `cart-external-header__title` 才是当你想改变其外观时应该抓取的把手。

<div id="small-changes-keep-both-classnames">
  ### 小改动：保留两个类名
</div>

如果你只是在现有结构内重新排序元素、重新命名标签或添加内容，请不要动这些类名。你可以免费保留内置外观，并通过针对 `cart-external-*` 挂钩的[自定义 CSS](/zh/aftersell/cart/custom-css) 重新设置样式。

<div id="restructuring-drop-both-classnames">
  ### 重构：去掉两个类名
</div>

一旦你要改变 DOM 结构而不只是微调，就把**两个**家族都从你的标记上移除，改用[你自己的类名](#option-1-your-own-classnames-plus-custom-css)。每个家族都有各自的移除理由。

\*\*去掉 `cart-internal-*` 是因为内置 CSS 是为内置 DOM 编写的。\*\*在重构后的标记上保留这些类，你就会继承一些假设了你已不再拥有的元素的布局规则：期望不同子元素的 flex 容器、针对已移动元素的间距、相对于已删除元素的定位。这通常表现为你自己的 CSS "不起作用"，实际上是内置规则赢了。

<Warning>
  \*\*去掉 `cart-external-*` 是因为它是共享名称，不属于你。\*\*这些类名在内置标记上有特定含义，而你的自定义 CSS 是为整个购物车编写一次的。如果重构后的模板重用它们，你写的任何规则都会同时作用于你的结构和内置结构。

  在你关闭自定义模板的那一刻就会出问题：区块恢复为内置标记，而你的 CSS 仍然指向它，现在为一个它从未针对过的 DOM 设置样式。使用你自己的前缀能让两者干净地分离，这样关闭模板就是一次干净的还原。
</Warning>

为你构建的内容设置样式有两种方式：

<div id="option-1-your-own-classnames-plus-custom-css">
  #### 方式 1：你自己的类名加自定义 CSS
</div>

最适合需要维护或复用的内容。给你的类加一个不会与他人冲突的前缀，通常是你的商店或品牌名称：

```jsx theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
function Header(props) {
  return (
    <div className="northwind-custom-header">
      <div className="northwind-custom-header__title" dangerouslySetInnerHTML={{ __html: props.title }} />
      <button type="button" className="northwind-custom-header__close" onClick={props.onClose}>
        &times;
      </button>
    </div>
  );
}
```

然后在购物车编辑器中，在左侧面板选择 **Cart settings**，并在右侧打开 **Custom CSS** 标签页：

```css theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
.northwind-custom-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px;
}

.northwind-custom-header__title {
  font-size: 18px;
  font-weight: 600;
}

.northwind-custom-header__close:hover {
  opacity: 0.6;
}
```

前缀比看起来更重要。没有前缀的话，像 `.header` 或 `.title` 这样的类就有可能与购物车自身的类、其他应用的模板或未来的区块发生冲突。

<div id="option-2-inline-styles">
  #### 方式 2：内联样式
</div>

无需在 CSS 面板之间来回切换，所有内容都在一处：

```jsx theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
```

适合布局脚手架和一次性需求。它的局限也是常见的那些：没有 `:hover` 或其他伪类，没有媒体查询，也无法跨区块复用。一旦你需要其中任何一项，就改用方式 1。

<div id="picking-an-approach">
  ### 选择一种方式
</div>

| 情况             | 建议做法                                          |
| -------------- | --------------------------------------------- |
| 结构相同，只是措辞或顺序不同 | 保留两个类名，通过针对 `cart-external-*` 的自定义 CSS 重新设置样式 |
| 新结构，需要长期维护的样式  | 使用你自己带前缀的类，去掉两个购物车类名家族                        |
| 新结构，只需几条快速布局规则 | 使用内联样式，去掉两个购物车类名家族                            |
| 跨多个区块有大量自定义代码  | 处处使用你自己带前缀的类，这样任何模板都能干净地关闭                    |

<Note>
  购物车渲染在 shadow root 中，所以你主题的样式表无法触及其内部。自定义模板的样式必须来自购物车自己的 **Custom CSS** 面板或内联样式，而不是来自你的主题。参阅[自定义 CSS](/zh/aftersell/cart/custom-css)。
</Note>

<div id="when-a-template-fails">
  ## 模板失败时
</div>

损坏的模板永远不会破坏购物车。该区块会渲染**空白**，周围的一切照常工作，这很安全但容易被忽视：症状就是你的区块位置出现一片空白。

| 失败类型  | 何时看到             | 在哪里报告                                                 |
| ----- | ---------------- | ----------------------------------------------------- |
| 类型错误  | 输入时              | 编辑器中的内联波浪线。它**不会**阻止编译——编译器会去除类型而不是检查它们               |
| 语法错误  | 点击 **Compile** 时 | 在编辑器中，尚未到达你的店面之前                                      |
| 渲染时崩溃 | 上线后，在店面上         | `console.error('[aftersell-cart] module crashed: …')` |

由于区块会静默消失而不是明显报错，发布前请务必在[预览](/zh/aftersell/cart/previewing-carts)中检查模板。如果某个区块不见了，先打开浏览器控制台。

有两点值得防范，因为假设相反情况的模板都会崩溃：

* \*\*可为 null 的 props。\*\*许多 props 在正常情况下就是 `null`（没有 logo 时的 `logoUrl`、没有图片时的 `imageUrl`、单变体产品上的 `variantTitle`）。使用前请先检查。
* **可能为空的数组。**`discountTags` 和 `discountCodes` 大多数时候都是 `[]`。

<div id="limitations">
  ## 限制
</div>

* \*\*自定义模板是显示层面的覆盖。\*\*要对购物车运行逻辑（订阅事件、添加商品、响应变化），请使用[自定义脚本](/zh/aftersell/cart/custom-scripts)和 [Cart SDK](/zh/aftersell/cart/sdk-overview)。
* \*\*几乎所有区块都支持自定义模板。\*\*例外是承载 Shopify 自有支付按钮的 **[Express payments](/zh/aftersell/cart/express-payments-block)** 区块，以及 **[Cart items](/zh/aftersell/cart/cart-items-block)** 容器本身，不过其中的 **Product** 行支持自定义模板。
* \*\*模板不能改变区块的根本功能。\*\*它改变的是区块数据的呈现方式，而不是数据或其背后的行为。

<div id="props-for-each-block">
  ## 每个区块的 props
</div>

每个区块传递各自的数据。完整的 prop 表格（含类型和实际示例）位于对应区块的页面：

| 区块                                                                                    | 接收的 props                                                                                                                   |
| ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| [Header](/zh/aftersell/cart/header-block#custom-template)                             | `title`、`logoUrl`、`leftSection`、`rightSection`、`itemCount`、`onClose`、`isLoading`                                            |
| [Banner](/zh/aftersell/cart/banner-block#custom-template)                             | `text`、`shouldUseTimer`、`isTimerExpiredAndShouldHide`、`isLoading`                                                           |
| [Rewards](/zh/aftersell/cart/rewards-block#custom-template)                           | `milestones`、`rewardsMessageHtml`、`showIcons`、`isLoading`                                                                   |
| [Cart items · Product](/zh/aftersell/cart/cart-items-block#custom-template)           | 25 个 props：每行的内容、标识符和数量控件                                                                                                   |
| [Subscription upgrade](/zh/aftersell/cart/subscription-upgrade-block#custom-template) | `view`、`selectPlan`、`onChange`、`oneTimeValue` 等                                                                             |
| [Summary](/zh/aftersell/cart/summary-block#custom-template)                           | `leftHtml`、`rightHtml`、`discountCodes`、`totalPrice`、`savings` 等                                                             |
| [Checkout button](/zh/aftersell/cart/checkout-button-block#custom-template)           | `label`、`href`、`isLoading`                                                                                                  |
| [Discount code](/zh/aftersell/cart/discount-code-block#custom-template)               | `discountCodeInput`、`placeholder`、`buttonText`、`isValidating`、`isInvalid`、`setDiscountCodeInput`、`handleSubmit`、`isLoading` |
| [Empty cart](/zh/aftersell/cart/empty-cart-block#custom-template)                     | `text`、`cta`、`href`                                                                                                         |
| [Image](/zh/aftersell/cart/image-block#custom-template)                               | `imageUrl`、`altText`、`maxHeight`、`fullWidth`                                                                                |
| [Notes](/zh/aftersell/cart/notes-block#custom-template)                               | `titleHtml`、`placeholder`、`noteInput`、`status`、`isExpanded`、`onNoteChange`、`onNoteBlur`、`onToggle` 等                        |
| [Product add-on](/zh/aftersell/cart/product-add-on-block#custom-template)             | `addonTitleHtml`、`descriptionHtml`、`priceHtml`、`imageUrl`、`format`、`isEnabled`、`handleAdd`、`handleToggle` 等                 |
| [Shipping protection](/zh/aftersell/cart/shipping-protection-block#custom-template)   | `titleHtml`、`descriptionHtml`、`priceHtml`、`imageUrl`、`format`、`isEnabled`、`handleAdd`、`handleToggle` 等                      |
| [Upsells](/zh/aftersell/cart/upsells-block#custom-template)                           | `title`、`addButtonText`、`layout`、`upsells`、`selectVariant`、`handleAdd`，以及轮播控件                                               |

[自定义代码](/zh/aftersell/cart/custom-code-blocks)区块是唯一**添加**标记而非替换区块渲染的界面，所以它的 props 有所不同：整个购物车，外加一个加入购物车操作。参阅[自定义代码区块 → Props](/zh/aftersell/cart/custom-code-blocks#props)。
