# realibox-ui-sdk

`realibox-ui-sdk` 提供产品目录、产品详情和 3D 配置能力。SDK 导出的是原生 Web Components，可以在 HTML、React、Vue 或其他前端项目中使用。

所有业务组件都必须先通过 `initSDK({ key, mode })` 完成初始化和授权。

## 安装或加载

### npm

```bash
npm install realibox-ui-sdk
```

或：

```bash
pnpm add realibox-ui-sdk
```

### CDN

无构建工具的静态 HTML 页面可以直接加载 UMD 文件。UMD 包会注册全部 SDK 组件，并通过 `window.RealiboxUISDK` 暴露初始化方法和 API。

```html
<script src="https://cdn.jsdelivr.net/npm/realibox-ui-sdk@latest/dist/realibox-ui-sdk.umd.js"></script>
```

生产项目建议将 `latest` 固定为经过验证的版本号，避免 SDK 升级影响线上页面。

## 初始化

使用任何组件前必须先执行 `initSDK`。

### npm / ESM

```ts
import { initSDK } from "realibox-ui-sdk";

initSDK({
  key: "your-key",
  mode: "production",
});
```

### HTML / CDN

```html
<script src="https://cdn.jsdelivr.net/npm/realibox-ui-sdk@latest/dist/realibox-ui-sdk.umd.js"></script>
<script>
  window.RealiboxUISDK.initSDK({
    key: "your-key",
    mode: "production",
  });
</script>
```

### `initSDK` 参数

| 参数名 | 类型 | 是否必填 | 说明 |
| :--- | :--- | :--- | :--- |
| `key` | `string` | 是 | SDK 授权密钥，不能为空。授权域名和运行环境需要与 key 匹配。 |
| `mode` | `"development" \| "production"` | 是 | `development` 使用测试环境，`production` 使用正式环境。 |
| `locale` | `string` | 否 | 默认语言，例如 `"zh-CN"`、`"en"`；默认读取浏览器语言。 |
| `apiBaseUrl` | `string` | 否 | 覆盖产品 API 地址；通常使用 `mode` 对应的默认地址。 |
| `viewerBaseUrl` | `string` | 否 | 覆盖 3D Viewer 地址；通常使用 `mode` 对应的默认地址。 |
| `routerMode` | `"history" \| "hash"` | 否 | 页面级组件生成链接时采用的路由模式，默认 `history`。无 rewrite 的静态部署建议使用 `hash`。 |
| `siteConfig` | `SiteConfig` | 否 | 全局站点、列表、详情、筛选、主题和 Viewer 控制配置。 |

语言解析优先级从高到低为：组件级 `siteConfig.locale`、`initSDK({ locale })`、`initSDK({ siteConfig: { locale } })`、浏览器语言、`"zh-CN"`。

`siteConfig.messages` 同时支持旧版的扁平文案和按语言配置的文案。按语言配置依次匹配精确语言（如 `en-US`）、基础语言（`en`），最后回退 `zh-CN`：

```ts
initSDK({
  key: "your-key",
  mode: "production",
  locale: "en-US",
  siteConfig: {
    messages: {
      "configurator.graphicsDefaultText": {
        "zh-CN": "新文字",
        en: "New text",
      },
      // 旧格式仍然有效，所有语言均使用该值。
      "detail.params": "TECHNICAL DETAILS",
    },
  },
});
```

完整的公共方法、Graphics2D、场景快照和事件说明见 [SDK API 文档](./docs/sdk-api/README.md)。

静态站点的完整初始化示例：

```js
window.RealiboxUISDK.initSDK({
  key: "your-key",
  mode: "production",
  apiBaseUrl: "https://your-api.example.com",
  viewerBaseUrl: "https://your-viewer.example.com",
  locale: "en",
  routerMode: "hash",
  siteConfig: {
    viewerControls: {
      objectSelection: true,
      materialSelection: true,
      surfaceProcess: true,
      colorPicker: true,
    },
  },
});
```

