# 操作指南

本节提供了使用该库的组件完成常见任务和实现特定工作流的实用分步指南。每个指南都设计为一套独立的操作说明，旨在帮助您高效地实现特定目标。

<x-cards data-columns="2">
  <x-card data-title="如何向 Header 添加自定义元素" data-icon="lucide:layout-template" data-href="#how-to-add-custom-elements-to-the-header">
    学习如何使用 `addons` 渲染属性来扩展 `Header` 组件，添加自定义按钮、导航或其他交互元素。
  </x-card>
  <x-card data-title="如何处理可选组件依赖" data-icon="lucide:puzzle" data-href="#how-to-handle-optional-component-dependencies">
    实现一个工作流，提示管理员按需安装所需组件，确保您的 Blocklet 功能顺利运行。
  </x-card>
  <x-card data-title="如何实现实时用户通知" data-icon="lucide:bell-ring" data-href="#how-to-implement-real-time-user-notifications">
    使用 WebSockets 建立一个实时通知系统，让用户随时了解应用内的重要事件。
  </x-card>
  <x-card data-title="如何使用 BlockletStudio 发布资源" data-icon="lucide:rocket" data-href="#how-to-publish-resources-with-blockletstudio">
    集成 `BlockletStudio` 组件，为发布和管理资源及依赖组件提供用户界面。
  </x-card>
</x-cards>

---

## 如何向 Header 添加自定义元素

`Header` 组件被设计为可扩展的。您可以利用 `addons` 属性，直接在页眉中添加自定义按钮、搜索栏或其他 React 组件。

### 目标

在主应用页眉中添加一个自定义的“聊天”按钮、一个搜索输入框以及其他操作图标。

### 前提条件

- 一个已安装 `@arcblock/ux` 库并能正常运行的 React 应用。
- 一个已有的 `<Header>` 组件实例。更多信息，请参阅 [Header 组件文档](./components-layout-header.md)。

### 步骤

`addons` 属性接受一个渲染函数。该函数接收默认插件（如会话管理器）作为其第一个参数，让您能够决定将自定义元素放置在默认元素的相对位置。

1.  **定义 `addons` 渲染属性**

    在您的 `Header` 组件中，向 `addons` 属性传递一个函数。该函数应返回您想要渲染的 JSX。

    ```jsx
    <Header
      meta={meta}
      addons={(defaultAddons, { navigation }) => {
        // 您的自定义组件将放在这里
        return (
          <>
            {/* 渲染您的自定义组件 */}
            {defaultAddons}
          </>
        );
      }}
    />
    ```

2.  **添加自定义组件**

    在渲染函数内部，您可以添加任何您需要的组件。在此示例中，我们将添加一个标准的 Material-UI `Button`、几个用于图标的 `AddonButton` 组件和一个 `Divider`。

3.  **与默认插件结合**

    通常的做法是渲染传入函数的 `defaultAddons`。这可以确保像区域设置切换器和会话管理器这样的基本元素仍然显示。您可以将自定义组件放置在 `defaultAddons` 之前或之后。

### 完整示例

以下是一个完整的示例，演示了如何向页眉添加多个自定义元素。这包括导航、按钮和图标，它们与默认的会话管理器一起渲染。

