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 +183 -2318
- package/dist/index.d.mts +647 -1353
- package/dist/index.d.ts +647 -1353
- package/dist/index.js +3176 -3505
- package/dist/index.mjs +3164 -3498
- package/docs/design-principles.md +102 -0
- package/docs/quick-reference.md +44 -0
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -1,2407 +1,272 @@
|
|
|
1
|
-
# HtmxAdminPlugin
|
|
1
|
+
# HtmxAdminPlugin v2
|
|
2
2
|
|
|
3
|
-
基于
|
|
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
|
-
|
|
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
|
-
###
|
|
16
|
+
### 安装
|
|
113
17
|
|
|
114
|
-
```
|
|
115
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
231
|
-
|
|
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(
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
509
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
65
|
+
## 使用模式
|
|
532
66
|
|
|
533
|
-
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
75
|
+
id: z.number().optional(),
|
|
76
|
+
title: z.string().min(1).describe("标题"),
|
|
77
|
+
content: z.string().min(10).describe("内容"),
|
|
542
78
|
});
|
|
543
79
|
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
553
|
-
|
|
84
|
+
// 数据操作函数
|
|
85
|
+
async function getArticleList(params: ListParams): Promise<ListResult<Article>> {
|
|
86
|
+
// 从数据库或其他数据源获取列表
|
|
554
87
|
}
|
|
555
|
-
```
|
|
556
88
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
606
|
-
|
|
607
|
-
type: "form",
|
|
608
|
-
})
|
|
609
|
-
class ArticleFormModule extends FormPageModule<Article> {
|
|
105
|
+
// 定义 PageModel
|
|
106
|
+
class ArticlePageModel extends PageModel<Article> {
|
|
610
107
|
constructor() {
|
|
611
|
-
super(
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
placeholder: "article-title",
|
|
127
|
+
// 可选:遮罩关闭配置(编辑/创建时建议设为 false,防止误操作)
|
|
128
|
+
closeOnBackdropClick: {
|
|
129
|
+
edit: false,
|
|
130
|
+
create: false,
|
|
131
|
+
detail: true,
|
|
629
132
|
},
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
required: true,
|
|
635
|
-
placeholder: "请输入文章内容",
|
|
133
|
+
// 可选:动态标题和描述配置(使用实际数据的标题作为页面标题)
|
|
134
|
+
getTitles: {
|
|
135
|
+
detail: (item) => item.title, // 详情页使用文章标题
|
|
136
|
+
edit: (item) => `编辑:${item.title}`, // 编辑页使用 "编辑:{文章标题}"
|
|
636
137
|
},
|
|
637
|
-
{
|
|
638
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
使用 Zod Schema 可以自动生成表单字段和验证规则:
|
|
146
|
+
### 无数据模型的页面
|
|
672
147
|
|
|
673
148
|
```typescript
|
|
674
|
-
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
171
|
+
### 只读场景
|
|
915
172
|
|
|
916
173
|
```typescript
|
|
917
|
-
|
|
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(
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
//
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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
|
-
|
|
971
|
-
|
|
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
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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
|
-
|
|
259
|
+
从 v1 迁移到 v2:
|
|
2394
260
|
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
- ✨ 数据源抽象
|
|
261
|
+
1. **移除数据源**:将数据源方法改为函数
|
|
262
|
+
2. **移除装饰器**:使用构造函数注册
|
|
263
|
+
3. **合并元数据**:将 `getTitle`、`getDescription` 等合并为构造函数参数
|
|
264
|
+
4. **权限迁移**:将页面级权限改为 Feature 级权限
|
|
2400
265
|
|
|
2401
|
-
##
|
|
266
|
+
## 设计文档
|
|
2402
267
|
|
|
2403
|
-
|
|
268
|
+
详细设计文档请参考 [DESIGN.md](./DESIGN.md)。
|
|
2404
269
|
|
|
2405
|
-
##
|
|
270
|
+
## 旧版本
|
|
2406
271
|
|
|
2407
|
-
|
|
272
|
+
旧版本代码已移至 `src-old` 目录,文档移至 `README-old.md`。
|