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.
@@ -0,0 +1,559 @@
1
+ # HtmxAdminPlugin 快速参考指南
2
+
3
+ ## 目录
4
+
5
+ - [插件配置](#插件配置)
6
+ - [模块定义](#模块定义)
7
+ - [数据源](#数据源)
8
+ - [常用方法](#常用方法)
9
+ - [组件速查](#组件速查)
10
+ - [响应头速查](#响应头速查)
11
+ - [常见场景](#常见场景)
12
+
13
+ ## 插件配置
14
+
15
+ ### 基本配置
16
+
17
+ ```typescript
18
+ const adminPlugin = new HtmxAdminPlugin({
19
+ title: "管理后台",
20
+ prefix: "/admin",
21
+ logo: "/logo.png", // 可选
22
+ });
23
+ ```
24
+
25
+ ### 完整配置
26
+
27
+ ```typescript
28
+ const adminPlugin = new HtmxAdminPlugin({
29
+ title: "管理后台",
30
+ prefix: "/admin",
31
+ logo: "/logo.png",
32
+ homePath: "/admin/dashboard", // 首页路径,访问 /admin 时重定向到此路径
33
+ navigation: [
34
+ {
35
+ label: "用户管理",
36
+ moduleName: "users",
37
+ icon: "👥",
38
+ },
39
+ ],
40
+ getUserInfo: async (ctx) => ({
41
+ name: "管理员",
42
+ email: "admin@example.com",
43
+ }),
44
+ generateBreadcrumb: (currentPath, navItems) => [
45
+ { label: "首页", href: "/admin" },
46
+ { label: "当前页" },
47
+ ],
48
+ });
49
+ ```
50
+
51
+ ## 模块定义
52
+
53
+ ### List 模块
54
+
55
+ ```typescript
56
+ @Module("products", {
57
+ type: "list",
58
+ })
59
+ class ProductListModule extends ListPageModule<Product> {
60
+ constructor() {
61
+ super(productDatasource);
62
+ }
63
+
64
+ renderColumn(field: string, value: any, item: Product): any {
65
+ // 自定义列渲染
66
+ }
67
+
68
+ getActions(item: Product) {
69
+ return [
70
+ this.action.detail("查看"),
71
+ this.action.delete("删除"),
72
+ ];
73
+ }
74
+ }
75
+ ```
76
+
77
+ ### Detail 模块
78
+
79
+ ```typescript
80
+ @Module("users", {
81
+ type: "detail",
82
+ })
83
+ class UserDetailModule extends DetailPageModule<User> {
84
+ constructor() {
85
+ super(userDatasource);
86
+ }
87
+
88
+ getFieldLabel(field: string): string {
89
+ // 自定义字段标签
90
+ }
91
+
92
+ renderField(field: string, value: any, item: User): any {
93
+ // 自定义字段渲染
94
+ }
95
+
96
+ getFieldGroups(item: User) {
97
+ // 字段分组
98
+ }
99
+ }
100
+ ```
101
+
102
+ ### Form 模块
103
+
104
+ ```typescript
105
+ @Module("articles", {
106
+ type: "form",
107
+ })
108
+ class ArticleFormModule extends FormPageModule<Article> {
109
+ constructor() {
110
+ super(articleDatasource);
111
+ }
112
+
113
+ getFormFields(item: Article | null) {
114
+ return [
115
+ { name: "title", label: "标题", type: "text", required: true },
116
+ { name: "content", label: "内容", type: "textarea", required: true },
117
+ ];
118
+ }
119
+
120
+ validateFormData(data: Record<string, any>, item: Article | null): string | null {
121
+ // 表单验证
122
+ }
123
+ }
124
+ ```
125
+
126
+ ### Custom 模块
127
+
128
+ ```typescript
129
+ @Module("dashboard", {
130
+ type: "custom",
131
+ })
132
+ class DashboardModule extends PageModule {
133
+ async render() {
134
+ return <div>自定义内容</div>;
135
+ }
136
+ }
137
+ ```
138
+
139
+ ## 数据源
140
+
141
+ ### 内存数据源
142
+
143
+ ```typescript
144
+ const datasource = new MemoryListDatasource<User>([
145
+ { id: 1, name: "张三", email: "zhangsan@example.com" },
146
+ { id: 2, name: "李四", email: "lisi@example.com" },
147
+ ]);
148
+ ```
149
+
150
+ ### 自定义数据源
151
+
152
+ ```typescript
153
+ class DatabaseDatasource<T> implements ListDatasource<T> {
154
+ async getList(params: ListParams): Promise<ListResult<T>> {
155
+ // 数据库查询
156
+ }
157
+
158
+ async deleteItem(id: string | number): Promise<boolean> {
159
+ // 数据库删除
160
+ }
161
+ }
162
+ ```
163
+
164
+ ## 常用方法
165
+
166
+ ### PathHelper
167
+
168
+ ```typescript
169
+ // 在模块中使用(通过 this.paths 访问)
170
+ this.paths.detail(id) // 生成详情页路径
171
+ this.paths.edit(id) // 生成编辑页路径
172
+ this.paths.create() // 生成新建页路径
173
+ this.paths.delete(id) // 生成删除操作路径
174
+ this.paths.list() // 生成列表页路径
175
+ this.paths.detail(id, true) // Dialog 模式
176
+ ```
177
+
178
+ ### 错误处理
179
+
180
+ ```typescript
181
+ // 在模块的 render 或 __handle 方法中
182
+ this.context.sendError("错误标题", "错误消息");
183
+ this.context.sendWarning("警告标题", "警告消息");
184
+ this.context.sendInfo("信息标题", "信息消息");
185
+ this.context.sendSuccess("成功标题", "成功消息");
186
+
187
+ // 或使用通用方法
188
+ this.context.sendNotification("error", "错误标题", "错误消息");
189
+ ```
190
+
191
+ ### URL 控制
192
+
193
+ ```typescript
194
+ // 设置推送 URL
195
+ this.context.setPushUrl("/admin/users/list");
196
+
197
+ // 设置页面刷新
198
+ this.context.setRefresh(true);
199
+ ```
200
+
201
+ ## 组件速查
202
+
203
+ ### 页面组件
204
+
205
+ ```typescript
206
+ import { Components } from "imean-service-engine";
207
+ const { Detail, Form, PageHeader, EmptyState, Dialog, ErrorAlert } = Components;
208
+
209
+ <Detail item={item} fieldLabels={{...}} actions={[...]} />
210
+ <Form fields={[...]} submitUrl="..." method="POST" />
211
+ <PageHeader title="标题" description="描述" />
212
+ <EmptyState message="暂无数据" />
213
+ <Dialog title="标题">{content}</Dialog>
214
+ <ErrorAlert message="错误消息" type="error" />
215
+ ```
216
+
217
+ ### 布局组件
218
+
219
+ ```typescript
220
+ const { Card, Button, ActionButton, Pagination, FilterCard } = Components;
221
+
222
+ <Card>{content}</Card>
223
+ <Button variant="primary" href="..." hxGet="...">按钮</Button>
224
+ <ActionButton label="操作" href="..." method="DELETE" />
225
+ <Pagination page={1} totalPages={10} basePath="/admin/users/list" />
226
+ <FilterCard fields={[...]} />
227
+ ```
228
+
229
+ ### 表单组件
230
+
231
+ ```typescript
232
+ const { FormField, Input, Textarea, Select, DateInput } = Components;
233
+
234
+ <FormField id="name" label="姓名" required>
235
+ <Input name="name" placeholder="请输入姓名" />
236
+ </FormField>
237
+
238
+ <FormField id="bio" label="简介">
239
+ <Textarea name="bio" rows={4} />
240
+ </FormField>
241
+
242
+ <FormField id="status" label="状态">
243
+ <Select name="status" options={[...]} />
244
+ </FormField>
245
+
246
+ <FormField id="date" label="日期">
247
+ <DateInput type="date" name="date" />
248
+ </FormField>
249
+ ```
250
+
251
+ ## 响应头速查
252
+
253
+ ### HX-Retarget
254
+
255
+ 指定响应内容的目标容器:
256
+
257
+ - `#main-content`: 主内容区域(默认)
258
+ - `#dialog-container`: 对话框容器(dialog 模式)
259
+ - `#error-container`: 错误通知容器(错误响应)
260
+ - `#sidebar`: 侧边栏(侧边栏切换)
261
+
262
+ ### HX-Push-Url
263
+
264
+ 控制是否更新 URL:
265
+
266
+ - 正常模式:设置 `HX-Push-Url` 更新 URL
267
+ - Dialog 模式:不设置 `HX-Push-Url`,保持当前 URL
268
+
269
+ ### HX-Reswap
270
+
271
+ 控制交换方式:
272
+
273
+ - `innerHTML`: 替换内容(默认)
274
+ - `beforeend`: 追加内容(错误消息)
275
+
276
+ ## 常见场景
277
+
278
+ ### 场景 1: 列表页自定义列渲染
279
+
280
+ ```typescript
281
+ renderColumn(field: string, value: any, item: T): any {
282
+ if (field === "status") {
283
+ return (
284
+ <span className={`px-2 py-1 rounded ${getStatusColor(value)}`}>
285
+ {getStatusLabel(value)}
286
+ </span>
287
+ );
288
+ }
289
+ return value;
290
+ }
291
+ ```
292
+
293
+ ### 场景 2: 详情页字段分组
294
+
295
+ ```typescript
296
+ getFieldGroups(item: T) {
297
+ return [
298
+ { title: "基本信息", fields: ["id", "name", "email"] },
299
+ { title: "其他信息", fields: ["createdAt", "updatedAt"] },
300
+ ];
301
+ }
302
+ ```
303
+
304
+ ### 场景 3: 表单验证
305
+
306
+ ```typescript
307
+ validateFormData(data: Record<string, any>, item: T | null): string | null {
308
+ if (!data.name || data.name.trim().length === 0) {
309
+ return "名称不能为空";
310
+ }
311
+ if (data.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
312
+ return "邮箱格式不正确";
313
+ }
314
+ return null;
315
+ }
316
+ ```
317
+
318
+ ### 场景 4: Dialog 模式
319
+
320
+ ```typescript
321
+ // 在操作按钮中使用
322
+ getActions(item: T) {
323
+ return [
324
+ { label: "查看", href: this.paths.detail(item.id) }, // 正常模式
325
+ { label: "快速查看", href: this.paths.detail(item.id, true) }, // Dialog 模式
326
+ ];
327
+ }
328
+ ```
329
+
330
+ ### 场景 5: 自定义页面
331
+
332
+ ```typescript
333
+ async render() {
334
+ const { Card, Button, PageHeader, StatCard } = Components;
335
+
336
+ return (
337
+ <div>
338
+ <PageHeader title="仪表盘" />
339
+ <div className="grid grid-cols-3 gap-4">
340
+ <StatCard title="总数" value="100" />
341
+ <StatCard title="今日" value="10" />
342
+ <StatCard title="待处理" value="5" />
343
+ </div>
344
+ </div>
345
+ );
346
+ }
347
+ ```
348
+
349
+ ### 场景 6: 错误处理
350
+
351
+ ```typescript
352
+ async render() {
353
+ try {
354
+ const result = await processData();
355
+ this.context.sendSuccess("操作成功", "数据已处理");
356
+ return <div>{result}</div>;
357
+ } catch (error) {
358
+ this.context.sendError("操作失败", error.message);
359
+ return <div>处理失败</div>;
360
+ }
361
+ }
362
+ ```
363
+
364
+ ### 场景 7: 统计信息
365
+
366
+ ```typescript
367
+ async getStats(params: ListParams) {
368
+ const result = await this.getList({ ...params, pageSize: 1000 });
369
+
370
+ return [
371
+ {
372
+ title: "总数",
373
+ value: result.total,
374
+ iconColor: "blue" as const,
375
+ },
376
+ {
377
+ title: "已完成",
378
+ value: result.items.filter(item => item.status === "completed").length,
379
+ change: 12.5,
380
+ changeLabel: "%",
381
+ iconColor: "green" as const,
382
+ },
383
+ ];
384
+ }
385
+ ```
386
+
387
+ ### 场景 8: 筛选器
388
+
389
+ ```typescript
390
+ getFilters(params: ListParams) {
391
+ return [
392
+ {
393
+ name: "status",
394
+ label: "状态",
395
+ options: [
396
+ { value: "pending", label: "待处理" },
397
+ { value: "completed", label: "已完成" },
398
+ ],
399
+ value: params.filters?.status,
400
+ },
401
+ ];
402
+ }
403
+ ```
404
+
405
+ ## 类型定义速查
406
+
407
+ ### 模块选项
408
+
409
+ ```typescript
410
+ interface HtmxAdminModuleOptions {
411
+ type: "list" | "detail" | "form" | "custom";
412
+ title?: string; // 模块标题(可选,默认使用模块名)
413
+ description?: string; // 模块描述(可选)
414
+ }
415
+ ```
416
+
417
+ ### 列表参数
418
+
419
+ ```typescript
420
+ interface ListParams {
421
+ page?: number;
422
+ pageSize?: number;
423
+ sortBy?: string;
424
+ sortOrder?: "asc" | "desc";
425
+ filters?: Record<string, any>;
426
+ }
427
+ ```
428
+
429
+ ### 列表结果
430
+
431
+ ```typescript
432
+ interface ListResult<T> {
433
+ items: T[];
434
+ total: number;
435
+ page: number;
436
+ pageSize: number;
437
+ totalPages: number;
438
+ }
439
+ ```
440
+
441
+ ### 表单字段
442
+
443
+ ```typescript
444
+ interface FormField {
445
+ name: string;
446
+ label: string;
447
+ type?: "text" | "email" | "number" | "textarea" | "select" | "date" | "datetime-local";
448
+ required?: boolean;
449
+ placeholder?: string;
450
+ options?: Array<{ value: string | number; label: string }>;
451
+ }
452
+ ```
453
+
454
+ ## 路由速查
455
+
456
+ ### List 模块
457
+
458
+ - `GET /{basePath}/list`: 列表页
459
+ - `DELETE /{basePath}/:id`: 删除操作
460
+
461
+ ### Detail 模块
462
+
463
+ - `GET /{basePath}/detail/:id`: 详情页
464
+ - `DELETE /{basePath}/detail/:id`: 删除操作
465
+
466
+ ### Form 模块
467
+
468
+ - `GET /{basePath}/new`: 新建表单
469
+ - `POST /{basePath}`: 创建操作
470
+ - `GET /{basePath}/edit/:id`: 编辑表单
471
+ - `PUT /{basePath}/:id`: 更新操作
472
+ - `DELETE /{basePath}/:id`: 删除操作(如果数据源支持)
473
+
474
+ ### Dialog 模式
475
+
476
+ - `GET /{basePath}/detail/:id?dialog=true`: 对话框详情
477
+ - `GET /{basePath}/new?dialog=true`: 对话框新建
478
+ - `GET /{basePath}/edit/:id?dialog=true`: 对话框编辑
479
+
480
+ ## 调试技巧
481
+
482
+ ### 1. 查看响应头
483
+
484
+ 在浏览器开发者工具的 Network 标签中查看响应头:
485
+
486
+ - `HX-Retarget`: 响应目标
487
+ - `HX-Push-Url`: URL 更新
488
+ - `HX-Reswap`: 交换方式
489
+
490
+ ### 2. 查看 HTMX 请求
491
+
492
+ HTMX 请求会包含以下请求头:
493
+
494
+ - `HX-Request: true`: 标识 HTMX 请求
495
+ - `HX-Target`: 请求目标(如果指定)
496
+ - `Referer`: 来源页面
497
+
498
+ ### 3. 查看 OOB 元素
499
+
500
+ 在响应 HTML 中查找 `hx-swap-oob` 属性:
501
+
502
+ ```html
503
+ <div id="nav-item-xxx" hx-swap-oob="true">...</div>
504
+ <div id="error-container" hx-swap-oob="innerHTML"></div>
505
+ ```
506
+
507
+ ### 4. 调试 Dialog
508
+
509
+ - 检查 URL 是否包含 `dialog=true`
510
+ - 检查 `#dialog-container` 是否存在
511
+ - 检查响应头 `HX-Retarget` 是否为 `#dialog-container`
512
+
513
+ ### 5. 调试错误
514
+
515
+ - 检查错误响应状态码
516
+ - 检查 `#error-container` 是否显示错误
517
+ - 检查错误消息格式是否正确
518
+
519
+ ## 常见问题
520
+
521
+ ### Q: Dialog 模式不工作?
522
+
523
+ A: 检查:
524
+ 1. URL 是否包含 `dialog=true`
525
+ 2. 响应头 `HX-Retarget` 是否为 `#dialog-container`
526
+ 3. `#dialog-container` 是否存在于 DOM 中
527
+
528
+ ### Q: 错误消息不显示?
529
+
530
+ A: 检查:
531
+ 1. 是否使用 `createErrorResponse`
532
+ 2. 响应头 `HX-Retarget` 是否为 `#error-container`
533
+ 3. `#error-container` 是否存在于 DOM 中
534
+
535
+ ### Q: 导航不更新?
536
+
537
+ A: 检查:
538
+ 1. OOB 响应是否包含导航更新元素
539
+ 2. 导航项的 `id` 是否正确
540
+ 3. `hx-swap-oob` 属性是否正确
541
+
542
+ ### Q: URL 状态不保存?
543
+
544
+ A: 检查:
545
+ 1. 是否设置了 `HX-Push-Url` 响应头
546
+ 2. URL 查询参数是否正确构建
547
+ 3. 筛选器和分页是否使用正确的参数名
548
+
549
+ ## 最佳实践
550
+
551
+ 1. **使用 PathHelper**: 通过 `this.paths` 简化路径生成
552
+ 2. **统一错误处理**: 使用 `context.sendNotification()` 发送通知
553
+ 3. **类型安全**: 充分利用 TypeScript 类型
554
+ 4. **组件复用**: 使用 `Components` 命名空间中的组件
555
+ 5. **响应式设计**: 使用 Tailwind CSS 响应式类
556
+ 6. **性能优化**: 使用分页和筛选减少数据量
557
+ 7. **安全性**: 在数据源层面实现权限检查
558
+ 8. **上下文访问**: 通过 `this.context` 访问请求状态和 helper 方法
559
+
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "imean-service-engine-htmx-plugin",
3
+ "version": "1.0.0",
4
+ "description": "HtmxAdminPlugin for IMean Service Engine",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsup",
9
+ "dev": "tsx examples/index.ts",
10
+ "test": "vitest --run",
11
+ "test:ui": "vitest --ui",
12
+ "test:coverage": "vitest --run --coverage",
13
+ "prepublishOnly": "npm run build && npm run test"
14
+ },
15
+ "keywords": [
16
+ "microservice",
17
+ "hono",
18
+ "framework",
19
+ "typescript"
20
+ ],
21
+ "files": [
22
+ "dist",
23
+ "docs",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "author": "",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@hono/node-server": "^1.19.7",
31
+ "hono": "^4.10.8",
32
+ "imean-service-engine": "^2.3.0",
33
+ "prettier": "^3.7.4",
34
+ "winston": "^3.19.0",
35
+ "zod": "^4.1.13"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^25.0.1",
39
+ "@vitest/coverage-v8": "4.0.15",
40
+ "@vitest/ui": "^4.0.15",
41
+ "imean-service-client": "^1.6.0",
42
+ "tsup": "^8.5.1",
43
+ "tsx": "^4.21.0",
44
+ "typescript": "^5.9.3",
45
+ "vitest": "^4.0.15"
46
+ }
47
+ }