关于 building-admin-dashboard-customizations
面向 Medusa Admin 仪表板扩展开发者的技能,可用于构建自定义组件和管理页面。重点介绍通过 Medusa SDK 获取数据、React Query 模式,以及按组件类型查阅实现参考。
主要功能
- 01构建仪表板组件和页面
- 02创建表单与模态框
- 03在表格和列表中展示数据
- 04Medusa SDK API 调用模式
- 05设计系统与组件指南
适用场景
- 01为商品或订单页面添加组件
- 02创建自定义管理页面
- 03构建读取并更新后端数据的表单
MCP 相关
该技能将实现指南与 MedusaDocs MCP Server 的用途区分开:技能用于规划和模式指导,MCP 服务用于查询组件属性、组件区域及 SDK 方法详情。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ medusajs/ medusa-agent-skills --skill building-admin-dashboard-customizations安装后在 Claude Code 中输入/building-admin-dashboard-customizations确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/data-loading.mddisplay-patterns.mdforms.mdnavigation.mdtable-selection.mdtypography.md详细介绍
Medusa Admin 仪表板自定义
使用 Admin SDK 和 Medusa UI 组件为 Medusa Admin 仪表板构建自定义 UI 扩展。
注意:“UI Routes”是自定义管理页面,与后端 API 路由不同(后者使用 building-with-medusa 技能)。
适用场景
以下任何管理 UI 开发任务都要加载此技能:
- 为商品/订单/客户页面创建小组件
- 构建自定义管理页面
- 实现表单和模态框
- 使用表格或列表展示数据
- 添加页面间的导航
此外,遇到以下情况时也要加载这些技能:
- **building-with-medusa:**构建管理 UI 会调用的后端 API 路由
- **building-storefronts:**开发店面而非管理仪表板
重要:按需加载参考文件
以下快速参考不足以用于实现。 编写对应组件的代码前,必须加载相关参考文件。
根据实现内容加载这些参考文件:
- 创建小组件? → 必须先加载
references/data-loading.md - 构建表单/模态框? → 必须先加载
references/forms.md - 在表格/列表中展示数据? → 必须先加载
references/display-patterns.md - 从大型数据集中选择? → 必须先加载
references/table-selection.md - 添加导航? → 必须先加载
references/navigation.md - 设置组件样式? → 必须先加载
references/typography.md
**最低要求:**实现前至少加载 1-2 个与具体任务相关的参考文件。
何时使用此技能,何时使用 MedusaDocs MCP 服务
⚠️ 重要:规划和实现前应首先查阅此技能。
以下用途应使用此技能(主要来源):
- 规划——了解如何组织管理 UI 功能
- 组件模式——小组件、页面、表单、表格、模态框
- 设计系统——字体排版、颜色、间距、语义类
- 数据加载——关键的独立查询模式、缓存失效
- 最佳实践——正确与错误的模式(例如,在挂载时执行展示查询)
- 重要规则——哪些做法不可取(常见错误,例如有条件地执行展示查询)
以下用途应使用 MedusaDocs MCP 服务(次要来源):
- 确定要使用的组件后,查询具体的组件属性签名
- 可用的小组件区域列表
- JS SDK 方法详情
- 配置选项参考
技能优先的原因:
- 技能包含独立的展示/模态框查询等关键模式,而 MCP 并未着重说明
- 技能展示正确与错误的模式;MCP 展示可用功能
- 规划需要理解模式,而不仅仅是查阅 API
重要设置规则
SDK 客户端配置
**重要:**始终使用准确的配置——不同的值会导致错误:
// src/admin/lib/client.ts
import Medusa from "@medusajs/js-sdk"
export const sdk = new Medusa({
baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
debug: import.meta.env.DEV,
auth: {
type: "session",
},
})
仅限 pnpm 用户
**重要:**编写任何代码前,先安装对等依赖:
# Find exact version from dashboard
pnpm list @tanstack/react-query --depth=10 | grep @medusajs/dashboard
# Install that exact version
pnpm add @tanstack/react-query@[exact-version]
# If using navigation (Link component)
pnpm list react-router-dom --depth=10 | grep @medusajs/dashboard
pnpm add react-router-dom@[exact-version]
**npm/yarn 用户:**不要安装这些包——它们已可用。
按优先级分类的规则
| 优先级 | 类别 | 影响 | 前缀 |
|---|---|---|---|
| 1 | 数据加载 | 严重 | data- |
| 2 | 设计系统 | 严重 | design- |
| 3 | 数据展示 | 高(包含重要的价格规则) | display- |
| 4 | 字体排版 | 高 | typo- |
| 5 | 表单和模态框 | 中 | form- |
| 6 | 选择模式 | 中 | select- |
快速参考
1. 数据加载(关键)
data-sdk-always- 所有 API 请求都必须始终使用 Medusa JS SDK - 绝不要使用常规的 fetch()(缺少身份验证标头会导致错误)data-sdk-method-choice- 对内置端点使用现有的 SDK 方法(sdk.admin.product.list()),对自定义路由使用sdk.client.fetch()data-display-on-mount- 显示数据的查询必须在挂载时加载(不能根据 UI 状态设置启用条件)data-separate-queries- 将显示数据查询与模态框/表单查询分开data-invalidate-display- 在变更后使显示数据查询失效,而不只是模态框查询data-loading-states- 始终显示加载状态(Spinner),不要显示空状态data-pnpm-install-first- pnpm 用户必须先安装 @tanstack/react-query,再开始编码
2. 设计系统(关键)
design-semantic-colors- 始终使用语义化颜色类(bg-ui-bg-base、text-ui-fg-subtle),绝不硬编码design-spacing- 区块内边距使用 px-6 py-4,列表使用 gap-2,项目使用 gap-3design-button-size- 小组件和表格中的按钮始终使用 size="small"design-medusa-components- 始终使用 Medusa UI 组件(Container、Button、Text),不要使用原始 HTML
3. 数据展示(高)
display-price-format- 关键:Medusa 中的价格按原值存储($49.99 = 49.99,而非以分为单位)。直接显示价格——绝不要除以 100
4. 排版(高)
typo-text-component- 始终使用 @medusajs/ui 中的 Text 组件,绝不要使用普通的 span/p 标签typo-labels- 标签/标题使用<Text size="small" leading="compact" weight="plus">typo-descriptions- 描述使用<Text size="small" leading="compact" className="text-ui-fg-subtle">typo-no-heading-widgets- 小组件中的小型区块绝不要使用 Heading(改用 Text)
5. 表单和模态框(中)
form-focusmodal-create- 创建新实体时使用 FocusModalform-drawer-edit- 编辑现有实体时使用 Drawerform-disable-pending- 变更期间始终禁用操作(disabled={mutation.isPending})form-show-loading- 在提交按钮上显示加载状态(isLoading={mutation.isPending})
6. 选择模式(中)
select-small-datasets- 对 2-10 个选项(状态、类型等)使用 Select 组件select-large-datasets- 对大型数据集(产品、分类等)使用带有 FocusModal 的 DataTableselect-search-config- 必须向 useDataTable 传入搜索配置,以避免出现“search not enabled”错误
关键数据加载模式
始终遵循此模式——绝不要有条件地加载显示数据:
// ✅ CORRECT - Separate queries with proper responsibilities
const RelatedProductsWidget = ({ data: product }) => {
const [modalOpen, setModalOpen] = useState(false)
// Display query - loads on mount
const { data: displayProducts } = useQuery({
queryFn: () => fetchSelectedProducts(selectedIds),
queryKey: ["related-products-display", product.id],
// No 'enabled' condition - loads immediately
})
// Modal query - loads when needed
const { data: modalProducts } = useQuery({
queryFn: () => sdk.admin.product.list({ limit: 10, offset: 0 }),
queryKey: ["products-selection"],
enabled: modalOpen, // OK for modal-only data
})
// Mutation with proper invalidation
const updateProduct = useMutation({
mutationFn: updateFunction,
onSuccess: () => {
// Invalidate display data query to refresh UI
queryClient.invalidateQueries({ queryKey: ["related-products-display", product.id] })
// Also invalidate the entity query
queryClient.invalidateQueries({ queryKey: ["product", product.id] })
// Note: No need to invalidate modal selection query
},
})
return (
<Container>
{/* Display uses displayProducts */}
{displayProducts?.map(p => <div key={p.id}>{p.title}</div>)}
<FocusModal open={modalOpen} onOpenChange={setModalOpen}>
{/* Modal uses modalProducts */}
</FocusModal>
</Container>
)
}
// ❌ WRONG - Single query with conditional loading
const BrokenWidget = ({ data: product }) => {
const [modalOpen, setModalOpen] = useState(false)
const { data } = useQuery({
queryFn: () => sdk.admin.product.list(),
enabled: modalOpen, // ❌ Display breaks on page refresh!
})
// Trying to display from modal query
const displayItems = data?.filter(item => ids.includes(item.id)) // No data until modal opens
return <div>{displayItems?.map(...)}</div> // Empty on mount!
}
这很重要,因为:
- 刷新页面时,模态框处于关闭状态,因此不会运行条件查询
- 用户看到的是空状态,而不是自己的数据
- 显示内容依赖于模态框交互(糟糕的用户体验)
常见错误检查清单
开始实现前,请确认你没有犯以下错误:
数据加载:
- 使用常规 fetch(),而不是 Medusa JS SDK(会导致缺少身份验证标头错误)
- 未使用内置端点的现有 SDK 方法(例如,使用 sdk.client.fetch("/admin/products"),而不是 sdk.admin.product.list())
- 根据模态框/UI 状态有条件地加载显示数据
- 使用同一个查询来处理显示和模态框
- 忘记在变更后使显示查询失效
- 未处理加载状态(显示空内容,而不是加载动画)
- pnpm 用户:编码前未安装 @tanstack/react-query
设计系统:
- 使用硬编码颜色,而不是语义类
- 忘记在组件中为按钮设置 size="small"
- 未使用 px-6 py-4 作为区块内边距
- 使用原生 HTML 元素,而不是 Medusa UI 组件
数据显示:
- 严重:显示价格时除以 100(价格按原值存储:$49.99 = 49.99,不是以美分为单位)
排版:
- 使用普通的 span/p 标签,而不是 Text 组件
- 未为标签设置 weight="plus"
- 未为描述使用 text-ui-fg-subtle
- 在较小的组件区块中使用 Heading
表单:
- 使用 Drawer 进行创建(应使用 FocusModal)
- 使用 FocusModal 进行编辑(应使用 Drawer)
- 变更期间未禁用按钮
- 提交时未显示加载状态
选择:
- 对少于 10 个项目使用 DataTable(大材小用)
- 对多于 10 个项目使用 Select(用户体验较差)
- 未在 useDataTable 中配置搜索(会导致错误)
可用参考文件
加载这些文件以了解详细模式:
references/data-loading.md - useQuery/useMutation patterns, cache invalidation
references/forms.md - FocusModal/Drawer patterns, validation
references/table-selection.md - Complete DataTable selection pattern
references/display-patterns.md - Lists, tables, cards for entities
references/typography.md - Text component patterns
references/navigation.md - Link, useNavigate, useParams patterns
每份参考文件都包含:
- 分步实现指南
- 正确与错误的代码示例
- 常见错误及解决方案
- 完整的可运行示例
与后端集成
⚠️ 严重:所有 API 请求始终使用 Medusa JS SDK——绝不要使用常规 fetch()
管理界面使用 SDK 连接后端 API 路由:
import { sdk } from "[LOCATE SDK INSTANCE IN PROJECT]"
// ✅ CORRECT - Built-in endpoint: Use existing SDK method
const { data: product } = useQuery({
queryKey: ["product", productId],
queryFn: () => sdk.admin.product.retrieve(productId),
})
// ✅ CORRECT - Custom endpoint: Use sdk.client.fetch()
const { data: reviews } = useQuery({
queryKey: ["reviews", product.id],
queryFn: () => sdk.client.fetch(`/admin/products/${product.id}/reviews`),
})
// ❌ WRONG - Using regular fetch
const { data } = useQuery({
queryKey: ["reviews", product.id],
queryFn: () => fetch(`http://localhost:9000/admin/products/${product.id}/reviews`),
// ❌ Error: Missing Authorization header!
})
// Mutation to custom backend route
const createReview = useMutation({
mutationFn: (data) => sdk.client.fetch("/admin/reviews", {
method: "POST",
body: data
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["reviews", product.id] })
toast.success("Review created")
},
})
必须使用 SDK 的原因:
- 管理端路由需要
Authorization和会话 Cookie 标头 - 商店端路由需要
x-publishable-api-key标头 - SDK 会自动处理所有必需的标头
- 不带标头的常规 fetch() → 身份验证/授权错误
- 使用现有 SDK 方法可提供更好的类型安全性
何时使用哪种方式:
- 内置端点:使用现有 SDK 方法(
sdk.admin.product.list()、sdk.store.product.list()) - 自定义端点:使用
sdk.client.fetch()访问自定义 API 路由
实现后端 API 路由时,请加载 building-with-medusa 技能。
Widget 与 UI Route
Widgets 扩展现有管理页面:
// src/admin/widgets/custom-widget.tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { DetailWidgetProps } from "@medusajs/framework/types"
const MyWidget = ({ data }: DetailWidgetProps<HttpTypes.AdminProduct>) => {
return <Container>Widget content</Container>
}
export const config = defineWidgetConfig({
zone: "product.details",
})
export default MyWidget
⚠️ .before / .after 不再控制位置(v2.17.2+):
自 Layout Composer 上线以来,管理后台用户会通过 dashboard 的 Editor 视图排列组件(包括 widgets),并将排列方式保存到数据库中。.before 和 .after 区域后缀已弃用:位于 product.details.before 的 widget 和位于 product.details.after 的 widget 会落在同一个注入区域中,最终顺序由用户配置决定。
- 不要根据后缀向用户承诺特定位置。应说明“该 widget 会显示在商品详情页中,并且可以在 Editor 视图中调整位置”。
.side仍有意义——它适用于双列页面布局的侧栏。- 对于新 widgets,优先使用不带后缀的区域(例如
product.details),除非项目已统一使用某个后缀。
v2.16.0 新增的区域涵盖草稿订单、礼品卡和商店余额账户(draft_order.*、gift_card.*、store_credit_account.*,包含 details/list/side 变体)。请向 MedusaDocs MCP 服务查询权威区域列表,而不要猜测区域名称。
UI Routes 会创建新的管理页面:
// src/admin/routes/custom-page/page.tsx
import { defineRouteConfig } from "@medusajs/admin-sdk"
const CustomPage = () => {
return <div>Page content</div>
}
export const config = defineRouteConfig({
label: "Custom Page",
})
export default CustomPage
**浏览器标签页标题(v2.17.2+):**默认情况下,UI route 的标签页标题为其 label。导出带有 seo 解析器的 handle 即可覆盖标题,也可以根据 route 的 loader 数据动态设置:
// src/admin/routes/brands/[id]/page.tsx
import { UIMatch } from "react-router-dom"
export const handle = {
seo: (match: UIMatch<BrandResponse>) => ({
title: match.loaderData?.brand.name || "Brand",
}),
}
如果 seo 未返回标题,dashboard 会依次回退到侧边栏标签、面包屑,然后是 Medusa。
**自定义注入区域(v2.16.0+):**自定义页面(最常见于插件中)可以使用 @medusajs/dashboard/components 中的 LayoutComposer 来构建页面布局,从而暴露自己的 widget 注入区域:
import { LayoutComposer } from "@medusajs/dashboard/components"
const BrandDetailsPage = () => (
<LayoutComposer
widgetsZonePrefix="brand.details" // exposes "brand.details" and "brand.details.side"
preferredLayoutId="core:two-column"
data={brand} // passed to widgets as their `data` prop
sections={{ main: <GeneralSection brand={brand} />, side: <MediaSection brand={brand} /> }}
/>
)
- 将区域命名为
{resource}.{page-context}(例如brand.list、brand.details),侧边区域则使用.side。绝不要添加.before/.after。 - 在
InjectionZoneRegistry接口中注册这些区域,以便defineWidgetConfig获得类型检查和自动补全;并在src/admin/tsconfig.json的include数组中加入"../../.medusa/types/augmentation-refs.d.ts"。 - 自定义区域出现未知区域的构建警告是预期行为——请确认区域名称拼写正确,而不要盲目忽略警告。
常见问题与解决方案
“Cannot find module”错误(pnpm 用户):
- 编码前安装 peer dependencies
- 使用 dashboard 中的准确版本
“No QueryClient set”错误:
- pnpm:安装 @tanstack/react-query
- npm/yarn:移除错误安装的包
“DataTable.Search not enabled”:
- 必须将搜索配置传递给 useDataTable
Widget 未刷新:
- 使显示查询失效,而不只是模态框查询
- 在查询键中包含所有依赖项
刷新后显示为空:
- 显示查询根据 UI 状态设置了条件
enabled - 移除该条件——显示数据必须在挂载时加载
后续步骤 - 测试你的实现
成功实现某项功能后,务必向用户提供以下后续步骤:
1. 启动开发服务器
如果服务器尚未运行,请启动它:
npm run dev # or pnpm dev / yarn dev
2. 访问管理后台
打开浏览器并前往:
使用你的管理员凭据登录。
3. 导航至自定义 UI
对于组件: 前往显示组件的页面。常见的组件区域:
- 产品组件: 前往 Products → 选择一个产品 → 组件会显示在该页面上
- 订单组件: 前往 Orders → 选择一个订单 → 组件会显示在该页面上
- 客户组件: 前往 Customers → 选择一位客户 → 组件会显示在该页面上
组件在页面中的具体位置由用户在控制面板的 Editor 视图(Layout Composer)中控制,而不是由区域的 .before/.after 后缀控制。告知用户,他们可以将组件拖动到所需位置。
对于 UI 路由(自定义页面):
- 在管理侧边栏/导航中查找自定义页面(取决于你配置的
label) - 或直接导航至:
http://localhost:9000/app/[your-route-path]
4. 测试功能
根据实现的内容,测试以下项目:
- 表单: 尝试创建/编辑实体,并验证校验和错误消息
- 表格: 测试分页、搜索、排序和行选择
- 数据展示: 验证数据能否正确加载,并在变更后刷新
- 模态框: 打开 FocusModal/Drawer,测试表单提交,并验证数据是否更新
- 导航: 点击链接并验证路由是否正常工作
后续步骤的呈现格式
实现后,始终以清晰、可执行的格式呈现后续步骤:
## Implementation Complete
The [feature name] has been successfully implemented. Here's how to see it:
### Start the Development Server
[command based on package manager]
### Access the Admin Dashboard
Open http://localhost:9000/app in your browser and log in.
### View Your Custom UI
**For Widgets:**
1. Navigate to [specific admin page, e.g., "Products"]
2. Select [an entity, e.g., "any product"]
3. Scroll to [zone location, e.g., "the bottom of the page"]
4. You'll see your "[widget name]" widget
**For UI Routes:**
1. Look for "[page label]" in the admin navigation
2. Or navigate directly to http://localhost:9000/app/[route-path]
### What to Test
1. [Specific test case 1]
2. [Specific test case 2]
3. [Specific test case 3]