imean-service-engine-htmx-plugin 1.2.0 → 2.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,102 @@
1
+ # 设计原则
2
+
3
+ ## 后端控制交换目标
4
+
5
+ ### 核心原则
6
+
7
+ **交换目标的控制应该完全交由后端控制,前端不应该设置 `hx-target` 和 `hx-swap` 属性。**
8
+
9
+ ### 为什么?
10
+
11
+ 1. **简洁性**:提供一个简单易用的后台管理框架,代码应该简洁
12
+ 2. **灵活性**:后端可以根据请求参数(如 `?dialog=true`)动态决定交换目标
13
+ 3. **错误处理**:如果请求失败,后端可以返回错误通知而不替换任何内容,不影响用户的现有工作状态
14
+
15
+ ### 示例场景
16
+
17
+ #### 场景 1:弹框模式
18
+
19
+ 用户点击一个链接,链接是 `?dialog=true`,后端应该:
20
+ - 检测到 `dialog=true` 参数
21
+ - 设置 `HX-Retarget: #dialog-container` 和 `HX-Reswap: innerHTML`
22
+ - 返回 Dialog 组件包装的内容
23
+
24
+ **前端代码**:
25
+ ```tsx
26
+ // ✅ 正确:使用 hx-get 触发 HTMX 请求,保留 href 用于普通跳转或新窗口打开
27
+ <a
28
+ href="/admin/articles/detail/1?dialog=true"
29
+ hx-get="/admin/articles/detail/1?dialog=true"
30
+ >
31
+ 在弹框中打开文章详情
32
+ </a>
33
+
34
+ // ❌ 错误:不应该在前端设置 hx-target,后端会根据 ?dialog=true 自动设置
35
+ <a
36
+ href="/admin/articles/detail/1?dialog=true"
37
+ hx-get="/admin/articles/detail/1?dialog=true"
38
+ hx-target="#dialog-container"
39
+ hx-swap="innerHTML"
40
+ >
41
+ 在弹框中打开文章详情
42
+ </a>
43
+
44
+ // ✅ 正确:只使用 href,用于普通跳转或新窗口打开(右键菜单)
45
+ <a href="/admin/articles/detail/1">
46
+ 在新窗口打开文章详情
47
+ </a>
48
+ ```
49
+
50
+ #### 场景 2:错误处理
51
+
52
+ 如果请求失败(例如权限不足、服务器错误等),后端应该:
53
+ - 返回错误通知(OOB swap)
54
+ - 设置 `HX-Reswap: none` 阻止替换任何内容
55
+ - 不影响用户的现有工作状态
56
+
57
+ **后端代码**:
58
+ ```typescript
59
+ // HTMX 请求失败时
60
+ if (isHtmxRequest) {
61
+ return ctx.html(
62
+ <>
63
+ <div id="error-container" hx-swap-oob="beforeend">
64
+ <ErrorAlert
65
+ type="error"
66
+ title="请求失败"
67
+ message={errorMessage}
68
+ />
69
+ </div>
70
+ <title>错误</title>
71
+ </>,
72
+ 500,
73
+ {
74
+ "HX-Reswap": "none", // 关键:不替换任何内容
75
+ }
76
+ );
77
+ }
78
+ ```
79
+
80
+ ### 实现细节
81
+
82
+ 1. **检测弹框模式**:
83
+ - 通过 `?dialog=true` 查询参数
84
+ - 通过 `HX-Target: #dialog-container` 请求头
85
+ - 通过 `Referer` 头中的 `dialog=true`
86
+
87
+ 2. **设置响应头**:
88
+ - 弹框模式:`HX-Retarget: #dialog-container`, `HX-Reswap: innerHTML`
89
+ - 普通模式:`HX-Retarget: #main-content`, `HX-Reswap: outerHTML`
90
+ - 错误/通知:`HX-Reswap: none`(不替换任何内容)
91
+
92
+ 3. **错误处理**:
93
+ - 所有错误都应该返回通知而不是替换内容
94
+ - 使用 OOB swap 将通知插入到 `#error-container`
95
+ - 设置 `HX-Reswap: none` 阻止内容替换
96
+
97
+ ### 优势
98
+
99
+ 1. **代码简洁**:前端代码不需要关心交换目标,只需要提供链接
100
+ 2. **灵活控制**:后端可以根据业务逻辑动态决定交换目标
101
+ 3. **错误友好**:错误不会破坏用户的当前工作状态
102
+ 4. **易于维护**:交换逻辑集中在一个地方,易于修改和测试
@@ -481,6 +481,50 @@ interface FormField {
481
481
  - `GET /{basePath}/new?dialog=true`: 对话框新建
482
482
  - `GET /{basePath}/edit/:id?dialog=true`: 对话框编辑
483
483
 
484
+ ### 弹窗配置
485
+
486
+ #### 弹窗大小
487
+
488
+ 支持的大小选项:
489
+ - `"sm"`: 小(max-w-md,约 448px)
490
+ - `"md"`: 中(max-w-lg,约 512px)
491
+ - `"lg"`: 大(max-w-2xl,约 672px)- **默认值**
492
+ - `"xl"`: 超大(max-w-4xl,约 896px)
493
+ - `"full"`: 全屏(max-w-7xl,约 1280px)
494
+
495
+ #### 遮罩关闭配置
496
+
497
+ - `closeOnBackdropClick: true`(默认):允许点击遮罩关闭
498
+ - `closeOnBackdropClick: false`:禁止点击遮罩关闭,只能通过关闭按钮关闭
499
+
500
+ #### 配置示例
501
+
502
+ ```typescript
503
+ // 通过 crud() 方法配置
504
+ this.features.crud({
505
+ permissionPrefix: "articles",
506
+ // ... 其他配置
507
+ dialogSizes: {
508
+ detail: "xl",
509
+ edit: "xl",
510
+ create: "xl",
511
+ },
512
+ closeOnBackdropClick: {
513
+ edit: false, // 编辑时禁止点击遮罩关闭
514
+ create: false, // 创建时禁止点击遮罩关闭
515
+ detail: true, // 详情页允许点击遮罩关闭
516
+ },
517
+ });
518
+
519
+ // 通过 CustomFeature 配置
520
+ this.features.custom("my-feature", new CustomFeature({
521
+ name: "my-feature",
522
+ dialogSize: "xl",
523
+ closeOnBackdropClick: false,
524
+ // ... 其他配置
525
+ }));
526
+ ```
527
+
484
528
  ## 调试技巧
485
529
 
486
530
  ### 1. 查看响应头
package/package.json CHANGED
@@ -1,15 +1,19 @@
1
1
  {
2
2
  "name": "imean-service-engine-htmx-plugin",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "description": "HtmxAdminPlugin for IMean Service Engine",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
8
  "build": "tsup",
9
- "dev": "tsx examples/index.ts",
9
+ "dev": "tsx examples-v2/index.ts",
10
10
  "test": "vitest --run",
11
11
  "test:ui": "vitest --ui",
12
12
  "test:coverage": "vitest --run --coverage",
13
+ "test:unit": "vitest --run --project unit",
14
+ "test:e2e": "playwright test",
15
+ "test:e2e:ui": "playwright test --ui",
16
+ "test:e2e:headed": "playwright test --headed",
13
17
  "prepublishOnly": "npm run build && npm run test"
14
18
  },
15
19
  "keywords": [
@@ -35,10 +39,13 @@
35
39
  "zod": "^4.1.13"
36
40
  },
37
41
  "devDependencies": {
42
+ "@playwright/test": "^1.57.0",
38
43
  "@types/node": "^25.0.1",
44
+ "@vitest/browser-playwright": "^4.0.16",
39
45
  "@vitest/coverage-v8": "4.0.15",
40
46
  "@vitest/ui": "^4.0.15",
41
47
  "imean-service-client": "^1.6.0",
48
+ "playwright": "^1.57.0",
42
49
  "tsup": "^8.5.1",
43
50
  "tsx": "^4.21.0",
44
51
  "typescript": "^5.9.3",