管理后台 UI 扩展
管理后台 UI 扩展允许你将自定义界面直接嵌入到 Shopify 管理后台中。无需强制商家导航到你的应用全页面,你可以在商家需要的地方精确展示功能——在产品页面、订单详情视图、客户资料等处。这些扩展使用沙箱环境在 Shopify 管理后台中渲染,并可以访问 Shopify 的 UI 组件。
扩展类型概览
Shopify 提供了多个类别的管理后台 UI 扩展:
| 扩展类型 | 出现位置 | 使用场景 |
|---|---|---|
| Admin Action | 资源页面的操作菜单 | 从上下文菜单触发工作流 |
| Admin Block | 嵌入在资源详情页 | 在 Shopify 数据旁边显示应用数据 |
| 产品配置 | 产品创建/编辑表单 | 向产品设置添加自定义字段 |
| 导航 | 管理后台侧边栏 | 深度链接到应用部分 |
| 批量操作 | 资源列表页 | 对多个项目进行操作 |
Admin Action 扩展
Admin Action 出现在资源页面的"更多操作"下拉菜单中。它们打开一个模态叠加层,你的扩展在其中渲染。
创建 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
管理后台扩展使用 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="Generate AI Description"
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>
);
}
管理后台扩展在沙箱环境中运行。你无法访问 window、document,也不能使用任意的 npm 包。只有 Shopify 提供的 UI 组件和 API 可用。如果你需要调用后端,请使用相对 URL 的 fetch() —— Shopify 会将请求代理到你的应用服务器。
Admin Block 扩展
Admin Block 是持久的 UI 面板,出现在资源详情页上。与 Action(以模态窗口打开)不同,Block 始终可见并实时更新。
// 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 管理后台中,无需重新部署。使用开发商店(而非正式商店)进行测试。
部署前检查清单
在将管理后台 UI 扩展部署到生产环境之前:
- 在多种屏幕尺寸上测试 —— 管理后台在桌面、平板和移动设备上使用
- 处理加载状态 —— 获取数据时始终显示加载指示器或骨架屏
- 处理错误状态 —— 网络故障会发生;显示有用的消息和重试选项
- 遵守速率限制 —— Admin API 有速率限制;尽可能缓存响应
- 本地化字符串 —— 使用
locales/目录存放所有面向用户的文本 - 检查无障碍性 —— 确保你的扩展支持键盘导航和屏幕阅读器
管理后台 UI 扩展的最大包大小为 512 KB(压缩后)。保持依赖最小化。不要捆绑大型库——使用 Shopify 提供的 UI 组件,而不是自带组件库。
继续学习结账扩展,了解如何使用自定义 UI、支付逻辑和运费规则自定义 Shopify 结账体验。