```javascript How to Add Custom Elements to the Header icon=logos:react
import { Box, Divider, Button } from '@mui/material';
import { SessionContext } from '@arcblock/did-connect-react/lib/Session';
import { AddonButton } from '@arcblock/ux/lib/Header/addon-button';
import NavMenu from '@arcblock/ux/lib/NavMenu';
import SessionManager from '@arcblock/ux/lib/SessionManager';
import Header from '@arcblock/ux/lib/Header';
import { Icon } from '@iconify/react';

// 用于演示的模拟数据
const mockBlockletMeta = {
  title: 'My App',
  description: 'A great application',
  logoUrl: 'https://www.arcblock.io/image-bin/uploads/eb1cf5d60cd85c42362920c49e3768cb.svg'
};

const mockSessionContextValue = {
  session: {
    user: {
      fullName: 'Demo User',
      did: 'z1ex...',
      role: 'admin',
    },
    // ... 其他会话属性
  },
};

export default function CustomHeaderGuide() {
  const meta = {
    ...mockBlockletMeta,
    enableConnect: true,
    enableLocale: true,
  };

  return (
    <SessionContext.Provider value={mockSessionContextValue}>
      <Header
        meta={meta}
        homeLink="https://www.arcblock.io"
        addons={(defaultAddons, { navigation }) => (
          <>
            {/* 1. 添加自定义导航 */}
            {navigation.navItems?.length > 0 && (
              <NavMenu
                activeId={navigation.activeId}
                items={navigation.navItems}
                className="header-nav"
              />
            )}
            {/* 2. 添加自定义按钮 */}
            <Button variant="contained" color="primary" size="small">
              Button
            </Button>
            {/* 3. 添加自定义图标按钮 */}
            <AddonButton icon={<Icon icon="tabler:message-circle" />}>Chat</AddonButton>
            <AddonButton icon={<Icon icon="tabler:bell" />} />
            <Divider orientation="vertical" flexItem sx={{ height: 12, alignSelf: 'center' }} />
            {/* 4. 渲染默认插件（包括会话管理器） */}
            {defaultAddons}
          </>
        )}
      />
    </SessionContext.Provider>
  );
}
```

---

## 如何处理可选组件依赖

许多 Blocklet 依赖其他组件来提供特定功能。`ComponentInstaller` 允许您构建依赖于可选组件的功能，如果这些组件未安装，它会为管理员提供一个用户友好的安装方式。

### 目标

保护一个需要安装其他 Blocklet 组件才能使用的功能。如果该组件缺失，管理员将看到一个安装界面，而其他用户则什么也看不到。

### 前提条件

- 您的应用中有一个依赖于其他组件的功能（例如，一个需要“媒体套件”Blocklet 的“媒体管理器”）。
- 组件依赖项的 DID。
- 熟悉用户角色（`owner`、`admin`）。

### 步骤

1.  **导入 `ComponentInstaller`**

    首先，从库中导入该组件。

    ```javascript
    import ComponentInstaller from '@arcblock/ux/lib/ComponentInstaller';
    ```

2.  **包裹您的功能**

    用 `ComponentInstaller` 包裹具有依赖项的组件或功能。

3.  **提供组件 DID**

    将所需组件的 DID 传递给 `did` 属性。如果存在多个依赖项，这可以是一个字符串，也可以是一个字符串数组。

4.  **为非管理员配置行为**

    使用 `noPermissionMute` 属性。当设置为 `true` 时，没有权限安装组件的用户（即非管理员）将不会看到安装提示。您可以提供一个 `fallback` 组件来替代显示。

### 完整示例

在此场景中，我们有一个 `MyFeatureButton`，它只应在 DID 为 `z8ia...` 的组件安装后才渲染。

```javascript How to Handle Optional Component Dependencies icon=logos:react
import ComponentInstaller from '@arcblock/ux/lib/ComponentInstaller';
import { Button } from '@mui/material';

const REQUIRED_COMPONENT_DID = 'z8ia1mAXo8ZE7ytGF36L5uBf9kD2kenhqFGp9'; // 示例 DID

// 这是需要依赖项的组件
function MyFeatureButton() {
  return <Button variant="contained">Use Awesome Feature</Button>;
}

export default function OptionalComponentGuide() {
  return (
    <ComponentInstaller
      // 要检查的组件的 DID
      did={REQUIRED_COMPONENT_DID}
      // 允许查看安装程序 UI 的角色列表
      roles={['owner', 'admin']}
      // 如果为 true，非管理员将看不到安装程序 UI
      noPermissionMute
      // 可选：如果组件未安装，向非管理员显示的内容
      fallback={<div>This feature is not available.</div>}
      // 成功安装后触发的回调
      onInstalled={() => console.log('Component was installed successfully!')}
    >
      {/* 只有在组件已安装的情况下才会渲染此子组件 */}
      <MyFeatureButton />
    </ComponentInstaller>
  );
}
```