`apiBaseUrl` 和 `viewerBaseUrl` 仅在需要代理、私有域名或指定部署地址时传入，不应直接照抄示例地址。

## 当前组件

主入口和 CDN UMD 包当前注册以下 8 个组件：

| 组件标签 | 用途 | 主要输入 | 是否依赖其他组件 |
| :--- | :--- | :--- | :--- |
| `product-3d-configurator` | 一体化 3D 配置区，自动查询产品的 3D 项目并组合 Viewer 与配置面板 | `product-id` 或 `project-id`、`share-code`、`site-config` | 内部自动组合 Viewer 和 Configurator |
| `product-list-page` | 完整产品目录页，包括站点壳、分类、筛选和产品网格 | `keyword`、`has-3d`、`org-id`、`share-code`、`site-config` | 否 |
| `product-detail-page` | 完整产品详情页，包括图片、属性、推荐产品和可用的 3D 配置区 | `product-id`、`share-code`、`site-config` | 内部自动组合 Viewer 和 Configurator |
| `product-detail` | 轻量产品基础信息和属性面板 | `product-id`、`share-code`、`site-config` | 否 |
| `product-sidebar` | 可筛选、可选择产品的侧栏列表 | 查询参数、`active-product-id`、`site-config` | 否；通过事件通知宿主页面 |
| `product-3d-viewer` | 独立 3D 产品 Viewer | `project-id`、Viewer 控制属性、`site-config` | 否 |
| `product-configurator` | 部件、材质、工艺和颜色配置面板 | `viewer-selector`、`site-config` | 是，必须绑定 `product-3d-viewer` |
| `site-shell` | 仅渲染 SiteConfig 中的站点 Header 和 Footer | `site-config` | 否 |

选择方式：

- 需要完整产品官网页面时，直接使用 `product-list-page` 和 `product-detail-page`。
- 已有自己的页面结构时，使用 `product-detail`、`product-sidebar`、`product-3d-viewer` 等基础组件自行组合。
- 只展示 3D 时，单独使用 `product-3d-viewer`。
- 需要编辑 3D 配置时，优先使用 `product-3d-configurator`；需要完全控制布局和生命周期时，再手工组合 Viewer 与 Configurator。

## 原生 HTML / JavaScript 完整接入

以下流程对应 [`examples/static-product-configurator.html`](./examples/static-product-configurator.html)。组合组件会在内部通过产品 ID 查询 3D 项目 ID，并创建 Viewer 和 Configurator。

最简接入只需要一个标签：

```html
<product-3d-configurator product-id="your-product-id"></product-3d-configurator>
```

已有 3D 项目 ID 时可以跳过产品详情请求：

```html
<product-3d-configurator project-id="your-project-id"></product-3d-configurator>
```

### 1. 准备页面容器

Viewer 必须有明确高度。`size="none"` 表示组件填满宿主容器，不会替宿主页面创建高度。

```html
<style>
  html,
  body {
    margin: 0;
    height: 100%;
  }

  #app {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 400px;
    height: 100%;
  }

  product-3d-viewer {
    display: block;
    width: 100%;
    height: 100%;
  }

  product-configurator {
    display: block;
    overflow: auto;
  }

  @media (max-width: 900px) {
    #app {
      grid-template-columns: 1fr;
      grid-template-rows: minmax(420px, 58vh) auto;
      height: auto;
      min-height: 100%;
    }
  }
</style>

<main id="app">正在加载...</main>
```

### 2. 加载、初始化并创建组合组件

`product-id` 和 `project-id` 同时存在时优先使用 `project-id`，不会再请求产品详情接口。

