# 操作指南

本節提供了實用的逐步指南，用於完成常見任務和使用該函式庫的元件實作特定工作流程。每個指南都設計為一組獨立的說明，旨在幫助您高效地達成特定成果。

<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` render prop 來擴展 `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` prop，直接將自訂按鈕、搜尋欄或其他 React 元件新增到 Header 中。

### 目標

在主應用程式 Header 中新增一個自訂的「聊天」按鈕、一個搜尋輸入框以及額外的操作圖示。

### 先決條件

- 一個已安裝 `@arcblock/ux` 函式庫且功能正常的 React 應用程式。
- 一個現有的 `<Header>` 元件實例。更多資訊，請參閱 [Header 元件文件](./components-layout-header.md)。

### 步驟

`addons` prop 接受一個 render 函式。此函式會接收預設的插件（如 session 管理器）作為其第一個參數，讓您能夠決定將自訂元素放置在相對於預設插件的位置。

1.  **定義 `addons` Render Prop**

    在您的 `Header` 元件中，將一個函式傳遞給 `addons` prop。此函式應返回您想要渲染的 JSX。

    ```jsx
    <Header
      meta={meta}
      addons={(defaultAddons, { navigation }) => {
        // 您的自訂元件將放置於此
        return (
          <>
            {/* 渲染您的自訂元件 */}
            {defaultAddons}
          </>
        );
      }}
    />
    ```

2.  **新增自訂元件**

    在 render 函式內部，您可以新增任何您需要的元件。在此範例中，我們將新增一個標準的 Material-UI `Button`、幾個用於圖示的 `AddonButton` 元件以及一個 `Divider`。

3.  **與預設插件結合**

    標準作法是渲染傳入函式的 `defaultAddons`。這能確保如地區設定切換器和 session 管理器等基本元素仍然顯示。您可以將自訂元件放置在 `defaultAddons` 之前或之後。

### 完整範例

以下是一個完整的範例，示範如何將多個自訂元素新增到 Header。這包括導覽、按鈕和圖示，並與預設的 session 管理器一同渲染。

```javascript 如何為 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',
    },
    // ... 其他 session 屬性
  },
};

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. 渲染預設插件 (包含 session 管理器) */}
            {defaultAddons}
          </>
        )}
      />
    </SessionContext.Provider>
  );
}
```

---

## 如何處理可選元件依賴

許多 blocklets 依賴其他元件來提供特定功能。`ComponentInstaller` 讓您能夠建構依賴於可選元件的功能，如果這些元件未被安裝，它會為管理員提供一個友善的介面來安裝它們。

### 目標

保護一個需要安裝另一個 blocklet 元件才能使用的功能。如果該元件缺失，管理員將會看到一個安裝介面，而其他使用者則不會看到任何內容。

### 先決條件

- 您的應用程式中有一個依賴其他元件的功能（例如，一個需要「Media Kit」blocklet 的「媒體管理器」）。
- 依賴元件的 DID。
- 熟悉使用者角色（`owner`、`admin`）。

### 步驟

1.  **匯入 `ComponentInstaller`**

    首先，從函式庫中匯入該元件。

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

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

    使用 `ComponentInstaller` 包裹具有依賴項的元件或功能。

3.  **提供元件 DID**

    將所需元件的 DID 傳遞給 `did` prop。如果有多個依賴項，這可以是一個字串或一個字串陣列。

4.  **為非管理員配置行為**

    使用 `noPermissionMute` prop。當設定為 `true` 時，沒有權限安裝元件的使用者（即非管理員）將不會看到安裝提示。您可以提供一個 `fallback` 元件來替代顯示。

### 完整範例

在此情境中，我們有一個 `MyFeatureButton`，只有在 DID 為 `z8ia...` 的元件被安裝後才應渲染。

```javascript 如何處理可選元件依賴 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` 元件是一個現成的解決方案，用於在 Header 中顯示未讀通知計數。

### 目標

在應用程式 Header 中新增一個通知鈴鐺圖示，該圖示會顯示帶有未讀通知數量的徽章，並即時更新。

### 先決條件

- 您的 blocklet 必須在支援通知服務的 Blocklet Server 版本（`1.16.42` 或更高）上執行。
- 一個用於放置通知圖示的 `<Header>` 元件。
- 一個提供使用者資訊的 session context。

### 關鍵概念

- **WebSocket 事件**：當通知被建立或讀取時，Blocklet Server 會廣播事件。
- **`useListenWsClient`**：一個用於獲取特定頻道（例如 'user'）的 WebSocket 客戶端實例的 hook。
- **事件命名**：事件的作用域限定於使用者和 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` render prop 中。

3.  **傳遞 Session 物件**

    `NotificationAddon` 元件需要 `session` 物件來識別目前使用者並管理未讀計數狀態。

### 完整範例

此範例展示如何將 `NotificationAddon` 整合到 `Header` 中。它將自動連接到 WebSocket、監聽事件並更新徽章計數。

```javascript 如何實作即時使用者通知 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: () => {}, // 狀態設定函式
    // ... 其他 session 屬性
  },
};

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` props 控制其可見性。

3.  **配置必要 Props**

    - `componentDid`：提供發布服務的工作室 blocklet 的 DID。
    - `title`, `description`：正在發布的項目的元資料。
    - `resourcesParams`：作為查詢參數傳遞給您的資源獲取 API 的物件。
    - `components`：一個要預選或必需的元件陣列。
    - `resources`：一個指定要預選的資源的物件。

4.  **處理生命週期事件**

    使用 `onOpened`、`onUploaded` 和 `onReleased` 回呼來回應發布生命週期中的事件，例如隱藏載入指示器或顯示成功訊息。

### 完整範例

此範例顯示一個按鈕，點擊後會打開 `BlockletStudio` 對話框。它還示範了如何管理載入狀態以提供更好的使用者體驗。

```javascript 如何使用 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);
        }}
      />
    </>
  );
}
```