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