```html
<script src="https://cdn.jsdelivr.net/npm/realibox-ui-sdk@latest/dist/realibox-ui-sdk.umd.js"></script>
<script>
  const config = {
    sdkKey: "your-key",
    mode: "production",
    productId: "your-product-id",
    locale: "en",
    siteConfig: {
      viewerControls: {
        objectSelection: true,
        materialSelection: true,
        surfaceProcess: true,
        colorPicker: true,
        pantone: true,
        rgb: true,
        cmyk: true,
        gradient: true,
      },
    },
  };

  const app = document.querySelector("#app");

  function showError(error) {
    app.textContent = `加载失败：${error instanceof Error ? error.message : String(error)}`;
  }

  async function main() {
    try {
      const sdk = window.RealiboxUISDK;
      if (!sdk) {
        throw new Error("SDK 脚本未加载成功");
      }

      sdk.initSDK({
        key: config.sdkKey,
        mode: config.mode,
        locale: config.locale,
        siteConfig: config.siteConfig,
      });

      renderConfigurator(config.productId);
    } catch (error) {
      showError(error);
    }
  }

  function renderConfigurator(productId) {
    app.replaceChildren();

    const configurator = document.createElement("product-3d-configurator");
    configurator.productId = productId;
    configurator.shareCode = config.shareCode || "";
    configurator.siteConfig = config.siteConfig;
    app.append(configurator);
  }

  main();
</script>
```

脚本应在 SDK UMD 加载完成后执行。使用 `async` 动态加载 CDN 脚本时，需要等待脚本的 `load` 事件后再访问 `window.RealiboxUISDK`。

### 3. 监听状态和错误

```js
const configurator = document.querySelector("product-3d-configurator");

configurator.addEventListener("viewer-ready", () => {
  console.log("3D 引擎已就绪");
});

configurator.addEventListener("viewer-loaded", (event) => {
  console.log("场景及初始配置已加载", event.detail);
});

configurator.addEventListener("viewer-error", (event) => {
  console.error("Viewer 加载失败", event.detail?.error);
});

configurator.addEventListener("config-change", (event) => {
  console.log("3D 配置发生变化", event.detail);
});

configurator.addEventListener("config-error", (event) => {
  console.error("配置命令失败", event.detail?.error);
});

configurator.addEventListener("product-load-error", (event) => {
  console.error("产品 3D 项目加载失败", event.detail?.error);
});
```

## HTML 属性与 JavaScript 属性

原生 HTML 标签使用 `kebab-case` attribute；通过 JavaScript 操作元素实例时使用 `camelCase` property。

```html
<product-3d-viewer
  project-id="your-project-id"
  part-selection="false"
  camera-focus="true"
  outline-render="true"
></product-3d-viewer>
```

等价的 JavaScript：

```js
const viewer = document.createElement("product-3d-viewer");
viewer.projectId = "your-project-id";
viewer.partSelection = false;
viewer.cameraFocus = true;
viewer.outlineRender = true;
```

Boolean attribute 支持空字符串、`"true"` 和 `"false"`。为避免原生 HTML 的标准 Boolean attribute 语义造成误解，推荐显式传 `"true"` 或 `"false"`。

`siteConfig` 是对象。动态页面优先通过 property 赋值：

```js
const listPage = document.querySelector("product-list-page");
listPage.siteConfig = {
  locale: "en",
  productList: { pageSize: 24 },
};
```

直接写在 HTML attribute 中时，值必须是合法 JSON：

```html
<product-list-page
  site-config='{"locale":"en","productList":{"pageSize":24}}'
></product-list-page>
```

## 各组件独立接入

### `product-3d-configurator`

原生前端推荐使用的一体化入口。组件负责产品详情查询、`project_id` 提取、加载/错误状态、Viewer 与面板绑定及响应式布局。

```html
<product-3d-configurator
  product-id="your-product-id"
  share-code="optional-share-code"
></product-3d-configurator>
```

可通过 `--pv-3d-configurator-min-height`、`--pv-3d-configurator-panel-width` 和 `--pv-3d-configurator-mobile-viewer-height` 调整布局。

### `product-list-page`

完整产品目录页，组件内部负责查询分类、筛选项和产品列表，并为产品卡片生成详情链接。

```html
<product-list-page
  keyword="bottle"
  has-3d="true"
  org-id="your-org-id"
  share-code="optional-share-code"
></product-list-page>
```

静态文件托管通常没有 history fallback，应在初始化时设置 `routerMode: "hash"`。使用 `history` 时，服务器需要把产品详情路径 rewrite 到应用入口。

