关于 vendure-admin-ui-writing
为 Vendure Admin UI 扩展开发提供 React 模式和官方约定指引,适合实现管理界面组件的开发者。内容涵盖路由、导航、GraphQL 数据操作、状态处理及常见问题。
主要功能
- 01遵循 Vendure Admin UI 的 React 模式
- 02使用 GraphQL hooks 获取数据
- 03注册路由与导航菜单
- 04处理加载状态和错误反馈
- 05使用生成的 GraphQL TypeScript 类型
适用场景
- 01创建 Vendure 管理界面扩展入口
- 02为扩展添加导航和页面路由
- 03构建列表、详情和表单组件
- 04排查路由、查询或通知问题
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ meriley/ claude-code-skills --skill vendure-admin-ui-writing安装后在 Claude Code 中输入/vendure-admin-ui-writing确认已加载。
附带文件references/EXAMPLES.mdreferences/PATTERNS.md
详细介绍
Vendure 管理界面编写
目的
指导按照 React 模式和官方约定创建 Vendure 管理界面扩展。
不适用场景
- 仅涉及插件结构(使用 vendure-plugin-writing)
- 仅涉及 GraphQL 架构(使用 vendure-graphql-writing)
- 审查 UI 代码(使用 vendure-admin-ui-reviewing)
禁止的模式
- 使用 Angular 模式(Vendure v2+ 使用 React)
- 直接调用 fetch(使用 Vendure GraphQL 钩子)
- 未使用 useInjector 注入服务
- 定义硬编码路由但未使用 registerReactRouteComponent
- 缺少页面元数据(标题、面包屑)
- 使用内联样式但未使用 CSS 变量
- 缺少加载和错误状态
- 直接操作 DOM
必须遵循的模式
- 使用
@vendure/admin-ui/react钩子获取数据 - 使用
useInjector(NotificationService)发送通知 - 使用
usePageMetadata()设置标题和面包屑 - 使用
registerReactRouteComponent()注册路由 - 使用
addNavMenuSection()添加导航 - 使用从生成的 GraphQL 代码中获取的正确 TypeScript 类型
- 为异步操作添加加载状态
- 处理错误并向用户提供反馈
工作流
步骤 1:创建 UI 扩展入口点
// ui/index.ts
import path from "path";
import { AdminUiExtension } from "@vendure/ui-devkit/compiler";
export const myPluginUiExtension: AdminUiExtension = {
id: "my-plugin-ui",
extensionPath: path.join(__dirname),
routes: [
{
route: "my-plugin",
filePath: "routes.ts",
},
],
providers: ["providers.ts"],
translations: {
en: path.join(__dirname, "translations/en.json"),
},
};
步骤 2:注册导航菜单
// ui/providers.ts
import { addNavMenuSection } from "@vendure/admin-ui/core";
export default [
addNavMenuSection(
{
id: "my-plugin",
label: "My Plugin",
items: [
{
id: "my-items",
label: "Items",
routerLink: ["/extensions/my-plugin/items"],
icon: "list",
},
{
id: "my-settings",
label: "Settings",
routerLink: ["/extensions/my-plugin/settings"],
icon: "cog",
},
],
},
"settings",
), // Position after 'settings' section
];
步骤 3:注册路由
// ui/routes.ts
import { registerReactRouteComponent } from "@vendure/admin-ui/react";
import { ItemList } from "./components/ItemList";
import { ItemDetail } from "./components/ItemDetail";
import { ItemForm } from "./components/ItemForm";
export default [
registerReactRouteComponent({
component: ItemList,
path: "items",
title: "Items",
breadcrumb: "Items",
}),
registerReactRouteComponent({
component: ItemDetail,
path: "items/:id",
title: "Item Details",
breadcrumb: "Details",
}),
registerReactRouteComponent({
component: ItemForm,
path: "items/new",
title: "New Item",
breadcrumb: "New",
}),
];
步骤 4:创建列表组件
// ui/components/ItemList.tsx
import { NotificationService } from '@vendure/admin-ui/core';
import {
Card,
ActionBar,
Link,
usePageMetadata,
PageBlock,
useQuery,
useMutation,
useInjector
} from '@vendure/admin-ui/react';
import * as React from 'react';
import { GET_ITEMS, DELETE_ITEM } from '../graphql';
import { GetItemsQuery, DeleteItemMutation } from '../gql/graphql';
export function ItemList() {
const { setTitle, setBreadcrumb } = usePageMetadata();
const notificationService = useInjector(NotificationService);
// Pagination state
const [currentPage, setCurrentPage] = React.useState(1);
const [itemsPerPage] = React.useState(25);
// Set page metadata
React.useEffect(() => {
setTitle('Items');
setBreadcrumb([
{ label: 'My Plugin', link: ['/extensions/my-plugin'] },
{ label: 'Items', link: ['/extensions/my-plugin/items'] }
]);
}, [setTitle, setBreadcrumb]);
// Query
const { data, loading, error, refetch } = useQuery<GetItemsQuery>(GET_ITEMS, {
variables: {
options: {
skip: (currentPage - 1) * itemsPerPage,
take: itemsPerPage,
}
}
});
// Delete mutation
const [deleteItem] = useMutation<DeleteItemMutation>(DELETE_ITEM);
const handleDelete = React.useCallback(async (id: string) => {
try {
await deleteItem({ input: { id } });
notificationService.success('Item deleted');
await refetch();
} catch {
notificationService.error('Failed to delete item');
}
}, [deleteItem, notificationService, refetch]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<PageBlock>
<ActionBar>
<div className="action-bar-start">
<h1>Items</h1>
</div>
<div className="action-bar-end">
<Link href="/extensions/my-plugin/items/new">
<button className="button primary">Add Item</button>
</Link>
</div>
</ActionBar>
<Card>
<table className="data-table">
<thead>
<tr>
<th>Name</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{data?.items.items.map(item => (
<tr key={item.id}>
<td>{item.name}</td>
<td>{new Date(item.createdAt).toLocaleDateString()}</td>
<td>
<Link href={`/extensions/my-plugin/items/${item.id}`}>
Edit
</Link>
<button onClick={() => void handleDelete(item.id)}>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</Card>
</PageBlock>
);
}
步骤 5:创建详情/表单组件
// ui/components/ItemDetail.tsx
import { NotificationService } from '@vendure/admin-ui/core';
import {
usePageMetadata,
PageBlock,
useQuery,
useMutation,
useInjector,
useRouteParams
} from '@vendure/admin-ui/react';
import * as React from 'react';
import { GET_ITEM, UPDATE_ITEM } from '../graphql';
export function ItemDetail() {
const { setTitle, setBreadcrumb } = usePageMetadata();
const notificationService = useInjector(NotificationService);
const { id } = useRouteParams();
const [name, setName] = React.useState('');
// Query
const { data, loading, error } = useQuery(GET_ITEM, {
variables: { id }
});
// Mutation
const [updateItem, { loading: saving }] = useMutation(UPDATE_ITEM);
// Populate form when data loads
React.useEffect(() => {
if (data?.item) {
setName(data.item.name);
setTitle(data.item.name);
setBreadcrumb([
{ label: 'Items', link: ['/extensions/my-plugin/items'] },
{ label: data.item.name, link: [] }
]);
}
}, [data, setTitle, setBreadcrumb]);
const handleSubmit = React.useCallback(async (e: React.FormEvent) => {
e.preventDefault();
try {
await updateItem({ input: { id, name } });
notificationService.success('Item updated');
} catch {
notificationService.error('Failed to update item');
}
}, [id, name, updateItem, notificationService]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<PageBlock>
<form onSubmit={(e) => void handleSubmit(e)}>
<div className="form-group">
<label htmlFor="name">Name</label>
<input
id="name"
type="text"
value={name}
onChange={e => setName(e.target.value)}
required
/>
</div>
<button type="submit" disabled={saving}>
{saving ? 'Saving...' : 'Save'}
</button>
</form>
</PageBlock>
);
}
步骤 6:设置 GraphQL 查询
// ui/graphql/queries.ts
import { gql } from "graphql-tag";
export const GET_ITEMS = gql`
query GetItems($options: ItemListOptions) {
items(options: $options) {
items {
id
name
createdAt
updatedAt
}
totalItems
}
}
`;
export const GET_ITEM = gql`
query GetItem($id: ID!) {
item(id: $id) {
id
name
createdAt
updatedAt
}
}
`;
// ui/graphql/mutations.ts
import { gql } from "graphql-tag";
export const CREATE_ITEM = gql`
mutation CreateItem($input: CreateItemInput!) {
createItem(input: $input) {
id
name
}
}
`;
export const UPDATE_ITEM = gql`
mutation UpdateItem($input: UpdateItemInput!) {
updateItem(input: $input) {
id
name
}
}
`;
export const DELETE_ITEM = gql`
mutation DeleteItem($input: DeleteItemInput!) {
deleteItem(input: $input)
}
`;
步骤 7:配置 GraphQL Codegen
# ui/codegen.yml
schema: http://localhost:3000/admin-api
documents: "./ui/graphql/**/*.ts"
generates:
./ui/gql/graphql.ts:
plugins:
- typescript
- typescript-operations
config:
avoidOptionals: true
maybeValue: T | null
常见模式
如需了解带代码示例的详细模式,请参阅 references/PATTERNS.md。
示例
如需查看完整的可运行示例,请参阅 references/EXAMPLES.md。
项目结构
ui/
├── index.ts # AdminUiExtension entry point
├── providers.ts # Navigation registration
├── routes.ts # Route registration
├── codegen.yml # GraphQL codegen config
├── components/
│ ├── ItemList.tsx
│ ├── ItemDetail.tsx
│ ├── ItemForm.tsx
│ └── common/
│ ├── ConfirmDialog.tsx
│ └── LoadingState.tsx
├── graphql/
│ ├── index.ts # Re-exports
│ ├── queries.ts
│ ├── mutations.ts
│ └── fragments.ts
├── gql/
│ └── graphql.ts # Generated types
├── hooks/
│ └── useItems.ts
├── styles/
│ └── common.css
└── translations/
└── en.json
故障排除
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 组件未渲染 | 未注册路由 | 在 routes.ts 中使用 registerReactRouteComponent 添加路由 |
| 导航项缺失 | Provider 未加载 | 将其添加到 providers.ts,并确保将其导出为默认导出 |
| 查询返回 undefined | 缺少生成的类型 | 运行 graphql-codegen |
| 通知未显示 | 缺少 useInjector | 使用 useInjector(NotificationService) |
| 页面标题错误 | 缺少 usePageMetadata | 在 useEffect 中调用 setTitle |
相关技能
- vendure-admin-ui-reviewing - UI 审查
- vendure-plugin-writing - 插件结构
- vendure-graphql-writing - GraphQL 架构
评论
支持 Markdown