mcpField
S
spree-dashboard

spree-dashboard

github.com/spree开发10104 次安装4 周前更新
代码仓库

关于 spree-dashboard

spree-dashboard 是面向开发者的技能,用于定制自己的 Spree 6 React 管理仪表板。它介绍如何通过仪表板 API 扩展插件、路由、导航、插槽、表格、表单和权限。

主要功能

  1. 01注册仪表板插件
  2. 02定制导航和路由
  3. 03通过插槽添加组件
  4. 04扩展表格和表单字段
  5. 05使用仪表板权限 API

适用场景

  1. 01为 Spree 管理仪表板添加自定义页面
  2. 02在内置管理页面中放置组件
  3. 03扩展管理表格和表单

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/spree/agent-skills --skill spree-dashboard

安装后在 Claude Code 中输入/spree-dashboard确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
public-api.md
recipes.md
slots.md

详细介绍

Spree Dashboard(React 管理后台)——应用内自定义

Spree 6 管理后台是一个仅与 Admin API(/api/v3/admin/*)通信的 React SPA。它目前处于开发者预览版阶段——软件包版本为 1.0.0-beta.x,API 仍可能变动;如果某个名称无法解析,请检查已安装的 barrel 文件(node_modules/@spree/dashboard-core/src/index.ts)。Rails spree_admin gem 已不复存在;没有 ERB 视图、局部模板、Stimulus 或 Turbo 可以覆盖。

本指南适用于自定义你自己的 Dashboard 应用。如果要将自定义内容打包成 npm 软件包,供其他商店安装,请参阅 spree-dashboard-plugins(使用相同的 API,打包方式不同)。

所在位置

my-store/
├── server/                 # Rails app (Spree gems, Admin API) — `backend/` in older projects
└── apps/
    ├── dashboard/          # the "host app" — you own this
    │   ├── src/main.tsx        # mounts <Dashboard>, imports plugins + virtual:spree-dashboard-plugins
    │   ├── src/plugins.ts      # ← your registrations
    │   ├── src/styles.css      # @import "@spree/dashboard/styles.css" + your token overrides
    │   ├── src/routeTree.gen.ts  # generated on every dev start/build (and by `spree add`) — commit it
    │   └── vite.config.ts      # spreeDashboardPlugin() + /api proxy
    └── seller-dashboard/   # optional marketplace seller panel (same model)
  • 新建项目:运行 npx create-spree-app my-store → 选择 Yes,添加 React Dashboard。现有项目:运行 spree add dashboard(--template <git-url|path>、--no-install)。
  • 开发:spree dev 会运行 API;运行 cd apps/dashboard && pnpm dev → http://localhost:5173。使用管理员邮箱和密码登录——无需 API 密钥(JWT 保存在内存中,刷新令牌使用 httpOnly cookie)。Vite 会将 /api 和 /rails 代理到 VITE_API_PROXY_TARGET(默认为 http://localhost:3000)。
  • 每个页面都限定于某个商店:URL 格式为 /$storeId/...。注册路径时不包含商店前缀。

技术栈:Vite、React 19、TanStack Router(基于文件的路由)+ TanStack Query v5、React Hook Form + Zod、Tailwind v4 + shadcn/ui + Base UI、lucide-react 图标、i18next、Biome、@spree/admin-sdk。

软件包——从 @spree/dashboard 导入
软件包包含内容
@spree/dashboard-ui设计系统:shadcn 基础组件、ResourceLayout、Empty*、RelativeTime、useConfirm、设计令牌。接收 props,不需要 Provider。
@spree/dashboard-core框架:注册表、defineDashboardPlugin、Provider/钩子(useAuth、usePermissions、useStore)、adminClient、ResourceTable、PageHeader、Slot。
@spree/dashboard应用外壳(路由、资源页面、本地化资源)以及重新导出上述两个软件包内容的门面。

在宿主应用中,始终使用 import { … } from '@spree/dashboard'。只有已发布的插件才直接导入 -core/-ui,且只能从各自的软件包入口导入,不能使用深层文件路径。这些软件包发布的是源代码;深层导入会导致 Vite 在已安装的应用中保留未转换的 CommonJS 依赖,使 Dashboard 在渲染前就崩溃(1.0.0-beta.4 的设置界面就遇到了这个问题)。完整列表:references/public-api.md。

代码放置位置(脚手架约定)
路径内容
src/plugins.ts仅存放注册项——记录你添加的内容
src/pages/每个文件对应一个界面
src/hooks/Admin API 封装(use-brands.ts)——组件不会直接调用 adminClient
src/tables/defineTable 调用
src/schemas/Zod 架构 + 表单值类型
src/slots/注入内置页面的小组件
src/locales/你的 admin.* 翻译包

defineDashboardPlugin — 唯一入口

// src/plugins.ts
import { defineDashboardPlugin, i18n } from '@spree/dashboard'
import { BarChartIcon } from 'lucide-react'
import en from './locales/en.json'
import { AnalyticsPage } from './pages/analytics'

i18n.addResourceBundle('en', 'translation', en, true, true) // translations first

defineDashboardPlugin({
  nav: [{ key: 'analytics', labelKey: 'admin.analytics.nav', path: '/analytics', icon: BarChartIcon, position: 650 }],
  routes: [{ key: 'analytics', path: '/analytics', component: AnalyticsPage }],
})
键作用
locales{ en: bundle, de: bundle } — 在调用中最先注册
nav数组(添加项)或 { add, remove, update, addChildren }
settingsNavGroups / settingsNav设置子导航分组,然后是条目(数组或 { add, remove, update })
routes在运行时注册到 /$storeId/… 下的页面
slots{ 'slot.name': [{ id, component, position?, if? }] }
tables{ products: { add, update, remove } } 列变更
formFields{ product: [{ name, from }] } 内置表单上的扩展字段
customFieldComponents{ 'namespace.key': Component } 自定义字段定义的自定义组件

注册表是模块单例。键/ID 必须唯一(包括不能与内置项重复)——重复项会在启动时抛出错误;facade 会收集所有错误,并一次性重新抛出(AggregateError)。每个注册表也有命令式 API(nav.add、registerSlot、tables.products.addColumn 等)——可用于精细控制或强类型插槽。

导航

import { nav, settingsNav } from '@spree/dashboard'

nav.addChild('products', { key: 'products.brands', labelKey: 'admin.brands.nav', path: '/brands', subject: 'Spree::Brand' })
nav.insertAfter('orders', { key: 'returns-hub', label: 'Returns hub', path: '/returns-hub' })
nav.update('orders', { position: 150 })
nav.remove('loyalty')
nav.add({ key: 'help', label: 'Help', path: '/help', section: 'bottom' })       // pinned to the footer
nav.add({ key: 'onboarding', label: 'Onboarding', path: '/onboarding',
  if: ({ store }) => !isConfigured(store), badge: PendingCountBadge })        // badge is a component
  • NavEntry:key、label | labelKey、path(必填,必须以 / 开头)、icon、section('main'|'bottom')、position、subject + action(默认为 'read')、if、badge、children。
  • 内置顶级键/位置:getting-started 50、home 100、orders 200、products 300、inventory 350、customers 400、sellers 450、loyalty 475、promotions 500、reports 600、settings(底部)。追加条目请使用 650 及以上的位置。
  • update(parent, { children }) 会替换子项——要在内置菜单下嵌套,请使用 addChild / addChildren。
  • 优先使用 labelKey,而不是 label: i18n.t(...):字面标签在注册时即固定,不会随语言切换而变化。
  • 设置分组:store、selling、shipping、marketplace、team、developer。设置路径会以 /$storeId/settings 为前缀。新增分组:settingsNavGroups: [{ key: 'integrations', labelKey: '…', position: 700 }],然后添加 settingsNav: [{ key: 'erp', label: 'ERP', path: '/erp', group: 'integrations', subject: 'Spree::Store', action: 'update' }](comingSoon: true 会显示禁用的“即将推出”条目)。

路由(自定义页面)

routes: [
  { key: 'reports', path: '/reports', component: ReportsPage, subject: 'Spree::Order' },
  { key: 'report', path: '/reports/$reportId', component: ReportPage },
]

function ReportPage({ params, storeId, searchParams }: { params: Record<string, string>; storeId: string; searchParams: Record<string, unknown> }) {
  return <ResourceLayout header={<PageHeader title="Report" />} main={<ReportBody id={params.reportId} />} />
}
  • 注册表中的路由会在导航时由全匹配路由(/_authenticated/$storeId/$)匹配,继承身份验证守卫和界面框架,并可延迟注册。直接导航时,subject 会呈现 403 页面——请在导航条目上配对使用相同的 subject。
  • 这些路由不在类型化路由树中,因此链接需要类型转换:<Link to={'/$storeId/reports/$reportId' as string} params={{ storeId, reportId } as never}>。
  • 同一 URL 上的任何已编译文件路由都会静默优先。文件路由(类型化、代码分割、validateSearch)是打包插件的实现方式——参见 spree-dashboard-plugins。
  • 请将页面状态保存在 URL 中(筛选器、排序、页码、打开的工作表 ?edit=prod_…),而不是 React 状态中。

插槽——内置页面中的小组件

defineDashboardPlugin({
  slots: { 'product.form_sidebar': [{ id: 'acme-brand-card', component: BrandCard as never, position: 50 }] },
})
// or fully typed:
registerSlot<{ product: Product }>('product.form_sidebar', { id: 'acme-brand-card', component: ({ product }) => <BrandCard product={product} /> })
  • 组件只会接收插槽上下文({ product }、{ order }、{ resource } 等)。尚未注入环境中的 permissions/store/user——请调用 usePermissions()、useStore()、useAuth()。条目的 if 只能访问插槽上下文。
  • 使用 facade 时,as never 是预期写法(插槽映射的类型已擦除);如需类型安全,请使用 registerSlot<Ctx>。removeSlot(name, id) / updateSlot(name, id, patch) 可更改任何条目,包括其他插件的条目。
  • 主要插槽:page.actions、page.actions_dropdown、page.tabs、product|category|collection|order|customer.form_sidebar、store.form_main、company.form_main|form_sidebar、seller.form_main|form_sidebar,以及各类型的编辑器插槽(payment_method.form.<type>、promotion.rule_form.<type> 等)。包含上下文的完整目录: references/slots.md。

表格

import { tables } from '@spree/dashboard'

tables.products.addColumn({ key: 'brand', label: 'Brand', default: true, render: (p) => p.brand?.name ?? '—' })
tables.products.updateColumn('price', { label: 'Retail price' })
tables.products.removeColumn('tags')
tables.orders.addColumn({ key: 'channel_id', label: 'Channel', displayable: false, filterable: true, filterType: 'enum',
  filterOptions: [{ value: 'ch_web', label: 'Web' }] })            // filter-only entry
  • ColumnDef:key、label、render、className、default(默认可见)、sortable、filterable、filterType(默认值为 string | boolean | number | currency | date | enum + filterOptions | resource + filterResource { queryKey, search, hydrate, getOptionLabel } | tags + taggableType)、ransackAttribute、displayable、expand、quickFilter。
  • 排序和筛选通过 Ransack 在服务端运行——该属性必须位于模型的 whitelisted_ransackable_attributes 中,否则请求会失败。
  • 对尚未注册的表进行的变更(内置表会在其页面首次加载时注册)会排队并重放——即使是可选功能也安全。内置表键包括 products、orders、customers、promotions、collections、stock-levels、gift-cards、store-credits、sellers、companies、returns、price-lists,……
  • 自定义列表页面:defineTable<T>('brands', { columns, searchParam, defaultSort, emptyMessage }) + <ResourceTable tableKey queryKey queryFn searchParams />(分页、筛选面板、列选择器、URL 双向同步)。bulkActions / rowActions / reorder 是自定义 <ResourceTable> 的属性——你无法将它们注入内置表格。

表单字段和自定义字段组件

  • 自定义字段定义(无需编写代码)已会在 Custom Fields 卡片中渲染,并随表单一起保存。替换某个定义的组件:customFieldComponents: { 'specs.color': ColorInput }(属性:id、ariaLabel、value、onChange、definition)。
  • 内置表单上的真实列:formFields: { product: [{ name: 'lead_time_days', from: (p) => p?.lead_time_days ?? null }] } + 通过 useHostForm() 绑定输入组件的插槽组件(form.register(...) 或 <Controller control={form.control}>)。它会填充数据、启用保存、随页面的 PATCH 请求发送,并在行内显示服务器返回的 422 错误。宿主表单:product、category、collection、store。切勿渲染嵌套的 <form>;在订单和客户页面上使用 useOptionalHostForm() 并自行保存。

两种方式的完整代码示例,以及页面操作、侧边栏组件和自定义列表页面:references/recipes.md。

权限——UI 限制不等于授权

import { Can, usePermissions } from '@spree/dashboard'

const { permissions, permissionKeys, rules, isLoading, refresh } = usePermissions()
permissions.can('update', 'Spree::Order')            // class-level, mirrors server abilities
permissions.isConditional('update', 'Spree::Order')  // true → record-level rules apply; expect a possible 403
permissionKeys.includes('write_orders')              // flat catalog keys the role holds on this store

<Can I="destroy" a="Spree::Product" fallback={null}><DeleteButton /></Can>
  • 在导航项、设置项和路由上设置 subject(+ action);在导航项上设置 if({ permissions, store, user });在组件内部限制插槽组件的显示。
  • 隐藏按钮只是 UX。Admin API 会对每个请求强制执行各控制器的 read_*/write_* 权限——你的自定义端点也必须如此(参见 spree-auth-permissions)。
  • 对于新资源,在后端注册目录作用域,以便为角色授予该资源,并使 permissions.can('read', 'MyApp::Report') 能够解析:
    # server/config/initializers/spree.rb
    Spree.permissions.register_scope(:reports, group: :analytics, resources: -> { [MyApp::Report] })
    
    Abilities 和 permission_keys 随 GET /api/v3/admin/me 一起提供;切换商店时会重新加载权限。

与后端通信

// src/hooks/use-brands.ts
import { adminClient, useResourceKey, useResourceMutation } from '@spree/dashboard'
import type { PaginatedResponse } from '@spree/admin-sdk'
import { useQuery } from '@tanstack/react-query'

export function useBrands(params?: Record<string, unknown>) {
  return useQuery({
    queryKey: useResourceKey('brands', params),   // → ['brands', storeId, params]
    queryFn: () => adminClient.request<PaginatedResponse<Brand>>('GET', '/brands', { params }),
  })
}

export function useUpdateBrand(id: string) {
  return useResourceMutation({
    mutationFn: (body: Partial<Brand>) => adminClient.request<Brand>('PATCH', `/brands/${id}`, { body }),
    invalidate: [['brands'], ['brands', id]],      // store id injected for you
    successMessage: 'Saved',
  })
}
  • adminClient 是已配置的 @spree/admin-sdk 单例:提供类型化资源(adminClient.products.list/get/update…)和 request<T>(method, path, { params, body }),其中路径相对于 /api/v3/admin。切勿创建第二个客户端或使用原始 fetch。
  • 每个查询键都必须限定到店铺:在钩子中使用 useResourceKey,ID 后续才会传入时使用 useResourceKeyBuilder(),在 React 之外使用 withStoreScope(key, storeId)。裸键 ['brands'] 会在切换店铺后将某个店铺的行数据提供给另一个店铺。
  • 错误类型为 SpreeError(status、code、details)。useResourceMutation 会为非 422 错误显示 toast,并对 422 保持静默,以便表单显示错误:catch (e) { if (!mapSpreeErrorsToForm(e, form.setError)) throw e }(:base → errors.root)。对于没有表单的页面(例如标题栏操作、没有对话框的按钮),请传入 showValidationErrors: true,否则服务器拒绝请求(返回含原因的 422)时,商家不会看到任何提示。
  • 跨资源筛选、排序或聚合应放在后端端点中,而不是发起 N 次客户端请求。
  • 所有 ID 都是带前缀的字符串(prod_…、or_…、cust_…)——切勿解析或去掉前缀。

端到端:新增产品属性(表单 + 列 + 筛选 + 排序)

除非需要 Ransack 筛选/排序或数据库保证,否则优先使用自定义字段定义。若需要真实列:

# 1. server/db/migrate/…_add_lead_time_days_to_spree_products.rb
class AddLeadTimeDaysToSpreeProducts < ActiveRecord::Migration[8.1]
  def change
    add_column :spree_products, :lead_time_days, :integer
    add_index :spree_products, :lead_time_days
  end
end

# 2. server/config/initializers/spree.rb — writable, filterable/sortable
Spree::Product.additional_permitted_attributes += [:lead_time_days]   # += (never << — it's frozen; never = — drops other extensions')
Spree::Product.whitelisted_ransackable_attributes |= %w[lead_time_days]
Spree.api.admin_product_serializer = 'MyApp::AdminProductSerializer'

# 3. server/app/serializers/my_app/admin_product_serializer.rb — readable
module MyApp
  class AdminProductSerializer < Spree::Api::V3::Admin::ProductSerializer
    typelize lead_time_days: [:number, nullable: true]
    attributes :lead_time_days
  end
end

# + a model validation — the dashboard renders its 422 message inline
// 4. apps/dashboard/src/plugins.ts
defineDashboardPlugin({
  formFields: { product: [{ name: 'lead_time_days', from: (p) => p?.lead_time_days ?? null }] },
  slots: { 'product.form_sidebar': [{ id: 'lead-time', component: LeadTimeCard as never, position: 60 }] },
  tables: { products: { add: [{
    key: 'lead_time_days', label: i18n.t('admin.fields.product.lead_time_days.label'),
    sortable: true, filterable: true, filterType: 'number', default: true,
    render: (p) => (p.lead_time_days != null ? `${p.lead_time_days} d` : '—'),
  }] } },
})

LeadTimeCard 使用 useHostForm<{ lead_time_days: number | null }>() 和 form.register('lead_time_days', { setValueAs: (v) => (v === '' || v == null ? null : Number(v)) })。表单 name、序列化器属性和允许的属性必须是同一个字符串。先验证后端:spree api patch products/prod_… --data '{"lead_time_days":14}' 和 spree api get "products?q[lead_time_days_gt]=7&sort=-lead_time_days"。自定义 Admin 控制器应改用 resource_permitted_attributes(参见 spree-resource、spree-decorators)。

主题

所有内容都使用 CSS 自定义属性。在 src/styles.css 的导入语句之后进行覆盖,并同时设置 :root 和 .dark(主题提供器只会切换 .dark 类):

@import "@spree/dashboard/styles.css";

:root { --primary: oklch(0.55 0.22 264); --radius: 0.25rem; }
.dark { --primary: oklch(0.72 0.19 264); }

@theme inline { --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; --text-base: 0.9375rem; }

令牌:--background/--foreground、--card*、--popover*、--muted*、--primary*(默认别名为 --foreground)、--secondary*、--accent*(以及 -hover、-strong)、--link、--ring、--destructive*、--status-{green,amber,red,blue}-{bg,border,fg}、--border*、--input、--radius、--sidebar*、--chart-1..5。在页面中使用 Tailwind 工具类(bg-card text-muted-foreground),而不是原始的 var(--…),并为正文链接设置 className="link"。卖家面板(@spree/seller-dashboard/styles.css)使用相同的令牌。

翻译

使用 i18next,单个 translation 命名空间,所有键都位于 admin. 下。在 plugins.ts 顶部使用 i18n.addResourceBundle(locale, 'translation', { admin: { … } }, true, true) 注册,或使用 defineDashboardPlugin({ locales: { en, de } }) 注册;在组件中使用 useTranslation(),在注册表中使用 labelKey。字段键:admin.fields.<resource>.<attribute>.label|placeholder,回退到 admin.fields.<attribute>.*。内置语言区域:en、de、es、fr、pl、ar、zh-CN。注册促销规则/操作、计算器、价格规则、集合规则、订单路由规则、配送方式规则、佣金规则、卖家要求、集成或权限的 gem,应提供 admin.types.<family>.<code>.{name,description}——否则将显示 API 标签(按店铺语言区域解析,而非 Admin 的语言区域)。服务器返回的 422 消息会原样显示——请使用 Rails i18n 为其本地化。

测试

cd apps/dashboard && pnpm test(Vitest,Node 环境,src/**/*.test.ts),以及 pnpm test:e2e(Playwright 针对 spree dev 和开发服务器运行;使用 E2E_BASE_URL 指定部署目标)。

  • 对 UI 和 API 之间的逻辑进行单元测试——查询键、载荷映射器、权限判断、筛选条件 → Ransack 映射。不要渲染组件来断言标记;不要测试单行的 adminClient.request 包装器。
    import { withStoreScope } from '@spree/dashboard'
    import { QueryClient } from '@tanstack/react-query'
    it('invalidates only the current store', () => {
      const qc = new QueryClient()
      qc.setQueryData(withStoreScope(['brands'], 'store_a'), [])
      qc.setQueryData(withStoreScope(['brands'], 'store_b'), [])
      qc.invalidateQueries({ queryKey: withStoreScope(['brands'], 'store_a') })
      expect(qc.getQueryCache().getAll().filter((q) => q.state.isInvalidated)).toHaveLength(1)
    })
    
  • 端到端测试:通过 UI 操作并断言结果(点击侧边栏链接,而不是 goto('/brands')——实际路径为 /:storeId/…),记录名称添加 Date.now() 后缀(各测试共享数据库;配置只运行一个工作线程)。
  • 推送前检查:pnpm typecheck && pnpm lint && pnpm test && pnpm build。

部署(概述——参见 spree-deployment)

  • 单节点(默认): spree_dashboard gem 会在 /dashboard 提供构建好的仪表板,路径为 Spree::Dashboard.dist_path / SPREE_DASHBOARD_DIST_PATH(卖家面板位于 /sellers)。官方镜像包含标准构建版本;使用 spree build --production [--tag …](或 docker build . -f server/Dockerfile)构建自定义版本——该命令会检测 apps/dashboard/,并使用 VITE_BASE_PATH=/dashboard/ 和相对 API URL 进行构建。
  • 静态托管 / CDN: 运行 VITE_SPREE_API_URL=https://api.example.com pnpm build,部署 dist/ 并设置 /* → /index.html 重写规则,确保两端都使用 HTTPS(刷新 cookie 为 SameSite=None; Secure),并在设置 → 开发者 → 允许的来源下添加来源。每个环境构建一次。
  • VITE_* 值会被编译进构建包——切勿在其中放入机密信息。

从 spree_admin(Spree 5.x)迁移

spree_admin仪表板
Spree.admin.navigation.sidebar.add/remove/updatenav.add/remove/update、nav.addChild;设置使用 settingsNav
spree:admin:scaffold + ERB CRUD 视图defineTable + <ResourceTable> 页面 + routes 条目(API 来自 spree:api_resource)
Spree.admin.tables.<t>.add/remove/updatetables.<t>.addColumn/removeColumn/updateColumn
Spree.admin.partials.<point> << 'partial'slots: { '<slot>': [...] }
在 _form.html.erb / product_form partial 中添加字段formFields + 使用 useHostForm() 的插槽组件,或自定义字段定义
Admin 控制器装饰器 / permitted_attributesAdmin API:Model.additional_permitted_attributes +=,序列化器通过 Spree.api.admin_*_serializer 配置
在视图中使用 CanCanCan 的 can?usePermissions() / <Can> / subject(仅用于 UX)+ 后端权限键
Stimulus 控制器、importmapReact 组件 + TanStack Query hooks
覆盖 admin Tailwind/CSS在 src/styles.css 中覆盖 CSS token
Spree.t 管理端字符串i18next admin.* 资源包

完整的 Spree 5.x → 6 迁移指南,请参见 spree-upgrade-5-to-6。

注意事项

  • 重复的键或 ID 会导致启动失败——命名空间键(acme-reports),并记住内置键(orders、products 等)。
  • 未限定作用域的查询键会导致不同存储之间的数据泄漏。 始终使用 useResourceKey。
  • 插槽小组件必须自行控制是否渲染——page.actions 会在每个页面上渲染;权限不会作为 props 传入。
  • nav.update(parent, { children }) 会丢弃内置子项——请使用 addChild。
  • 可排序或可筛选的列若未添加到 Ransack 允许列表中,会在请求时而非构建时失败。
  • 注册表路由若被相同 URL 下的文件路由遮蔽,就永远不会渲染——也不会报错。
  • 标签不会动态更新: label: i18n.t(...) 不会随语言切换而变化;请使用 labelKey。
  • 在宿主表单插槽内嵌套 <form> 会破坏页面的保存功能。请使用 useHostForm()。
  • 切勿在表单值类型中嵌入 SDK 实体类型——RHF 会遍历嵌套键,导致 TS 编译器崩溃;请映射到扁平的值类型。
  • 不要放宽仪表板的依赖项固定版本。 @spree/dashboard-ui 会编译进你的应用,因此其依赖项会精确固定为 Spree 测试过的版本(@base-ui/react 1.8.0、recharts 3.10.1 等)。较新版本的 Base UI 若被引入(通过覆盖、插入符号范围,或低于 1.0.0-beta.7 的仪表板软件包),会在首次渲染前因 useSyncExternalStore 相关的 SyntaxError 而失败。请一并升级 @spree/dashboard* 软件包,不要自行升级 Base UI。
  • 不要直接访问 Rails。 缺少的端点应首先添加到 Admin API(spree-resource、spree-api-v3)。

延伸阅读

  • 文档(已安装):node_modules/@spree/docs/dist/developer/dashboard/——overview.md、concepts.md、customization/{quickstart,navigation,routes,slots,tables,permissions,backend,theming,translations}.md、recipes/*.md、slots-catalog.md、public-api.md、deployment.md;教程 developer/tutorial/dashboard.md 和 developer/tutorial/testing.md。在线文档:https://spreecommerce.org/docs/developer/dashboard/overview
  • 你的应用中的 apps/dashboard/AGENTS.md(约定)和 README.md。
  • 源码:packages/dashboard-core/src/{plugin.ts,lib/*-registry.ts}、packages/dashboard/src/nav/default.ts(内置导航)、https://github.com/spree/spree 中的 packages/dashboard-plugin-example(完整的 Brands 示例)
  • 相关技能:spree-dashboard-plugins、spree-auth-permissions、spree-api-v3、spree-typescript-sdk、spree-resource、spree-catalog(自定义字段)、spree-deployment。

评论

支持 Markdown