### `product-detail-page`

完整详情页只需要产品 ID；组件内部加载产品信息，并在产品包含 `project_id` 时提供 3D Viewer 和配置面板。

```html
<product-detail-page
  product-id="your-product-id"
  share-code="optional-share-code"
></product-detail-page>
```

### `product-detail`

只需要轻量产品信息和属性表时使用，不包含完整图库、推荐产品和 3D 配置区。

```html
<product-detail product-id="your-product-id"></product-detail>
```

### `product-3d-viewer`

```html
<div style="width: 100%; height: 600px">
  <product-3d-viewer
    project-id="your-project-id"
    size="none"
    object-selection="true"
    material-selection="true"
    surface-process="true"
    color-picker="true"
    graphic-customization="true"
    outline-render="true"
  ></product-3d-viewer>
</div>
```

| HTML 属性 | JavaScript property | 类型 | 默认值 | 说明 |
| :--- | :--- | :--- | :--- | :--- |
| `project-id` | `projectId` | `string` | `""` | 3D 项目 ID，使用 Viewer 时必填。 |
| `size` | `size` | `"sm" \| "md" \| "lg" \| "none"` | `"none"` | Viewer 预设尺寸；`none` 填满宿主容器。 |
| `show-top-header` | `showTopHeader` | `boolean` | `false` | 是否显示 Viewer 顶部区域。 |
| `part-selection` | `partSelection` | `boolean` | `false` | 是否启用部件切换能力。 |
| `camera-focus` | `cameraFocus` | `boolean` | `false` | 是否启用相机聚焦。 |
| `design` | `design` | `boolean` | `false` | 是否启用设计能力。 |
| `outline-render` | `outlineRender` | `boolean` | `true` | 是否启用选中描边。 |
| `object-selection` | `objectSelection` | `boolean` | `true` | 是否启用对象选择。 |
| `material-selection` | `materialSelection` | `boolean` | `true` | 是否启用材质选择。 |
| `surface-process` | `surfaceProcess` | `boolean` | `true` | 是否启用表面工艺选择。 |
| `color-picker` | `colorPicker` | `boolean` | `true` | 是否启用颜色配置。 |
| `graphic-customization` | `graphicCustomization` | `boolean` | `false` | 是否启用 Graphics2D 图案编辑；SDK 模式下应同时开启 `objectSelection`，鼠标点击 3D 才会切换当前部件或贴图区域。 |
| `site-config` | `siteConfig` | `SiteConfig \| string` | - | 组件级 SiteConfig。 |
| `debug` | `debug` | `boolean` | `false` | 是否输出并派发 Viewer 通信调试信息；命令超时始终会输出诊断错误。 |

Viewer 还提供 `getConfiguratorState()`、`selectObject()`、`setObjectVisible()`、`selectMaterial()`、`selectSurfaceProcess()`、`setColor()`、`getGradientState()`、`setGradientColor()` 和 `resetConfig()` 等实例方法。应在 `viewer-ready` 或 `viewer-loaded` 后调用：

```js
const viewer = document.querySelector("product-3d-viewer");

viewer.addEventListener("viewer-ready", async () => {
  const state = await viewer.getConfiguratorState();
  console.log(state);
});
```

图文定制通过 Viewer 的只读 `graphics2d` client 调用。能力与目标应从 Viewer 查询，不要由宿主猜测：

```js
const capabilities = await viewer.getViewerCapabilities();
if (capabilities.graphics2d.enabled) {
  const targets = await viewer.graphics2d.listTargets();
  const session = await viewer.graphics2d.beginSession(targets[0].id);
  await viewer.graphics2d.addImage({
    sessionId: session.sessionId,
    src: "https://cdn.example.com/logo.png",
  });
  await viewer.graphics2d.addText({ sessionId: session.sessionId, content: "Hello" });
  await viewer.graphics2d.commit(session.sessionId);
}
```

