Skip to main content

Admin UI 擴充功能

Admin UI 擴充功能讓您將自訂介面直接嵌入 Shopify 管理後台。無需強迫商家導覽到您的應用程式完整頁面,您可以將功能精確地呈現在商家需要的地方——產品頁面、訂單詳情檢視、顧客個人資料等。這些擴充功能使用沙箱化環境在 Shopify 管理後台中渲染,可存取 Shopify 的 UI 元件。

擴充功能類型概覽

Shopify 提供數種類別的 Admin UI 擴充功能:

擴充功能類型出現位置使用案例
Admin Action資源頁面的操作選單從上下文選單觸發工作流程
Admin Block嵌入在資源詳情頁面上在 Shopify 資料旁顯示應用程式資料
產品設定產品建立/編輯表單為產品設定新增自訂欄位
導覽管理後台側邊欄深層連結到應用程式區段
批量操作資源列表頁面同時操作多個項目

Admin Action 擴充功能

Admin actions 出現在資源頁面的「更多操作」下拉選單中。它們開啟一個模態覆蓋層,您的擴充功能在其中渲染。

建立 Admin Action

# 產生 admin action 擴充功能
shopify app generate extension --template admin_action --name ai-description-generator

這會建立擴充功能骨架:

extensions/ai-description-generator/
├── src/
│ └── ActionExtension.tsx
├── locales/
│ └── en.default.json
├── shopify.extension.toml
└── package.json

擴充功能設定

# extensions/ai-description-generator/shopify.extension.toml
api_version = "2025-01"

[[extensions]]
type = "admin_action"
name = "Generate AI Description"
handle = "ai-description-generator"

[[extensions.targeting]]
module = "./src/ActionExtension.tsx"
target = "admin.product-details.action.render"
擴充功能目標

target 欄位決定您的擴充功能出現在哪裡。常見目標包括:

  • admin.product-details.action.render —— 產品詳情頁面操作
  • admin.order-details.action.render —— 訂單詳情頁面操作
  • admin.customer-details.action.render —— 顧客詳情頁面操作
  • admin.product-index.action.render —— 產品列表頁面操作

建構 Action UI

Admin 擴充功能使用 Shopify 的遠端 UI 元件——專為沙箱化擴充功能環境設計的 Polaris 元件子集:

// extensions/ai-description-generator/src/ActionExtension.tsx
import { useEffect, useState } from 'react';
import {
reactExtension,
useApi,
AdminAction,
BlockStack,
Button,
InlineStack,
ProgressIndicator,
Select,
Text,
TextField,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.product-details.action.render';

export default reactExtension(TARGET, () => <GenerateDescriptionAction />);

function GenerateDescriptionAction() {
const { data, close, intents } = useApi(TARGET);
const [tone, setTone] = useState('professional');
const [generatedDescription, setGeneratedDescription] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const productId = data.selected?.[0]?.id;

async function handleGenerate() {
setLoading(true);
setError(null);

try {
const response = await fetch('/api/generate-description', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productId,
tone,
}),
});

if (!response.ok) {
throw new Error('Failed to generate description');
}

const result = await response.json();
setGeneratedDescription(result.description);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
}