当管理员访问此页面且组件未安装时，他们会看到一个允许他们安装的弹出窗口。普通用户将看到回退消息。一旦安装，所有用户都将看到 `MyFeatureButton`。

---

## 如何实现实时用户通知

您可以通过监听来自 Blocklet Server 的 WebSocket 事件，为用户提供实时反馈和通知。`NotificationAddon` 组件是一个即用型解决方案，用于在页眉中显示未读通知计数。

### 目标

在应用页眉中添加一个通知铃铛图标，该图标会显示带有未读通知数量的徽章，并实时更新。

### 前提条件

- 您的 Blocklet 必须运行在支持通知服务的 Blocklet Server 版本上（`1.16.42` 或更高）。
- 一个用于放置通知图标的 `<Header>` 组件。
- 一个提供用户信息的会话上下文。

### 关键概念

- **WebSocket 事件**：当通知被创建或读取时，Blocklet Server 会广播事件。
- **`useListenWsClient`**：一个用于获取特定频道（例如 'user'）的 WebSocket 客户端实例的钩子。
- **事件命名**：事件的作用域限定于用户和 Blocklet。新通知的格式为 `${blocklet.did}/${user.did}/notification:blocklet:create`。

### 步骤

`NotificationAddon` 组件封装了监听 WebSocket 事件和显示未读计数所需的所有逻辑。

1.  **导入 `NotificationAddon`**

    ```javascript
    import NotificationAddon from '@arcblock/ux/lib/common/notification-addon';
    ```

2.  **添加到 Header `addons`**

    使用此组件最简单的方法是将其添加到 `Header` 的 `addons` 渲染属性中。

3.  **传递会话对象**

    `NotificationAddon` 组件需要 `session` 对象来识别当前用户并管理未读计数状态。

### 完整示例

此示例展示了如何将 `NotificationAddon` 集成到 `Header` 中。它将自动连接到 WebSocket，监听事件，并更新徽章计数。

```javascript How to Implement Real-time User Notifications icon=logos:react
import { SessionContext } from '@arcblock/did-connect-react/lib/Session';
import Header from '@arcblock/ux/lib/Header';
import NotificationAddon from '@arcblock/ux/lib/common/notification-addon';

// 用于演示的模拟数据
const mockBlockletMeta = {
  title: 'My App',
  description: 'A great application',
  logoUrl: 'https://www.arcblock.io/image-bin/uploads/eb1cf5d60cd85c42362920c49e3768cb.svg'
};

const mockSessionContextValue = {
  session: {
    user: {
      fullName: 'Demo User',
      did: 'z1ex...',
      role: 'admin',
    },
    unReadCount: 3, // 初始未读计数
    setUnReadCount: () => {}, // 状态设置函数
    // ... 其他会话属性
  },
};

export default function NotificationGuide() {
  const meta = { ...mockBlockletMeta, enableConnect: true };

  return (
    <SessionContext.Provider value={mockSessionContextValue}>
      <Header
        meta={meta}
        addons={(defaultAddons) => (
          <>
            {/* NotificationAddon 将处理实时更新 */}
            <NotificationAddon session={mockSessionContextValue.session} />
            {defaultAddons}
          </>
        )}
      />
    </SessionContext.Provider>
  );
}
```

当为已登录用户创建新通知时，铃铛图标上的徽章计数将自动增加。点击该图标将导航用户到他们的通知页面。

---

## 如何使用 BlockletStudio 发布资源