`graphics2d` 还提供元素更新/删除、表面工艺、`rollback()`、`exportState()` 和 `importState()`。Viewer 返回的命令错误会抛出 `ViewerCommandError`，可读取 `code`、`message` 和 `details`。

#### 场景配置快照

场景快照会保存项目内所有可配置模型部件的显隐、材质、表面工艺、纯色或渐变配色以及渐变工艺。可以直接导出 JSON 存入浏览器缓存或服务端，并在相同项目的新 Viewer 加载完成后恢复：

```js
// 当前路由：保存
const json = await viewer.exportSceneJSON();
sessionStorage.setItem(`scene:${viewer.projectId}`, json);

// 新路由：恢复
viewer.addEventListener("viewer-loaded", async () => {
  const cached = sessionStorage.getItem(`scene:${viewer.projectId}`);
  if (!cached) return;

  const result = await viewer.loadSceneJSON(cached);
  console.log("场景恢复完成", result.issues);
}, { once: true });
```

也可以直接使用对象接口：

```js
const snapshot = await viewer.exportSceneSnapshot();
await viewer.loadSceneSnapshot(snapshot);
```

默认情况下，快照 `projectId` 与当前 Viewer 不一致，或部件、材质、工艺 ID 已失效时，加载会在修改场景前失败。需要兼容场景资源调整时，可以显式跳过缺失项：

```js
const result = await viewer.loadSceneJSON(cached, {
  missingObject: "skip",
  missingOption: "skip",
  missingGraphicsTarget: "skip",
});
console.log(result.issues);
```

开启图文定制时，快照同时保存所有 Graphics2D target 的图片、文字、变换、字体和表面工艺。旧快照没有 `graphics2d` 字段时不会清空当前图文；可用 `missingGraphicsTarget: "skip"` 跳过已经移除的贴图区域。

普通 JSON 快照中的 Graphics2D 图片地址必须是可重新访问的 HTTP(S) URL。如果宿主直接传入了本地 Blob，可以使用场景资源包接口保留二进制：

```js
const bundle = await viewer.exportSceneBundle();
// bundle.snapshot 可以写入 JSON；bundle.assets 中的 Blob 建议写入 IndexedDB。

await viewer.loadSceneBundle({
  snapshot: storedSnapshot,
  assets: blobsLoadedFromIndexedDB,
});
```

资源包快照使用 `asset://graphics2d/...` 引用本地资源，加载时 SDK 会把对应 Blob 通过 `postMessage` 结构化克隆重新传给 Viewer。示例工程将场景清单写入 `sessionStorage`，Blob 写入 IndexedDB。快照仍不包含相机、灯光和标注。严格校验失败时不会修改场景；如果底层引擎在实际应用过程中报错，Viewer 会尝试恢复应用前状态，并在结构化错误的 `details.rollbackApplied` 中报告恢复结果。

### `product-configurator`

Configurator 不会自己创建 Viewer，必须通过 `viewer-selector` 找到页面中的 `product-3d-viewer`。

```html
<div class="viewer-config-layout">
  <product-3d-viewer
    id="main-viewer"
    project-id="your-project-id"
  ></product-3d-viewer>
  <product-configurator viewer-selector="#main-viewer"></product-configurator>
</div>
```

如果页面只有一个 Viewer，可以省略 `viewer-selector`，默认选择第一个 `product-3d-viewer`。页面存在多个 Viewer 时必须为每组组件配置唯一 ID 和选择器。

通过 SiteConfig 显式开启内置图案与工艺面板：

```js
const configurator = document.querySelector("product-configurator");
configurator.siteConfig = {
  viewerControls: {
    graphicCustomization: true,
  },
};
```

开启后，配置器会在 Viewer 支持 Graphics2D protocol v1 且存在可编辑区域时显示“外观配置 / 图案与工艺”。面板支持多个贴图和多个文字，可设置 Viewer 当前可用字体、字重、文字颜色、表面工艺、大小、横纵位置、旋转和翻转；ui-sdk 不上传图片或字体文件，离开编辑默认 rollback。拖动阶段使用低清纹理，松开后由 Viewer 自适应生成最高 4096 的最终纹理。

