关于 spree-dashboard
spree-dashboard 是面向开发者的技能,用于定制自己的 Spree 6 React 管理仪表板。它介绍如何通过仪表板 API 扩展插件、路由、导航、插槽、表格、表单和权限。
主要功能
- 01注册仪表板插件
- 02定制导航和路由
- 03通过插槽添加组件
- 04扩展表格和表单字段
- 05使用仪表板权限 API
适用场景
- 01为 Spree 管理仪表板添加自定义页面
- 02在内置管理页面中放置组件
- 03扩展管理表格和表单
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ spree/ agent-skills --skill spree-dashboard安装后在 Claude Code 中输入/spree-dashboard确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/public-api.mdrecipes.mdslots.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-started50、home100、orders200、products300、inventory350、customers400、sellers450、loyalty475、promotions500、reports600、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')能够解析:
Abilities 和# server/config/initializers/spree.rb Spree.permissions.register_scope(:reports, group: :analytics, resources: -> { [MyApp::Report] })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_dashboardgem 会在/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/update | nav.add/remove/update、nav.addChild;设置使用 settingsNav |
spree:admin:scaffold + ERB CRUD 视图 | defineTable + <ResourceTable> 页面 + routes 条目(API 来自 spree:api_resource) |
Spree.admin.tables.<t>.add/remove/update | tables.<t>.addColumn/removeColumn/updateColumn |
Spree.admin.partials.<point> << 'partial' | slots: { '<slot>': [...] } |
在 _form.html.erb / product_form partial 中添加字段 | formFields + 使用 useHostForm() 的插槽组件,或自定义字段定义 |
Admin 控制器装饰器 / permitted_attributes | Admin API:Model.additional_permitted_attributes +=,序列化器通过 Spree.api.admin_*_serializer 配置 |
在视图中使用 CanCanCan 的 can? | usePermissions() / <Can> / subject(仅用于 UX)+ 后端权限键 |
| Stimulus 控制器、importmap | React 组件 + 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/react1.8.0、recharts3.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。