`BlockletStudio` 组件提供了一个完整的、可嵌入的 UI，用于发布资源和组件。它处理用户连接、资源选择和发布流程，将一个复杂的工作流简化为一个单一的组件。

### 目标

添加一个按钮，该按钮可以打开一个对话框，允许用户从 Blocklet 中选择并发布文件和依赖组件。

### 前提条件

- 您的 Blocklet 中有一个 API 端点，用于返回可用资源列表。
- 提供发布 UI 的组件（“工作室”组件）的 DID。

### 步骤

1.  **导入 `BlockletStudio` 并管理状态**

    您需要使用组件状态来管理工作室对话框的可见性。

    ```javascript
    import { useState } from 'react';
    import { BlockletStudio } from '@arcblock/ux/lib/BlockletStudio';
    import { Button, CircularProgress } from '@mui/material';
    ```

2.  **渲染组件**

    将 `<BlockletStudio />` 组件放置在您的应用中，并使用 `open` 和 `setOpen` 属性控制其可见性。

3.  **配置基本属性**

    - `componentDid`：提供发布服务的工作室 Blocklet 的 DID。
    - `title`、`description`：正在发布的项目的元数据。
    - `resourcesParams`：作为查询参数传递给您的资源获取 API 的对象。
    - `components`：要预选或必须包含的组件数组。
    - `resources`：指定要预选的资源的对象。

4.  **处理生命周期事件**

    使用 `onOpened`、`onUploaded` 和 `onReleased` 回调来响应发布生命周期中的事件，例如隐藏加载指示器或显示成功消息。

### 完整示例

此示例展示了一个按钮，点击后会打开 `BlockletStudio` 对话框。它还演示了如何管理加载状态以提供更好的用户体验。

```javascript How to Publish Resources with BlockletStudio icon=logos:react
import { useState } from 'react';
import { Button, CircularProgress } from '@mui/material';
import { BlockletStudio } from '@arcblock/ux/lib/BlockletStudio';

// 提供工作室/发布 UI 的 Blocklet 的 DID
const AI_STUDIO_COMPONENT_DID = 'z8ia1mAXo8ZE7ytGF36L5uBf9kD2kenhqFGp9'; // 示例 DID

export default function PublisherGuide() {
  const [isStudioOpen, setStudioOpen] = useState(false);
  const [isOpening, setOpening] = useState(false);

  const handleShowDialog = () => {
    setOpening(true);
    setStudioOpen(true);
  };

  return (
    <>
      <Button
        variant="contained"
        onClick={handleShowDialog}
        disabled={isOpening}
        startIcon={isOpening ? <CircularProgress size={16} /> : null}>
        Publish to Studio
      </Button>

      <BlockletStudio
        // 控制对话框的可见性
        open={isStudioOpen}
        setOpen={setStudioOpen}
        // 已发布项目的基本信息
        title="My Demo Project"
        description="This is a project published from my blocklet."
        // 工作室服务组件的 DID
        componentDid={AI_STUDIO_COMPONENT_DID}
        // 发送到您的 Blocklet 资源 API 的参数
        resourcesParams={{ projectId: 'test-project-123' }}
        // 预选组件
        components={[
          { did: 'z8ia3xzq2tMq8CRHfaXj1BTYJyYnEcHbqP8cJ', included: true, required: true },
          { did: 'z2qZyjnsRffFtn2PDnDwDHTRbAu53RpKqDtFZ', included: true, required: false },
        ]}
        // 预选资源
        resources={{
          // 键是资源提供者 Blocklet 的 DID
          z8iZpog7mcgcgBZzTiXJCWESvmnRrQmnd3XBB: [
            'template-448698592710885376',
            'example-448698592710885376',
          ],
        }}
        // 事件处理程序
        onOpened={() => setOpening(false)}
        onUploaded={() => alert('Upload complete!')}
        onReleased={() => {
          alert('Successfully released!');
          setStudioOpen(false);
        }}
      />
    </>
  );
}
```