「重置」始终恢复项目首次加载完成时的初始状态，不会读取已保存的 session 场景。场景 Bundle 仅用于页面间流转或用户主动加载；本地图片 Blob 存入宿主选择的二进制存储，文字随 JSON 状态保存。

宿主也可以显式打开或关闭内置编辑器：

```js
await configurator.openGraphicsEditor();
await configurator.closeGraphicsEditor({ action: "rollback" });
```

需要自行构建 UI 时，可使用 Viewer 上的高层 controller；底层 `viewer.graphics2d` 命令仍保持兼容：

```js
const editor = viewer.graphics2d.createEditor();
await editor.open();
await editor.selectTarget("target-id");
await editor.addImage(file); // File/Blob 原样传入，不经过 SDK 上传或压缩
await editor.addText(); // 使用当前语言的 configurator.graphicsDefaultText
await editor.updateTransform({ x: 0.5, y: 0.5, scale: 1.2 }, { final: true });
await editor.close(); // 默认 rollback
```

### `product-sidebar`

Sidebar 负责产品查询和选择，通过 `product-select` 事件把选中产品交给宿主页面。

```html
<product-sidebar
  id="product-picker"
  keyword="bottle"
  limit="12"
  has-3d="true"
  auto-select-first="true"
></product-sidebar>

<script>
  const sidebar = document.querySelector("#product-picker");

  sidebar.addEventListener("product-select", (event) => {
    const { product, userInteraction } = event.detail;
    console.log("选中产品", product, userInteraction);
  });
</script>
```

| HTML 属性 | JavaScript property | 类型 | 默认值 | 说明 |
| :--- | :--- | :--- | :--- | :--- |
| `keyword` | `keyword` | `string` | `""` | 搜索关键词。 |
| `limit` | `limit` | `number` | `8` | 每次查询数量。 |
| `skip` | `skip` | `number` | `0` | 查询偏移量。 |
| `has-3d` | `has3d` | `boolean` | - | 是否只查询具有 3D 项目的产品。 |
| `org-id` | `orgId` | `string` | `""` | 组织 ID。 |
| `share-code` | `shareCode` | `string` | `""` | 分享码。 |
| `category-ids` | `categoryIds` | `string` | `""` | 逗号分隔的分类 ID。 |
| `recommend-types` | `recommendTypes` | `string` | `""` | 逗号分隔的推荐类型。 |
| `sort-by` | `sortBy` | `string` | `"update_time"` | 排序字段。 |
| `sort-order` | `sortOrder` | `string` | `"desc"` | 排序方向。 |
| `status-filter` | `statusFilter` | `number` | - | 产品状态筛选。 |
| `auto-select-first` | `autoSelectFirst` | `boolean` | `true` | 是否自动选择首个产品。 |
| `active-product-id` | `activeProductId` | `string` | `""` | 当前选中的产品 ID。 |
| `site-config` | `siteConfig` | `SiteConfig \| string` | - | 组件级 SiteConfig。 |

### `site-shell`

`site-shell` 根据 SiteConfig 连续渲染 Header 和 Footer，可用于独立预览站点壳。它不提供默认 slot，不能把宿主业务内容插入 Header 和 Footer 之间；需要完整目录或详情布局时，应优先使用 `product-list-page` 或 `product-detail-page`。

```html
<site-shell id="site-shell"></site-shell>

<script>
  document.querySelector("#site-shell").siteConfig = {
    brand: { name: "YOUR BRAND" },
  };
</script>
```

## 事件参考

