mcpField
B
building-admin-dashboard-customizations

building-admin-dashboard-customizations

github.com/medusajs开发2344.4k 次安装1 周前更新
代码仓库

关于 building-admin-dashboard-customizations

面向 Medusa Admin 仪表板扩展开发者的技能,可用于构建自定义组件和管理页面。重点介绍通过 Medusa SDK 获取数据、React Query 模式,以及按组件类型查阅实现参考。

主要功能

  1. 01构建仪表板组件和页面
  2. 02创建表单与模态框
  3. 03在表格和列表中展示数据
  4. 04Medusa SDK API 调用模式
  5. 05设计系统与组件指南

适用场景

  1. 01为商品或订单页面添加组件
  2. 02创建自定义管理页面
  3. 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.md
display-patterns.md
forms.md
navigation.md
table-selection.md
typography.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-3
  • design-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 - 创建新实体时使用 FocusModal
  • form-drawer-edit - 编辑现有实体时使用 Drawer
  • form-disable-pending - 变更期间始终禁用操作(disabled={mutation.isPending})
  • form-show-loading - 在提交按钮上显示加载状态(isLoading={mutation.isPending})
6. 选择模式(中)
  • select-small-datasets - 对 2-10 个选项(状态、类型等)使用 Select 组件
  • select-large-datasets - 对大型数据集(产品、分类等)使用带有 FocusModal 的 DataTable
  • select-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]

评论

支持 Markdown