imean-service-engine-htmx-plugin 1.2.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,2407 +1,272 @@
1
- # HtmxAdminPlugin 完整文档
1
+ # HtmxAdminPlugin v2
2
2
 
3
- 基于 HTMX + Tailwind CSS + Hyperscript + JSX 的管理后台插件,采用后端驱动的统一响应架构,支持快速构建现代化的管理后台系统。
3
+ 基于 PageModel Feature 的简化管理后台插件。
4
4
 
5
- ## 目录
5
+ ## 设计理念
6
6
 
7
- - [概述](#概述)
8
- - [核心特性](#核心特性)
9
- - [架构设计](#架构设计)
10
- - [快速开始](#快速开始)
11
- - [模块类型](#模块类型)
12
- - [使用相同模块名完成 CRUD](#使用相同模块名完成-crud)
13
- - [数据源](#数据源)
14
- - [组件系统](#组件系统)
15
- - [路由系统](#路由系统)
16
- - [响应机制](#响应机制)
17
- - [错误处理](#错误处理)
18
- - [高级功能](#高级功能)
19
- - [API 参考](#api-参考)
20
- - [最佳实践](#最佳实践)
21
- - [示例项目](#示例项目)
7
+ ### 核心原则
22
8
 
23
- ## 概述
24
-
25
- HtmxAdminPlugin 是一个功能完整的管理后台插件,通过简单的配置即可生成包含列表、详情、表单等页面的完整管理系统。插件采用后端驱动的统一响应架构,所有响应都通过 `HX-Retarget` 响应头明确指定目标容器,简化前端代码。
26
-
27
- ### 技术栈
28
-
29
- - **HTMX**: 用于动态内容加载和部分页面更新
30
- - **Tailwind CSS**: 用于样式设计
31
- - **Hyperscript**: 用于客户端交互逻辑
32
- - **Hono/jsx**: 用于服务端 JSX 渲染
33
- - **TypeScript**: 完整的类型支持
34
-
35
- ### 设计理念
36
-
37
- 1. **后端驱动**: 所有响应都通过后端响应头控制,前端无需指定 `hx-target` 或 `hx-swap`
38
- 2. **统一架构**: 使用 OOB (Out-of-Band) 交换机制统一处理页面更新
39
- 3. **类型安全**: 完整的 TypeScript 类型定义
40
- 4. **可扩展**: 通过继承基类和重写方法实现自定义
41
- 5. **零配置**: 默认配置即可使用,支持深度定制
42
-
43
- ## 核心特性
44
-
45
- ### ✨ 主要功能
46
-
47
- - ✅ **自动路由生成**: 根据模块类型自动生成 RESTful 路由
48
- - ✅ **列表页面**: 自动生成列表页,支持分页、筛选、排序
49
- - ✅ **详情页面**: 支持字段分组、自定义渲染、完全自定义
50
- - ✅ **表单页面**: 支持新建/编辑,表单验证,多种字段类型
51
- - ✅ **自定义页面**: 完全自定义的页面渲染
52
- - ✅ **模态对话框**: 支持弹窗模式查看和编辑,不丢失列表状态
53
- - ✅ **错误处理**: 统一的错误处理机制,全局错误通知
54
- - ✅ **URL 状态管理**: 筛选和分页状态记录在 URL 中
55
- - ✅ **响应式设计**: 适配移动端和桌面端
56
- - ✅ **动画效果**: 对话框和错误通知的流畅动画
57
-
58
- ### 🎨 UI 特性
59
-
60
- - 现代化的界面设计
61
- - 流畅的页面切换动画
62
- - 全局加载指示器
63
- - 响应式布局
64
- - 可折叠侧边栏
65
- - 面包屑导航
66
-
67
- ## 架构设计
68
-
69
- ### 统一响应架构
70
-
71
- 插件采用后端驱动的统一响应架构,通过 `RouteHandler` 统一处理所有请求:
72
-
73
- 1. **默认行为**: 所有响应默认交换到 `#main-content`
74
- 2. **Dialog 模式**: 检测到 `dialog=true` 参数时,自动交换到 `#dialog-container`
75
- 3. **错误响应**: 通过 `HtmxAdminContext.sendNotification()` 发送错误通知
76
- 4. **响应头控制**: 所有响应都通过 `HX-Retarget` 响应头明确指定目标容器
77
-
78
- ### 响应头控制
79
-
80
- 所有响应都通过 `HX-Retarget` 响应头明确指定目标容器:
81
-
82
- - `#main-content`: 主内容区域(默认)
83
- - `#dialog-container`: 对话框容器(dialog 模式)
84
- - `#admin-layout`: 片段请求时的布局容器
85
-
86
- ### 通知机制
87
-
88
- 使用 `HtmxAdminContext` 的通知机制实现错误和成功消息:
89
-
90
- - **发送通知**: 通过 `context.sendNotification()` 或便捷方法(`sendError`, `sendSuccess` 等)
91
- - **通知显示**: 通知会在响应时通过 OOB 更新到错误容器
92
- - **多通知支持**: 支持多个通知同时显示
93
-
94
- ### 模块系统
95
-
96
- 插件支持四种模块类型,所有模块类型都继承自 `PageModule` 基类:
97
-
98
- 1. **List**: 列表展示模块(`ListPageModule` 继承自 `PageModule`)
99
- 2. **Detail**: 详情展示模块(`DetailPageModule` 继承自 `PageModule`)
100
- 3. **Form**: 表单模块(`FormPageModule` 继承自 `PageModule`)
101
- 4. **Custom**: 自定义页面模块(直接继承 `PageModule`)
102
-
103
- #### 架构优势
104
-
105
- - **统一基类**: 所有页面模块都继承自 `PageModule`,具有相同的处理过程和结构
106
- - **默认行为**: 每种类型提供默认的渲染逻辑,满足大部分场景
107
- - **灵活扩展**: 可以重写 `render()` 方法完全自定义渲染,即使使用 `list`、`detail`、`form` 类型
108
- - **代码复用**: 通用属性(`basePath`、`__moduleName`、`__moduleMetadata`、`getBreadcrumbs`)在基类中统一管理
9
+ 1. **围绕模型设计**:使用 `PageModel` 作为核心抽象,统一处理有数据模型和无数据模型的场景
10
+ 2. **权限与 Feature 绑定**:每个 Feature 声明自己的权限,支持灵活的权限控制
11
+ 3. **函数式数据操作**:移除数据源机制,直接使用函数处理数据操作
12
+ 4. **简洁统一**:单一类、统一模式,降低学习成本
109
13
 
110
14
  ## 快速开始
111
15
 
112
- ### 1. 安装和导入
16
+ ### 安装
113
17
 
114
- ```typescript
115
- import {
116
- Factory,
117
- HtmxAdminPlugin,
118
- ListPageModule,
119
- DetailPageModule,
120
- FormPageModule,
121
- PageModule,
122
- MemoryListDatasource,
123
- Components // 组件命名空间
124
- } from "imean-service-engine";
18
+ ```bash
19
+ npm install imean-service-engine-htmx-plugin
125
20
  ```
126
21
 
127
- ### 2. 创建插件实例
22
+ ### 基础使用
128
23
 
129
24
  ```typescript
25
+ import { Factory } from "imean-service-engine";
26
+ import { HtmxAdminPlugin, PageModel, CustomFeature } from "imean-service-engine-htmx-plugin";
27
+
28
+ // 创建插件
130
29
  const adminPlugin = new HtmxAdminPlugin({
131
- title: "用户管理系统",
30
+ title: "管理后台",
132
31
  prefix: "/admin",
133
- logo: "/logo.png", // 可选
134
- navigation: [ // 可选,自定义导航结构
135
- {
136
- label: "用户管理",
137
- moduleName: "users",
138
- icon: "👥"
139
- }
140
- ],
141
- authProvider: { // 可选,认证和权限提供者
142
- cookieKey: "auth_token", // Cookie 键名(默认 "auth_token")
143
- async tokenToUser(token: string, ctx: Context) {
144
- // 从 token 获取用户信息
145
- const user = await getUserByToken(token);
146
- return {
147
- name: user.name,
148
- email: user.email,
149
- avatar: user.avatar,
150
- permissions: user.permissions, // 用户权限列表
151
- };
152
- },
153
- // checkPermission 可选,不提供则使用默认实现
154
- async checkPermission(user, operation, context) {
155
- // 自定义权限检查逻辑
156
- return { allowed: true };
157
- },
158
- },
159
- });
160
- ```
161
-
162
- ### 3. 创建引擎
163
-
164
- ```typescript
165
- const { Module, Microservice } = Factory.create(adminPlugin);
166
- ```
167
-
168
- ### 4. 定义数据模型
169
-
170
- ```typescript
171
- interface User {
172
- id: number;
173
- name: string;
174
- email: string;
175
- role: string;
176
- createdAt: Date;
177
- }
178
- ```
179
-
180
- ### 5. 创建数据源
181
-
182
- ```typescript
183
- const userDatasource = new MemoryListDatasource<User>([
184
- { id: 1, name: "张三", email: "zhangsan@example.com", role: "管理员", createdAt: new Date() },
185
- { id: 2, name: "李四", email: "lisi@example.com", role: "用户", createdAt: new Date() },
186
- ]);
187
- ```
188
-
189
- ### 6. 定义模块
190
-
191
- ```typescript
192
- @Module("users", {
193
- type: "list",
194
- })
195
- class UserListModule extends ListPageModule<User> {
196
- constructor() {
197
- super(userDatasource);
198
- }
199
-
200
- // 自定义列渲染
201
- renderColumn(field: string, value: any, item: User): any {
202
- if (field === "role") {
203
- const color = value === "管理员"
204
- ? "bg-blue-100 text-blue-800"
205
- : "bg-gray-100 text-gray-800";
206
- return `<span class="px-2 py-1 rounded text-xs ${color}">${value}</span>`;
207
- }
208
- return value;
209
- }
210
-
211
- // 自定义操作按钮
212
- getActions(item: User) {
213
- return [
214
- this.action.detail("查看"),
215
- this.action.edit("编辑"),
216
- this.action.delete("删除"),
217
- ];
218
- }
219
- }
220
- ```
221
-
222
- ### 7. 启动引擎
223
-
224
- ```typescript
225
- const engine = new Microservice({
226
- name: "admin-service",
227
- version: "1.0.0",
228
32
  });
229
33
 
230
- const port = await engine.start(3000);
231
- console.log(`管理后台已启动: http://localhost:${port}/admin`);
232
- ```
233
-
234
- ## 模块类型
235
-
236
- ### List 模块(列表页)
237
-
238
- 用于列表展示,支持分页、筛选、排序和删除操作。继承自 `PageModule`。
239
-
240
- #### 基本用法
241
-
242
- ```typescript
243
- @Module("products", {
244
- type: "list",
245
- })
246
- class ProductListModule extends ListPageModule<Product> {
247
- constructor() {
248
- super(productDatasource);
249
- }
250
- }
251
- ```
252
-
253
- #### 自定义渲染
254
-
255
- 可以重写 `render()` 方法来自定义列表页的渲染:
256
-
257
- ```typescript
258
- @Module("products", {
259
- type: "list",
260
- })
261
- class ProductListModule extends ListPageModule<Product> {
262
- constructor() {
263
- super(productDatasource);
264
- }
265
-
266
- // 完全自定义渲染
267
- async render() {
268
- const result = await this.getList(parseListParams(this.context.ctx));
269
- return (
270
- <div>
271
- <h1>自定义产品列表</h1>
272
- {/* 自定义布局和样式 */}
273
- </div>
274
- );
275
- }
276
- }
277
- ```
278
-
279
- #### 可选方法
280
-
281
- **`renderColumn(field: string, value: any, item: T): any`**
282
- - 自定义列的渲染方式
283
- - 返回 JSX 元素或 HTML 字符串
284
-
285
- **`getStats(params: ListParams): Promise<Array<StatCardProps>> | Array<StatCardProps>`**
286
- - 返回统计卡片数据
287
- - 用于在列表页顶部显示 KPI 统计
288
-
289
- **`getFilters(params: ListParams): Array<FilterField>`**
290
- - 返回筛选器配置
291
- - 用于在列表页显示筛选器
292
-
293
- **`getTableTitle(): string`**
294
- - 返回表格标题
295
- - 如果不定义,默认使用模块名
296
-
297
- **`getTableActions(params: ListParams, basePath: string): Array<ActionButtonProps>`**
298
- - 返回表格操作按钮配置
299
- - 如导出、清空、刷新等
300
-
301
- **`getDescription(): string`**
302
- - 返回页面描述文本
303
- - 显示在页面标题下方
304
-
305
- **`getActions(item: T): Array<ActionButtonProps>`**
306
- - 返回每行的操作按钮
307
- - 如果不定义,根据模块元数据智能生成
308
-
309
- #### 示例
310
-
311
- ```typescript
312
- @Module("tasks", {
313
- type: "list",
314
- })
315
- class TaskListModule extends ListPageModule<Task> {
316
- constructor() {
317
- super(taskDatasource);
318
- }
319
-
320
- // 自定义列渲染
321
- renderColumn(field: string, value: any, item: Task): any {
322
- if (field === "status") {
323
- const statusMap = {
324
- pending: { label: "待处理", color: "bg-yellow-100 text-yellow-800" },
325
- "in-progress": { label: "进行中", color: "bg-blue-100 text-blue-800" },
326
- completed: { label: "已完成", color: "bg-green-100 text-green-800" },
327
- };
328
- const status = statusMap[value as keyof typeof statusMap];
329
- return (
330
- <span className={`px-2 py-1 rounded text-xs ${status.color}`}>
331
- {status.label}
332
- </span>
333
- );
334
- }
335
- if (field === "priority") {
336
- const priorityMap = {
337
- low: "🟢",
338
- medium: "🟡",
339
- high: "🔴",
340
- };
341
- return `${priorityMap[value as keyof typeof priorityMap]} ${value}`;
342
- }
343
- return value;
344
- }
345
-
346
- // 统计信息
347
- async getStats(params: ListParams) {
348
- const result = await this.getList({ ...params, pageSize: 1000 });
349
- const total = result.total;
350
- const completed = result.items.filter((item) => item.status === "completed").length;
351
- const pending = result.items.filter((item) => item.status === "pending").length;
352
-
353
- return [
354
- {
355
- title: "总任务数",
356
- value: total,
357
- iconColor: "blue" as const,
358
- },
359
- {
360
- title: "已完成",
361
- value: completed,
362
- change: ((completed / total) * 100).toFixed(1),
363
- changeLabel: "%",
364
- iconColor: "green" as const,
365
- },
366
- {
367
- title: "待处理",
368
- value: pending,
369
- iconColor: "yellow" as const,
370
- },
371
- ];
372
- }
373
-
374
- // 筛选器
375
- getFilters(params: ListParams) {
376
- return [
377
- {
378
- name: "status",
379
- label: "状态",
380
- options: [
381
- { value: "pending", label: "待处理" },
382
- { value: "in-progress", label: "进行中" },
383
- { value: "completed", label: "已完成" },
384
- ],
385
- value: params.filters?.status,
386
- },
387
- {
388
- name: "priority",
389
- label: "优先级",
390
- options: [
391
- { value: "low", label: "低" },
392
- { value: "medium", label: "中" },
393
- { value: "high", label: "高" },
394
- ],
395
- value: params.filters?.priority,
396
- },
397
- ];
398
- }
399
- }
400
- ```
401
-
402
- ### Detail 模块(详情页)
403
-
404
- 用于详情展示,支持字段分组、自定义渲染和完全自定义。
405
-
406
- #### 基本用法
407
-
408
- ```typescript
409
- @Module("users", {
410
- type: "detail",
411
- })
412
- class UserDetailModule extends DetailPageModule<User> {
413
- constructor() {
414
- super(userDatasource);
415
- }
416
- }
417
- ```
418
-
419
- #### 可选方法
420
-
421
- **`getFieldLabel(field: string): string`**
422
- - 自定义字段的中文标签
423
- - 如果不定义,默认使用字段名
424
-
425
- **`renderField(field: string, value: any, item: T): any`**
426
- - 自定义字段的渲染方式
427
- - 返回 JSX 元素或 HTML 字符串
428
-
429
- **`getFieldGroups(item: T): Array<{ title: string; fields: string[] }> | null`**
430
- - 定义字段分组
431
- - 返回 `null` 或 `undefined` 表示不分组,平铺显示
432
-
433
- **`getVisibleFields(item: T): string[] | null`**
434
- - 控制字段的显示顺序和可见性
435
- - 返回 `null` 或 `undefined` 表示显示所有字段
436
-
437
- **`getDetailActions(item: T): Array<ActionButtonProps>`**
438
- - 返回详情页的操作按钮
439
- - 如果不定义,根据模块元数据智能生成
440
-
441
- #### 示例
442
-
443
- ```typescript
444
- @Module("users", {
445
- type: "detail",
446
- })
447
- class UserDetailModule extends DetailPageModule<User> {
34
+ // 创建页面模型
35
+ class LoginPageModel extends PageModel {
448
36
  constructor() {
449
- super(userDatasource);
450
- }
451
-
452
- // 字段标签
453
- getFieldLabel(field: string): string {
454
- const labels: Record<string, string> = {
455
- id: "ID",
456
- name: "姓名",
457
- email: "邮箱",
458
- role: "角色",
459
- createdAt: "创建时间",
460
- };
461
- return labels[field] || field;
462
- }
463
-
464
- // 字段渲染
465
- renderField(field: string, value: any, item: User): any {
466
- if (field === "role") {
467
- const color = value === "管理员"
468
- ? "bg-blue-100 text-blue-800"
469
- : "bg-gray-100 text-gray-800";
470
- return (
471
- <span className={`px-2 py-1 rounded text-xs ${color}`}>
472
- {value}
473
- </span>
474
- );
475
- }
476
- if (field === "createdAt") {
477
- return new Date(value).toLocaleString("zh-CN");
478
- }
479
- return value;
480
- }
481
-
482
- // 字段分组
483
- getFieldGroups(item: User) {
484
- return [
485
- {
486
- title: "基本信息",
487
- fields: ["id", "name", "email"],
488
- },
489
- {
490
- title: "权限信息",
491
- fields: ["role"],
492
- },
493
- {
494
- title: "其他信息",
495
- fields: ["createdAt"],
37
+ super("login", undefined, {
38
+ title: "登录",
39
+ useAdminLayout: false,
40
+ });
41
+
42
+ this.features.custom("login", new CustomFeature({
43
+ name: "login",
44
+ permission: null,
45
+ routes: [
46
+ { method: "get", path: "" },
47
+ { method: "post", path: "" },
48
+ ],
49
+ handler: async (context) => {
50
+ // 处理逻辑
496
51
  },
497
- ];
52
+ }));
498
53
  }
499
54
  }
500
- ```
501
-
502
- ### Form 模块(表单页)
503
-
504
- 用于表单(新建/编辑),支持表单字段定义、验证和表单回填。
505
-
506
- #### 基本用法
507
55
 
508
- ```typescript
509
- @Module("articles", {
510
- type: "form",
511
- })
512
- class ArticleFormModule extends FormPageModule<Article> {
513
- constructor() {
514
- super(articleDatasource);
515
- }
56
+ // 注册页面
57
+ adminPlugin.registerPage(new LoginPageModel());
516
58
 
517
- // 必须实现:定义表单字段(或提供 Zod Schema)
518
- getFormFields(item: Article | null): Array<FormFieldType> {
519
- return [
520
- { name: "title", label: "标题", type: "text", required: true },
521
- { name: "content", label: "内容", type: "textarea", required: true },
522
- { name: "status", label: "状态", type: "select", required: true, options: [
523
- { value: "draft", label: "草稿" },
524
- { value: "published", label: "已发布" },
525
- ]},
526
- ];
527
- }
528
- }
59
+ // 创建引擎
60
+ const { Microservice } = Factory.create(adminPlugin);
61
+ const engine = new Microservice({ name: "admin-service" });
62
+ await engine.start(3000);
529
63
  ```
530
64
 
531
- #### 使用 Zod Schema(推荐)
65
+ ## 使用模式
532
66
 
533
- 如果提供了 Zod Schema,表单字段和验证会自动生成:
67
+ ### 有数据模型的页面
534
68
 
535
69
  ```typescript
70
+ import { PageModel, type ModelSchema } from "imean-service-engine-htmx-plugin";
536
71
  import { z } from "zod";
537
72
 
73
+ // 定义 Schema
538
74
  const ArticleSchema = z.object({
539
- title: z.string().min(1, "标题不能为空").describe("标题"),
540
- content: z.string().min(10, "内容至少10个字符").describe("内容"),
541
- status: z.enum(["draft", "published"]).describe("状态"),
75
+ id: z.number().optional(),
76
+ title: z.string().min(1).describe("标题"),
77
+ content: z.string().min(10).describe("内容"),
542
78
  });
543
79
 
544
- @Module("articles", {
545
- type: "form",
546
- })
547
- class ArticleFormModule extends FormPageModule<Article> {
548
- constructor() {
549
- super(articleDatasource, ArticleSchema);
550
- }
80
+ const articleModelSchema: ModelSchema<Article> = {
81
+ schema: ArticleSchema,
82
+ };
551
83
 
552
- // 不需要实现 getFormFields,会自动从 Schema 生成
553
- // 不需要实现 validateFormData,会自动使用 Schema 验证
84
+ // 数据操作函数
85
+ async function getArticleList(params: ListParams): Promise<ListResult<Article>> {
86
+ // 从数据库或其他数据源获取列表
554
87
  }
555
- ```
556
88
 
557
- #### 必需方法
558
-
559
- **`getFormFields(item: T | null): Array<FormFieldType>`**
560
- - 如果提供了 Zod Schema,则不需要实现(会自动生成)
561
- - 否则必须实现
562
- - 定义表单的字段配置
563
- - `item` 为 `null` 表示新建,否则表示编辑
564
-
565
- #### 可选方法
566
-
567
- **`getFieldLabel(field: string): string`**
568
- - 自定义字段的中文标签
569
- - 如果不定义,使用 `getFormFields` 中定义的 `label` 或 Schema 的 `description`
570
-
571
- **`validateFormData(data: Record<string, any>, item: T | null): string | null`**
572
- - 表单验证
573
- - 如果提供了 Zod Schema,会自动使用 Schema 验证
574
- - 返回 `null` 表示验证通过,返回字符串表示错误信息
575
-
576
- #### 表单回填
577
-
578
- 表单验证失败时,用户提交的值会自动回填到表单中,避免用户重新填写:
579
-
580
- - 验证失败时,提交的数据会自动回填
581
- - 编辑模式下,验证失败时会合并原数据和提交数据
582
- - 表单使用 JSON 编码(`hx-encoding="json"`)提交数据
583
-
584
- #### 提交成功后的重定向
585
-
586
- 表单提交成功后的重定向逻辑:
587
-
588
- - **创建操作**:优先跳转到详情页(如果存在),否则跳转到列表页
589
- - **更新操作**:跳转到详情页
590
-
591
- #### 表单字段类型
89
+ async function getArticle(id: string | number): Promise<Article | null> {
90
+ // 获取单条数据
91
+ }
592
92
 
593
- 支持以下字段类型:
93
+ async function createArticle(data: Partial<Article>): Promise<Article> {
94
+ // 创建数据
95
+ }
594
96
 
595
- - `text`: 文本输入框
596
- - `email`: 邮箱输入框
597
- - `number`: 数字输入框
598
- - `textarea`: 文本域
599
- - `select`: 选择框
600
- - `date`: 日期选择器
601
- - `datetime-local`: 日期时间选择器
97
+ async function updateArticle(id: string | number, data: Partial<Article>): Promise<Article | null> {
98
+ // 更新数据
99
+ }
602
100
 
603
- #### 示例
101
+ async function deleteArticle(id: string | number): Promise<boolean> {
102
+ // 删除数据
103
+ }
604
104
 
605
- ```typescript
606
- @Module("articles", {
607
- type: "form",
608
- })
609
- class ArticleFormModule extends FormPageModule<Article> {
105
+ // 定义 PageModel
106
+ class ArticlePageModel extends PageModel<Article> {
610
107
  constructor() {
611
- super(articleDatasource);
612
- }
613
-
614
- getFormFields(item: Article | null): Array<FormFieldType> {
615
- return [
616
- {
617
- name: "title",
618
- label: "标题",
619
- type: "text",
620
- required: true,
621
- placeholder: "请输入文章标题",
108
+ super("articles", articleModelSchema, {
109
+ title: "文章管理",
110
+ description: "管理文章内容",
111
+ });
112
+
113
+ // 注册 CRUD Feature
114
+ this.features.crud({
115
+ permissionPrefix: "articles",
116
+ getList: getArticleList,
117
+ getItem: getArticle,
118
+ createItem: createArticle,
119
+ updateItem: updateArticle,
120
+ deleteItem: deleteArticle,
121
+ // 可选:弹窗大小配置(sm, md, lg, xl, full)
122
+ dialogSizes: {
123
+ detail: "xl",
124
+ edit: "xl",
125
+ create: "xl",
622
126
  },
623
- {
624
- name: "slug",
625
- label: "URL 别名",
626
- type: "text",
627
- required: true,
628
- placeholder: "article-title",
127
+ // 可选:遮罩关闭配置(编辑/创建时建议设为 false,防止误操作)
128
+ closeOnBackdropClick: {
129
+ edit: false,
130
+ create: false,
131
+ detail: true,
629
132
  },
630
- {
631
- name: "content",
632
- label: "内容",
633
- type: "textarea",
634
- required: true,
635
- placeholder: "请输入文章内容",
133
+ // 可选:动态标题和描述配置(使用实际数据的标题作为页面标题)
134
+ getTitles: {
135
+ detail: (item) => item.title, // 详情页使用文章标题
136
+ edit: (item) => `编辑:${item.title}`, // 编辑页使用 "编辑:{文章标题}"
636
137
  },
637
- {
638
- name: "category",
639
- label: "分类",
640
- type: "select",
641
- required: true,
642
- options: [
643
- { value: "tech", label: "技术" },
644
- { value: "life", label: "生活" },
645
- { value: "news", label: "新闻" },
646
- ],
138
+ getDescriptions: {
139
+ detail: (item) => item.excerpt || `作者:${item.author}`, // 详情页使用文章摘要
647
140
  },
648
- {
649
- name: "publishedAt",
650
- label: "发布时间",
651
- type: "datetime-local",
652
- },
653
- ];
654
- }
655
-
656
- // 表单验证(如果提供了 Zod Schema,会自动使用 Schema 验证,此方法可选)
657
- validateFormData(data: Record<string, any>, item: Article | null): string | null {
658
- if (!data.title || data.title.trim().length === 0) {
659
- return "标题不能为空";
660
- }
661
- if (data.slug && !/^[a-z0-9-]+$/.test(data.slug)) {
662
- return "URL 别名只能包含小写字母、数字和连字符";
663
- }
664
- return null;
141
+ });
665
142
  }
666
143
  }
667
144
  ```
668
145
 
669
- #### 使用 Zod Schema 示例(推荐)
670
-
671
- 使用 Zod Schema 可以自动生成表单字段和验证规则:
146
+ ### 无数据模型的页面
672
147
 
673
148
  ```typescript
674
- import { z } from "zod";
675
-
676
- const ArticleSchema = z.object({
677
- title: z.string().min(1, "标题不能为空").describe("标题"),
678
- slug: z.string().regex(/^[a-z0-9-]+$/, "URL 别名只能包含小写字母、数字和连字符").describe("URL 别名"),
679
- content: z.string().min(10, "内容至少10个字符").describe("内容"),
680
- category: z.enum(["tech", "life", "news"]).describe("分类"),
681
- publishedAt: z.string().datetime().optional().describe("发布时间"),
682
- });
683
-
684
- @Module("articles", {
685
- type: "form",
686
- })
687
- class ArticleFormModule extends FormPageModule<Article> {
149
+ class LoginPageModel extends PageModel {
688
150
  constructor() {
689
- // 传入 Schema,自动生成表单字段和验证
690
- super(articleDatasource, ArticleSchema);
691
- }
692
-
693
- // 可选:自定义字段标签(覆盖 Schema 的 description)
694
- getFieldLabels?(): Record<string, string> {
695
- return {
696
- publishedAt: "发布日期",
697
- };
698
- }
699
-
700
- // 可选:自定义字段类型(覆盖自动推断的类型)
701
- getFieldTypes?(): Record<string, "date" | "datetime-local"> {
702
- return {
703
- publishedAt: "datetime-local",
704
- };
705
- }
706
-
707
- // 可选:排除某些字段(如 id、createdAt 等)
708
- getExcludeFields?(): string[] {
709
- return ["id", "createdAt", "updatedAt"];
710
- }
711
-
712
- // 不需要实现 getFormFields 和 validateFormData,会自动从 Schema 生成
713
- }
714
- ```
715
-
716
- ### Custom 模块(自定义页面)
717
-
718
- 用于自定义页面渲染,如 dashboard、说明页等。
719
-
720
- #### 不使用管理后台布局
721
-
722
- 如果页面需要完全自定义布局(不使用侧边栏和头部),可以设置 `useAdminLayout: false`:
723
-
724
- ```typescript
725
- @Module("standalone-page", {
726
- type: "custom",
727
- useAdminLayout: false, // 不使用管理后台布局
728
- })
729
- class StandalonePageModule extends PageModule {
730
- async render() {
731
- return (
732
- <div className="min-h-screen bg-gradient-to-br from-blue-500 to-purple-600">
733
- <div className="container mx-auto px-4 py-16">
734
- <h1 className="text-4xl font-bold text-white mb-8">独立页面</h1>
735
- <p className="text-white text-lg">
736
- 这个页面不使用管理后台的侧边栏和头部布局
737
- </p>
738
- {/* 完全自定义的内容 */}
739
- </div>
740
- </div>
741
- );
742
- }
743
- }
744
- ```
745
-
746
- **注意事项**:
747
- - 当 `useAdminLayout: false` 时,页面只使用 `BaseLayout`(包含 HTML 基础结构和脚本)
748
- - 不会显示侧边栏、头部、面包屑等管理后台元素
749
- - 适合开发登录页、独立功能页面、全屏应用等特殊场景
750
- - 片段请求时,内容会交换到 `#main-content` 容器(而不是 `#admin-layout`)
751
-
752
- ## 使用相同模块名完成 CRUD
753
-
754
- 插件支持使用相同的模块名、不同的页面类型来配合完成完整的 CRUD(创建、读取、更新、删除)操作。这是插件的核心设计理念之一。
755
-
756
- ### 设计原理
757
-
758
- 当多个模块使用相同的模块名时,插件会:
759
-
760
- 1. **自动识别模块关系**:通过模块名将不同类型的模块关联在一起
761
- 2. **共享基础路径**:所有模块共享相同的基础路径(`/{prefix}/{moduleName}`)
762
- 3. **自动生成操作按钮**:根据存在的模块类型自动生成操作按钮(查看、编辑、删除等)
763
- 4. **智能路由跳转**:表单提交后自动跳转到详情页或列表页
764
-
765
- ### 完整示例
766
-
767
- 以下是一个完整的任务管理 CRUD 示例,使用相同的模块名 `"tasks"` 但不同的类型:
768
-
769
- #### 1. 定义数据模型和数据源
770
-
771
- ```typescript
772
- // models.ts
773
- export interface Task {
774
- id: string;
775
- title: string;
776
- description: string;
777
- status: "pending" | "in-progress" | "completed" | "cancelled";
778
- priority: "low" | "medium" | "high" | "urgent";
779
- assignee: string;
780
- dueDate: string;
781
- createdAt: string;
782
- updatedAt: string;
783
- }
784
-
785
- // datasources.ts
786
- import { MemoryListDatasource } from "imean-service-engine";
787
- import type { Task } from "./models";
788
-
789
- const tasks: Task[] = [
790
- {
791
- id: "1",
792
- title: "完成项目文档",
793
- description: "编写项目使用文档",
794
- status: "in-progress",
795
- priority: "high",
796
- assignee: "张三",
797
- dueDate: "2024-01-15",
798
- createdAt: "2024-01-01",
799
- updatedAt: "2024-01-10",
800
- },
801
- // ... 更多数据
802
- ];
803
-
804
- export const taskDatasource = new MemoryListDatasource<Task>(tasks);
805
- ```
806
-
807
- #### 2. 创建列表页模块(List)
808
-
809
- ```typescript
810
- import { ListPageModule, type HtmxAdminModuleOptions } from "imean-service-engine";
811
- import { Module } from "../config";
812
- import { taskDatasource } from "../datasources";
813
- import type { Task } from "../models";
814
-
815
- @Module("tasks", {
816
- type: "list",
817
- title: "任务管理",
818
- description: "管理和跟踪所有任务",
819
- } as HtmxAdminModuleOptions)
820
- export class TaskListModule extends ListPageModule<Task> {
821
- getDatasource() {
822
- return taskDatasource;
823
- }
824
-
825
- // 自定义字段标签
826
- getFieldLabel(field: string): string {
827
- const labels: Record<string, string> = {
828
- id: "任务ID",
829
- title: "标题",
830
- status: "状态",
831
- priority: "优先级",
832
- assignee: "负责人",
833
- dueDate: "截止日期",
834
- };
835
- return labels[field] || field;
836
- }
837
-
838
- // 自定义列渲染
839
- renderColumn(field: string, value: any, item: Task): any {
840
- if (field === "status") {
841
- const statusMap = {
842
- pending: { label: "待处理", color: "bg-yellow-100 text-yellow-800" },
843
- "in-progress": { label: "进行中", color: "bg-blue-100 text-blue-800" },
844
- completed: { label: "已完成", color: "bg-green-100 text-green-800" },
845
- cancelled: { label: "已取消", color: "bg-gray-100 text-gray-800" },
846
- };
847
- const status = statusMap[value as keyof typeof statusMap];
848
- return (
849
- <span className={`px-2 py-1 rounded text-xs ${status.color}`}>
850
- {status.label}
851
- </span>
852
- );
853
- }
854
- return value;
855
- }
856
- }
857
- ```
858
-
859
- #### 3. 创建详情页模块(Detail)
860
-
861
- ```typescript
862
- import { DetailPageModule, type HtmxAdminModuleOptions } from "imean-service-engine";
863
- import { Module } from "../config";
864
- import { taskDatasource } from "../datasources";
865
- import type { Task } from "../models";
866
-
867
- @Module("tasks", {
868
- type: "detail",
869
- title: "任务详情",
870
- description: "查看任务详细信息",
871
- } as HtmxAdminModuleOptions)
872
- export class TaskDetailModule extends DetailPageModule<Task> {
873
- getDatasource() {
874
- return taskDatasource;
875
- }
876
-
877
- // 自定义字段标签
878
- getFieldLabel(field: string): string {
879
- const labels: Record<string, string> = {
880
- id: "任务ID",
881
- title: "标题",
882
- description: "描述",
883
- status: "状态",
884
- priority: "优先级",
885
- assignee: "负责人",
886
- dueDate: "截止日期",
887
- createdAt: "创建时间",
888
- updatedAt: "更新时间",
889
- };
890
- return labels[field] || field;
891
- }
892
-
893
- // 自定义字段渲染
894
- renderField(field: string, value: any, item: Task): any {
895
- if (field === "status") {
896
- const statusMap = {
897
- pending: { label: "待处理", color: "bg-yellow-100 text-yellow-800" },
898
- "in-progress": { label: "进行中", color: "bg-blue-100 text-blue-800" },
899
- completed: { label: "已完成", color: "bg-green-100 text-green-800" },
900
- cancelled: { label: "已取消", color: "bg-gray-100 text-gray-800" },
901
- };
902
- const status = statusMap[value as keyof typeof statusMap];
903
- return (
904
- <span className={`px-3 py-1 rounded text-sm font-medium ${status.color}`}>
905
- {status.label}
906
- </span>
907
- );
908
- }
909
- return value;
151
+ super("login", undefined, {
152
+ title: "登录",
153
+ useAdminLayout: false,
154
+ });
155
+
156
+ this.features.custom("login", new CustomFeature({
157
+ name: "login",
158
+ permission: null,
159
+ routes: [
160
+ { method: "get", path: "" },
161
+ { method: "post", path: "" },
162
+ ],
163
+ handler: async (context) => {
164
+ // 处理逻辑
165
+ },
166
+ }));
910
167
  }
911
168
  }
912
169
  ```
913
170
 
914
- #### 4. 创建表单页模块(Form)
171
+ ### 只读场景
915
172
 
916
173
  ```typescript
917
- import { FormPageModule, type HtmxAdminModuleOptions } from "imean-service-engine";
918
- import { z } from "zod";
919
- import { Module } from "../config";
920
- import { taskDatasource } from "../datasources";
921
- import type { Task } from "../models";
922
-
923
- // 定义 Zod Schema(可选,用于自动生成表单字段和验证)
924
- const TaskSchema = z.object({
925
- title: z.string().min(1, "标题不能为空").describe("任务标题"),
926
- description: z.string().optional().describe("任务描述"),
927
- status: z.enum(["pending", "in-progress", "completed", "cancelled"]).describe("任务状态"),
928
- priority: z.enum(["low", "medium", "high", "urgent"]).describe("优先级"),
929
- assignee: z.string().min(1, "负责人不能为空").describe("负责人"),
930
- dueDate: z.string().describe("截止日期"),
931
- });
932
-
933
- @Module("tasks", {
934
- type: "form",
935
- title: "任务表单",
936
- description: "创建或编辑任务",
937
- } as HtmxAdminModuleOptions)
938
- export class TaskFormModule extends FormPageModule<Task> {
939
- // 在构造函数中传入 schema(可选)
174
+ class ArticleReadonlyPageModel extends PageModel<Article> {
940
175
  constructor() {
941
- super(TaskSchema);
942
- }
943
-
944
- getDatasource() {
945
- return taskDatasource;
946
- }
947
-
948
- // 如果使用 Zod Schema,表单字段会自动生成,无需重写 getFormFields
949
- // 如果需要自定义字段,可以重写 getFormFields 方法
950
- getFormFields(item: Task | null): Array<{
951
- name: string;
952
- label: string;
953
- type?: "text" | "email" | "number" | "textarea" | "select" | "date" | "datetime-local";
954
- required?: boolean;
955
- placeholder?: string;
956
- options?: Array<{ value: string | number; label: string }>;
957
- }> {
958
- // 如果提供了 schema,会自动调用 generateFormFieldsFromSchema
959
- // 这里可以添加自定义逻辑或直接返回父类实现
960
- return super.getFormFields(item);
176
+ super("articles-readonly", articleModelSchema, {
177
+ title: "文章查看(只读)",
178
+ });
179
+
180
+ this.features.crud({
181
+ permissionPrefix: "articles",
182
+ getList: getArticleList,
183
+ getItem: getArticle,
184
+ createItem: createArticle, // 必须提供,但不会注册
185
+ updateItem: updateArticle,
186
+ features: {
187
+ list: true,
188
+ detail: true,
189
+ create: false, // 禁用创建
190
+ edit: false, // 禁用编辑
191
+ delete: false, // 禁用删除
192
+ },
193
+ });
961
194
  }
962
195
  }
963
196
  ```
964
197
 
965
- ### 自动生成的路由
198
+ ## 权限系统
966
199
 
967
- 当使用相同的模块名注册多个类型的模块后,插件会自动生成以下路由:
200
+ ### 权限格式
968
201
 
969
- ```
970
- GET /admin/tasks/list → 列表页(TaskListModule)
971
- GET /admin/tasks/detail/:id → 详情页(TaskDetailModule)
972
- GET /admin/tasks/new → 新建表单(TaskFormModule)
973
- GET /admin/tasks/edit/:id → 编辑表单(TaskFormModule)
974
- POST /admin/tasks → 创建操作(TaskFormModule)
975
- PUT /admin/tasks/:id → 更新操作(TaskFormModule)
976
- DELETE /admin/tasks/:id → 删除操作(TaskFormModule)
977
- ```
202
+ - 默认格式:`{modelName}.{featureName}`
203
+ - 例如:`articles.list`、`articles.create`、`articles.delete`
204
+ - 自定义权限:可以在 Feature 配置中覆盖
978
205
 
979
- ### 自动生成的操作按钮
206
+ ### 权限绑定
980
207
 
981
- 插件会根据存在的模块类型自动生成操作按钮:
208
+ - 每个 Feature 声明自己的 `permission`
209
+ - 插件在路由注册时自动进行权限检查
210
+ - 支持 `null` 表示开放访问
982
211
 
983
- #### 列表页操作按钮
212
+ ## API 参考
984
213
 
985
- 在列表页中,如果存在详情页和表单页,会自动生成"查看"和"编辑"按钮:
214
+ ### PageModel
986
215
 
987
216
  ```typescript
988
- // TaskListModule 中,无需手动定义,插件会自动生成:
989
- getActions(item: Task) {
990
- // 自动生成:
991
- // - 如果存在 detail 模块 → "查看" 按钮
992
- // - 如果存在 form 模块 → "编辑" 按钮
993
- // - 如果数据源支持删除 → "删除" 按钮
217
+ class PageModel<T extends { id: string | number } = any> {
218
+ constructor(
219
+ modelName: string,
220
+ schema?: ModelSchema<T>,
221
+ metadata?: PageMetadata
222
+ );
223
+
224
+ getMetadata(): PageMetadata;
225
+ hasModel(): boolean;
226
+ validate(data: unknown): ValidationResult;
227
+ getFormFields(): FormField[];
228
+ getListFields(): string[];
229
+ getDetailFields(): string[];
230
+ getFieldLabel(fieldName: string): string;
994
231
  }
995
232
  ```
996
233
 
997
- #### 详情页操作按钮
998
-
999
- 在详情页中,如果存在列表页和表单页,会自动生成"返回列表"和"编辑"按钮:
234
+ ### FeatureRegistry
1000
235
 
1001
236
  ```typescript
1002
- // TaskDetailModule 中,无需手动定义,插件会自动生成:
1003
- getDetailActions(item: Task) {
1004
- // 自动生成:
1005
- // - 如果存在 list 模块 → "返回列表" 按钮
1006
- // - 如果存在 form 模块 → "编辑" 按钮
1007
- // - 如果数据源支持删除 → "删除" 按钮
237
+ class FeatureRegistry<T> {
238
+ crud(options: CRUDOptions): void;
239
+ list(feature: Feature<T>): void;
240
+ detail(feature: Feature<T>): void;
241
+ create(feature: Feature<T>): void;
242
+ edit(feature: Feature<T>): void;
243
+ delete(feature: Feature<T>): void;
244
+ custom(name: string, feature: Feature<T>): void;
1008
245
  }
1009
246
  ```
1010
247
 
1011
- ### 智能跳转
1012
-
1013
- #### 表单提交后的跳转
1014
-
1015
- 表单提交成功后,插件会根据存在的模块类型智能跳转。这个逻辑已经在 `FormPageModule` 的默认实现中处理,无需手动编写:
1016
-
1017
- ```typescript
1018
- // FormPageModule 内部已经实现了智能跳转逻辑:
1019
- // - 创建成功后,如果存在详情页,优先跳转到详情页
1020
- // - 否则跳转到列表页(如果存在)
1021
- // - 更新成功后,跳转到详情页
1022
-
1023
- // 如果需要自定义跳转逻辑,可以重写 render 方法或使用 context.redirect()
1024
- // 但通常不需要,因为默认行为已经足够智能
1025
- ```
1026
-
1027
- #### 面包屑自动生成
1028
-
1029
- 面包屑会根据存在的模块类型自动生成:
1030
-
1031
- - **列表页**: `首页 => 任务管理`
1032
- - **详情页**: `首页 => 任务管理 => 任务详情`
1033
- - **表单页(新建)**: `首页 => 任务管理 => 新建任务`
1034
- - **表单页(编辑)**: `首页 => 任务管理 => 编辑任务`
1035
-
1036
- ### 模块元数据
1037
-
1038
- 插件会自动为每个模块设置元数据,记录该模块名下的所有模块类型:
248
+ ### HtmxAdminPlugin
1039
249
 
1040
250
  ```typescript
1041
- // TaskListModule、TaskDetailModule、TaskFormModule 中都可以访问:
1042
- this.context.moduleMetadata = {
1043
- title: "任务管理",
1044
- description: "管理和跟踪所有任务",
1045
- basePath: "/admin/tasks",
1046
- hasList: true, // 因为存在 TaskListModule
1047
- hasDetail: true, // 因为存在 TaskDetailModule
1048
- hasForm: true, // 因为存在 TaskFormModule
1049
- hasCustom: false, // 没有自定义页面
251
+ class HtmxAdminPlugin {
252
+ registerPage<T>(page: PageModel<T>): this;
253
+ registerPages(...pages: PageModel<any>[]): this;
1050
254
  }
1051
255
  ```
1052
256
 
1053
- ### 最佳实践
1054
-
1055
- 1. **使用相同的模块名**:对于同一业务实体的不同页面,使用相同的模块名
1056
- 2. **共享数据源**:所有模块类型使用相同的数据源实例
1057
- 3. **统一字段标签**:在不同模块中使用相同的字段标签映射
1058
- 4. **利用自动生成**:让插件自动生成操作按钮和路由,减少重复代码
1059
- 5. **自定义渲染**:根据需要重写 `renderColumn`、`renderField` 等方法
1060
-
1061
- ### 组合方式
1062
-
1063
- 你可以根据需要组合不同的模块类型:
1064
-
1065
- - **完整 CRUD**: `list` + `detail` + `form`(推荐)
1066
- - **只读列表**: `list` + `detail`(无表单)
1067
- - **简单表单**: `form`(无列表和详情)
1068
- - **自定义页面**: `custom`(完全自定义)
1069
-
1070
- ### 注意事项
1071
-
1072
- 1. **模块名必须一致**:所有相关模块必须使用完全相同的模块名(区分大小写)
1073
- 2. **数据源类型匹配**:List 模块使用 `ListDatasource`,Detail 和 Form 模块使用 `DetailDatasource` 或 `FormDatasource`
1074
- 3. **字段一致性**:不同模块中使用的字段名应该保持一致
1075
- 4. **路径生成**:使用 `this.paths` 生成路径,确保路径一致性
1076
-
1077
- #### 基本用法
1078
-
1079
- ```typescript
1080
- @Module("dashboard", {
1081
- type: "custom",
1082
- })
1083
- class DashboardModule extends PageModule {
1084
- async render() {
1085
- return (
1086
- <div>
1087
- <h1>仪表盘</h1>
1088
- <p>欢迎使用管理后台</p>
1089
- {/* 自定义内容 */}
1090
- </div>
1091
- );
1092
- }
1093
- }
1094
- ```
1095
-
1096
- #### 必需方法
1097
-
1098
- **`render(): any | Promise<any>`**
1099
- - 必须实现
1100
- - 返回页面的 JSX 内容
1101
- - 可以通过 `this.context.ctx` 访问 Hono Context
1102
- - 可以通过 `this.context` 访问 HtmxAdminContext
1103
- - 可以使用 `Components` 命名空间中的组件
1104
-
1105
- #### 示例
1106
-
1107
- ```typescript
1108
- import { Components } from "imean-service-engine";
1109
-
1110
- @Module("dashboard", {
1111
- type: "custom",
1112
- })
1113
- class DashboardModule extends PageModule {
1114
- async render() {
1115
- const { Card, Button, PageHeader, StatCard } = Components;
1116
-
1117
- return (
1118
- <div>
1119
- <PageHeader title="仪表盘" description="系统概览和快速操作" />
1120
-
1121
- {/* 统计卡片 */}
1122
- <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
1123
- <StatCard
1124
- title="总用户数"
1125
- value="1,234"
1126
- change={12.5}
1127
- changeLabel="%"
1128
- iconColor="blue"
1129
- />
1130
- <StatCard
1131
- title="今日订单"
1132
- value="56"
1133
- change={-5.2}
1134
- changeLabel="%"
1135
- iconColor="green"
1136
- />
1137
- <StatCard
1138
- title="待处理任务"
1139
- value="23"
1140
- iconColor="yellow"
1141
- />
1142
- </div>
1143
-
1144
- {/* 快速操作 */}
1145
- <Card>
1146
- <h2 className="text-lg font-semibold mb-4">快速操作</h2>
1147
- <div className="space-y-2">
1148
- <Button variant="primary" href="/admin/users/new" hxGet="/admin/users/new">
1149
- 新建用户
1150
- </Button>
1151
- <Button variant="secondary" href="/admin/products/list" hxGet="/admin/products/list">
1152
- 查看产品
1153
- </Button>
1154
- </div>
1155
- </Card>
1156
- </div>
1157
- );
1158
- }
1159
- }
1160
- ```
1161
-
1162
- ## 数据源
1163
-
1164
- ### 数据源接口
1165
-
1166
- 插件定义了三种数据源接口:
1167
-
1168
- #### ListDatasource(列表数据源)
1169
-
1170
- ```typescript
1171
- interface ListDatasource<T = any> {
1172
- /** 获取列表数据 */
1173
- getList(params: ListParams): Promise<ListResult<T>>;
1174
- /** 删除数据(可选,如果不提供则列表页不显示删除操作) */
1175
- deleteItem?(id: string | number): Promise<boolean>;
1176
- }
1177
- ```
1178
-
1179
- #### DetailDatasource(详情数据源)
1180
-
1181
- ```typescript
1182
- interface DetailDatasource<T = any> {
1183
- /** 获取单条数据 */
1184
- getItem(id: string | number): Promise<T | null>;
1185
- /** 删除数据(可选) */
1186
- deleteItem?(id: string | number): Promise<boolean>;
1187
- }
1188
- ```
1189
-
1190
- #### FormDatasource(表单数据源)
1191
-
1192
- ```typescript
1193
- interface FormDatasource<T = any> {
1194
- /** 获取单条数据(编辑时使用) */
1195
- getItem?(id: string | number): Promise<T | null>;
1196
- /** 更新数据 */
1197
- updateItem?(id: string | number, data: Partial<T>): Promise<T | null>;
1198
- /** 创建数据 */
1199
- createItem?(data: Partial<T>): Promise<T>;
1200
- }
1201
- ```
1202
-
1203
- ### ListParams(列表查询参数)
1204
-
1205
- ```typescript
1206
- interface ListParams {
1207
- /** 页码(从1开始) */
1208
- page?: number;
1209
- /** 每页数量 */
1210
- pageSize?: number;
1211
- /** 排序字段 */
1212
- sortBy?: string;
1213
- /** 排序方向 */
1214
- sortOrder?: "asc" | "desc";
1215
- /** 筛选条件 */
1216
- filters?: Record<string, any>;
1217
- }
1218
- ```
1219
-
1220
- ### ListResult(列表查询结果)
1221
-
1222
- ```typescript
1223
- interface ListResult<T> {
1224
- /** 数据列表 */
1225
- items: T[];
1226
- /** 总数量 */
1227
- total: number;
1228
- /** 当前页码 */
1229
- page: number;
1230
- /** 每页数量 */
1231
- pageSize: number;
1232
- /** 总页数 */
1233
- totalPages: number;
1234
- }
1235
- ```
1236
-
1237
- ### MemoryListDatasource(内存数据源)
1238
-
1239
- 插件提供了一个内存数据源实现,适合开发和测试:
1240
-
1241
- ```typescript
1242
- import { MemoryListDatasource } from "imean-service-engine";
1243
-
1244
- const userDatasource = new MemoryListDatasource<User>([
1245
- { id: 1, name: "张三", email: "zhangsan@example.com" },
1246
- { id: 2, name: "李四", email: "lisi@example.com" },
1247
- ]);
1248
- ```
1249
-
1250
- ### 自定义数据源
1251
-
1252
- 实现对应的数据源接口即可:
1253
-
1254
- ```typescript
1255
- class DatabaseListDatasource<T> implements ListDatasource<T> {
1256
- async getList(params: ListParams): Promise<ListResult<T>> {
1257
- // 从数据库查询数据
1258
- const offset = (params.page - 1) * params.pageSize;
1259
- const items = await db.query(
1260
- `SELECT * FROM users LIMIT ${params.pageSize} OFFSET ${offset}`
1261
- );
1262
- const total = await db.query(`SELECT COUNT(*) FROM users`);
1263
-
1264
- return {
1265
- items,
1266
- total,
1267
- page: params.page || 1,
1268
- pageSize: params.pageSize || 10,
1269
- totalPages: Math.ceil(total / params.pageSize),
1270
- };
1271
- }
1272
-
1273
- async deleteItem(id: string | number): Promise<boolean> {
1274
- // 从数据库删除数据
1275
- const result = await db.query(`DELETE FROM users WHERE id = ?`, [id]);
1276
- return result.affectedRows > 0;
1277
- }
1278
- }
1279
- ```
1280
-
1281
- ## 组件系统
1282
-
1283
- ### 组件命名空间
1284
-
1285
- 所有外部可用的组件都通过 `Components` 命名空间导出:
1286
-
1287
- ```typescript
1288
- import { Components } from "imean-service-engine";
1289
-
1290
- const { Detail, Form, Button, Card, PageHeader } = Components;
1291
- ```
1292
-
1293
- ### 可用组件
1294
-
1295
- #### 页面组件
1296
-
1297
- - **`Detail`**: 详情页组件
1298
- - **`Form`**: 表单组件
1299
- - **`PageHeader`**: 页面头部组件
1300
- - **`EmptyState`**: 空状态组件
1301
- - **`Dialog`**: 对话框组件
1302
- - **`ErrorAlert`**: 错误提示组件
1303
-
1304
- #### 布局组件
1305
-
1306
- - **`Card`**: 卡片组件
1307
- - **`Button`**: 按钮组件
1308
- - **`ActionButton`**: 操作按钮组件
1309
- - **`Pagination`**: 分页组件
1310
- - **`FilterCard`**: 筛选器组件
1311
-
1312
- #### 数据展示组件
1313
-
1314
- - **`StatCard`**: 统计卡片组件
1315
- - **`ActivityCard`**: 活动卡片组件
1316
- - **`SystemStatusCard`**: 系统状态卡片组件
1317
- - **`Badge`**: 徽章组件
1318
-
1319
- #### 表单组件
1320
-
1321
- - **`FormField`**: 表单字段包装器(标签 + 输入框 + 错误提示)
1322
- - **`Input`**: 输入框组件(支持 text, email, password, number 等类型)
1323
- - **`Textarea`**: 文本域组件
1324
- - **`Select`**: 选择框组件
1325
- - **`DateInput`**: 日期输入组件(支持 date, datetime-local, time 等类型)
1326
-
1327
- ### 组件使用示例
1328
-
1329
- #### Detail 组件
1330
-
1331
- ```typescript
1332
- import { Components } from "imean-service-engine";
1333
-
1334
- const { Detail } = Components;
1335
-
1336
- <Detail
1337
- item={user}
1338
- fieldLabels={{
1339
- name: "姓名",
1340
- email: "邮箱",
1341
- role: "角色",
1342
- }}
1343
- fieldRenderers={{
1344
- role: (value) => (
1345
- <span className="px-2 py-1 rounded bg-blue-100 text-blue-800">
1346
- {value}
1347
- </span>
1348
- ),
1349
- }}
1350
- fieldGroups={[
1351
- { title: "基本信息", fields: ["name", "email"] },
1352
- { title: "权限信息", fields: ["role"] },
1353
- ]}
1354
- actions={[
1355
- { label: "编辑", href: `/admin/users/${user.id}/edit` },
1356
- { label: "删除", href: `/admin/users/${user.id}`, method: "DELETE" },
1357
- ]}
1358
- title="用户详情"
1359
- />
1360
- ```
1361
-
1362
- #### Form 组件
1363
-
1364
- ```typescript
1365
- import { Components } from "imean-service-engine";
1366
-
1367
- const { Form } = Components;
1368
-
1369
- <Form
1370
- fields={[
1371
- { name: "name", label: "姓名", type: "text", required: true },
1372
- { name: "email", label: "邮箱", type: "email", required: true },
1373
- ]}
1374
- initialData={user}
1375
- submitUrl={`/admin/users/${user.id}`}
1376
- method="PUT"
1377
- cancelUrl="/admin/users/list"
1378
- />
1379
- ```
1380
-
1381
- #### Button 组件
1382
-
1383
- ```typescript
1384
- import { Components } from "imean-service-engine";
1385
-
1386
- const { Button } = Components;
1387
-
1388
- <Button
1389
- variant="primary"
1390
- size="md"
1391
- href="/admin/users/new"
1392
- hxGet="/admin/users/new"
1393
- >
1394
- 新建用户
1395
- </Button>
1396
- ```
1397
-
1398
- #### 表单组件
1399
-
1400
- ```typescript
1401
- import { Components } from "imean-service-engine";
1402
-
1403
- const { FormField, Input, Textarea, Select, DateInput } = Components;
1404
-
1405
- <FormField id="name" label="姓名" required>
1406
- <Input name="name" placeholder="请输入姓名" />
1407
- </FormField>
1408
-
1409
- <FormField id="email" label="邮箱" required>
1410
- <Input type="email" name="email" placeholder="请输入邮箱" />
1411
- </FormField>
1412
-
1413
- <FormField id="bio" label="简介">
1414
- <Textarea name="bio" rows={4} placeholder="请输入简介" />
1415
- </FormField>
1416
-
1417
- <FormField id="status" label="状态" required>
1418
- <Select
1419
- name="status"
1420
- options={[
1421
- { value: "active", label: "活跃" },
1422
- { value: "inactive", label: "未激活" },
1423
- ]}
1424
- placeholder="请选择状态"
1425
- />
1426
- </FormField>
1427
-
1428
- <FormField id="birthday" label="生日">
1429
- <DateInput type="date" name="birthday" />
1430
- </FormField>
1431
- ```
1432
-
1433
- ## 路由系统
1434
-
1435
- ### 自动路由生成
1436
-
1437
- 插件会根据模块类型自动生成 RESTful 路由:
1438
-
1439
- #### List 模块路由
1440
-
1441
- - `GET /{basePath}/list`: 列表页面
1442
- - `DELETE /{basePath}/:id`: 删除操作(如果数据源支持)
1443
-
1444
- #### Detail 模块路由
1445
-
1446
- - `GET /{basePath}/detail/:id`: 详情页面
1447
- - `DELETE /{basePath}/detail/:id`: 删除操作(如果数据源支持)
1448
-
1449
- #### Form 模块路由
1450
-
1451
- - `GET /{basePath}/new`: 新建表单页面
1452
- - `POST /{basePath}`: 创建操作
1453
- - `GET /{basePath}/edit/:id`: 编辑表单页面
1454
- - `PUT /{basePath}/:id`: 更新操作
1455
- - `DELETE /{basePath}/:id`: 删除操作(如果数据源支持)
1456
-
1457
- #### Custom 模块路由
1458
-
1459
- - `GET /{basePath}`: 自定义页面
1460
-
1461
- ### Dialog 模式
1462
-
1463
- 所有详情页和表单页都支持 dialog 模式,通过 URL 参数 `dialog=true` 启用:
1464
-
1465
- - `GET /{basePath}/detail/:id?dialog=true`: 在对话框中显示详情
1466
- - `GET /{basePath}/new?dialog=true`: 在对话框中显示新建表单
1467
- - `GET /{basePath}/edit/:id?dialog=true`: 在对话框中显示编辑表单
1468
-
1469
- Dialog 模式的特点:
1470
- - 内容显示在模态对话框中
1471
- - 不丢失列表页的状态
1472
- - 适合快速查看和编辑
1473
- - 关闭对话框后自动返回列表页
1474
-
1475
- ### URL 状态管理
1476
-
1477
- 列表页的筛选和分页状态会自动记录在 URL 中:
1478
-
1479
- - 筛选条件:`?status=pending&priority=high`
1480
- - 分页:`?page=2&pageSize=20`
1481
- - 排序:`?sortBy=createdAt&sortOrder=desc`
1482
-
1483
- 刷新页面后状态会自动恢复。
1484
-
1485
- ## 响应机制
1486
-
1487
- ### 统一响应架构
1488
-
1489
- 插件采用后端驱动的统一响应架构,通过 `RouteHandler` 统一处理所有请求:
1490
-
1491
- 1. **RouteHandler**: 所有路由都通过 `RouteHandler.handle()` 处理
1492
- 2. **模块实例化**: 每次请求创建新的模块实例,通过 `__init(context)` 初始化
1493
- 3. **请求处理**: 调用模块的 `__handle()` 方法(默认调用 `render()`)
1494
- 4. **响应构建**: 根据请求类型(完整页面/片段)构建响应
1495
-
1496
- ### 响应流程
1497
-
1498
- ```
1499
- 请求 → RouteHandler.handle()
1500
- → 创建 HtmxAdminContext
1501
- → 创建模块实例
1502
- → 调用 moduleInstance.__init(context)
1503
- → 调用 moduleInstance.__handle()
1504
- → 默认调用 moduleInstance.render()
1505
- → 设置页面元数据(title, description, breadcrumbs)
1506
- → 构建响应(完整页面或片段)
1507
- → 返回响应
1508
- ```
1509
-
1510
- ### 响应头控制
1511
-
1512
- 所有响应都通过 `HX-Retarget` 响应头明确指定目标容器:
1513
-
1514
- - `#main-content`: 主内容区域(默认,完整页面)
1515
- - `#dialog-container`: 对话框容器(dialog 模式)
1516
- - `#admin-layout`: 片段请求时的布局容器
1517
-
1518
- ### HX-Push-Url 控制
1519
-
1520
- URL 更新由后端通过 `HtmxAdminContext.redirect()` 控制:
1521
-
1522
- - 正常模式:调用 `context.redirect(url)` 更新 URL
1523
- - Dialog 模式:不设置 redirectUrl,保持当前 URL
1524
-
1525
- ## 错误处理
1526
-
1527
- ### 统一错误处理
1528
-
1529
- 插件提供了统一的错误处理机制,通过 `HtmxAdminContext` 发送通知:
1530
-
1531
- #### 发送错误通知
1532
-
1533
- ```typescript
1534
- // 在模块的 render 或 __handle 方法中
1535
- if (!item) {
1536
- this.context.sendError("未找到数据", "数据不存在");
1537
- return <div>未找到数据</div>;
1538
- }
1539
- ```
1540
-
1541
- #### 发送成功通知
1542
-
1543
- ```typescript
1544
- // 操作成功后
1545
- this.context.sendSuccess("操作成功", "数据已成功保存");
1546
- ```
1547
-
1548
- #### 发送警告通知
1549
-
1550
- ```typescript
1551
- this.context.sendWarning("警告", "操作可能影响其他数据");
1552
- ```
1553
-
1554
- #### 发送信息通知
1555
-
1556
- ```typescript
1557
- this.context.sendInfo("提示", "操作成功完成");
1558
- ```
1559
-
1560
- ### 错误响应机制
1561
-
1562
- 错误通知会自动:
1563
-
1564
- 1. 添加到 `context.notifications` 队列
1565
- 2. 在响应时通过 OOB 更新到错误容器
1566
- 3. 显示在页面右上角的全局错误通知区域
1567
- 4. 支持多个通知同时显示
1568
- 5. 每个通知可以单独关闭
1569
-
1570
- ### RouteHandler 错误处理
1571
-
1572
- `RouteHandler` 会自动捕获模块处理过程中的异常:
1573
-
1574
- ```typescript
1575
- try {
1576
- adminContext.content = await moduleInstance.__handle();
1577
- // ...
1578
- } catch (error) {
1579
- // 业务错误,自动处理
1580
- this.handleError(adminContext, error);
1581
- }
1582
- ```
1583
-
1584
- ## 高级功能
1585
-
1586
- ### PathHelper(路径助手)
1587
-
1588
- `PathHelper` 用于简化路径生成,通过 `this.paths` 访问:
1589
-
1590
- ```typescript
1591
- // 在模块中使用
1592
- getActions(item: User) {
1593
- return [
1594
- { label: "查看", href: this.paths.detail(item.id) },
1595
- { label: "编辑", href: this.paths.edit(item.id) },
1596
- { label: "删除", href: this.paths.delete(item.id), method: "DELETE" },
1597
- { label: "快速查看", href: this.paths.detail(item.id, true) }, // Dialog 模式
1598
- ];
1599
- }
1600
- ```
1601
-
1602
- #### 可用方法
1603
-
1604
- - `detail(id: string | number, dialog?: boolean)`: 生成详情页路径
1605
- - `edit(id: string | number, dialog?: boolean)`: 生成编辑页路径
1606
- - `create(dialog?: boolean)`: 生成新建页路径
1607
- - `delete(id: string | number)`: 生成删除操作路径
1608
- - `list()`: 生成列表页路径
1609
- - `base()`: 返回基础路径
1610
-
1611
- ### 模块元数据
1612
-
1613
- 插件会自动为每个模块设置元数据,通过 `this.context.moduleMetadata` 访问:
1614
-
1615
- ```typescript
1616
- interface ModuleTypeInfo {
1617
- title: string; // 模块标题
1618
- description: string; // 模块描述
1619
- basePath: string; // 模块基础路径
1620
- hasList: boolean; // 是否有列表页
1621
- hasDetail: boolean; // 是否有详情页
1622
- hasForm: boolean; // 是否有表单页
1623
- hasCustom: boolean; // 是否有自定义页
1624
- }
1625
- ```
1626
-
1627
- 可以在模块方法中使用:
1628
-
1629
- ```typescript
1630
- getActions(item: T) {
1631
- const actions = [];
1632
-
1633
- if (this.context.moduleMetadata.hasDetail) {
1634
- actions.push({ label: "查看", href: this.paths.detail(item.id) });
1635
- }
1636
-
1637
- if (this.context.moduleMetadata.hasForm) {
1638
- actions.push({ label: "编辑", href: this.paths.edit(item.id) });
1639
- }
1640
-
1641
- return actions;
1642
- }
1643
- ```
1644
-
1645
- ### 自定义渲染
1646
-
1647
- #### 列表页自定义渲染
1648
-
1649
- ```typescript
1650
- renderColumn(field: string, value: any, item: T): any {
1651
- // 返回 JSX 或 HTML 字符串
1652
- if (field === "avatar") {
1653
- return <img src={value} className="w-10 h-10 rounded-full" />;
1654
- }
1655
- return value;
1656
- }
1657
- ```
1658
-
1659
- #### 详情页自定义渲染
1660
-
1661
- ```typescript
1662
- renderField(field: string, value: any, item: T): any {
1663
- // 返回 JSX 或 HTML 字符串
1664
- if (field === "avatar") {
1665
- return <img src={value} className="w-20 h-20 rounded-full" />;
1666
- }
1667
- return value;
1668
- }
1669
- ```
1670
-
1671
- #### 完全自定义详情页
1672
-
1673
- ```typescript
1674
- async render() {
1675
- const idParam = this.context.ctx.req.param("id");
1676
- const item = await this.getItem(idParam);
1677
-
1678
- if (!item) {
1679
- return <div>未找到数据</div>;
1680
- }
1681
-
1682
- return (
1683
- <div className="custom-detail-layout">
1684
- {/* 完全自定义的布局和样式 */}
1685
- <h1>{item.name}</h1>
1686
- {/* ... */}
1687
- </div>
1688
- );
1689
- }
1690
- ```
1691
-
1692
- ### 导航配置
1693
-
1694
- #### 集中式导航配置
1695
-
1696
- ```typescript
1697
- const adminPlugin = new HtmxAdminPlugin({
1698
- navigation: [
1699
- {
1700
- label: "用户管理",
1701
- moduleName: "users",
1702
- icon: "👥",
1703
- },
1704
- {
1705
- label: "产品管理",
1706
- moduleName: "products",
1707
- icon: "📦",
1708
- },
1709
- {
1710
- label: "系统设置",
1711
- icon: "⚙️",
1712
- children: [
1713
- {
1714
- label: "用户设置",
1715
- moduleName: "settings",
1716
- icon: "👤",
1717
- },
1718
- {
1719
- label: "系统配置",
1720
- href: "/admin/config",
1721
- icon: "🔧",
1722
- },
1723
- ],
1724
- },
1725
- ],
1726
- });
1727
- ```
1728
-
1729
- #### 导航配置
1730
-
1731
- 导航必须通过 `navigation` 配置手动定义。如果不提供 `navigation` 配置,导航将为空。
1732
-
1733
- **注意**: 导航配置是必需的,插件不会自动从模块生成导航。
1734
-
1735
- ### 面包屑
1736
-
1737
- 面包屑是页面模块的一部分,每个模块都可以定义自己的面包屑。
1738
-
1739
- #### 默认实现
1740
-
1741
- 每个基类都提供了默认的 `getBreadcrumbs()` 方法:
1742
-
1743
- - **ListPageModule**: 首页 => 列表页
1744
- - **DetailPageModule**: 首页 => 列表页(如果存在)=> 详情
1745
- - **FormPageModule**: 首页 => 列表页(如果存在)=> 新建/编辑
1746
- - **PageModule**: 首页 => 自定义页面
1747
-
1748
- #### 自定义面包屑
1749
-
1750
- 可以在模块中重写 `getBreadcrumbs()` 方法来自定义面包屑:
1751
-
1752
- ```typescript
1753
- @Module("users", {
1754
- type: "detail",
1755
- })
1756
- class UserDetailModule extends DetailPageModule<User> {
1757
- constructor() {
1758
- super(userDatasource);
1759
- }
1760
-
1761
- // 自定义面包屑
1762
- getBreadcrumbs(): BreadcrumbItem[] {
1763
- const item = await this.getItem(this.context.ctx.req.param("id"));
1764
- return [
1765
- { label: "首页", href: this.context.pluginOptions.prefix },
1766
- { label: "用户管理", href: this.paths.list() },
1767
- { label: item ? `${item.name} 的详情` : "用户详情" },
1768
- ];
1769
- }
1770
- }
1771
- ```
1772
-
1773
-
1774
- ### 侧边栏折叠
1775
-
1776
- 侧边栏支持折叠功能:
1777
-
1778
- - 点击切换按钮可以折叠/展开侧边栏
1779
- - 折叠状态保存在 Cookie 中
1780
- - 刷新页面后状态会恢复
1781
-
1782
- ## API 参考
1783
-
1784
- ### HtmxAdminPluginOptions
1785
-
1786
- ```typescript
1787
- interface HtmxAdminPluginOptions {
1788
- /** 站点标题 */
1789
- title?: string;
1790
- /** 站点 Logo URL(可选) */
1791
- logo?: string;
1792
- /** 管理后台路径前缀(默认 /admin) */
1793
- prefix?: string;
1794
- /** 首页路径(访问插件默认路径时重定向到此路径,如果未配置则使用第一个模块的路径) */
1795
- homePath?: string;
1796
- /** 导航配置(集中式声明导航结构,支持嵌套) */
1797
- navigation?: NavItemConfig[];
1798
- /** 认证提供者(用于鉴权和权限控制) */
1799
- authProvider?: AuthProvider;
1800
- }
1801
- ```
1802
-
1803
- ### AuthProvider
1804
-
1805
- ```typescript
1806
- interface AuthProvider {
1807
- /** Cookie 键名(默认 "auth_token") */
1808
- cookieKey?: string;
1809
- /** 从 token 获取用户信息(必需) */
1810
- tokenToUser: (token: string, ctx: Context) => Promise<UserInfo | null>;
1811
- /** 自定义权限检查(可选,不提供则使用默认实现) */
1812
- checkPermission?: (
1813
- user: UserInfo | null,
1814
- operation: string,
1815
- context: HtmxAdminContext
1816
- ) => PermissionResult | Promise<PermissionResult>;
1817
- }
1818
- ```
1819
-
1820
- ### UserInfo
1821
-
1822
- ```typescript
1823
- interface UserInfo {
1824
- name?: string;
1825
- avatar?: string;
1826
- email?: string;
1827
- /** 用户权限列表(支持通配符和禁止权限) */
1828
- permissions?: string[];
1829
- [key: string]: any;
1830
- }
1831
- ```
1832
-
1833
- ### PermissionResult
1834
-
1835
- ```typescript
1836
- interface PermissionResult {
1837
- allowed: boolean;
1838
- message?: string;
1839
- operationId?: string;
1840
- redirectUrl?: string;
1841
- }
1842
- ```
1843
-
1844
- ### HtmxAdminModuleOptions
1845
-
1846
- ```typescript
1847
- interface HtmxAdminModuleOptions {
1848
- /** 模块类型 */
1849
- type: "list" | "detail" | "form" | "custom";
1850
- /** 模块标题(可选,默认使用模块名) */
1851
- title?: string;
1852
- /** 模块描述(可选) */
1853
- description?: string;
1854
- /** 是否使用管理后台布局(默认 true,设置为 false 时不使用侧边栏和头部,只使用 BaseLayout) */
1855
- useAdminLayout?: boolean;
1856
- }
1857
- ```
1858
-
1859
- ### PageModule(基类)
1860
-
1861
- ```typescript
1862
- abstract class PageModule {
1863
- /** HtmxAdmin 上下文对象 */
1864
- public context!: HtmxAdminContext;
1865
-
1866
- /** PathHelper 实例(用于生成路径) */
1867
- public paths!: PathHelper;
1868
-
1869
- /** 初始化模块实例(由 RouteHandler 调用) */
1870
- __init(context: HtmxAdminContext): void;
1871
-
1872
- /** 获取页面标题 */
1873
- getTitle(): string;
1874
-
1875
- /** 获取页面描述 */
1876
- getDescription(): string;
1877
-
1878
- /** 获取面包屑 */
1879
- getBreadcrumbs(): BreadcrumbItem[];
1880
-
1881
- /** 处理请求的统一入口(由 RouteHandler 调用) */
1882
- async __handle(): Promise<any>;
1883
-
1884
- /** 渲染页面内容(必须实现) */
1885
- abstract render(): any | Promise<any>;
1886
- }
1887
- ```
1888
-
1889
- ### ListPageModule
1890
-
1891
- ```typescript
1892
- abstract class ListPageModule<T extends { id: string | number } = { id: string | number }> extends PageModule {
1893
- /** ID 字段名(默认 "id") */
1894
- protected readonly idField: string = "id";
1895
-
1896
- /** 获取数据源(抽象方法,必须实现) */
1897
- abstract getDatasource(): ListDatasource<T>;
1898
-
1899
- /** 获取列表数据 */
1900
- async getList(params: ListParams): Promise<ListResult<T>>;
1901
-
1902
- /** 删除数据(可选,如果数据源支持删除) */
1903
- async deleteItem(id: string | number): Promise<boolean>;
1904
-
1905
- /** 自定义列渲染(可选) */
1906
- renderColumn?(field: string, value: any, item: T): any;
1907
-
1908
- /** 获取统计信息(可选) */
1909
- getStats?(params: ListParams): Promise<Array<StatCardProps>> | Array<StatCardProps>;
1910
-
1911
- /** 获取筛选器(可选) */
1912
- getFilters?(params: ListParams): Array<FilterField>;
1913
-
1914
- /** 获取表格标题(可选) */
1915
- getTableTitle?(): string;
1916
-
1917
- /** 获取表格操作按钮(可选) */
1918
- getTableActions?(params: ListParams, basePath: string): Array<ActionButtonProps>;
1919
-
1920
- /** 获取操作按钮(可选) */
1921
- getActions?(item: T): Array<ActionButtonProps>;
1922
-
1923
- /** 渲染页面内容(可重写:自定义渲染) */
1924
- async render(): Promise<any>;
1925
- }
1926
- ```
1927
-
1928
- ### DetailPageModule
1929
-
1930
- ```typescript
1931
- abstract class DetailPageModule<T = any> extends PageModule {
1932
- /** ID 字段名(默认 "id") */
1933
- protected readonly idField: string = "id";
1934
-
1935
- /** 获取数据源(抽象方法,必须实现) */
1936
- abstract getDatasource(): DetailDatasource<T>;
1937
-
1938
- /** 获取单条数据 */
1939
- async getItem(id: string | number): Promise<T | null>;
1940
-
1941
- /** 删除数据(可选,如果数据源支持删除) */
1942
- async deleteItem(id: string | number): Promise<boolean>;
1943
-
1944
- /** 获取字段标签(可选) */
1945
- getFieldLabel?(field: string): string;
1946
-
1947
- /** 渲染字段值(可选) */
1948
- renderField?(field: string, value: any, item: T): any;
1949
-
1950
- /** 获取字段分组(可选) */
1951
- getFieldGroups?(item: T): Array<{ title: string; fields: string[] }> | null;
1952
-
1953
- /** 获取可见字段(可选) */
1954
- getVisibleFields?(item: T): string[] | null;
1955
-
1956
- /** 获取详情页操作按钮(可选) */
1957
- getDetailActions?(item: T): Array<ActionButtonProps>;
1958
-
1959
- /** 渲染页面内容(可重写:自定义渲染) */
1960
- async render(): Promise<any>;
1961
- }
1962
- ```
1963
-
1964
- ### FormPageModule
1965
-
1966
- ```typescript
1967
- abstract class FormPageModule<T = any> extends PageModule {
1968
- /** 数据源 */
1969
- protected datasource: FormDatasource<T>;
1970
-
1971
- /** ID 字段名(默认 "id") */
1972
- protected idField: string = "id";
1973
-
1974
- /** Zod Schema(可选,如果提供则自动生成表单字段和校验) */
1975
- protected schema?: z.ZodObject<any>;
1976
-
1977
- constructor(datasource: FormDatasource<T>, schema?: z.ZodObject<any>);
1978
-
1979
- /** 获取数据源(抽象方法,必须实现) */
1980
- abstract getDatasource(): FormDatasource<T>;
1981
-
1982
- /** 获取单条数据(编辑时使用) */
1983
- async getItem(id: string | number): Promise<T | null>;
1984
-
1985
- /** 获取表单字段定义(如果提供了 schema,则自动生成) */
1986
- getFormFields(item: T | null): Array<FormFieldType>;
1987
-
1988
- /** 验证表单数据(如果提供了 schema,则自动校验) */
1989
- validateFormData(data: Record<string, any>, item: T | null): string | null;
1990
-
1991
- /** 处理请求的统一入口(重写以处理不同 HTTP method) */
1992
- async __handle(): Promise<any>;
1993
-
1994
- /** 渲染页面内容(可重写:自定义渲染) */
1995
- async render(formData?: Record<string, any>): Promise<any>;
1996
- }
1997
- ```
1998
-
1999
- ### PageModule(自定义页面)
2000
-
2001
- ```typescript
2002
- abstract class PageModule {
2003
- /** HtmxAdmin 上下文对象 */
2004
- public context!: HtmxAdminContext;
2005
-
2006
- /** PathHelper 实例 */
2007
- public paths!: PathHelper;
2008
-
2009
- /** 渲染页面内容(Custom 类型必须实现) */
2010
- abstract render(): any | Promise<any>;
2011
- }
2012
- ```
2013
-
2014
- ### HtmxAdminContext(上下文对象)
2015
-
2016
- `HtmxAdminContext` 是插件核心的上下文对象,封装了请求处理过程中的所有状态和操作。每个模块实例都可以通过 `this.context` 访问。
2017
-
2018
- #### 属性
2019
-
2020
- ```typescript
2021
- class HtmxAdminContext {
2022
- /** 模块元数据 */
2023
- public readonly moduleMetadata: ModuleTypeInfo;
2024
-
2025
- /** 插件选项 */
2026
- public readonly pluginOptions: Required<HtmxAdminPluginOptions>;
2027
-
2028
- /** Hono Context(用于访问请求信息) */
2029
- public readonly ctx: Context;
2030
-
2031
- /** 之前的模块名(从 Referer 中提取) */
2032
- public readonly previousModuleName?: string;
2033
-
2034
- /** 是否是片段请求(HTMX 请求) */
2035
- public readonly isFragment: boolean;
2036
-
2037
- /** 是否是对话框请求 */
2038
- public readonly isDialog: boolean;
2039
-
2040
- /** 响应目标容器 */
2041
- public readonly target: string;
2042
-
2043
- /** 交换方式 */
2044
- public readonly swap: string;
2045
-
2046
- /** 用户信息 */
2047
- public readonly userInfo: UserInfo | null;
2048
-
2049
- /** 通知队列 */
2050
- public readonly notifications: Notification[];
2051
-
2052
- /** 页面标题(用于 HX-Title 和页面展示) */
2053
- public title: string;
2054
-
2055
- /** 页面描述(用于SEO和页面展示) */
2056
- public description: string;
2057
-
2058
- /** 面包屑项 */
2059
- public breadcrumbs: BreadcrumbItem[];
2060
-
2061
- /** 主要内容 */
2062
- public content: any;
2063
-
2064
- /** 需要重定向的 URL */
2065
- public redirectUrl?: string;
2066
-
2067
- /** 是否需要刷新页面(用于 HX-Refresh) */
2068
- public refresh: boolean;
2069
- }
2070
- ```
2071
-
2072
- #### 方法
2073
-
2074
- ##### 通知方法
2075
-
2076
- ```typescript
2077
- // 发送通知
2078
- sendNotification(type: NotificationType, title: string, message: string): void;
2079
-
2080
- // 便捷方法
2081
- sendError(title: string, message: string): void;
2082
- sendWarning(title: string, message: string): void;
2083
- sendInfo(title: string, message: string): void;
2084
- sendSuccess(title: string, message: string): void;
2085
- ```
2086
-
2087
- **示例**:
2088
-
2089
- ```typescript
2090
- // 在模块中使用(通常在 validateFormData 或自定义逻辑中)
2091
- validateFormData(data: Record<string, any>, item: T | null): string | null {
2092
- // 业务逻辑验证
2093
- if (!data.title || data.title.trim() === "") {
2094
- this.context.sendError("验证失败", "标题不能为空");
2095
- return "标题不能为空";
2096
- }
2097
- return null;
2098
- }
2099
-
2100
- // 或者在重写的 render 方法中使用
2101
- async render() {
2102
- // 自定义逻辑
2103
- if (someCondition) {
2104
- this.context.sendSuccess("操作成功", "数据已保存");
2105
- }
2106
- }
2107
- ```
2108
-
2109
- ##### URL 控制方法
2110
-
2111
- ```typescript
2112
- // 设置重定向 URL(用于 HX-Push-Url)
2113
- redirect(url: string): void;
2114
-
2115
- // 设置是否需要刷新页面(用于 HX-Refresh)
2116
- setRefresh(refresh: boolean = true): void;
2117
- ```
2118
-
2119
- **示例**:
2120
-
2121
- ```typescript
2122
- // 表单提交后的重定向逻辑已经在 FormPageModule 中自动处理
2123
- // 如果需要自定义,可以在 validateFormData 或重写 render 方法中使用
2124
- validateFormData(data: Record<string, any>, item: T | null): string | null {
2125
- // 验证逻辑
2126
- if (validationPassed) {
2127
- // 注意:重定向通常在 FormPageModule 的 handleCreate/handleUpdate 中处理
2128
- // 这里只是示例,实际使用中不需要手动调用
2129
- this.context.redirect(`${this.context.moduleMetadata.basePath}/list`);
2130
- }
2131
- return null;
2132
- }
2133
- ```
2134
-
2135
- ##### 模块元数据检查方法
2136
-
2137
- ```typescript
2138
- // 检查是否有列表页面
2139
- hasList(): boolean;
2140
-
2141
- // 检查是否有详情页面
2142
- hasDetail(): boolean;
2143
-
2144
- // 检查是否有表单页面
2145
- hasForm(): boolean;
2146
-
2147
- // 检查是否有自定义页面
2148
- hasCustom(): boolean;
2149
- ```
2150
-
2151
- **示例**:
2152
-
2153
- ```typescript
2154
- // 根据模块配置动态生成操作按钮
2155
- getActions(item: T) {
2156
- const actions = [];
2157
-
2158
- if (this.context.hasDetail()) {
2159
- actions.push({ label: "查看", href: this.paths.detail(item.id) });
2160
- }
2161
-
2162
- if (this.context.hasForm()) {
2163
- actions.push({ label: "编辑", href: this.paths.edit(item.id) });
2164
- }
2165
-
2166
- return actions;
2167
- }
2168
- ```
2169
-
2170
- #### 使用场景
2171
-
2172
- 1. **访问请求信息**:通过 `this.context.ctx` 访问 Hono Context
2173
- 2. **发送通知**:使用 `sendNotification` 或便捷方法发送错误/成功消息
2174
- 3. **控制重定向**:使用 `redirect()` 设置 URL 更新
2175
- 4. **检查请求类型**:通过 `isFragment`、`isDialog` 判断请求类型
2176
- 5. **访问模块元数据**:通过 `moduleMetadata` 获取模块信息
2177
-
2178
- ### PathHelper(路径助手)
2179
-
2180
- `PathHelper` 用于简化路径生成,避免手动拼接路径字符串。每个模块实例都可以通过 `this.paths` 访问。
2181
-
2182
- #### 方法
2183
-
2184
- ```typescript
2185
- class PathHelper {
2186
- /** 生成详情页路径 */
2187
- detail(id: string | number, dialog?: boolean): string;
2188
-
2189
- /** 生成编辑页路径 */
2190
- edit(id: string | number, dialog?: boolean): string;
2191
-
2192
- /** 生成新建页路径 */
2193
- create(dialog?: boolean): string;
2194
-
2195
- /** 生成删除操作路径 */
2196
- delete(id: string | number): string;
2197
-
2198
- /** 生成列表页路径 */
2199
- list(): string;
2200
-
2201
- /** 返回基础路径 */
2202
- base(): string;
2203
- }
2204
- ```
2205
-
2206
- #### 使用示例
2207
-
2208
- ```typescript
2209
- // 在模块中使用
2210
- class UserListModule extends ListPageModule<User> {
2211
- getActions(item: User) {
2212
- return [
2213
- // 普通模式
2214
- { label: "查看", href: this.paths.detail(item.id) },
2215
- { label: "编辑", href: this.paths.edit(item.id) },
2216
- { label: "删除", href: this.paths.delete(item.id), method: "DELETE" },
2217
-
2218
- // Dialog 模式
2219
- { label: "快速查看", href: this.paths.detail(item.id, true) },
2220
- { label: "快速编辑", href: this.paths.edit(item.id, true) },
2221
- ];
2222
- }
2223
-
2224
- getTableActions(params: ListParams, basePath: string) {
2225
- return [
2226
- { label: "新建", hxGet: this.paths.create() },
2227
- { label: "刷新", hxGet: this.paths.list() },
2228
- ];
2229
- }
2230
- }
2231
- ```
2232
-
2233
- #### 路径生成规则
2234
-
2235
- - **详情页**: `{basePath}/detail/{id}` 或 `{basePath}/detail/{id}?dialog=true`
2236
- - **编辑页**: `{basePath}/edit/{id}` 或 `{basePath}/edit/{id}?dialog=true`
2237
- - **新建页**: `{basePath}/new` 或 `{basePath}/new?dialog=true`
2238
- - **删除操作**: `{basePath}/detail/{id}`(使用 DELETE 方法)
2239
- - **列表页**: `{basePath}/list`
2240
- - **基础路径**: `{basePath}`
2241
-
2242
- #### 优势
2243
-
2244
- 1. **类型安全**:避免手动拼接路径时的拼写错误
2245
- 2. **统一管理**:路径规则集中管理,修改时只需更新一处
2246
- 3. **Dialog 支持**:自动处理 dialog 模式的参数
2247
- 4. **代码简洁**:减少重复的路径拼接代码
2248
-
2249
- ## 最佳实践
2250
-
2251
- ### 1. 模块命名
2252
-
2253
- - 使用复数形式:`users`, `products`, `tasks`
2254
- - 使用小写字母和连字符:`user-profiles`, `product-categories`
2255
- - 避免使用保留字:`admin`, `api`, `static`
2256
-
2257
- ### 2. 数据源设计
2258
-
2259
- - 将数据源封装为独立的类或函数
2260
- - 实现完整的数据源接口
2261
- - 处理错误情况(如数据不存在)
2262
-
2263
- ### 3. 字段渲染
2264
-
2265
- - 使用 `renderColumn` 和 `renderField` 自定义字段显示
2266
- - 返回 JSX 元素以获得更好的类型支持
2267
- - 保持渲染逻辑简洁
2268
-
2269
- ### 4. 表单验证
2270
-
2271
- - **推荐使用 Zod Schema**:自动生成表单字段和验证规则,减少重复代码
2272
- - 在 `validateFormData` 中进行业务逻辑验证(如果使用 Schema,会自动验证)
2273
- - 返回清晰的错误信息
2274
- - 前端也会进行基本的 HTML5 验证
2275
- - **表单回填**:验证失败时,用户提交的值会自动回填到表单中,避免用户重新填写
2276
- - **JSON 编码**:表单使用 `hx-encoding="json"` 提交数据,后端自动解析
2277
-
2278
- ### 5. 错误处理
2279
-
2280
- - 使用 `context.sendNotification()` 或便捷方法发送通知
2281
- - 提供有意义的错误消息
2282
- - 区分用户错误和系统错误
2283
-
2284
- ### 6. 性能优化
2285
-
2286
- - 使用分页避免加载过多数据
2287
- - 在数据源层面实现筛选和排序
2288
- - 使用缓存减少数据库查询
2289
-
2290
- ### 7. 安全性
2291
-
2292
- - **认证和权限**: 通过 `AuthProvider` 实现认证和权限控制
2293
- - **权限声明**: 模块通过 `getRequiredPermission()` 声明所需权限
2294
- - **权限匹配**: 支持通配符和禁止权限,提供灵活的权限管理
2295
- - **输入验证**: 在数据源层面验证输入
2296
- - **SQL 注入防护**: 使用参数化查询防止 SQL 注入
2297
-
2298
- ## 示例项目
2299
-
2300
- 插件提供了一个完整的示例项目,展示所有功能特性:
2301
-
2302
- ### 运行示例
2303
-
2304
- ```bash
2305
- # 启动示例项目
2306
- npm run dev
2307
-
2308
- # 或直接运行
2309
- node dist/examples/index.js
2310
- ```
2311
-
2312
- 访问 `http://localhost:3000/admin` 即可查看示例。
2313
-
2314
- ### 示例模块
2315
-
2316
- - **Dashboard**: 自定义页面示例
2317
- - **TaskManagement**: CRUD 完整示例
2318
- - **ProductCatalog**: 列表页示例
2319
- - **UserProfile**: 详情页示例
2320
- - **ArticleManagement**: 表单页示例
2321
-
2322
- ### 示例数据
2323
-
2324
- - 45 条任务数据
2325
- - 40+ 条产品数据
2326
- - 32 条用户数据
2327
- - 30 条文章数据
2328
-
2329
- 更多示例请参考 `examples/` 目录。
2330
-
2331
- ## 常见问题
2332
-
2333
- ### Q: 如何自定义页面布局?
2334
-
2335
- A: 可以通过重写 `render` 方法完全自定义详情页,或使用 `Custom` 类型模块创建自定义页面。
2336
-
2337
- ### Q: 如何添加自定义路由?
2338
-
2339
- A: 可以在模块的 `render` 方法中处理不同的路径,或使用 Hono 的路由系统添加额外的路由。
2340
-
2341
- ### Q: 如何实现权限控制?
2342
-
2343
- A: 可以在数据源层面实现权限检查,或在路由处理函数中添加中间件。
2344
-
2345
- ### Q: 如何自定义样式?
2346
-
2347
- A: 所有组件都使用 Tailwind CSS,可以通过添加自定义类名或修改 Tailwind 配置来自定义样式。
2348
-
2349
- ### Q: 如何集成数据库?
2350
-
2351
- A: 实现对应的数据源接口,在接口方法中调用数据库查询。
2352
-
2353
- ### Q: Dialog 模式如何工作?
2354
-
2355
- A: 通过 URL 参数 `dialog=true` 启用,内容会显示在模态对话框中,响应会自动交换到 `#dialog-container`。
2356
-
2357
- ### Q: 错误消息如何显示?
2358
-
2359
- A: 通过 `context.sendNotification()` 或便捷方法发送通知,通知会显示在页面右上角的全局错误通知区域,支持多个通知同时显示。
2360
-
2361
- ### Q: 表单验证失败后如何回填数据?
2362
-
2363
- A: 表单验证失败时,用户提交的值会自动回填到表单中。表单使用 JSON 编码(`hx-encoding="json"`)提交数据,后端会自动解析并回填。
2364
-
2365
- ### Q: 表单提交成功后如何控制重定向?
2366
-
2367
- A: 使用 `context.redirect(url)` 方法设置重定向 URL。创建操作优先跳转到详情页(如果存在),否则跳转到列表页;更新操作跳转到详情页。
2368
-
2369
- ### Q: 如何在模块中访问 Hono Context?
2370
-
2371
- A: 通过 `this.context.ctx` 访问 Hono Context,通过 `this.context` 访问 HtmxAdminContext。
2372
-
2373
- ### Q: 如何生成操作按钮的路径?
2374
-
2375
- A: 使用 `this.paths`(PathHelper 实例)生成路径,例如 `this.paths.detail(id)`、`this.paths.edit(id)` 等。
2376
-
2377
- ## 更新日志
2378
-
2379
- ### v2.0.0
2380
-
2381
- - ✨ 统一响应架构:后端驱动的响应机制
2382
- - ✨ Dialog 模式:支持弹窗查看和编辑
2383
- - ✨ 错误处理:统一的错误处理机制
2384
- - ✨ URL 状态管理:筛选和分页状态记录在 URL
2385
- - ✨ 动画效果:对话框和错误通知的流畅动画
2386
- - ✨ 表单回填:验证失败时自动回填用户提交的值
2387
- - ✨ Zod Schema 支持:自动生成表单字段和验证规则
2388
- - ✨ 表单 JSON 编码:使用 `hx-encoding="json"` 提交数据
2389
- - ✨ 智能重定向:创建操作优先跳转到详情页
2390
- - 🔧 代码优化:移除重复代码,统一响应处理
2391
- - 🔧 API 更新:`setPushUrl()` 改为 `redirect()`
257
+ ## 迁移指南
2392
258
 
2393
- ### v1.0.0
259
+ v1 迁移到 v2:
2394
260
 
2395
- - 🎉 初始版本发布
2396
- - ✨ 支持 List、Detail、Form、Custom 四种模块类型
2397
- - 自动路由生成
2398
- - 组件系统
2399
- - ✨ 数据源抽象
261
+ 1. **移除数据源**:将数据源方法改为函数
262
+ 2. **移除装饰器**:使用构造函数注册
263
+ 3. **合并元数据**:将 `getTitle`、`getDescription` 等合并为构造函数参数
264
+ 4. **权限迁移**:将页面级权限改为 Feature 级权限
2400
265
 
2401
- ## 许可证
266
+ ## 设计文档
2402
267
 
2403
- MIT License
268
+ 详细设计文档请参考 [DESIGN.md](./DESIGN.md)。
2404
269
 
2405
- ## 贡献
270
+ ## 旧版本
2406
271
 
2407
- 欢迎提交 Issue Pull Request!
272
+ 旧版本代码已移至 `src-old` 目录,文档移至 `README-old.md`。