| 组件 | 事件 | `event.detail` | 说明 |
| :--- | :--- | :--- | :--- |
| `product-3d-viewer` | `viewer-frame-load` | 无 | Viewer iframe 页面完成浏览器加载。 |
| `product-3d-viewer` | `viewer-ready` | 通常为空 | 3D 引擎已可接收命令。 |
| `product-3d-viewer` | `viewer-loaded` | 初始配置状态 | 场景数据和初始配置已加载。 |
| `product-3d-viewer` | `viewer-error` | `{ error }` | Viewer 在限定时间内未就绪。 |
| `product-3d-viewer` | `config-change` | 配置状态增量 | Viewer 内部的配置发生变化。 |
| `product-3d-viewer` | `selection-changed` | `{ objectId, graphicsTargetIds }` | 鼠标或程序化选择变化。 |
| `product-3d-viewer` | `graphics2d.changed` | `{ sessionId, targetId }` | Graphics2D 会话中的图文状态发生变化。 |
| `product-3d-viewer` | `graphics2d.session-ended` | `{ sessionId, targetId, committed }` | 图文会话已保存或放弃。 |
| `product-3d-viewer` | `graphics2d.error` | `{ command, error }` | Graphics2D 命令失败；`error` 含 `code`、`message` 和可选 `details`。 |
| `product-3d-viewer` | `scene-snapshot-applied` | `SceneSnapshotApplyResult` | 完整场景快照已应用。 |
| `product-3d-viewer` | `debug-log` | 调试记录 | `debug` 开启时的通信日志。 |
| `product-configurator` | `config-change` | 当前配置状态 | 用户通过配置面板完成修改。 |
| `product-configurator` | `config-error` | `{ error }` | 配置命令执行失败。 |
| `product-configurator` | `graphics2d-mode-change` | `{ mode }` | 外观配置与图案编辑模式发生切换。 |
| `product-configurator` | `graphics2d-change` | `{ targetId, element, reason }` | 图案区域、贴图、变换或工艺发生变化。 |
| `product-configurator` | `graphics2d-error` | `{ error }` | 高层图案编辑操作失败；错误包含 `code`、`stage` 和 `recoverable`。 |
| `product-sidebar` | `product-select` | `{ product, userInteraction }` | 选择产品；`userInteraction` 表示是否由用户操作触发。 |

这些事件均可冒泡并穿过组件边界，可以在组件本身或上层容器监听。

## npm 原生 ESM 按需接入

使用 Vite、Rollup、webpack 等构建工具，但不使用 React/Vue 时，可以选择全量入口或组件子路径。

### 全量注册

主入口会注册所有组件：

```js
import { initSDK } from "realibox-ui-sdk";

initSDK({ key: "your-key", mode: "production" });
```

HTML 中随后可以直接使用任意 SDK 标签。

### 按组件导入

按需接入时先导入 `init`，再导入需要注册的组件：

```js
import { initSDK } from "realibox-ui-sdk/init";
import "realibox-ui-sdk/components/product-3d-viewer";
import "realibox-ui-sdk/components/product-configurator";

initSDK({ key: "your-key", mode: "production" });
```

可用组件子路径：

```text
realibox-ui-sdk/components/product-3d-viewer
realibox-ui-sdk/components/product-3d-configurator
realibox-ui-sdk/components/product-configurator
realibox-ui-sdk/components/product-detail
realibox-ui-sdk/components/product-detail-page
realibox-ui-sdk/components/product-list-page
realibox-ui-sdk/components/product-sidebar
realibox-ui-sdk/components/site-shell
```

`realibox-ui-sdk/init` 同时负责注入 SDK 样式。不要只导入组件文件而遗漏初始化入口。

浏览器不能直接解析 npm 包名形式的 bare import。完全没有构建工具的 HTML 页面应使用前面的 CDN UMD 方式，而不是直接在 `<script type="module">` 中写 `import ... from "realibox-ui-sdk"`。

## SiteConfig

`SiteConfig` 用于统一配置主题、品牌、列表、详情、筛选、别名和 Viewer 控制项。常用入口如下：

- 全局配置：`initSDK({ siteConfig })`。
- 单组件覆盖：通过组件的 `siteConfig` property 或 `site-config` JSON attribute 传入。
- 数据驱动文案：通过 `siteConfig.aliases` 按语言自定义分类、属性、属性值和推荐类型名称。
- Viewer 与 Configurator：两者应传入同一份 SiteConfig，确保控制项和界面一致。