async function handleApply() {
try {
const response = await fetch('shopify:admin/api/graphql.json', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
mutation updateProduct($input: ProductInput!) {
productUpdate(input: $input) {
product {
id
descriptionHtml
}
userErrors {
field
message
}
}
}
`,
variables: {
input: {
id: productId,
descriptionHtml: generatedDescription,
},
},
}),
});

close();
} catch (err) {
setError('Failed to update product');
}
}

return (
<AdminAction
title="產生 AI 描述"
primaryAction={
<Button
onPress={handleApply}
disabled={!generatedDescription}
>
套用描述
</Button>
}
secondaryAction={
<Button onPress={() => close()}>取消</Button>
}
>
<BlockStack gap="large">
<Select
label="語調"
value={tone}
onChange={setTone}
options={[
{ label: '專業', value: 'professional' },
{ label: '休閒友善', value: 'casual' },
{ label: '奢華', value: 'luxurious' },
{ label: '技術性', value: 'technical' },
{ label: '活潑', value: 'playful' },
]}
/>

<Button onPress={handleGenerate} disabled={loading}>
{loading ? '產生中...' : '產生描述'}
</Button>

{loading && <ProgressIndicator size="small" />}

{error && (
<Text tone="critical">{error}</Text>
)}

{generatedDescription && (
<BlockStack gap="small">
<Text variant="headingSm">產生的描述</Text>
<TextField
label="描述"
value={generatedDescription}
onChange={setGeneratedDescription}
multiline={6}
autoComplete="off"
/>
</BlockStack>
)}
</BlockStack>
</AdminAction>
);
}
擴充功能沙箱化

Admin 擴充功能在沙箱化環境中執行。您無法存取 windowdocument,或使用任意 npm 套件。只有 Shopify 提供的 UI 元件和 API 可用。如果您需要呼叫後端,使用 fetch() 搭配相對 URL——Shopify 會將請求代理到您的應用程式伺服器。

Admin Block 擴充功能

Admin blocks 是出現在資源詳情頁面上的持久 UI 面板。與 actions(以模態框開啟)不同,blocks 始終可見且即時更新。

// extensions/loyalty-block/src/BlockExtension.tsx
import { useEffect, useState } from 'react';
import {
reactExtension,
useApi,
AdminBlock,
BlockStack,
InlineStack,
Text,
Badge,
Divider,
ProgressIndicator,
Heading,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.customer-details.block.render';

export default reactExtension(TARGET, () => <LoyaltyBlock />);

function LoyaltyBlock() {
const { data } = useApi(TARGET);
const [loyaltyData, setLoyaltyData] = useState<any>(null);
const [loading, setLoading] = useState(true);

const customerId = data.selected?.[0]?.id;

useEffect(() => {
async function fetchLoyaltyData() {
try {
const response = await fetch(
`/api/loyalty/customer?id=${encodeURIComponent(customerId)}`
);
const result = await response.json();
setLoyaltyData(result);
} catch (err) {
console.error('Failed to fetch loyalty data:', err);
} finally {
setLoading(false);
}
}

if (customerId) {
fetchLoyaltyData();
}
}, [customerId]);

if (loading) {
return (
<AdminBlock title="忠誠度計畫">
<ProgressIndicator size="small" />
</AdminBlock>
);
}

if (!loyaltyData) {
return (
<AdminBlock title="忠誠度計畫">
<Text>此顧客尚未加入忠誠度計畫。</Text>
</AdminBlock>
);
}

return (
<AdminBlock title="忠誠度計畫">
<BlockStack gap="base">
<InlineStack gap="base" blockAlignment="center">
<Text variant="headingSm">等級:</Text>
<Badge tone={loyaltyData.tier === 'Gold' ? 'warning' : 'info'}>
{loyaltyData.tier}
</Badge>
</InlineStack>

<Divider />

<InlineStack gap="large">
<BlockStack gap="tight">
<Text variant="bodySm" tone="subdued">點數餘額</Text>
<Heading>{loyaltyData.pointsBalance.toLocaleString()}</Heading>
</BlockStack>

<BlockStack gap="tight">
<Text variant="bodySm" tone="subdued">終身點數</Text>
<Heading>{loyaltyData.lifetimePoints.toLocaleString()}</Heading>
</BlockStack>

<BlockStack gap="tight">
<Text variant="bodySm" tone="subdued">兌換次數</Text>
<Heading>{loyaltyData.totalRedemptions}</Heading>
</BlockStack>
</InlineStack>

<Divider />

<Text variant="bodySm" tone="subdued">
{new Date(loyaltyData.enrolledAt).toLocaleDateString()} 起成為會員
</Text>
</BlockStack>
</AdminBlock>
);
}

產品設定擴充功能

產品設定擴充功能將自訂欄位新增到產品建立和編輯體驗中。這些非常適合需要額外產品中繼資料的應用程式。

// extensions/product-ai-config/src/ProductConfig.tsx
import { useState } from 'react';
import {
reactExtension,
useApi,
AdminBlock,
BlockStack,
Checkbox,
Select,
Text,
TextField,
} from '@shopify/ui-extensions-react/admin';

const TARGET = 'admin.product-details.configuration.render';

export default reactExtension(TARGET, () => <ProductAIConfig />);

function ProductAIConfig() {
const { data, applyMetafieldsChange } = useApi(TARGET);

const [autoDescription, setAutoDescription] = useState(false);
const [targetAudience, setTargetAudience] = useState('general');
const [keywords, setKeywords] = useState('');

async function handleAutoDescriptionToggle(checked: boolean) {
setAutoDescription(checked);
await applyMetafieldsChange({
type: 'updateMetafield',
namespace: 'ai_config',
key: 'auto_description',
value: String(checked),
valueType: 'boolean',
});
}

async function handleAudienceChange(value: string) {
setTargetAudience(value);
await applyMetafieldsChange({
type: 'updateMetafield',
namespace: 'ai_config',
key: 'target_audience',
value,
valueType: 'single_line_text_field',
});
}

async function handleKeywordsChange(value: string) {
setKeywords(value);
await applyMetafieldsChange({
type: 'updateMetafield',
namespace: 'ai_config',
key: 'seo_keywords',
value,
valueType: 'single_line_text_field',
});
}

return (
<AdminBlock title="AI 設定">
<BlockStack gap="base">
<Text variant="bodySm" tone="subdued">
設定 AI 工具如何與此產品互動。
</Text>

<Checkbox
label="儲存時自動產生描述"
checked={autoDescription}
onChange={handleAutoDescriptionToggle}
/>

<Select
label="目標受眾"
value={targetAudience}
onChange={handleAudienceChange}
options={[
{ label: '一般', value: 'general' },
{ label: '年輕成人(18-25)', value: 'young_adults' },
{ label: '專業人士', value: 'professionals' },
{ label: '家長', value: 'parents' },
{ label: '愛好者', value: 'enthusiasts' },
]}
/>

<TextField
label="SEO 關鍵字"
value={keywords}
onChange={handleKeywordsChange}
helpText="以逗號分隔的關鍵字,用於在 AI 產生的內容中強調"
autoComplete="off"
/>
</BlockStack>
</AdminBlock>
);
}

部署擴充功能

擴充功能使用 Shopify CLI 與您的應用程式一起部署:

# 開發模式——即時重新載入
shopify app dev

# 部署到生產環境
shopify app deploy

# 僅部署擴充功能(跳過應用程式程式碼)
shopify app deploy --reset

擴充功能版本管理

每次部署都會建立擴充功能的新版本。Shopify 管理推出:

# 檢查擴充功能狀態
shopify app versions list

# 發布特定版本
shopify app release --version 1.2.0
開發工作流程

在開發期間,shopify app dev 啟動隧道並為您的擴充功能啟用熱重新載入。擴充功能程式碼的變更會立即反映在 Shopify 管理後台中,無需重新部署。使用開發商店(而非正式商店)進行測試。

部署前檢查清單

在將 Admin UI 擴充功能部署到生產環境前:

  1. 在多種螢幕尺寸上測試——管理後台在桌面、平板和行動裝置上使用
  2. 處理載入狀態——在擷取資料時始終顯示載入指示器或骨架
  3. 處理錯誤狀態——網路失敗會發生;顯示有用的訊息和重試選項
  4. 遵守速率限制——Admin API 有速率限制;盡可能快取回應
  5. 本地化字串——使用 locales/ 目錄處理所有面向使用者的文字
  6. 檢查無障礙功能——確保您的擴充功能可透過鍵盤導覽且對螢幕閱讀器友善
擴充功能大小限制

Admin UI 擴充功能有最大 512 KB 的打包大小(壓縮後)。保持依賴項最少。不要打包大型函式庫——使用 Shopify 提供的 UI 元件,而不是自帶元件庫。

下一步

繼續閱讀 結帳擴充功能 以了解如何使用自訂 UI、付款邏輯和運送規則自訂 Shopify 結帳體驗。