完整配置目录见 [`docs/site-config/README.md`](./docs/site-config/README.md)。

常用配置能力包括：

- `filterFields`：配置 `product-list-page` 和 `product-sidebar` 的筛选项及后端字段映射。
- `filterDisplay: "configured" | "all"`：只显示已配置筛选项，或动态显示全部可用筛选项。
- `scope: "attribute" | "recommend_type" | "has_3d"`：指定筛选值进入属性筛选、推荐类型或顶层 `has_3d` 参数。
- `enumWhitelist`：按分类 code 或属性值白名单限制前端展示内容。
- `aliases`：按语言配置分类、属性、属性值、布尔值和推荐类型的显示名。
- `productCard`：统一控制列表页和详情页推荐卡片的名称对齐、hover 图片缩放及编码显示。
- `productList`：配置分页、主筛选、搜索位置和 View More 按钮等列表行为。
- `productDetail`：配置详情字段、图库箭头、产品编码和标题样式。
- `viewerControls`：统一控制 Viewer 和 Configurator 中开放的选择、工艺和颜色能力。

工程化项目可以在 TypeScript 中创建 SiteConfig，以使用函数、常量复用和类型检查；HTML attribute 只能传递纯 JSON，不能包含函数或注释。

## React

```tsx
import { initSDK } from "realibox-ui-sdk";

initSDK({
  key: "your-key",
  mode: "production",
});

export default function App() {
  return <product-3d-viewer projectId="your-project-id" />;
}
```

React JSX 使用 JavaScript property 命名，例如 `projectId`、`partSelection` 和 `cameraFocus`。如果运行时使用 CDN 而不是导入 SDK，需要额外加载 React 类型入口：

```ts
import type {} from "realibox-ui-sdk/react";
```

## Vue

SDK 组件是 Web Components，不是 Vue SFC。Vue 项目需要将使用的标签声明为自定义元素。

`vite.config.js`：

```js
import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          isCustomElement: (tag) => tag.startsWith("product-") || tag === "site-shell",
        },
      },
    }),
  ],
});
```

`src/vite-env.d.ts`：

```ts
/// <reference types="vite/client" />

import type {} from "realibox-ui-sdk/vue";
```

`tsconfig.json` 需要启用严格模板检查：

```json
{
  "vueCompilerOptions": {
    "strictTemplates": true
  }
}
```

`App.vue`：

```vue
<script setup lang="ts">
import { initSDK } from "realibox-ui-sdk";

initSDK({
  key: "your-key",
  mode: "production",
});
</script>

<template>
  <product-3d-viewer projectId="your-project-id" />
</template>
```

Volar 会把 Vue 模板中的 `project-id` 规范化为 `projectId` 做类型匹配，因此 SDK 的 Vue 类型入口使用 camelCase property 名。

## 常见问题

### 组件显示授权错误

确认已经在创建组件前调用 `initSDK`，并检查 key、`mode`、当前页面域名和授权环境是否匹配。

### CDN 加载后 `window.RealiboxUISDK` 不存在

检查 CDN 地址和网络请求是否成功，并确认初始化脚本在 UMD 脚本加载完成后执行。

### Viewer 空白或高度为 0

为 Viewer 的父容器设置明确的宽高。使用 `size="none"` 时组件只会填满父容器。

### 已有产品 ID，但 Viewer 无法加载

`product-id` 和 `project-id` 是不同参数。先调用 `getProductDetailApi(productId)`，再读取 `detail.info.project_id` 传给 Viewer。

### Configurator 一直处于加载状态

确认目标 Viewer 已存在、`viewer-selector` 能选中正确元素、Viewer 具有有效 `project-id`，且两者位于同一个 `document` 中。

### 静态部署点击产品后出现 404

初始化时使用 `routerMode: "hash"`，或在服务器上配置 history 路径回退到应用入口。

### `site-config` 无法解析

HTML attribute 必须是合法 JSON，不能包含函数、注释或未转义的引号。复杂配置推荐通过 JavaScript property 赋值。
