imean-service-engine-htmx-plugin 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2373 -0
- package/dist/index.d.mts +1484 -0
- package/dist/index.d.ts +1484 -0
- package/dist/index.js +3099 -0
- package/dist/index.mjs +3087 -0
- package/docs/README.md +87 -0
- package/docs/architecture.md +564 -0
- package/docs/quick-reference.md +559 -0
- package/package.json +47 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,3087 @@
|
|
|
1
|
+
import { jsx, jsxs, Fragment } from 'hono/jsx/jsx-runtime';
|
|
2
|
+
import { PluginPriority, logger } from 'imean-service-engine';
|
|
3
|
+
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
function safeRender(value) {
|
|
10
|
+
if (value === null || value === void 0) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
if (typeof value === "object" && value !== null && "type" in value) {
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
if (typeof value === "string") {
|
|
17
|
+
if (/<[^>]+>/.test(value)) {
|
|
18
|
+
return /* @__PURE__ */ jsx("span", { dangerouslySetInnerHTML: { __html: value } });
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
var Button = (props) => {
|
|
25
|
+
const {
|
|
26
|
+
children,
|
|
27
|
+
variant = "primary",
|
|
28
|
+
size = "md",
|
|
29
|
+
disabled = false,
|
|
30
|
+
className = "",
|
|
31
|
+
hxGet,
|
|
32
|
+
hxPost,
|
|
33
|
+
hxPut,
|
|
34
|
+
hxDelete,
|
|
35
|
+
hxTarget,
|
|
36
|
+
hxSwap,
|
|
37
|
+
hxPushUrl,
|
|
38
|
+
hxIndicator,
|
|
39
|
+
hxConfirm,
|
|
40
|
+
hxHeaders,
|
|
41
|
+
...rest
|
|
42
|
+
} = props;
|
|
43
|
+
const baseClasses = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2";
|
|
44
|
+
const variantClasses = {
|
|
45
|
+
primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
|
|
46
|
+
secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500",
|
|
47
|
+
danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
|
|
48
|
+
ghost: "bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-500"
|
|
49
|
+
};
|
|
50
|
+
const sizeClasses = {
|
|
51
|
+
sm: "px-3 py-1.5 text-sm",
|
|
52
|
+
md: "px-4 py-2 text-sm",
|
|
53
|
+
lg: "px-6 py-3 text-base"
|
|
54
|
+
};
|
|
55
|
+
const classes = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${disabled ? "opacity-50 cursor-not-allowed" : ""} ${className}`;
|
|
56
|
+
const htmxAttrs = {};
|
|
57
|
+
if (hxGet) htmxAttrs["hx-get"] = hxGet;
|
|
58
|
+
if (hxPost) htmxAttrs["hx-post"] = hxPost;
|
|
59
|
+
if (hxPut) htmxAttrs["hx-put"] = hxPut;
|
|
60
|
+
if (hxDelete) htmxAttrs["hx-delete"] = hxDelete;
|
|
61
|
+
if (hxTarget) htmxAttrs["hx-target"] = hxTarget;
|
|
62
|
+
if (hxSwap) htmxAttrs["hx-swap"] = hxSwap;
|
|
63
|
+
if (hxPushUrl !== void 0)
|
|
64
|
+
htmxAttrs["hx-push-url"] = hxPushUrl === true ? "true" : hxPushUrl;
|
|
65
|
+
if (hxIndicator) htmxAttrs["hx-indicator"] = hxIndicator;
|
|
66
|
+
if (hxConfirm) htmxAttrs["hx-confirm"] = hxConfirm;
|
|
67
|
+
if (hxHeaders) htmxAttrs["hx-headers"] = hxHeaders;
|
|
68
|
+
const Tag = hxGet || hxPost || hxPut || hxDelete ? "a" : "button";
|
|
69
|
+
const href = rest.href ?? hxGet ?? "#";
|
|
70
|
+
return /* @__PURE__ */ jsx(
|
|
71
|
+
Tag,
|
|
72
|
+
{
|
|
73
|
+
className: classes,
|
|
74
|
+
disabled,
|
|
75
|
+
href,
|
|
76
|
+
...htmxAttrs,
|
|
77
|
+
...rest,
|
|
78
|
+
children
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
var Card = (props) => {
|
|
83
|
+
const {
|
|
84
|
+
children,
|
|
85
|
+
title,
|
|
86
|
+
className = "",
|
|
87
|
+
shadow = true,
|
|
88
|
+
bordered = false,
|
|
89
|
+
noPadding = false
|
|
90
|
+
} = props;
|
|
91
|
+
const baseClasses = "bg-white rounded-lg";
|
|
92
|
+
const shadowClass = shadow ? "shadow-sm hover:shadow-md transition-shadow" : "";
|
|
93
|
+
const borderClass = bordered ? "border border-gray-200" : "";
|
|
94
|
+
const paddingClass = noPadding ? "" : "p-6";
|
|
95
|
+
return /* @__PURE__ */ jsxs(
|
|
96
|
+
"div",
|
|
97
|
+
{
|
|
98
|
+
className: `${baseClasses} ${shadowClass} ${borderClass} ${className}`,
|
|
99
|
+
children: [
|
|
100
|
+
title && /* @__PURE__ */ jsx("div", { className: "px-6 py-4 border-b border-gray-200", children: /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title }) }),
|
|
101
|
+
/* @__PURE__ */ jsx("div", { className: noPadding ? "" : paddingClass, children })
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
var PageHeader = (props) => {
|
|
107
|
+
const { title, description, actions, className = "" } = props;
|
|
108
|
+
return /* @__PURE__ */ jsx("div", { className: `mb-6 ${className}`, children: /* @__PURE__ */ jsxs("div", { className: "flex justify-between items-start", children: [
|
|
109
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
110
|
+
/* @__PURE__ */ jsx("h1", { className: "text-3xl font-bold text-gray-900 mb-2", children: title }),
|
|
111
|
+
description && /* @__PURE__ */ jsx("p", { className: "text-gray-600", children: description })
|
|
112
|
+
] }),
|
|
113
|
+
actions && /* @__PURE__ */ jsx("div", { className: "flex gap-2", children: actions })
|
|
114
|
+
] }) });
|
|
115
|
+
};
|
|
116
|
+
var Detail = (props) => {
|
|
117
|
+
const {
|
|
118
|
+
item,
|
|
119
|
+
fieldLabels = {},
|
|
120
|
+
fieldRenderers = {},
|
|
121
|
+
fieldGroups,
|
|
122
|
+
visibleFields,
|
|
123
|
+
actions,
|
|
124
|
+
title = "\u8BE6\u60C5",
|
|
125
|
+
isDialog = false
|
|
126
|
+
} = props;
|
|
127
|
+
const getFieldLabel = (field) => {
|
|
128
|
+
return fieldLabels[field] || field;
|
|
129
|
+
};
|
|
130
|
+
const renderFieldValue = (field, value) => {
|
|
131
|
+
if (fieldRenderers[field]) {
|
|
132
|
+
return safeRender(fieldRenderers[field](value, item));
|
|
133
|
+
}
|
|
134
|
+
if (value === null || value === void 0) {
|
|
135
|
+
return /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "-" });
|
|
136
|
+
}
|
|
137
|
+
if (typeof value === "boolean") {
|
|
138
|
+
return value ? "\u662F" : "\u5426";
|
|
139
|
+
}
|
|
140
|
+
if (typeof value === "object") {
|
|
141
|
+
return /* @__PURE__ */ jsx("pre", { className: "text-sm", children: JSON.stringify(value, null, 2) });
|
|
142
|
+
}
|
|
143
|
+
return String(value);
|
|
144
|
+
};
|
|
145
|
+
const getAllFields = () => {
|
|
146
|
+
if (visibleFields) {
|
|
147
|
+
return visibleFields;
|
|
148
|
+
}
|
|
149
|
+
return Object.keys(item);
|
|
150
|
+
};
|
|
151
|
+
const renderField = (field) => {
|
|
152
|
+
const value = item[field];
|
|
153
|
+
return /* @__PURE__ */ jsxs("div", { className: "py-4", children: [
|
|
154
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2", children: getFieldLabel(field) }),
|
|
155
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm text-gray-900", children: renderFieldValue(field, value) })
|
|
156
|
+
] });
|
|
157
|
+
};
|
|
158
|
+
const renderFieldGroup = (group) => {
|
|
159
|
+
const fields2 = group.fields.filter((field) => item.hasOwnProperty(field));
|
|
160
|
+
if (fields2.length === 0) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return /* @__PURE__ */ jsx(Card, { title: group.title, children: /* @__PURE__ */ jsx("div", { className: "divide-y divide-gray-200", children: fields2.map(renderField) }) });
|
|
164
|
+
};
|
|
165
|
+
const fields = getAllFields();
|
|
166
|
+
const actionButtons = actions && actions.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex gap-2", children: actions.map((action, index) => {
|
|
167
|
+
const href = typeof action.href === "string" ? action.href : action.href(item);
|
|
168
|
+
const isDelete = action.method === "delete";
|
|
169
|
+
const target = isDialog ? "#dialog-container" : "#main-content";
|
|
170
|
+
return /* @__PURE__ */ jsx(
|
|
171
|
+
Button,
|
|
172
|
+
{
|
|
173
|
+
variant: isDelete ? "danger" : "primary",
|
|
174
|
+
href,
|
|
175
|
+
hxGet: !isDelete ? href : void 0,
|
|
176
|
+
hxDelete: isDelete ? href : void 0,
|
|
177
|
+
hxConfirm: isDelete ? "\u786E\u5B9A\u8981\u5220\u9664\u5417\uFF1F" : void 0,
|
|
178
|
+
hxHeaders: isDelete ? JSON.stringify({ "HX-Target": target }) : void 0,
|
|
179
|
+
children: action.label
|
|
180
|
+
},
|
|
181
|
+
index
|
|
182
|
+
);
|
|
183
|
+
}) }) : null;
|
|
184
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
185
|
+
!isDialog && /* @__PURE__ */ jsx(PageHeader, { title, actions: actionButtons }),
|
|
186
|
+
isDialog && actionButtons && /* @__PURE__ */ jsx("div", { className: "mb-4 flex justify-end", children: actionButtons }),
|
|
187
|
+
fieldGroups && fieldGroups.length > 0 ? (
|
|
188
|
+
// 分组显示
|
|
189
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-6", children: fieldGroups.map(renderFieldGroup) })
|
|
190
|
+
) : (
|
|
191
|
+
// 平铺显示
|
|
192
|
+
/* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx("div", { className: "divide-y divide-gray-200", children: fields.map(renderField) }) })
|
|
193
|
+
)
|
|
194
|
+
] });
|
|
195
|
+
return /* @__PURE__ */ jsx("div", { children: content });
|
|
196
|
+
};
|
|
197
|
+
function DetailContent(props) {
|
|
198
|
+
const {
|
|
199
|
+
item,
|
|
200
|
+
fieldLabels,
|
|
201
|
+
fieldRenderers,
|
|
202
|
+
fieldGroups,
|
|
203
|
+
visibleFields,
|
|
204
|
+
actions,
|
|
205
|
+
title,
|
|
206
|
+
isDialog
|
|
207
|
+
} = props;
|
|
208
|
+
const detailActions = actions && actions.length > 0 ? actions.map((action) => ({
|
|
209
|
+
label: action.label,
|
|
210
|
+
href: typeof action.href === "string" ? action.href : ((item2) => action.href(item2)),
|
|
211
|
+
method: action.method,
|
|
212
|
+
class: action.class
|
|
213
|
+
})) : void 0;
|
|
214
|
+
const convertedFieldRenderers = fieldRenderers ? Object.keys(fieldRenderers).reduce((acc, key) => {
|
|
215
|
+
acc[key] = (value, item2) => fieldRenderers[key](value, item2);
|
|
216
|
+
return acc;
|
|
217
|
+
}, {}) : void 0;
|
|
218
|
+
return /* @__PURE__ */ jsx(
|
|
219
|
+
Detail,
|
|
220
|
+
{
|
|
221
|
+
item,
|
|
222
|
+
fieldLabels,
|
|
223
|
+
fieldRenderers: convertedFieldRenderers,
|
|
224
|
+
fieldGroups,
|
|
225
|
+
visibleFields,
|
|
226
|
+
actions: detailActions,
|
|
227
|
+
title,
|
|
228
|
+
isDialog
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/utils/path.ts
|
|
234
|
+
var PathHelper = class {
|
|
235
|
+
basePath;
|
|
236
|
+
constructor(basePath) {
|
|
237
|
+
this.basePath = basePath;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* 详情页动作
|
|
241
|
+
* 生成路径: {basePath}/detail/{id}
|
|
242
|
+
* @param label 按钮标签
|
|
243
|
+
* @param dialog 是否在对话框中打开
|
|
244
|
+
*/
|
|
245
|
+
detail(id, dialog = false) {
|
|
246
|
+
const url = `${this.basePath}/detail/${id}`;
|
|
247
|
+
return dialog ? `${url}?dialog=true` : url;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* 编辑页动作
|
|
251
|
+
* 生成路径: {basePath}/edit/{id}
|
|
252
|
+
* @param dialog 是否在对话框中打开
|
|
253
|
+
*/
|
|
254
|
+
edit(id, dialog = false) {
|
|
255
|
+
const url = `${this.basePath}/edit/${id}`;
|
|
256
|
+
return dialog ? `${url}?dialog=true` : url;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* 删除动作
|
|
260
|
+
* 生成路径: {basePath}/detail/{id},方法: DELETE
|
|
261
|
+
*/
|
|
262
|
+
delete(id) {
|
|
263
|
+
return `${this.basePath}/detail/${id}`;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* 新建页动作
|
|
267
|
+
* 生成路径: {basePath}/new
|
|
268
|
+
* @param label 按钮标签
|
|
269
|
+
* @param dialog 是否在对话框中打开
|
|
270
|
+
*/
|
|
271
|
+
create(dialog = false) {
|
|
272
|
+
const url = dialog ? `${this.basePath}/new?dialog=true` : `${this.basePath}/new`;
|
|
273
|
+
return url;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* 列表页动作
|
|
277
|
+
* 生成路径: {basePath}/list
|
|
278
|
+
*/
|
|
279
|
+
list() {
|
|
280
|
+
return `${this.basePath}/list`;
|
|
281
|
+
}
|
|
282
|
+
base() {
|
|
283
|
+
return this.basePath;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
function moduleNameToPath(moduleName) {
|
|
287
|
+
if (moduleName === moduleName.toLowerCase() && !/[A-Z]/.test(moduleName)) {
|
|
288
|
+
return `/${moduleName}`;
|
|
289
|
+
}
|
|
290
|
+
const path = moduleName.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
|
|
291
|
+
return `/${path}`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/base/base-page.ts
|
|
295
|
+
var PageModule = class {
|
|
296
|
+
/** HtmxAdmin 上下文对象 */
|
|
297
|
+
context;
|
|
298
|
+
/** Action Helper 实例 */
|
|
299
|
+
paths;
|
|
300
|
+
/**
|
|
301
|
+
* 初始化模块实例(私有方法,仅由 RouteHandler 调用)
|
|
302
|
+
* 用于设置模块的基础属性
|
|
303
|
+
*/
|
|
304
|
+
__init(context) {
|
|
305
|
+
this.context = context;
|
|
306
|
+
this.paths = new PathHelper(this.context.moduleMetadata.basePath);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* 获取页面标题
|
|
310
|
+
* 子类可以重写此方法来返回页面标题
|
|
311
|
+
* 默认实现:使用模块名
|
|
312
|
+
*/
|
|
313
|
+
getTitle() {
|
|
314
|
+
return this.context.moduleMetadata.title;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* 获取页面描述
|
|
318
|
+
* 子类可以重写此方法来返回页面描述(用于SEO和页面展示)
|
|
319
|
+
* 默认实现:返回空字符串
|
|
320
|
+
*/
|
|
321
|
+
getDescription() {
|
|
322
|
+
return this.context.moduleMetadata.description;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* 获取面包屑
|
|
326
|
+
* 子类可以重写此方法来自定义面包屑
|
|
327
|
+
* 默认实现:首页 => 自定义页面
|
|
328
|
+
*/
|
|
329
|
+
getBreadcrumbs() {
|
|
330
|
+
const breadcrumbs = [
|
|
331
|
+
{ label: "\u9996\u9875", href: this.context.pluginOptions.prefix },
|
|
332
|
+
{ label: this.context.moduleMetadata.title }
|
|
333
|
+
];
|
|
334
|
+
return breadcrumbs;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* 处理请求的统一入口(由 RouteHandler 调用)
|
|
338
|
+
* 默认实现:直接调用 render 方法
|
|
339
|
+
* 子类可以重写此方法来处理请求级别的逻辑(如根据 HTTP method 分发)
|
|
340
|
+
*
|
|
341
|
+
* @param adminContext HtmxAdmin 上下文对象(必需),包含请求状态和 helper 方法
|
|
342
|
+
* @returns 页面内容
|
|
343
|
+
*/
|
|
344
|
+
async __handle() {
|
|
345
|
+
return await this.render();
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
var DetailPageModule = class extends PageModule {
|
|
349
|
+
/** ID 字段名(默认 "id") */
|
|
350
|
+
idField = "id";
|
|
351
|
+
/**
|
|
352
|
+
* 获取字段标签(可选)
|
|
353
|
+
* 子类可以重写此方法来自定义字段的中文标签
|
|
354
|
+
*/
|
|
355
|
+
getFieldLabel(field) {
|
|
356
|
+
return field;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* 渲染字段值(可选)
|
|
360
|
+
* 子类可以重写此方法来自定义字段的渲染方式
|
|
361
|
+
*/
|
|
362
|
+
renderField(field, value, item) {
|
|
363
|
+
return value;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* 获取字段分组(可选)
|
|
367
|
+
* 子类可以重写此方法来定义字段分组
|
|
368
|
+
* 返回 null 或 undefined 表示不分组,平铺显示
|
|
369
|
+
*/
|
|
370
|
+
getFieldGroups(item) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* 获取可见字段列表(可选)
|
|
375
|
+
* 子类可以重写此方法来控制字段的显示顺序和可见性
|
|
376
|
+
* 返回 null 或 undefined 表示显示所有字段
|
|
377
|
+
*/
|
|
378
|
+
getVisibleFields(item) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* 获取详情页操作按钮(可选)
|
|
383
|
+
* 子类可以重写此方法来添加详情页的操作按钮
|
|
384
|
+
* 如果不定义,则根据模块元数据智能生成默认操作按钮
|
|
385
|
+
*/
|
|
386
|
+
getDetailActions(item) {
|
|
387
|
+
const id = item[this.idField];
|
|
388
|
+
const actions = [];
|
|
389
|
+
if (this.context.moduleMetadata.hasList) {
|
|
390
|
+
actions.push({
|
|
391
|
+
label: "\u8FD4\u56DE\u5217\u8868",
|
|
392
|
+
href: this.paths.list()
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
if (this.context.moduleMetadata.hasForm) {
|
|
396
|
+
actions.push({
|
|
397
|
+
label: "\u7F16\u8F91",
|
|
398
|
+
href: this.paths.edit(id)
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
if (this.getDatasource().deleteItem) {
|
|
402
|
+
actions.push({
|
|
403
|
+
label: "\u5220\u9664",
|
|
404
|
+
href: this.paths.delete(id)
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
return actions;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* 渲染详情页面(默认实现)
|
|
411
|
+
* 子类可以重写此方法来自定义渲染
|
|
412
|
+
*/
|
|
413
|
+
async render() {
|
|
414
|
+
const idParam = this.context.ctx.req.param("id");
|
|
415
|
+
if (!idParam) {
|
|
416
|
+
throw new Error("Detail page requires id parameter");
|
|
417
|
+
}
|
|
418
|
+
let item = await this.getDatasource().getItem(idParam);
|
|
419
|
+
if (!item && !isNaN(Number(idParam))) {
|
|
420
|
+
item = await this.getDatasource().getItem(Number(idParam));
|
|
421
|
+
}
|
|
422
|
+
if (!item) {
|
|
423
|
+
return /* @__PURE__ */ jsx("div", { className: "text-center py-12", children: /* @__PURE__ */ jsx("p", { className: "text-gray-500", children: "\u672A\u627E\u5230\u6570\u636E" }) });
|
|
424
|
+
}
|
|
425
|
+
const allFields = Object.keys(item);
|
|
426
|
+
const fieldLabels = {};
|
|
427
|
+
for (const field of allFields) {
|
|
428
|
+
fieldLabels[field] = this.getFieldLabel ? this.getFieldLabel(field) : field;
|
|
429
|
+
}
|
|
430
|
+
const fieldRenderers = {};
|
|
431
|
+
if (this.renderField) {
|
|
432
|
+
for (const field of allFields) {
|
|
433
|
+
fieldRenderers[field] = (value, item2) => this.renderField(field, value, item2);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const fieldGroups = this.getFieldGroups ? this.getFieldGroups(item) : void 0;
|
|
437
|
+
const visibleFields = this.getVisibleFields ? this.getVisibleFields(item) : void 0;
|
|
438
|
+
const actions = this.getDetailActions ? this.getDetailActions(item) : [];
|
|
439
|
+
return /* @__PURE__ */ jsx(
|
|
440
|
+
DetailContent,
|
|
441
|
+
{
|
|
442
|
+
item,
|
|
443
|
+
fieldLabels,
|
|
444
|
+
fieldRenderers: Object.keys(fieldRenderers).length > 0 ? fieldRenderers : void 0,
|
|
445
|
+
fieldGroups: fieldGroups || void 0,
|
|
446
|
+
visibleFields: visibleFields || void 0,
|
|
447
|
+
actions,
|
|
448
|
+
title: this.context.moduleMetadata.title,
|
|
449
|
+
isDialog: this.context.isDialog
|
|
450
|
+
}
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
var DateInput = (props) => {
|
|
455
|
+
const {
|
|
456
|
+
id,
|
|
457
|
+
name,
|
|
458
|
+
type = "date",
|
|
459
|
+
value,
|
|
460
|
+
defaultValue,
|
|
461
|
+
required,
|
|
462
|
+
disabled,
|
|
463
|
+
readOnly,
|
|
464
|
+
min,
|
|
465
|
+
max,
|
|
466
|
+
className = "",
|
|
467
|
+
...rest
|
|
468
|
+
} = props;
|
|
469
|
+
const baseClasses = "block w-full px-4 py-2.5 text-sm text-gray-900 bg-white border border-gray-300 rounded-lg shadow-sm transition-all duration-200";
|
|
470
|
+
const focusClasses = "focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
471
|
+
const disabledClasses = disabled ? "bg-gray-50 text-gray-500 cursor-not-allowed" : "";
|
|
472
|
+
const readOnlyClasses = readOnly ? "bg-gray-50 text-gray-700 cursor-default" : "";
|
|
473
|
+
const errorClasses = rest["data-error"] ? "border-red-300 focus:border-red-500 focus:ring-red-500" : "";
|
|
474
|
+
const classes = `${baseClasses} ${focusClasses} ${disabledClasses} ${readOnlyClasses} ${errorClasses} ${className}`.trim();
|
|
475
|
+
return /* @__PURE__ */ jsx(
|
|
476
|
+
"input",
|
|
477
|
+
{
|
|
478
|
+
type,
|
|
479
|
+
id,
|
|
480
|
+
name,
|
|
481
|
+
value,
|
|
482
|
+
defaultValue,
|
|
483
|
+
required,
|
|
484
|
+
disabled,
|
|
485
|
+
readOnly,
|
|
486
|
+
min,
|
|
487
|
+
max,
|
|
488
|
+
className: classes,
|
|
489
|
+
...rest
|
|
490
|
+
}
|
|
491
|
+
);
|
|
492
|
+
};
|
|
493
|
+
var FormField = (props) => {
|
|
494
|
+
const { label, id, required, error, help, children, className = "" } = props;
|
|
495
|
+
return /* @__PURE__ */ jsxs("div", { className, children: [
|
|
496
|
+
/* @__PURE__ */ jsxs(
|
|
497
|
+
"label",
|
|
498
|
+
{
|
|
499
|
+
htmlFor: id,
|
|
500
|
+
className: "block text-sm font-semibold text-gray-700 mb-2",
|
|
501
|
+
children: [
|
|
502
|
+
label,
|
|
503
|
+
required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", children: "*" })
|
|
504
|
+
]
|
|
505
|
+
}
|
|
506
|
+
),
|
|
507
|
+
children,
|
|
508
|
+
error && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-red-600", children: error }),
|
|
509
|
+
help && !error && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-gray-500", children: help })
|
|
510
|
+
] });
|
|
511
|
+
};
|
|
512
|
+
var Input = (props) => {
|
|
513
|
+
const {
|
|
514
|
+
id,
|
|
515
|
+
name,
|
|
516
|
+
type = "text",
|
|
517
|
+
value,
|
|
518
|
+
defaultValue,
|
|
519
|
+
placeholder,
|
|
520
|
+
required,
|
|
521
|
+
disabled,
|
|
522
|
+
readOnly,
|
|
523
|
+
min,
|
|
524
|
+
max,
|
|
525
|
+
step,
|
|
526
|
+
className = "",
|
|
527
|
+
...rest
|
|
528
|
+
} = props;
|
|
529
|
+
const baseClasses = "block w-full px-4 py-2.5 text-sm text-gray-900 bg-white border border-gray-300 rounded-lg shadow-sm transition-all duration-200";
|
|
530
|
+
const focusClasses = "focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
531
|
+
const disabledClasses = disabled ? "bg-gray-50 text-gray-500 cursor-not-allowed" : "";
|
|
532
|
+
const readOnlyClasses = readOnly ? "bg-gray-50 text-gray-700 cursor-default" : "";
|
|
533
|
+
const errorClasses = rest["data-error"] ? "border-red-300 focus:border-red-500 focus:ring-red-500" : "";
|
|
534
|
+
const classes = `${baseClasses} ${focusClasses} ${disabledClasses} ${readOnlyClasses} ${errorClasses} ${className}`.trim();
|
|
535
|
+
return /* @__PURE__ */ jsx(
|
|
536
|
+
"input",
|
|
537
|
+
{
|
|
538
|
+
type,
|
|
539
|
+
id,
|
|
540
|
+
name,
|
|
541
|
+
value,
|
|
542
|
+
defaultValue,
|
|
543
|
+
placeholder,
|
|
544
|
+
required,
|
|
545
|
+
disabled,
|
|
546
|
+
readOnly,
|
|
547
|
+
min,
|
|
548
|
+
max,
|
|
549
|
+
step,
|
|
550
|
+
className: classes,
|
|
551
|
+
...rest
|
|
552
|
+
}
|
|
553
|
+
);
|
|
554
|
+
};
|
|
555
|
+
var Select = (props) => {
|
|
556
|
+
const {
|
|
557
|
+
id,
|
|
558
|
+
name,
|
|
559
|
+
value,
|
|
560
|
+
defaultValue,
|
|
561
|
+
required,
|
|
562
|
+
disabled,
|
|
563
|
+
options,
|
|
564
|
+
placeholder = "\u8BF7\u9009\u62E9",
|
|
565
|
+
className = "",
|
|
566
|
+
...rest
|
|
567
|
+
} = props;
|
|
568
|
+
const baseClasses = "block w-full px-4 py-2.5 text-sm text-gray-900 bg-white border border-gray-300 rounded-lg shadow-sm transition-all duration-200 appearance-none bg-[url('data:image/svg+xml;charset=utf-8,%3Csvg xmlns=\\'http://www.w3.org/2000/svg\\' fill=\\'none\\' viewBox=\\'0 0 20 20\\'%3E%3Cpath stroke=\\'%236b7280\\' stroke-linecap=\\'round\\' stroke-linejoin=\\'round\\' stroke-width=\\'1.5\\' d=\\'M6 8l4 4 4-4\\'/%3E%3C/svg%3E')] bg-[length:1.5em_1.5em] bg-[right_0.75rem_center] bg-no-repeat pr-10";
|
|
569
|
+
const focusClasses = "focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
570
|
+
const disabledClasses = disabled ? "bg-gray-50 text-gray-500 cursor-not-allowed" : "";
|
|
571
|
+
const errorClasses = rest["data-error"] ? "border-red-300 focus:border-red-500 focus:ring-red-500" : "";
|
|
572
|
+
const classes = `${baseClasses} ${focusClasses} ${disabledClasses} ${errorClasses} ${className}`.trim();
|
|
573
|
+
return /* @__PURE__ */ jsxs(
|
|
574
|
+
"select",
|
|
575
|
+
{
|
|
576
|
+
id,
|
|
577
|
+
name,
|
|
578
|
+
value,
|
|
579
|
+
defaultValue,
|
|
580
|
+
required,
|
|
581
|
+
disabled,
|
|
582
|
+
className: classes,
|
|
583
|
+
...rest,
|
|
584
|
+
children: [
|
|
585
|
+
!required && /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: placeholder }),
|
|
586
|
+
options.map((option) => /* @__PURE__ */ jsx(
|
|
587
|
+
"option",
|
|
588
|
+
{
|
|
589
|
+
value: option.value,
|
|
590
|
+
disabled: option.disabled,
|
|
591
|
+
selected: value === option.value || defaultValue === option.value,
|
|
592
|
+
children: option.label
|
|
593
|
+
},
|
|
594
|
+
option.value
|
|
595
|
+
))
|
|
596
|
+
]
|
|
597
|
+
}
|
|
598
|
+
);
|
|
599
|
+
};
|
|
600
|
+
var Textarea = (props) => {
|
|
601
|
+
const {
|
|
602
|
+
id,
|
|
603
|
+
name,
|
|
604
|
+
value,
|
|
605
|
+
defaultValue,
|
|
606
|
+
placeholder,
|
|
607
|
+
required,
|
|
608
|
+
disabled,
|
|
609
|
+
readOnly,
|
|
610
|
+
rows = 4,
|
|
611
|
+
className = "",
|
|
612
|
+
...rest
|
|
613
|
+
} = props;
|
|
614
|
+
const baseClasses = "block w-full px-4 py-2.5 text-sm text-gray-900 bg-white border border-gray-300 rounded-lg shadow-sm transition-all duration-200 resize-y";
|
|
615
|
+
const focusClasses = "focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
616
|
+
const disabledClasses = disabled ? "bg-gray-50 text-gray-500 cursor-not-allowed" : "";
|
|
617
|
+
const readOnlyClasses = readOnly ? "bg-gray-50 text-gray-700 cursor-default" : "";
|
|
618
|
+
const errorClasses = rest["data-error"] ? "border-red-300 focus:border-red-500 focus:ring-red-500" : "";
|
|
619
|
+
const classes = `${baseClasses} ${focusClasses} ${disabledClasses} ${readOnlyClasses} ${errorClasses} ${className}`.trim();
|
|
620
|
+
return /* @__PURE__ */ jsx(
|
|
621
|
+
"textarea",
|
|
622
|
+
{
|
|
623
|
+
id,
|
|
624
|
+
name,
|
|
625
|
+
value,
|
|
626
|
+
defaultValue,
|
|
627
|
+
placeholder,
|
|
628
|
+
required,
|
|
629
|
+
disabled,
|
|
630
|
+
readOnly,
|
|
631
|
+
rows,
|
|
632
|
+
className: classes,
|
|
633
|
+
...rest
|
|
634
|
+
}
|
|
635
|
+
);
|
|
636
|
+
};
|
|
637
|
+
var Form = (props) => {
|
|
638
|
+
const {
|
|
639
|
+
item,
|
|
640
|
+
fields,
|
|
641
|
+
submitUrl,
|
|
642
|
+
method = item ? "PUT" : "POST",
|
|
643
|
+
redirectUrl,
|
|
644
|
+
title = item ? "\u7F16\u8F91" : "\u65B0\u5EFA",
|
|
645
|
+
cancelUrl,
|
|
646
|
+
isDialog = false,
|
|
647
|
+
formData
|
|
648
|
+
} = props;
|
|
649
|
+
const getFieldValue = (fieldName) => {
|
|
650
|
+
if (formData && Object.prototype.hasOwnProperty.call(formData, fieldName)) {
|
|
651
|
+
const value = formData[fieldName];
|
|
652
|
+
return value === null || value === void 0 ? "" : value;
|
|
653
|
+
}
|
|
654
|
+
if (item && Object.prototype.hasOwnProperty.call(item, fieldName)) {
|
|
655
|
+
const value = item[fieldName];
|
|
656
|
+
return value === null || value === void 0 ? "" : value;
|
|
657
|
+
}
|
|
658
|
+
return "";
|
|
659
|
+
};
|
|
660
|
+
const renderFieldInput = (field) => {
|
|
661
|
+
const value = getFieldValue(field.name);
|
|
662
|
+
const fieldId = `field-${field.name}`;
|
|
663
|
+
switch (field.type || "text") {
|
|
664
|
+
case "textarea":
|
|
665
|
+
return /* @__PURE__ */ jsx(
|
|
666
|
+
Textarea,
|
|
667
|
+
{
|
|
668
|
+
id: fieldId,
|
|
669
|
+
name: field.name,
|
|
670
|
+
value,
|
|
671
|
+
required: field.required,
|
|
672
|
+
placeholder: field.placeholder,
|
|
673
|
+
rows: 4
|
|
674
|
+
}
|
|
675
|
+
);
|
|
676
|
+
case "select":
|
|
677
|
+
return /* @__PURE__ */ jsx(
|
|
678
|
+
Select,
|
|
679
|
+
{
|
|
680
|
+
id: fieldId,
|
|
681
|
+
name: field.name,
|
|
682
|
+
value,
|
|
683
|
+
required: field.required,
|
|
684
|
+
options: field.options || [],
|
|
685
|
+
placeholder: "\u8BF7\u9009\u62E9"
|
|
686
|
+
}
|
|
687
|
+
);
|
|
688
|
+
case "number":
|
|
689
|
+
return /* @__PURE__ */ jsx(
|
|
690
|
+
Input,
|
|
691
|
+
{
|
|
692
|
+
type: "number",
|
|
693
|
+
id: fieldId,
|
|
694
|
+
name: field.name,
|
|
695
|
+
value,
|
|
696
|
+
required: field.required,
|
|
697
|
+
placeholder: field.placeholder
|
|
698
|
+
}
|
|
699
|
+
);
|
|
700
|
+
case "date":
|
|
701
|
+
case "datetime-local":
|
|
702
|
+
return /* @__PURE__ */ jsx(
|
|
703
|
+
DateInput,
|
|
704
|
+
{
|
|
705
|
+
type: field.type,
|
|
706
|
+
id: fieldId,
|
|
707
|
+
name: field.name,
|
|
708
|
+
value: value ? field.type === "date" ? value.split("T")[0] : value : "",
|
|
709
|
+
required: field.required
|
|
710
|
+
}
|
|
711
|
+
);
|
|
712
|
+
default:
|
|
713
|
+
const inputType = field.type === "email" ? "email" : "text";
|
|
714
|
+
return /* @__PURE__ */ jsx(
|
|
715
|
+
Input,
|
|
716
|
+
{
|
|
717
|
+
type: inputType,
|
|
718
|
+
id: fieldId,
|
|
719
|
+
name: field.name,
|
|
720
|
+
value,
|
|
721
|
+
required: field.required,
|
|
722
|
+
placeholder: field.placeholder
|
|
723
|
+
}
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
const target = isDialog ? "#dialog-container" : "#main-content";
|
|
728
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
729
|
+
!isDialog && /* @__PURE__ */ jsx(PageHeader, { title }),
|
|
730
|
+
/* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(
|
|
731
|
+
"form",
|
|
732
|
+
{
|
|
733
|
+
"hx-put": method === "PUT" ? submitUrl : void 0,
|
|
734
|
+
"hx-post": method === "POST" ? submitUrl : void 0,
|
|
735
|
+
"hx-encoding": "json",
|
|
736
|
+
"hx-headers": JSON.stringify({
|
|
737
|
+
"HX-Target": target,
|
|
738
|
+
"HX-Redirect-Url": redirectUrl || "",
|
|
739
|
+
"Content-Type": "application/json"
|
|
740
|
+
}),
|
|
741
|
+
children: /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
742
|
+
fields.map((field) => {
|
|
743
|
+
const fieldId = `field-${field.name}`;
|
|
744
|
+
return /* @__PURE__ */ jsx(
|
|
745
|
+
FormField,
|
|
746
|
+
{
|
|
747
|
+
id: fieldId,
|
|
748
|
+
label: field.label,
|
|
749
|
+
required: field.required,
|
|
750
|
+
children: field.render ? field.render(getFieldValue(field.name), item) : renderFieldInput(field)
|
|
751
|
+
},
|
|
752
|
+
field.name
|
|
753
|
+
);
|
|
754
|
+
}),
|
|
755
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-3 pt-6 border-t border-gray-200", children: [
|
|
756
|
+
cancelUrl && (isDialog ? /* @__PURE__ */ jsx(
|
|
757
|
+
Button,
|
|
758
|
+
{
|
|
759
|
+
type: "button",
|
|
760
|
+
variant: "secondary",
|
|
761
|
+
className: "min-w-[80px]",
|
|
762
|
+
_: "on click \r\n add .dialog-exit to .dialog-backdrop\r\n add .dialog-content-exit to .dialog-content\r\n wait 200ms\r\n set #dialog-container's innerHTML to '' end",
|
|
763
|
+
children: "\u53D6\u6D88"
|
|
764
|
+
}
|
|
765
|
+
) : /* @__PURE__ */ jsx(
|
|
766
|
+
Button,
|
|
767
|
+
{
|
|
768
|
+
variant: "secondary",
|
|
769
|
+
href: cancelUrl,
|
|
770
|
+
hxGet: cancelUrl,
|
|
771
|
+
className: "min-w-[80px]",
|
|
772
|
+
children: "\u53D6\u6D88"
|
|
773
|
+
}
|
|
774
|
+
)),
|
|
775
|
+
/* @__PURE__ */ jsx(
|
|
776
|
+
Button,
|
|
777
|
+
{
|
|
778
|
+
type: "submit",
|
|
779
|
+
variant: "primary",
|
|
780
|
+
className: "min-w-[100px] font-semibold",
|
|
781
|
+
children: "\u4FDD\u5B58"
|
|
782
|
+
}
|
|
783
|
+
)
|
|
784
|
+
] })
|
|
785
|
+
] })
|
|
786
|
+
}
|
|
787
|
+
) })
|
|
788
|
+
] });
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
// src/utils/zod-form.tsx
|
|
792
|
+
function getFieldDescription(schema) {
|
|
793
|
+
const def = schema._def;
|
|
794
|
+
if (def?.description) {
|
|
795
|
+
return def.description;
|
|
796
|
+
}
|
|
797
|
+
if (def?.innerType) {
|
|
798
|
+
return getFieldDescription(def.innerType);
|
|
799
|
+
}
|
|
800
|
+
return void 0;
|
|
801
|
+
}
|
|
802
|
+
function isRequiredField(schema) {
|
|
803
|
+
const def = schema._def;
|
|
804
|
+
if (def?.type === "optional" || def?.typeName === "ZodOptional") {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
if (def?.type === "nullable" || def?.typeName === "ZodNullable") {
|
|
808
|
+
return isRequiredField(def.innerType);
|
|
809
|
+
}
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
function inferFieldType(schema) {
|
|
813
|
+
const def = schema._def;
|
|
814
|
+
const typeName = def?.type || def?.typeName;
|
|
815
|
+
if (typeName === "optional" || typeName === "ZodOptional") {
|
|
816
|
+
return inferFieldType(def.innerType);
|
|
817
|
+
}
|
|
818
|
+
if (typeName === "nullable" || typeName === "ZodNullable") {
|
|
819
|
+
return inferFieldType(def.innerType);
|
|
820
|
+
}
|
|
821
|
+
if (typeName === "enum" || typeName === "ZodEnum") {
|
|
822
|
+
return "select";
|
|
823
|
+
}
|
|
824
|
+
if (typeName === "nativeEnum" || typeName === "ZodNativeEnum") {
|
|
825
|
+
return "select";
|
|
826
|
+
}
|
|
827
|
+
if (typeName === "string" || typeName === "ZodString") {
|
|
828
|
+
if (def?.checks) {
|
|
829
|
+
const hasEmailCheck = def.checks.some(
|
|
830
|
+
(check) => check.kind === "email"
|
|
831
|
+
);
|
|
832
|
+
if (hasEmailCheck) {
|
|
833
|
+
return "email";
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return "text";
|
|
837
|
+
}
|
|
838
|
+
if (typeName === "number" || typeName === "ZodNumber") {
|
|
839
|
+
return "number";
|
|
840
|
+
}
|
|
841
|
+
if (typeName === "date" || typeName === "ZodDate") {
|
|
842
|
+
return "date";
|
|
843
|
+
}
|
|
844
|
+
if (typeName === "boolean" || typeName === "ZodBoolean") {
|
|
845
|
+
return "select";
|
|
846
|
+
}
|
|
847
|
+
return "text";
|
|
848
|
+
}
|
|
849
|
+
function extractEnumOptions(schema) {
|
|
850
|
+
const def = schema._def;
|
|
851
|
+
const typeName = def?.type || def?.typeName;
|
|
852
|
+
if (typeName === "optional" || typeName === "ZodOptional") {
|
|
853
|
+
return extractEnumOptions(def.innerType);
|
|
854
|
+
}
|
|
855
|
+
if (typeName === "nullable" || typeName === "ZodNullable") {
|
|
856
|
+
return extractEnumOptions(def.innerType);
|
|
857
|
+
}
|
|
858
|
+
if (typeName === "enum" || typeName === "ZodEnum") {
|
|
859
|
+
const values = def?.values || [];
|
|
860
|
+
return values.map((value) => ({
|
|
861
|
+
value,
|
|
862
|
+
label: value
|
|
863
|
+
}));
|
|
864
|
+
}
|
|
865
|
+
if (typeName === "nativeEnum" || typeName === "ZodNativeEnum") {
|
|
866
|
+
const enumObject = def?.values;
|
|
867
|
+
if (enumObject) {
|
|
868
|
+
return Object.entries(enumObject).filter(
|
|
869
|
+
([_, value]) => typeof value === "string" || typeof value === "number"
|
|
870
|
+
).map(([key, value]) => ({
|
|
871
|
+
value,
|
|
872
|
+
label: key
|
|
873
|
+
}));
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (typeName === "boolean" || typeName === "ZodBoolean") {
|
|
877
|
+
return [
|
|
878
|
+
{ value: "true", label: "\u662F" },
|
|
879
|
+
{ value: "false", label: "\u5426" }
|
|
880
|
+
];
|
|
881
|
+
}
|
|
882
|
+
return void 0;
|
|
883
|
+
}
|
|
884
|
+
function generateFormFieldsFromSchema(schema, options) {
|
|
885
|
+
const def = schema._def;
|
|
886
|
+
if (!def || !def.shape) {
|
|
887
|
+
return [];
|
|
888
|
+
}
|
|
889
|
+
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
890
|
+
if (!shape || typeof shape !== "object") {
|
|
891
|
+
return [];
|
|
892
|
+
}
|
|
893
|
+
const fields = [];
|
|
894
|
+
const {
|
|
895
|
+
fieldLabels = {},
|
|
896
|
+
fieldPlaceholders = {},
|
|
897
|
+
fieldOptions = {},
|
|
898
|
+
fieldTypes = {},
|
|
899
|
+
excludeFields = []
|
|
900
|
+
} = options || {};
|
|
901
|
+
for (const [fieldName, fieldSchema] of Object.entries(shape)) {
|
|
902
|
+
if (excludeFields.includes(fieldName)) {
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
const zodSchema = fieldSchema;
|
|
906
|
+
const description = getFieldDescription(zodSchema);
|
|
907
|
+
const label = fieldLabels[fieldName] || description || fieldName;
|
|
908
|
+
const type = fieldTypes[fieldName] || inferFieldType(zodSchema);
|
|
909
|
+
const required = isRequiredField(zodSchema);
|
|
910
|
+
const placeholder = fieldPlaceholders[fieldName];
|
|
911
|
+
const options2 = fieldOptions[fieldName] || extractEnumOptions(zodSchema);
|
|
912
|
+
fields.push({
|
|
913
|
+
name: fieldName,
|
|
914
|
+
label,
|
|
915
|
+
type,
|
|
916
|
+
required,
|
|
917
|
+
placeholder,
|
|
918
|
+
options: options2
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
return fields;
|
|
922
|
+
}
|
|
923
|
+
function validateFormDataWithSchema(schema, data) {
|
|
924
|
+
try {
|
|
925
|
+
const result = schema.safeParse(data);
|
|
926
|
+
if (result.success) {
|
|
927
|
+
return { success: true, data: result.data };
|
|
928
|
+
} else {
|
|
929
|
+
const issues = result.error.issues || [];
|
|
930
|
+
if (issues.length === 0) {
|
|
931
|
+
return { success: false, error: "\u9A8C\u8BC1\u5931\u8D25" };
|
|
932
|
+
}
|
|
933
|
+
const firstError = issues[0];
|
|
934
|
+
const fieldName = firstError.path && firstError.path.length > 0 ? firstError.path.join(".") : "";
|
|
935
|
+
const errorMessage = firstError.message;
|
|
936
|
+
const formattedError = fieldName ? `${fieldName}: ${errorMessage}` : errorMessage;
|
|
937
|
+
return { success: false, error: formattedError };
|
|
938
|
+
}
|
|
939
|
+
} catch (error) {
|
|
940
|
+
return {
|
|
941
|
+
success: false,
|
|
942
|
+
error: error instanceof Error ? error.message : "\u9A8C\u8BC1\u5931\u8D25"
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
var FormPageModule = class extends PageModule {
|
|
947
|
+
/** ID 字段名(默认 "id") */
|
|
948
|
+
idField = "id";
|
|
949
|
+
/** Zod Schema(可选,如果提供则自动生成表单字段和校验) */
|
|
950
|
+
schema;
|
|
951
|
+
constructor(schema) {
|
|
952
|
+
super();
|
|
953
|
+
this.schema = schema;
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* 获取单条数据(编辑时使用)
|
|
957
|
+
*/
|
|
958
|
+
async getItem(id) {
|
|
959
|
+
const datasource = this.getDatasource();
|
|
960
|
+
return await datasource.getItem?.(id) || null;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* 获取字段标签(可选)
|
|
964
|
+
* 子类可以重写此方法来自定义字段的中文标签
|
|
965
|
+
*/
|
|
966
|
+
getFieldLabel(field) {
|
|
967
|
+
return field;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* 获取表单字段定义
|
|
971
|
+
*
|
|
972
|
+
* 如果提供了 schema,则自动从 schema 生成字段定义
|
|
973
|
+
* 否则子类必须实现此方法来定义表单的字段
|
|
974
|
+
*/
|
|
975
|
+
getFormFields(item) {
|
|
976
|
+
if (this.schema) {
|
|
977
|
+
return generateFormFieldsFromSchema(this.schema, {
|
|
978
|
+
fieldLabels: this.getFieldLabels?.(),
|
|
979
|
+
fieldPlaceholders: this.getFieldPlaceholders?.(),
|
|
980
|
+
fieldOptions: this.getFieldOptions?.(),
|
|
981
|
+
fieldTypes: this.getFieldTypes?.(),
|
|
982
|
+
excludeFields: this.getExcludeFields?.()
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
throw new Error(
|
|
986
|
+
"getFormFields must be implemented or schema must be provided"
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* 验证表单数据
|
|
991
|
+
*
|
|
992
|
+
* 如果提供了 schema,则自动使用 schema 进行校验
|
|
993
|
+
* 否则子类可以重写此方法来验证表单数据
|
|
994
|
+
*/
|
|
995
|
+
validateFormData(data, item) {
|
|
996
|
+
if (this.schema) {
|
|
997
|
+
const result = validateFormDataWithSchema(this.schema, data);
|
|
998
|
+
if (!result.success) {
|
|
999
|
+
return result.error;
|
|
1000
|
+
}
|
|
1001
|
+
return null;
|
|
1002
|
+
}
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* 处理请求的统一入口(重写基类方法)
|
|
1007
|
+
* 根据 HTTP method 处理不同请求:
|
|
1008
|
+
* - GET: 渲染表单页面(调用 render)
|
|
1009
|
+
* - POST: 创建数据
|
|
1010
|
+
* - PUT: 更新数据
|
|
1011
|
+
* - DELETE: 删除数据
|
|
1012
|
+
* 子类可以重写此方法来自定义请求处理逻辑
|
|
1013
|
+
*/
|
|
1014
|
+
async __handle() {
|
|
1015
|
+
const ctx = this.context.ctx;
|
|
1016
|
+
const method = ctx.req.method;
|
|
1017
|
+
const id = ctx.req.param("id") || null;
|
|
1018
|
+
if (method === "POST") {
|
|
1019
|
+
return await this.handleCreate(ctx, this.context);
|
|
1020
|
+
} else if (method === "PUT") {
|
|
1021
|
+
return await this.handleUpdate(ctx, this.context, id);
|
|
1022
|
+
} else if (method === "DELETE") {
|
|
1023
|
+
return await this.handleDelete(ctx, this.context, id);
|
|
1024
|
+
}
|
|
1025
|
+
return await this.render();
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* 渲染表单页面(默认实现)
|
|
1029
|
+
* 子类可以重写此方法来自定义渲染
|
|
1030
|
+
* @param formData 表单数据(用于回填验证失败时的值)
|
|
1031
|
+
*/
|
|
1032
|
+
async render(formData) {
|
|
1033
|
+
const referer = this.context.ctx.req.header("Referer");
|
|
1034
|
+
let pathname = new URL(this.context.ctx.req.url).pathname;
|
|
1035
|
+
const method = this.context.ctx.req.method;
|
|
1036
|
+
if (referer && ["POST", "PUT", "DELETE"].includes(method)) {
|
|
1037
|
+
try {
|
|
1038
|
+
const refererUrl = new URL(referer);
|
|
1039
|
+
pathname = refererUrl.pathname;
|
|
1040
|
+
} catch (e) {
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const url = new URL(this.context.ctx.req.url);
|
|
1044
|
+
const isDialog = url.searchParams.get("dialog") === "true";
|
|
1045
|
+
const isEdit = pathname.includes("/edit");
|
|
1046
|
+
const id = isEdit ? this.context.ctx.req.param("id") : null;
|
|
1047
|
+
let item = null;
|
|
1048
|
+
if (isEdit && id) {
|
|
1049
|
+
item = await this.getItem(id);
|
|
1050
|
+
if (!item) {
|
|
1051
|
+
return /* @__PURE__ */ jsx("div", { className: "text-center py-12", children: /* @__PURE__ */ jsx("p", { className: "text-gray-500", children: "\u672A\u627E\u5230\u6570\u636E" }) });
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
const fields = this.getFormFields(item);
|
|
1055
|
+
if (fields.length === 0) {
|
|
1056
|
+
return /* @__PURE__ */ jsx("div", { className: "text-center py-12", children: /* @__PURE__ */ jsx("p", { className: "text-gray-500", children: "\u6CA1\u6709\u53EF\u7F16\u8F91\u7684\u5B57\u6BB5" }) });
|
|
1057
|
+
}
|
|
1058
|
+
const moduleName = this.context.moduleMetadata.title;
|
|
1059
|
+
const basePath = this.context.moduleMetadata.basePath;
|
|
1060
|
+
const hasList = this.context.moduleMetadata.hasList;
|
|
1061
|
+
let cancelUrl;
|
|
1062
|
+
if (hasList && basePath) {
|
|
1063
|
+
cancelUrl = isEdit ? `${basePath}/detail/${id}` : `${basePath}/list`;
|
|
1064
|
+
}
|
|
1065
|
+
const formContent = /* @__PURE__ */ jsx(
|
|
1066
|
+
Form,
|
|
1067
|
+
{
|
|
1068
|
+
item,
|
|
1069
|
+
fields,
|
|
1070
|
+
submitUrl: isEdit ? `${basePath}/${id}` : basePath,
|
|
1071
|
+
method: isEdit ? "PUT" : "POST",
|
|
1072
|
+
redirectUrl: isEdit ? `${basePath}/detail/${id}` : hasList ? `${basePath}/list` : void 0,
|
|
1073
|
+
title: moduleName + (isEdit ? " - \u7F16\u8F91" : " - \u65B0\u5EFA"),
|
|
1074
|
+
cancelUrl,
|
|
1075
|
+
isDialog,
|
|
1076
|
+
formData
|
|
1077
|
+
}
|
|
1078
|
+
);
|
|
1079
|
+
return formContent;
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* 处理创建请求(POST)
|
|
1083
|
+
*/
|
|
1084
|
+
async handleCreate(ctx, adminContext) {
|
|
1085
|
+
let body = {};
|
|
1086
|
+
try {
|
|
1087
|
+
const contentType = ctx.req.header("Content-Type") || "";
|
|
1088
|
+
if (contentType.includes("application/json")) {
|
|
1089
|
+
body = await ctx.req.json();
|
|
1090
|
+
} else {
|
|
1091
|
+
const formData = await ctx.req.parseBody();
|
|
1092
|
+
body = formData;
|
|
1093
|
+
}
|
|
1094
|
+
} catch (e) {
|
|
1095
|
+
body = {};
|
|
1096
|
+
}
|
|
1097
|
+
const validationError = this.validateFormData(body, null);
|
|
1098
|
+
if (validationError) {
|
|
1099
|
+
adminContext.sendNotification("error", "\u9A8C\u8BC1\u5931\u8D25", validationError);
|
|
1100
|
+
return await this.renderFormPage(adminContext, null, false, body);
|
|
1101
|
+
}
|
|
1102
|
+
try {
|
|
1103
|
+
const newItem = await this.getDatasource().createItem?.(
|
|
1104
|
+
body
|
|
1105
|
+
);
|
|
1106
|
+
const basePath = this.context.moduleMetadata.basePath;
|
|
1107
|
+
const hasDetail = this.context.moduleMetadata.hasDetail;
|
|
1108
|
+
const hasList = this.context.moduleMetadata.hasList;
|
|
1109
|
+
if (adminContext.isDialog) {
|
|
1110
|
+
adminContext.sendNotification("success", "\u521B\u5EFA\u6210\u529F", "\u6570\u636E\u5DF2\u6210\u529F\u521B\u5EFA");
|
|
1111
|
+
adminContext.setRefresh(true);
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
if (hasDetail) {
|
|
1115
|
+
adminContext.redirect(
|
|
1116
|
+
`${basePath}/detail/${newItem[this.idField]}`
|
|
1117
|
+
);
|
|
1118
|
+
} else if (hasList) {
|
|
1119
|
+
adminContext.redirect(`${basePath}/list`);
|
|
1120
|
+
}
|
|
1121
|
+
adminContext.sendNotification("success", "\u521B\u5EFA\u6210\u529F", "\u6570\u636E\u5DF2\u6210\u529F\u521B\u5EFA");
|
|
1122
|
+
return await this.render();
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
this.context.sendNotification(
|
|
1125
|
+
"error",
|
|
1126
|
+
"\u521B\u5EFA\u5931\u8D25",
|
|
1127
|
+
error instanceof Error ? error.message : "\u521B\u5EFA\u5931\u8D25"
|
|
1128
|
+
);
|
|
1129
|
+
return await this.renderFormPage(adminContext, null, false, body);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* 处理更新请求(PUT)
|
|
1134
|
+
*/
|
|
1135
|
+
async handleUpdate(ctx, adminContext, id) {
|
|
1136
|
+
let body = {};
|
|
1137
|
+
try {
|
|
1138
|
+
const contentType = ctx.req.header("Content-Type") || "";
|
|
1139
|
+
if (contentType.includes("application/json")) {
|
|
1140
|
+
body = await ctx.req.json();
|
|
1141
|
+
} else {
|
|
1142
|
+
const formData = await ctx.req.parseBody();
|
|
1143
|
+
body = formData;
|
|
1144
|
+
}
|
|
1145
|
+
} catch (e) {
|
|
1146
|
+
body = {};
|
|
1147
|
+
}
|
|
1148
|
+
const item = await this.getItem(id);
|
|
1149
|
+
if (!item) {
|
|
1150
|
+
adminContext.sendNotification(
|
|
1151
|
+
"error",
|
|
1152
|
+
"\u672A\u627E\u5230\u6570\u636E",
|
|
1153
|
+
"\u65E0\u6CD5\u627E\u5230\u8981\u7F16\u8F91\u7684\u6570\u636E"
|
|
1154
|
+
);
|
|
1155
|
+
return /* @__PURE__ */ jsx("div", { className: "text-center py-12", children: /* @__PURE__ */ jsx("p", { className: "text-gray-500", children: "\u672A\u627E\u5230\u6570\u636E" }) });
|
|
1156
|
+
}
|
|
1157
|
+
const validationError = this.validateFormData(body, item);
|
|
1158
|
+
if (validationError) {
|
|
1159
|
+
adminContext.sendNotification("error", "\u9A8C\u8BC1\u5931\u8D25", validationError);
|
|
1160
|
+
const mergedData = { ...item, ...body };
|
|
1161
|
+
return await this.renderFormPage(adminContext, item, true, mergedData);
|
|
1162
|
+
}
|
|
1163
|
+
try {
|
|
1164
|
+
const datasource = this.getDatasource();
|
|
1165
|
+
if (datasource.updateItem) {
|
|
1166
|
+
await datasource.updateItem(id, body);
|
|
1167
|
+
}
|
|
1168
|
+
if (adminContext.isDialog) {
|
|
1169
|
+
adminContext.sendNotification("success", "\u66F4\u65B0\u6210\u529F", "\u6570\u636E\u5DF2\u6210\u529F\u66F4\u65B0");
|
|
1170
|
+
adminContext.setRefresh(true);
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
const basePath = this.context.moduleMetadata.basePath;
|
|
1174
|
+
this.context.redirect(`${basePath}/detail/${id}`);
|
|
1175
|
+
this.context.sendNotification("success", "\u66F4\u65B0\u6210\u529F", "\u6570\u636E\u5DF2\u6210\u529F\u66F4\u65B0");
|
|
1176
|
+
return this.context.content;
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
adminContext.sendNotification(
|
|
1179
|
+
"error",
|
|
1180
|
+
"\u66F4\u65B0\u5931\u8D25",
|
|
1181
|
+
error instanceof Error ? error.message : "\u66F4\u65B0\u5931\u8D25"
|
|
1182
|
+
);
|
|
1183
|
+
const mergedData = { ...item, ...body };
|
|
1184
|
+
return await this.renderFormPage(adminContext, item, true, mergedData);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* 处理删除请求(DELETE)
|
|
1189
|
+
* 注意:FormDatasource 不包含 deleteItem,删除操作通常由 List 或 Detail 模块处理
|
|
1190
|
+
* 这里提供默认实现,子类可以重写
|
|
1191
|
+
*/
|
|
1192
|
+
async handleDelete(ctx, adminContext, id) {
|
|
1193
|
+
adminContext.sendNotification(
|
|
1194
|
+
"error",
|
|
1195
|
+
"\u4E0D\u652F\u6301\u5220\u9664\u64CD\u4F5C",
|
|
1196
|
+
"\u8868\u5355\u6A21\u5757\u4E0D\u652F\u6301\u5220\u9664\u64CD\u4F5C\uFF0C\u8BF7\u4F7F\u7528\u5217\u8868\u6216\u8BE6\u60C5\u6A21\u5757"
|
|
1197
|
+
);
|
|
1198
|
+
const basePath = this.context.moduleMetadata.basePath;
|
|
1199
|
+
const hasList = this.context.moduleMetadata.hasList;
|
|
1200
|
+
if (hasList) {
|
|
1201
|
+
adminContext.redirect(`${basePath}/list`);
|
|
1202
|
+
} else {
|
|
1203
|
+
adminContext.redirect(basePath);
|
|
1204
|
+
}
|
|
1205
|
+
return null;
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* 渲染表单页面(辅助方法)
|
|
1209
|
+
* 用于在验证失败时重新渲染表单页面
|
|
1210
|
+
* @param adminContext 上下文对象
|
|
1211
|
+
* @param item 原始数据项(编辑时)
|
|
1212
|
+
* @param isEdit 是否是编辑模式
|
|
1213
|
+
* @param formData 表单数据(用于回填验证失败时的值)
|
|
1214
|
+
*/
|
|
1215
|
+
async renderFormPage(adminContext, item, isEdit, formData) {
|
|
1216
|
+
return await this.render(formData);
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
var FilterCard = (props) => {
|
|
1220
|
+
const {
|
|
1221
|
+
title,
|
|
1222
|
+
fields,
|
|
1223
|
+
filterButtonText = "\u7B5B\u9009",
|
|
1224
|
+
filterButtonHxGet,
|
|
1225
|
+
preserveParams = [],
|
|
1226
|
+
className = ""
|
|
1227
|
+
} = props;
|
|
1228
|
+
return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs("form", { "hx-get": filterButtonHxGet, className: "space-y-4", children: [
|
|
1229
|
+
title && /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title }),
|
|
1230
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 items-end", children: [
|
|
1231
|
+
fields.map((field) => /* @__PURE__ */ jsxs("div", { children: [
|
|
1232
|
+
/* @__PURE__ */ jsx(
|
|
1233
|
+
"label",
|
|
1234
|
+
{
|
|
1235
|
+
htmlFor: field.name,
|
|
1236
|
+
className: "block text-sm font-medium text-gray-700 mb-2",
|
|
1237
|
+
children: field.label
|
|
1238
|
+
}
|
|
1239
|
+
),
|
|
1240
|
+
/* @__PURE__ */ jsx(
|
|
1241
|
+
Select,
|
|
1242
|
+
{
|
|
1243
|
+
id: field.name,
|
|
1244
|
+
name: field.name,
|
|
1245
|
+
value: field.value,
|
|
1246
|
+
defaultValue: field.defaultValue,
|
|
1247
|
+
options: field.options,
|
|
1248
|
+
className: "w-full"
|
|
1249
|
+
}
|
|
1250
|
+
)
|
|
1251
|
+
] }, field.name)),
|
|
1252
|
+
filterButtonHxGet && /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, { type: "submit", variant: "primary", className: "w-full", children: filterButtonText }) })
|
|
1253
|
+
] })
|
|
1254
|
+
] }) });
|
|
1255
|
+
};
|
|
1256
|
+
var StatCard = (props) => {
|
|
1257
|
+
const {
|
|
1258
|
+
title,
|
|
1259
|
+
value,
|
|
1260
|
+
change,
|
|
1261
|
+
changeLabel = "\u76F8\u6BD4\u4E0A\u6708",
|
|
1262
|
+
iconColor = "blue",
|
|
1263
|
+
icon,
|
|
1264
|
+
className = ""
|
|
1265
|
+
} = props;
|
|
1266
|
+
const iconColorClasses = {
|
|
1267
|
+
blue: "bg-blue-100 text-blue-600",
|
|
1268
|
+
green: "bg-green-100 text-green-600",
|
|
1269
|
+
yellow: "bg-yellow-100 text-yellow-600",
|
|
1270
|
+
purple: "bg-purple-100 text-purple-600",
|
|
1271
|
+
red: "bg-red-100 text-red-600",
|
|
1272
|
+
indigo: "bg-indigo-100 text-indigo-600"
|
|
1273
|
+
};
|
|
1274
|
+
const changeColorClass = change === void 0 ? "" : change > 0 ? "text-green-600" : change < 0 ? "text-red-600" : "text-gray-600";
|
|
1275
|
+
const defaultIcon = /* @__PURE__ */ jsx(
|
|
1276
|
+
"svg",
|
|
1277
|
+
{
|
|
1278
|
+
className: "w-6 h-6",
|
|
1279
|
+
fill: "none",
|
|
1280
|
+
stroke: "currentColor",
|
|
1281
|
+
viewBox: "0 0 24 24",
|
|
1282
|
+
children: /* @__PURE__ */ jsx(
|
|
1283
|
+
"path",
|
|
1284
|
+
{
|
|
1285
|
+
strokeLinecap: "round",
|
|
1286
|
+
strokeLinejoin: "round",
|
|
1287
|
+
strokeWidth: 2,
|
|
1288
|
+
d: change !== void 0 && change > 0 ? "M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" : change !== void 0 && change < 0 ? "M13 17h8m0 0V9m0 8l-8-8-4 4-6 6" : "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
|
1289
|
+
}
|
|
1290
|
+
)
|
|
1291
|
+
}
|
|
1292
|
+
);
|
|
1293
|
+
return /* @__PURE__ */ jsx(
|
|
1294
|
+
"div",
|
|
1295
|
+
{
|
|
1296
|
+
className: `bg-white rounded-lg shadow-sm p-6 hover:shadow-md transition-shadow ${className}`,
|
|
1297
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1298
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
|
|
1299
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-gray-600 mb-1", children: title }),
|
|
1300
|
+
/* @__PURE__ */ jsx("p", { className: "text-3xl font-bold text-gray-900 mb-2", children: typeof value === "number" ? value.toLocaleString() : value }),
|
|
1301
|
+
change !== void 0 && /* @__PURE__ */ jsxs("p", { className: `text-sm font-medium ${changeColorClass}`, children: [
|
|
1302
|
+
change > 0 ? /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center", children: [
|
|
1303
|
+
/* @__PURE__ */ jsx(
|
|
1304
|
+
"svg",
|
|
1305
|
+
{
|
|
1306
|
+
className: "w-4 h-4 mr-1",
|
|
1307
|
+
fill: "none",
|
|
1308
|
+
stroke: "currentColor",
|
|
1309
|
+
viewBox: "0 0 24 24",
|
|
1310
|
+
children: /* @__PURE__ */ jsx(
|
|
1311
|
+
"path",
|
|
1312
|
+
{
|
|
1313
|
+
strokeLinecap: "round",
|
|
1314
|
+
strokeLinejoin: "round",
|
|
1315
|
+
strokeWidth: 2,
|
|
1316
|
+
d: "M5 10l7-7m0 0l7 7m-7-7v18"
|
|
1317
|
+
}
|
|
1318
|
+
)
|
|
1319
|
+
}
|
|
1320
|
+
),
|
|
1321
|
+
Math.abs(change),
|
|
1322
|
+
"%"
|
|
1323
|
+
] }) : change < 0 ? /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center", children: [
|
|
1324
|
+
/* @__PURE__ */ jsx(
|
|
1325
|
+
"svg",
|
|
1326
|
+
{
|
|
1327
|
+
className: "w-4 h-4 mr-1",
|
|
1328
|
+
fill: "none",
|
|
1329
|
+
stroke: "currentColor",
|
|
1330
|
+
viewBox: "0 0 24 24",
|
|
1331
|
+
children: /* @__PURE__ */ jsx(
|
|
1332
|
+
"path",
|
|
1333
|
+
{
|
|
1334
|
+
strokeLinecap: "round",
|
|
1335
|
+
strokeLinejoin: "round",
|
|
1336
|
+
strokeWidth: 2,
|
|
1337
|
+
d: "M19 14l-7 7m0 0l-7-7m7 7V3"
|
|
1338
|
+
}
|
|
1339
|
+
)
|
|
1340
|
+
}
|
|
1341
|
+
),
|
|
1342
|
+
Math.abs(change),
|
|
1343
|
+
"%"
|
|
1344
|
+
] }) : /* @__PURE__ */ jsxs("span", { children: [
|
|
1345
|
+
change,
|
|
1346
|
+
"%"
|
|
1347
|
+
] }),
|
|
1348
|
+
" ",
|
|
1349
|
+
changeLabel
|
|
1350
|
+
] })
|
|
1351
|
+
] }),
|
|
1352
|
+
/* @__PURE__ */ jsx(
|
|
1353
|
+
"div",
|
|
1354
|
+
{
|
|
1355
|
+
className: `flex-shrink-0 w-12 h-12 rounded-lg flex items-center justify-center ${iconColorClasses[iconColor]}`,
|
|
1356
|
+
children: icon || defaultIcon
|
|
1357
|
+
}
|
|
1358
|
+
)
|
|
1359
|
+
] })
|
|
1360
|
+
}
|
|
1361
|
+
);
|
|
1362
|
+
};
|
|
1363
|
+
var ActionButton = (props) => {
|
|
1364
|
+
const { label, href, method, className = "", item } = props;
|
|
1365
|
+
const hrefValue = typeof href === "function" ? href(item || {}) : href;
|
|
1366
|
+
const isDelete = method === "delete";
|
|
1367
|
+
const variant = isDelete ? "danger" : "ghost";
|
|
1368
|
+
const size = "sm";
|
|
1369
|
+
return /* @__PURE__ */ jsx(
|
|
1370
|
+
Button,
|
|
1371
|
+
{
|
|
1372
|
+
variant,
|
|
1373
|
+
size,
|
|
1374
|
+
className: `${isDelete ? "" : "text-blue-600 hover:text-blue-800"} ${className}`,
|
|
1375
|
+
href: hrefValue,
|
|
1376
|
+
hxGet: !isDelete ? hrefValue : void 0,
|
|
1377
|
+
hxDelete: isDelete ? hrefValue : void 0,
|
|
1378
|
+
hxPushUrl: !isDelete,
|
|
1379
|
+
hxConfirm: isDelete ? "\u786E\u5B9A\u5220\u9664\u5417\uFF1F" : void 0,
|
|
1380
|
+
hxHeaders: isDelete ? JSON.stringify({ "HX-Target": "#main-content" }) : void 0,
|
|
1381
|
+
children: label
|
|
1382
|
+
}
|
|
1383
|
+
);
|
|
1384
|
+
};
|
|
1385
|
+
var EmptyState = (props) => {
|
|
1386
|
+
const { message = "\u6682\u65E0\u6570\u636E", children } = props;
|
|
1387
|
+
return /* @__PURE__ */ jsx("div", { className: "text-center py-12", children: children || /* @__PURE__ */ jsx("p", { className: "text-gray-500 text-sm", children: message }) });
|
|
1388
|
+
};
|
|
1389
|
+
var Pagination = (props) => {
|
|
1390
|
+
const {
|
|
1391
|
+
page,
|
|
1392
|
+
pageSize,
|
|
1393
|
+
total,
|
|
1394
|
+
totalPages,
|
|
1395
|
+
baseUrl,
|
|
1396
|
+
currentParams = {}
|
|
1397
|
+
} = props;
|
|
1398
|
+
if (totalPages <= 1) {
|
|
1399
|
+
return null;
|
|
1400
|
+
}
|
|
1401
|
+
const start = (page - 1) * pageSize + 1;
|
|
1402
|
+
const end = Math.min(page * pageSize, total);
|
|
1403
|
+
const buildUrl = (targetPage) => {
|
|
1404
|
+
const url = new URL(baseUrl, "http://localhost");
|
|
1405
|
+
for (const [key, value] of Object.entries(currentParams)) {
|
|
1406
|
+
if (value !== void 0 && value !== null && value !== "") {
|
|
1407
|
+
url.searchParams.set(key, String(value));
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
url.searchParams.set("page", String(targetPage));
|
|
1411
|
+
return url.pathname + url.search;
|
|
1412
|
+
};
|
|
1413
|
+
return /* @__PURE__ */ jsxs("div", { className: "px-6 py-4 flex items-center justify-between border-t border-gray-200 bg-gray-50", children: [
|
|
1414
|
+
/* @__PURE__ */ jsxs("div", { className: "text-sm text-gray-700", children: [
|
|
1415
|
+
"\u663E\u793A ",
|
|
1416
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: start }),
|
|
1417
|
+
" \u5230",
|
|
1418
|
+
" ",
|
|
1419
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: end }),
|
|
1420
|
+
" \u6761\uFF0C\u5171",
|
|
1421
|
+
" ",
|
|
1422
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: total }),
|
|
1423
|
+
" \u6761"
|
|
1424
|
+
] }),
|
|
1425
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
1426
|
+
page > 1 && /* @__PURE__ */ jsx(
|
|
1427
|
+
Button,
|
|
1428
|
+
{
|
|
1429
|
+
variant: "secondary",
|
|
1430
|
+
size: "sm",
|
|
1431
|
+
hxGet: buildUrl(page - 1),
|
|
1432
|
+
href: buildUrl(page - 1),
|
|
1433
|
+
children: "\u4E0A\u4E00\u9875"
|
|
1434
|
+
}
|
|
1435
|
+
),
|
|
1436
|
+
page < totalPages && /* @__PURE__ */ jsx(
|
|
1437
|
+
Button,
|
|
1438
|
+
{
|
|
1439
|
+
variant: "secondary",
|
|
1440
|
+
size: "sm",
|
|
1441
|
+
hxGet: buildUrl(page + 1),
|
|
1442
|
+
href: buildUrl(page + 1),
|
|
1443
|
+
children: "\u4E0B\u4E00\u9875"
|
|
1444
|
+
}
|
|
1445
|
+
)
|
|
1446
|
+
] })
|
|
1447
|
+
] });
|
|
1448
|
+
};
|
|
1449
|
+
var STYLES = {
|
|
1450
|
+
header: {
|
|
1451
|
+
container: "px-6 py-4 border-b border-gray-200 flex items-center justify-between",
|
|
1452
|
+
title: "text-lg font-semibold text-gray-900",
|
|
1453
|
+
actions: "flex gap-2"
|
|
1454
|
+
},
|
|
1455
|
+
table: {
|
|
1456
|
+
container: "overflow-x-auto",
|
|
1457
|
+
table: "min-w-full divide-y divide-gray-200",
|
|
1458
|
+
thead: "bg-gray-50",
|
|
1459
|
+
th: {
|
|
1460
|
+
base: "px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider whitespace-nowrap",
|
|
1461
|
+
stickyLeft: "sticky left-0 z-20 bg-gray-50 border-r border-gray-200 whitespace-nowrap",
|
|
1462
|
+
stickyRight: "sticky right-0 z-20 bg-gray-50 border-l border-gray-200 whitespace-nowrap"
|
|
1463
|
+
},
|
|
1464
|
+
tbody: "bg-white divide-y divide-gray-200",
|
|
1465
|
+
tr: "group hover:bg-gray-50 transition-colors",
|
|
1466
|
+
td: {
|
|
1467
|
+
base: "px-6 py-4 whitespace-nowrap text-sm text-gray-900",
|
|
1468
|
+
stickyLeft: "sticky left-0 z-10 bg-white group-hover:bg-gray-50 transition-colors border-r border-gray-200",
|
|
1469
|
+
stickyRight: "sticky right-0 z-10 bg-white group-hover:bg-gray-50 transition-colors border-l border-gray-200"
|
|
1470
|
+
}
|
|
1471
|
+
},
|
|
1472
|
+
actionLink: {
|
|
1473
|
+
container: "flex gap-3",
|
|
1474
|
+
base: "text-sm hover:underline",
|
|
1475
|
+
default: "text-blue-600 hover:text-blue-800",
|
|
1476
|
+
delete: "text-red-600 hover:text-red-800"
|
|
1477
|
+
},
|
|
1478
|
+
actionButton: "flex gap-2 flex-wrap"
|
|
1479
|
+
};
|
|
1480
|
+
function TableHeader(props) {
|
|
1481
|
+
const { title, tableActions } = props;
|
|
1482
|
+
const showHeader = title && title.trim() || tableActions && tableActions.length > 0;
|
|
1483
|
+
if (!showHeader) return null;
|
|
1484
|
+
return /* @__PURE__ */ jsxs("div", { className: STYLES.header.container, children: [
|
|
1485
|
+
title && title.trim() ? /* @__PURE__ */ jsx("h3", { className: STYLES.header.title, children: title }) : /* @__PURE__ */ jsx("div", {}),
|
|
1486
|
+
tableActions && tableActions.length > 0 && /* @__PURE__ */ jsx("div", { className: STYLES.header.actions, children: tableActions.map((action, idx) => /* @__PURE__ */ jsx(
|
|
1487
|
+
Button,
|
|
1488
|
+
{
|
|
1489
|
+
variant: action.variant || "secondary",
|
|
1490
|
+
href: action.href,
|
|
1491
|
+
hxGet: action.hxGet,
|
|
1492
|
+
hxPost: action.hxPost,
|
|
1493
|
+
hxDelete: action.hxDelete,
|
|
1494
|
+
hxConfirm: action.hxConfirm,
|
|
1495
|
+
children: action.label
|
|
1496
|
+
},
|
|
1497
|
+
idx
|
|
1498
|
+
)) })
|
|
1499
|
+
] });
|
|
1500
|
+
}
|
|
1501
|
+
function TableHeaderRow(props) {
|
|
1502
|
+
const { idColumn, otherColumns, hasActions } = props;
|
|
1503
|
+
return /* @__PURE__ */ jsxs("tr", { children: [
|
|
1504
|
+
idColumn && /* @__PURE__ */ jsx("th", { className: `${STYLES.table.th.base} ${STYLES.table.th.stickyLeft}`, children: idColumn.label }),
|
|
1505
|
+
otherColumns.map((col) => /* @__PURE__ */ jsx("th", { className: STYLES.table.th.base, children: col.label }, col.key)),
|
|
1506
|
+
hasActions && /* @__PURE__ */ jsx(
|
|
1507
|
+
"th",
|
|
1508
|
+
{
|
|
1509
|
+
className: `${STYLES.table.th.base} ${STYLES.table.th.stickyRight}`,
|
|
1510
|
+
children: "\u64CD\u4F5C"
|
|
1511
|
+
}
|
|
1512
|
+
)
|
|
1513
|
+
] });
|
|
1514
|
+
}
|
|
1515
|
+
function TableCell(props) {
|
|
1516
|
+
const { column, item, isSticky, stickyClass } = props;
|
|
1517
|
+
const value = item[column.key];
|
|
1518
|
+
const content = column.render ? safeRender(column.render(value, item)) : String(value || "");
|
|
1519
|
+
const className = isSticky ? `${STYLES.table.td.base} ${stickyClass || ""}` : STYLES.table.td.base;
|
|
1520
|
+
return /* @__PURE__ */ jsx("td", { className, children: content });
|
|
1521
|
+
}
|
|
1522
|
+
function ActionLink(props) {
|
|
1523
|
+
const { action, item } = props;
|
|
1524
|
+
const hrefValue = action.href(item);
|
|
1525
|
+
const isDelete = action.method === "delete";
|
|
1526
|
+
const className = `${STYLES.actionLink.base} ${isDelete ? STYLES.actionLink.delete : STYLES.actionLink.default}`;
|
|
1527
|
+
return /* @__PURE__ */ jsx(
|
|
1528
|
+
"a",
|
|
1529
|
+
{
|
|
1530
|
+
href: hrefValue,
|
|
1531
|
+
className,
|
|
1532
|
+
"hx-get": !isDelete ? hrefValue : void 0,
|
|
1533
|
+
"hx-delete": isDelete ? hrefValue : void 0,
|
|
1534
|
+
"hx-confirm": isDelete ? "\u786E\u5B9A\u5220\u9664\u5417\uFF1F" : void 0,
|
|
1535
|
+
children: action.label
|
|
1536
|
+
}
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
function ActionCell(props) {
|
|
1540
|
+
const { actions, item, actionStyle } = props;
|
|
1541
|
+
if (!actions || actions.length === 0) return null;
|
|
1542
|
+
return /* @__PURE__ */ jsx("td", { className: `${STYLES.table.td.base} ${STYLES.table.td.stickyRight}`, children: actionStyle === "link" ? /* @__PURE__ */ jsx("div", { className: STYLES.actionLink.container, children: actions.map((action, idx) => /* @__PURE__ */ jsx(ActionLink, { action, item }, idx)) }) : /* @__PURE__ */ jsx("div", { className: STYLES.actionButton, children: actions.map((action, idx) => /* @__PURE__ */ jsx(
|
|
1543
|
+
ActionButton,
|
|
1544
|
+
{
|
|
1545
|
+
label: action.label,
|
|
1546
|
+
href: action.href,
|
|
1547
|
+
method: action.method,
|
|
1548
|
+
className: action.class,
|
|
1549
|
+
item
|
|
1550
|
+
},
|
|
1551
|
+
idx
|
|
1552
|
+
)) }) });
|
|
1553
|
+
}
|
|
1554
|
+
function TableRow(props) {
|
|
1555
|
+
const { item, idColumn, otherColumns, actions, actionStyle } = props;
|
|
1556
|
+
return /* @__PURE__ */ jsxs("tr", { className: STYLES.table.tr, children: [
|
|
1557
|
+
idColumn && /* @__PURE__ */ jsx(
|
|
1558
|
+
TableCell,
|
|
1559
|
+
{
|
|
1560
|
+
column: idColumn,
|
|
1561
|
+
item,
|
|
1562
|
+
isSticky: true,
|
|
1563
|
+
stickyClass: STYLES.table.td.stickyLeft
|
|
1564
|
+
}
|
|
1565
|
+
),
|
|
1566
|
+
otherColumns.map((col) => /* @__PURE__ */ jsx(TableCell, { column: col, item }, col.key)),
|
|
1567
|
+
actions && actions.length > 0 && /* @__PURE__ */ jsx(ActionCell, { actions, item, actionStyle })
|
|
1568
|
+
] });
|
|
1569
|
+
}
|
|
1570
|
+
var Table = (props) => {
|
|
1571
|
+
const {
|
|
1572
|
+
items,
|
|
1573
|
+
columns,
|
|
1574
|
+
actions,
|
|
1575
|
+
pagination,
|
|
1576
|
+
title,
|
|
1577
|
+
tableActions,
|
|
1578
|
+
actionStyle = "link"
|
|
1579
|
+
} = props;
|
|
1580
|
+
const idColumn = columns.find((col) => col.key === "id");
|
|
1581
|
+
const otherColumns = columns.filter((col) => col.key !== "id");
|
|
1582
|
+
const hasActions = Boolean(actions && actions.length > 0);
|
|
1583
|
+
return /* @__PURE__ */ jsxs(Card, { shadow: true, noPadding: true, className: "overflow-hidden", children: [
|
|
1584
|
+
/* @__PURE__ */ jsx(TableHeader, { title, tableActions }),
|
|
1585
|
+
items.length === 0 ? /* @__PURE__ */ jsx(EmptyState, { message: "\u6682\u65E0\u6570\u636E" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1586
|
+
/* @__PURE__ */ jsx("div", { className: STYLES.table.container, children: /* @__PURE__ */ jsxs("table", { className: STYLES.table.table, children: [
|
|
1587
|
+
/* @__PURE__ */ jsx("thead", { className: STYLES.table.thead, children: /* @__PURE__ */ jsx(
|
|
1588
|
+
TableHeaderRow,
|
|
1589
|
+
{
|
|
1590
|
+
columns,
|
|
1591
|
+
idColumn,
|
|
1592
|
+
otherColumns,
|
|
1593
|
+
hasActions
|
|
1594
|
+
}
|
|
1595
|
+
) }),
|
|
1596
|
+
/* @__PURE__ */ jsx("tbody", { className: STYLES.table.tbody, children: items.map((item) => /* @__PURE__ */ jsx(
|
|
1597
|
+
TableRow,
|
|
1598
|
+
{
|
|
1599
|
+
item,
|
|
1600
|
+
idColumn,
|
|
1601
|
+
otherColumns,
|
|
1602
|
+
actions,
|
|
1603
|
+
actionStyle
|
|
1604
|
+
},
|
|
1605
|
+
item.id
|
|
1606
|
+
)) })
|
|
1607
|
+
] }) }),
|
|
1608
|
+
pagination && /* @__PURE__ */ jsx(Pagination, { ...pagination })
|
|
1609
|
+
] })
|
|
1610
|
+
] });
|
|
1611
|
+
};
|
|
1612
|
+
function ListContent(props) {
|
|
1613
|
+
const {
|
|
1614
|
+
result,
|
|
1615
|
+
params,
|
|
1616
|
+
pageTitle,
|
|
1617
|
+
pageDescription,
|
|
1618
|
+
tableTitle,
|
|
1619
|
+
columns,
|
|
1620
|
+
actions,
|
|
1621
|
+
stats = [],
|
|
1622
|
+
filters = [],
|
|
1623
|
+
tableActions = [],
|
|
1624
|
+
hasFormModule,
|
|
1625
|
+
createPath,
|
|
1626
|
+
listPath
|
|
1627
|
+
} = props;
|
|
1628
|
+
const currentParams = {
|
|
1629
|
+
pageSize: params.pageSize,
|
|
1630
|
+
sortBy: params.sortBy,
|
|
1631
|
+
sortOrder: params.sortOrder,
|
|
1632
|
+
...params.filters
|
|
1633
|
+
// 包含所有筛选条件
|
|
1634
|
+
};
|
|
1635
|
+
const tableActionsList = actions && actions.length > 0 ? actions.map((action) => ({
|
|
1636
|
+
label: action.label,
|
|
1637
|
+
href: typeof action.href === "string" ? () => action.href : action.href,
|
|
1638
|
+
method: action.method,
|
|
1639
|
+
class: action.class
|
|
1640
|
+
})) : void 0;
|
|
1641
|
+
const finalTableActions = tableActions.length > 0 ? tableActions : [
|
|
1642
|
+
{
|
|
1643
|
+
label: "\u5237\u65B0",
|
|
1644
|
+
hxGet: listPath,
|
|
1645
|
+
variant: "primary"
|
|
1646
|
+
}
|
|
1647
|
+
];
|
|
1648
|
+
const createButton = hasFormModule ? /* @__PURE__ */ jsx(Button, { variant: "primary", href: createPath, hxGet: createPath, children: "\u65B0\u5EFA" }) : null;
|
|
1649
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
1650
|
+
/* @__PURE__ */ jsx(
|
|
1651
|
+
PageHeader,
|
|
1652
|
+
{
|
|
1653
|
+
title: pageTitle,
|
|
1654
|
+
description: pageDescription,
|
|
1655
|
+
actions: createButton
|
|
1656
|
+
}
|
|
1657
|
+
),
|
|
1658
|
+
stats.length > 0 && /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6", children: stats.map((stat, idx) => /* @__PURE__ */ jsx(StatCard, { ...stat }, idx)) }),
|
|
1659
|
+
filters.length > 0 && /* @__PURE__ */ jsx(
|
|
1660
|
+
FilterCard,
|
|
1661
|
+
{
|
|
1662
|
+
fields: filters,
|
|
1663
|
+
filterButtonText: "\u7B5B\u9009",
|
|
1664
|
+
filterButtonHxGet: listPath
|
|
1665
|
+
}
|
|
1666
|
+
),
|
|
1667
|
+
/* @__PURE__ */ jsx(
|
|
1668
|
+
Table,
|
|
1669
|
+
{
|
|
1670
|
+
items: result.items,
|
|
1671
|
+
columns: columns.map((col) => ({
|
|
1672
|
+
key: col.key,
|
|
1673
|
+
label: col.label,
|
|
1674
|
+
render: col.render ? (value, item) => col.render(value, item) : void 0
|
|
1675
|
+
})),
|
|
1676
|
+
actions: tableActionsList?.map((action) => ({
|
|
1677
|
+
label: action.label,
|
|
1678
|
+
href: (item) => action.href(item),
|
|
1679
|
+
method: action.method,
|
|
1680
|
+
class: action.class
|
|
1681
|
+
})),
|
|
1682
|
+
pagination: {
|
|
1683
|
+
page: result.page,
|
|
1684
|
+
pageSize: result.pageSize,
|
|
1685
|
+
total: result.total,
|
|
1686
|
+
totalPages: result.totalPages,
|
|
1687
|
+
baseUrl: listPath,
|
|
1688
|
+
currentParams
|
|
1689
|
+
},
|
|
1690
|
+
title: tableTitle,
|
|
1691
|
+
tableActions: finalTableActions.length > 0 ? finalTableActions : void 0,
|
|
1692
|
+
actionStyle: "link"
|
|
1693
|
+
}
|
|
1694
|
+
)
|
|
1695
|
+
] });
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// src/utils/params.ts
|
|
1699
|
+
function parseListParams(ctx) {
|
|
1700
|
+
const url = new URL(ctx.req.url);
|
|
1701
|
+
const systemParams = /* @__PURE__ */ new Set(["page", "pageSize", "sortBy", "sortOrder"]);
|
|
1702
|
+
const filters = {};
|
|
1703
|
+
for (const [key, value] of url.searchParams.entries()) {
|
|
1704
|
+
if (!systemParams.has(key) && value !== "") {
|
|
1705
|
+
filters[key] = value;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
return {
|
|
1709
|
+
page: parseInt(url.searchParams.get("page") || "1", 10),
|
|
1710
|
+
pageSize: parseInt(url.searchParams.get("pageSize") || "10", 10),
|
|
1711
|
+
sortBy: url.searchParams.get("sortBy") || void 0,
|
|
1712
|
+
sortOrder: url.searchParams.get("sortOrder") || void 0,
|
|
1713
|
+
filters: Object.keys(filters).length > 0 ? filters : void 0
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
var ListPageModule = class extends PageModule {
|
|
1717
|
+
/** ID 字段名(默认 "id") */
|
|
1718
|
+
idField = "id";
|
|
1719
|
+
/**
|
|
1720
|
+
* 获取列表数据
|
|
1721
|
+
*/
|
|
1722
|
+
async getList(params) {
|
|
1723
|
+
return await this.getDatasource().getList(params);
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* 删除数据(可选,如果数据源支持删除)
|
|
1727
|
+
*/
|
|
1728
|
+
async deleteItem(id) {
|
|
1729
|
+
const datasource = this.getDatasource();
|
|
1730
|
+
if (datasource.deleteItem) {
|
|
1731
|
+
return await datasource.deleteItem(id);
|
|
1732
|
+
}
|
|
1733
|
+
return false;
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* 获取字段标签(可选)
|
|
1737
|
+
* 子类可以重写此方法来自定义字段的中文标签
|
|
1738
|
+
*/
|
|
1739
|
+
getFieldLabel(field) {
|
|
1740
|
+
return field;
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* 自定义列渲染(可选)
|
|
1744
|
+
* 子类可以重写此方法来自定义列的渲染逻辑
|
|
1745
|
+
*/
|
|
1746
|
+
renderColumn(field, value, item) {
|
|
1747
|
+
return value;
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* 获取统计信息(可选)
|
|
1751
|
+
* 子类可以重写此方法来返回 KPI 统计卡片数据
|
|
1752
|
+
* 返回的数组将渲染为 StatCard 组件
|
|
1753
|
+
*/
|
|
1754
|
+
getStats(params) {
|
|
1755
|
+
return [];
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* 获取筛选器配置(可选)
|
|
1759
|
+
* 子类可以重写此方法来返回筛选器字段配置
|
|
1760
|
+
* 返回的配置将渲染为 FilterCard 组件
|
|
1761
|
+
*/
|
|
1762
|
+
getFilters(params) {
|
|
1763
|
+
return [];
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* 获取表格标题(可选)
|
|
1767
|
+
* 子类可以重写此方法来返回表格标题
|
|
1768
|
+
* 如果不定义,默认使用模块名
|
|
1769
|
+
*/
|
|
1770
|
+
getTableTitle() {
|
|
1771
|
+
return "";
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* 获取表格操作按钮(可选)
|
|
1775
|
+
* 子类可以重写此方法来返回表格操作按钮配置(如导出、清空、刷新等)
|
|
1776
|
+
* 如果不定义,默认会生成一个刷新按钮
|
|
1777
|
+
*/
|
|
1778
|
+
getTableActions(params, basePath) {
|
|
1779
|
+
return [];
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* 自定义操作按钮(可选)
|
|
1783
|
+
* 子类可以重写此方法来添加自定义操作按钮
|
|
1784
|
+
* 如果不定义,则根据模块元数据智能生成默认操作按钮
|
|
1785
|
+
*/
|
|
1786
|
+
getActions(item) {
|
|
1787
|
+
const id = item[this.idField];
|
|
1788
|
+
const actions = [];
|
|
1789
|
+
if (this.context.moduleMetadata.hasDetail) {
|
|
1790
|
+
actions.push({
|
|
1791
|
+
label: "\u67E5\u770B",
|
|
1792
|
+
href: this.paths.detail(id)
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
if (this.context.moduleMetadata.hasForm) {
|
|
1796
|
+
actions.push({
|
|
1797
|
+
label: "\u7F16\u8F91",
|
|
1798
|
+
href: this.paths.edit(id)
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
if (this.getDatasource().deleteItem) {
|
|
1802
|
+
actions.push({
|
|
1803
|
+
label: "\u5220\u9664",
|
|
1804
|
+
href: this.paths.delete(id)
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
return actions;
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* 渲染列表页面(默认实现)
|
|
1811
|
+
* 子类可以重写此方法来自定义渲染
|
|
1812
|
+
*/
|
|
1813
|
+
async render() {
|
|
1814
|
+
const params = parseListParams(this.context.ctx);
|
|
1815
|
+
const result = await this.getList(params);
|
|
1816
|
+
const pageTitle = this.getTitle();
|
|
1817
|
+
const pageDescription = this.getDescription();
|
|
1818
|
+
const tableTitle = this.getTableTitle ? this.getTableTitle() : pageTitle;
|
|
1819
|
+
const columns = result.items.length > 0 ? Object.keys(result.items[0]).map((key) => ({
|
|
1820
|
+
key,
|
|
1821
|
+
label: this.getFieldLabel ? this.getFieldLabel(key) : key,
|
|
1822
|
+
render: this.renderColumn ? (value, item) => this.renderColumn(key, value, item) : void 0
|
|
1823
|
+
})) : [];
|
|
1824
|
+
const actions = this.getActions && result.items.length > 0 ? this.getActions(result.items[0]) : [];
|
|
1825
|
+
const statsResult = this.getStats ? await Promise.resolve(this.getStats(params)) : [];
|
|
1826
|
+
const stats = Array.isArray(statsResult) ? statsResult : [];
|
|
1827
|
+
const filters = this.getFilters ? this.getFilters(params) : [];
|
|
1828
|
+
let tableActions = this.getTableActions ? this.getTableActions(params, this.paths.base()) : [];
|
|
1829
|
+
const convertedActions = actions.map((action) => ({
|
|
1830
|
+
label: action.label,
|
|
1831
|
+
href: typeof action.href === "string" ? action.href : ((item) => action.href(item)),
|
|
1832
|
+
method: action.method,
|
|
1833
|
+
class: action.class
|
|
1834
|
+
}));
|
|
1835
|
+
return /* @__PURE__ */ jsx(
|
|
1836
|
+
ListContent,
|
|
1837
|
+
{
|
|
1838
|
+
result,
|
|
1839
|
+
params,
|
|
1840
|
+
pageTitle,
|
|
1841
|
+
pageDescription,
|
|
1842
|
+
tableTitle,
|
|
1843
|
+
columns,
|
|
1844
|
+
actions: convertedActions,
|
|
1845
|
+
stats,
|
|
1846
|
+
filters,
|
|
1847
|
+
tableActions,
|
|
1848
|
+
hasFormModule: this.context.moduleMetadata.hasForm,
|
|
1849
|
+
createPath: this.paths.create(),
|
|
1850
|
+
listPath: this.paths.list()
|
|
1851
|
+
}
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
|
|
1856
|
+
// src/datasource/index.ts
|
|
1857
|
+
var MemoryListDatasource = class {
|
|
1858
|
+
data;
|
|
1859
|
+
constructor(data = []) {
|
|
1860
|
+
this.data = data;
|
|
1861
|
+
}
|
|
1862
|
+
async getList(params = {}) {
|
|
1863
|
+
const {
|
|
1864
|
+
page = 1,
|
|
1865
|
+
pageSize = 10,
|
|
1866
|
+
sortBy,
|
|
1867
|
+
sortOrder = "asc",
|
|
1868
|
+
filters = {}
|
|
1869
|
+
} = params;
|
|
1870
|
+
let filtered = this.data.filter((item) => {
|
|
1871
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
1872
|
+
if (value === void 0 || value === null || value === "") {
|
|
1873
|
+
continue;
|
|
1874
|
+
}
|
|
1875
|
+
const itemValue = item[key];
|
|
1876
|
+
if (typeof itemValue === "string" && !itemValue.toLowerCase().includes(String(value).toLowerCase())) {
|
|
1877
|
+
return false;
|
|
1878
|
+
}
|
|
1879
|
+
if (itemValue !== value) {
|
|
1880
|
+
return false;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
return true;
|
|
1884
|
+
});
|
|
1885
|
+
if (sortBy) {
|
|
1886
|
+
filtered.sort((a, b) => {
|
|
1887
|
+
const aVal = a[sortBy];
|
|
1888
|
+
const bVal = b[sortBy];
|
|
1889
|
+
const comparison = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
1890
|
+
return sortOrder === "asc" ? comparison : -comparison;
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
const start = (page - 1) * pageSize;
|
|
1894
|
+
const end = start + pageSize;
|
|
1895
|
+
const items = filtered.slice(start, end);
|
|
1896
|
+
return {
|
|
1897
|
+
items,
|
|
1898
|
+
total: filtered.length,
|
|
1899
|
+
page,
|
|
1900
|
+
pageSize,
|
|
1901
|
+
totalPages: Math.ceil(filtered.length / pageSize)
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
async getItem(id) {
|
|
1905
|
+
return this.data.find((item) => {
|
|
1906
|
+
if (item.id === id) {
|
|
1907
|
+
return true;
|
|
1908
|
+
}
|
|
1909
|
+
if (typeof item.id === "number" && typeof id === "string") {
|
|
1910
|
+
return item.id === Number(id);
|
|
1911
|
+
}
|
|
1912
|
+
if (typeof item.id === "string" && typeof id === "number") {
|
|
1913
|
+
return Number(item.id) === id;
|
|
1914
|
+
}
|
|
1915
|
+
return false;
|
|
1916
|
+
}) || null;
|
|
1917
|
+
}
|
|
1918
|
+
async deleteItem(id) {
|
|
1919
|
+
const index = this.data.findIndex((item) => {
|
|
1920
|
+
if (item.id === id) {
|
|
1921
|
+
return true;
|
|
1922
|
+
}
|
|
1923
|
+
if (typeof item.id === "number" && typeof id === "string") {
|
|
1924
|
+
return item.id === Number(id);
|
|
1925
|
+
}
|
|
1926
|
+
if (typeof item.id === "string" && typeof id === "number") {
|
|
1927
|
+
return Number(item.id) === id;
|
|
1928
|
+
}
|
|
1929
|
+
return false;
|
|
1930
|
+
});
|
|
1931
|
+
if (index === -1) {
|
|
1932
|
+
return false;
|
|
1933
|
+
}
|
|
1934
|
+
this.data.splice(index, 1);
|
|
1935
|
+
return true;
|
|
1936
|
+
}
|
|
1937
|
+
async updateItem(id, data) {
|
|
1938
|
+
const index = this.data.findIndex((item) => {
|
|
1939
|
+
if (item.id === id) {
|
|
1940
|
+
return true;
|
|
1941
|
+
}
|
|
1942
|
+
if (typeof item.id === "number" && typeof id === "string") {
|
|
1943
|
+
return item.id === Number(id);
|
|
1944
|
+
}
|
|
1945
|
+
if (typeof item.id === "string" && typeof id === "number") {
|
|
1946
|
+
return Number(item.id) === id;
|
|
1947
|
+
}
|
|
1948
|
+
return false;
|
|
1949
|
+
});
|
|
1950
|
+
if (index === -1) {
|
|
1951
|
+
return null;
|
|
1952
|
+
}
|
|
1953
|
+
this.data[index] = { ...this.data[index], ...data };
|
|
1954
|
+
return this.data[index];
|
|
1955
|
+
}
|
|
1956
|
+
async createItem(data) {
|
|
1957
|
+
const maxId = this.data.length > 0 ? Math.max(
|
|
1958
|
+
...this.data.map(
|
|
1959
|
+
(item) => typeof item.id === "number" ? item.id : 0
|
|
1960
|
+
)
|
|
1961
|
+
) : 0;
|
|
1962
|
+
const newId = maxId + 1;
|
|
1963
|
+
const newItem = { ...data, id: newId };
|
|
1964
|
+
this.data.push(newItem);
|
|
1965
|
+
return newItem;
|
|
1966
|
+
}
|
|
1967
|
+
};
|
|
1968
|
+
var Dialog = (props) => {
|
|
1969
|
+
const {
|
|
1970
|
+
title,
|
|
1971
|
+
children,
|
|
1972
|
+
showClose = true,
|
|
1973
|
+
closeUrl,
|
|
1974
|
+
className = "",
|
|
1975
|
+
size = "lg"
|
|
1976
|
+
} = props;
|
|
1977
|
+
const sizeClasses = {
|
|
1978
|
+
sm: "max-w-md",
|
|
1979
|
+
md: "max-w-lg",
|
|
1980
|
+
lg: "max-w-2xl",
|
|
1981
|
+
xl: "max-w-4xl",
|
|
1982
|
+
full: "max-w-7xl"
|
|
1983
|
+
};
|
|
1984
|
+
return /* @__PURE__ */ jsx(
|
|
1985
|
+
"div",
|
|
1986
|
+
{
|
|
1987
|
+
className: "fixed inset-0 bg-black bg-opacity-50 z-[100] flex items-center justify-center p-4 dialog-backdrop",
|
|
1988
|
+
style: {
|
|
1989
|
+
animation: "fadeIn 0.2s ease-out"
|
|
1990
|
+
},
|
|
1991
|
+
_: "on click if event.target is me \r\n add .dialog-exit to me\r\n add .dialog-content-exit to .dialog-content\r\n wait 200ms\r\n set #dialog-container's innerHTML to '' end",
|
|
1992
|
+
children: /* @__PURE__ */ jsxs(
|
|
1993
|
+
"div",
|
|
1994
|
+
{
|
|
1995
|
+
className: `bg-gray-50 rounded-lg shadow-xl ${sizeClasses[size]} w-full max-h-[90vh] overflow-hidden flex flex-col dialog-content ${className}`,
|
|
1996
|
+
style: {
|
|
1997
|
+
animation: "slideIn 0.3s ease-out"
|
|
1998
|
+
},
|
|
1999
|
+
_: "on click call event.stopPropagation()",
|
|
2000
|
+
children: [
|
|
2001
|
+
(title || showClose) && /* @__PURE__ */ jsxs("div", { className: "px-6 py-4 border-b border-gray-200 bg-white flex items-center justify-between", children: [
|
|
2002
|
+
title && /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title }),
|
|
2003
|
+
showClose && /* @__PURE__ */ jsx(
|
|
2004
|
+
"button",
|
|
2005
|
+
{
|
|
2006
|
+
className: "text-gray-400 hover:text-gray-600 transition-colors",
|
|
2007
|
+
_: "on click \r\n add .dialog-exit to .dialog-backdrop\r\n add .dialog-content-exit to .dialog-content\r\n wait 200ms\r\n set #dialog-container's innerHTML to '' end",
|
|
2008
|
+
children: /* @__PURE__ */ jsx(
|
|
2009
|
+
"svg",
|
|
2010
|
+
{
|
|
2011
|
+
className: "w-6 h-6",
|
|
2012
|
+
fill: "none",
|
|
2013
|
+
stroke: "currentColor",
|
|
2014
|
+
viewBox: "0 0 24 24",
|
|
2015
|
+
children: /* @__PURE__ */ jsx(
|
|
2016
|
+
"path",
|
|
2017
|
+
{
|
|
2018
|
+
strokeLinecap: "round",
|
|
2019
|
+
strokeLinejoin: "round",
|
|
2020
|
+
strokeWidth: 2,
|
|
2021
|
+
d: "M6 18L18 6M6 6l12 12"
|
|
2022
|
+
}
|
|
2023
|
+
)
|
|
2024
|
+
}
|
|
2025
|
+
)
|
|
2026
|
+
}
|
|
2027
|
+
)
|
|
2028
|
+
] }),
|
|
2029
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto p-6 bg-gray-50", children })
|
|
2030
|
+
]
|
|
2031
|
+
}
|
|
2032
|
+
)
|
|
2033
|
+
}
|
|
2034
|
+
);
|
|
2035
|
+
};
|
|
2036
|
+
var ErrorAlert = (props) => {
|
|
2037
|
+
const {
|
|
2038
|
+
title,
|
|
2039
|
+
message,
|
|
2040
|
+
type = "error",
|
|
2041
|
+
showClose = true,
|
|
2042
|
+
className = ""
|
|
2043
|
+
} = props;
|
|
2044
|
+
const typeClasses = {
|
|
2045
|
+
error: "bg-red-50 border-red-200 text-red-800",
|
|
2046
|
+
warning: "bg-yellow-50 border-yellow-200 text-yellow-800",
|
|
2047
|
+
info: "bg-blue-50 border-blue-200 text-blue-800",
|
|
2048
|
+
success: "bg-green-50 border-green-200 text-green-800"
|
|
2049
|
+
};
|
|
2050
|
+
const iconColors = {
|
|
2051
|
+
error: "text-red-400",
|
|
2052
|
+
warning: "text-yellow-400",
|
|
2053
|
+
info: "text-blue-400",
|
|
2054
|
+
success: "text-green-400"
|
|
2055
|
+
};
|
|
2056
|
+
const icons = {
|
|
2057
|
+
error: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx(
|
|
2058
|
+
"path",
|
|
2059
|
+
{
|
|
2060
|
+
fillRule: "evenodd",
|
|
2061
|
+
d: "M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z",
|
|
2062
|
+
clipRule: "evenodd"
|
|
2063
|
+
}
|
|
2064
|
+
) }),
|
|
2065
|
+
warning: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx(
|
|
2066
|
+
"path",
|
|
2067
|
+
{
|
|
2068
|
+
fillRule: "evenodd",
|
|
2069
|
+
d: "M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z",
|
|
2070
|
+
clipRule: "evenodd"
|
|
2071
|
+
}
|
|
2072
|
+
) }),
|
|
2073
|
+
info: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx(
|
|
2074
|
+
"path",
|
|
2075
|
+
{
|
|
2076
|
+
fillRule: "evenodd",
|
|
2077
|
+
d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z",
|
|
2078
|
+
clipRule: "evenodd"
|
|
2079
|
+
}
|
|
2080
|
+
) }),
|
|
2081
|
+
success: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx(
|
|
2082
|
+
"path",
|
|
2083
|
+
{
|
|
2084
|
+
fillRule: "evenodd",
|
|
2085
|
+
d: "M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z",
|
|
2086
|
+
clipRule: "evenodd"
|
|
2087
|
+
}
|
|
2088
|
+
) })
|
|
2089
|
+
};
|
|
2090
|
+
return /* @__PURE__ */ jsx(
|
|
2091
|
+
"div",
|
|
2092
|
+
{
|
|
2093
|
+
className: `border rounded-lg p-4 shadow-lg mb-2 ${typeClasses[type]} ${className} animate-[slideInRight_0.3s_ease-out]`,
|
|
2094
|
+
role: "alert",
|
|
2095
|
+
style: {
|
|
2096
|
+
animation: "slideInRight 0.3s ease-out"
|
|
2097
|
+
},
|
|
2098
|
+
_: "on click if event.target is me or event.target.closest('button') \r\n add .error-alert-exit to me\r\n wait 300ms\r\n remove me end",
|
|
2099
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex items-start", children: [
|
|
2100
|
+
/* @__PURE__ */ jsx("div", { className: `flex-shrink-0 ${iconColors[type]}`, children: icons[type] }),
|
|
2101
|
+
/* @__PURE__ */ jsxs("div", { className: "ml-3 flex-1 min-w-0", children: [
|
|
2102
|
+
title && /* @__PURE__ */ jsx("h3", { className: "text-sm font-medium mb-1", children: title }),
|
|
2103
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm break-words whitespace-pre-wrap", children: message })
|
|
2104
|
+
] }),
|
|
2105
|
+
showClose && /* @__PURE__ */ jsx(
|
|
2106
|
+
"button",
|
|
2107
|
+
{
|
|
2108
|
+
className: "ml-4 flex-shrink-0 text-gray-400 hover:text-gray-600 transition-colors",
|
|
2109
|
+
type: "button",
|
|
2110
|
+
children: /* @__PURE__ */ jsx(
|
|
2111
|
+
"svg",
|
|
2112
|
+
{
|
|
2113
|
+
className: "w-5 h-5",
|
|
2114
|
+
fill: "none",
|
|
2115
|
+
stroke: "currentColor",
|
|
2116
|
+
viewBox: "0 0 24 24",
|
|
2117
|
+
children: /* @__PURE__ */ jsx(
|
|
2118
|
+
"path",
|
|
2119
|
+
{
|
|
2120
|
+
strokeLinecap: "round",
|
|
2121
|
+
strokeLinejoin: "round",
|
|
2122
|
+
strokeWidth: 2,
|
|
2123
|
+
d: "M6 18L18 6M6 6l12 12"
|
|
2124
|
+
}
|
|
2125
|
+
)
|
|
2126
|
+
}
|
|
2127
|
+
)
|
|
2128
|
+
}
|
|
2129
|
+
)
|
|
2130
|
+
] })
|
|
2131
|
+
}
|
|
2132
|
+
);
|
|
2133
|
+
};
|
|
2134
|
+
var Breadcrumb = (props) => {
|
|
2135
|
+
const { items } = props;
|
|
2136
|
+
if (items.length === 0) {
|
|
2137
|
+
return null;
|
|
2138
|
+
}
|
|
2139
|
+
return /* @__PURE__ */ jsx("nav", { className: "flex", "aria-label": "Breadcrumb", children: /* @__PURE__ */ jsx("ol", { className: "flex items-center space-x-2", children: items.map((item, index) => /* @__PURE__ */ jsxs("li", { className: "flex items-center", children: [
|
|
2140
|
+
index > 0 && /* @__PURE__ */ jsx(
|
|
2141
|
+
"svg",
|
|
2142
|
+
{
|
|
2143
|
+
className: "w-5 h-5 text-gray-400 mx-2",
|
|
2144
|
+
fill: "currentColor",
|
|
2145
|
+
viewBox: "0 0 20 20",
|
|
2146
|
+
children: /* @__PURE__ */ jsx(
|
|
2147
|
+
"path",
|
|
2148
|
+
{
|
|
2149
|
+
fillRule: "evenodd",
|
|
2150
|
+
d: "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z",
|
|
2151
|
+
clipRule: "evenodd"
|
|
2152
|
+
}
|
|
2153
|
+
)
|
|
2154
|
+
}
|
|
2155
|
+
),
|
|
2156
|
+
item.href ? /* @__PURE__ */ jsx(
|
|
2157
|
+
"a",
|
|
2158
|
+
{
|
|
2159
|
+
href: item.href,
|
|
2160
|
+
className: "text-sm font-medium text-gray-500 hover:text-gray-700",
|
|
2161
|
+
"hx-get": item.href,
|
|
2162
|
+
"hx-push-url": "true",
|
|
2163
|
+
children: item.label
|
|
2164
|
+
}
|
|
2165
|
+
) : /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-gray-900", children: item.label })
|
|
2166
|
+
] }, index)) }) });
|
|
2167
|
+
};
|
|
2168
|
+
var Header = (props) => {
|
|
2169
|
+
const { breadcrumbs = [], userInfo, sidebarCollapsed = false } = props;
|
|
2170
|
+
return /* @__PURE__ */ jsx("header", { className: "bg-white border-b border-gray-200 shadow-sm sticky top-0 z-40", children: /* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
2171
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4 flex-1 min-w-0", children: [
|
|
2172
|
+
/* @__PURE__ */ jsx(
|
|
2173
|
+
"a",
|
|
2174
|
+
{
|
|
2175
|
+
className: "p-2 rounded-lg hover:bg-gray-100 transition-colors inline-block flex-shrink-0",
|
|
2176
|
+
title: sidebarCollapsed ? "\u5C55\u5F00\u4FA7\u8FB9\u680F" : "\u6298\u53E0\u4FA7\u8FB9\u680F",
|
|
2177
|
+
"hx-get": "",
|
|
2178
|
+
children: sidebarCollapsed ? (
|
|
2179
|
+
// 展开图标(向右箭头)
|
|
2180
|
+
/* @__PURE__ */ jsx(
|
|
2181
|
+
"svg",
|
|
2182
|
+
{
|
|
2183
|
+
className: "w-5 h-5 text-gray-600",
|
|
2184
|
+
fill: "none",
|
|
2185
|
+
stroke: "currentColor",
|
|
2186
|
+
viewBox: "0 0 24 24",
|
|
2187
|
+
children: /* @__PURE__ */ jsx(
|
|
2188
|
+
"path",
|
|
2189
|
+
{
|
|
2190
|
+
strokeLinecap: "round",
|
|
2191
|
+
strokeLinejoin: "round",
|
|
2192
|
+
strokeWidth: 2,
|
|
2193
|
+
d: "M9 5l7 7-7 7"
|
|
2194
|
+
}
|
|
2195
|
+
)
|
|
2196
|
+
}
|
|
2197
|
+
)
|
|
2198
|
+
) : (
|
|
2199
|
+
// 折叠图标(三条横线)
|
|
2200
|
+
/* @__PURE__ */ jsx(
|
|
2201
|
+
"svg",
|
|
2202
|
+
{
|
|
2203
|
+
className: "w-5 h-5 text-gray-600",
|
|
2204
|
+
fill: "none",
|
|
2205
|
+
stroke: "currentColor",
|
|
2206
|
+
viewBox: "0 0 24 24",
|
|
2207
|
+
children: /* @__PURE__ */ jsx(
|
|
2208
|
+
"path",
|
|
2209
|
+
{
|
|
2210
|
+
strokeLinecap: "round",
|
|
2211
|
+
strokeLinejoin: "round",
|
|
2212
|
+
strokeWidth: 2,
|
|
2213
|
+
d: "M4 6h16M4 12h16M4 18h16"
|
|
2214
|
+
}
|
|
2215
|
+
)
|
|
2216
|
+
}
|
|
2217
|
+
)
|
|
2218
|
+
)
|
|
2219
|
+
}
|
|
2220
|
+
),
|
|
2221
|
+
breadcrumbs.length > 0 && /* @__PURE__ */ jsx(Breadcrumb, { items: breadcrumbs })
|
|
2222
|
+
] }),
|
|
2223
|
+
userInfo && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 flex-shrink-0", children: [
|
|
2224
|
+
/* @__PURE__ */ jsxs("div", { className: "text-right hidden sm:block", children: [
|
|
2225
|
+
userInfo.name && /* @__PURE__ */ jsx("div", { className: "text-sm font-medium text-gray-900", children: userInfo.name }),
|
|
2226
|
+
userInfo.email && /* @__PURE__ */ jsx("div", { className: "text-xs text-gray-500", children: userInfo.email })
|
|
2227
|
+
] }),
|
|
2228
|
+
userInfo.avatar ? /* @__PURE__ */ jsx(
|
|
2229
|
+
"img",
|
|
2230
|
+
{
|
|
2231
|
+
src: userInfo.avatar,
|
|
2232
|
+
alt: userInfo.name || "\u7528\u6237",
|
|
2233
|
+
className: "w-8 h-8 rounded-full"
|
|
2234
|
+
}
|
|
2235
|
+
) : /* @__PURE__ */ jsx("div", { className: "w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white text-sm font-medium", children: (userInfo.name || userInfo.email || "U").charAt(0).toUpperCase() })
|
|
2236
|
+
] })
|
|
2237
|
+
] }) }) });
|
|
2238
|
+
};
|
|
2239
|
+
var LoadingBar = () => {
|
|
2240
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2241
|
+
/* @__PURE__ */ jsx(
|
|
2242
|
+
"div",
|
|
2243
|
+
{
|
|
2244
|
+
id: "loading-bar",
|
|
2245
|
+
className: "fixed top-0 left-0 right-0 h-1 bg-transparent z-50 opacity-0 transition-opacity duration-200",
|
|
2246
|
+
children: /* @__PURE__ */ jsx("div", { className: "h-full bg-gradient-to-r from-blue-500 via-blue-600 to-blue-500 loading-bar-progress" })
|
|
2247
|
+
}
|
|
2248
|
+
),
|
|
2249
|
+
/* @__PURE__ */ jsx("style", { children: `
|
|
2250
|
+
@keyframes loading-progress {
|
|
2251
|
+
0% { transform: translateX(-100%); width: 0%; }
|
|
2252
|
+
50% { width: 70%; }
|
|
2253
|
+
100% { transform: translateX(100%); width: 100%; }
|
|
2254
|
+
}
|
|
2255
|
+
#loading-bar.htmx-request {
|
|
2256
|
+
opacity: 1 !important;
|
|
2257
|
+
}
|
|
2258
|
+
#loading-bar.htmx-request .loading-bar-progress {
|
|
2259
|
+
animation: loading-progress 1.5s ease-in-out infinite;
|
|
2260
|
+
}
|
|
2261
|
+
` })
|
|
2262
|
+
] });
|
|
2263
|
+
};
|
|
2264
|
+
function renderNavItem(item, currentPath, collapsed = false, isChild = false) {
|
|
2265
|
+
const isActive = currentPath === item.href || currentPath && currentPath.startsWith(item.href + "/");
|
|
2266
|
+
const hasActiveChild = item.children?.some(
|
|
2267
|
+
(child) => currentPath === child.href || currentPath && currentPath.startsWith(child.href + "/")
|
|
2268
|
+
);
|
|
2269
|
+
const navItemId = `nav-item-${item.href.replace(/[^a-zA-Z0-9]/g, "-")}`;
|
|
2270
|
+
return /* @__PURE__ */ jsxs("li", { id: navItemId, className: "relative group", children: [
|
|
2271
|
+
/* @__PURE__ */ jsxs(
|
|
2272
|
+
"a",
|
|
2273
|
+
{
|
|
2274
|
+
href: item.href,
|
|
2275
|
+
className: `flex items-center ${collapsed ? "justify-center px-2" : "px-4"} py-2 rounded-lg transition-colors ${isActive || hasActiveChild ? "bg-blue-600 text-white shadow-md" : "text-gray-300 hover:bg-gray-700 hover:text-white"}`,
|
|
2276
|
+
"hx-get": item.href,
|
|
2277
|
+
"hx-push-url": "true",
|
|
2278
|
+
title: collapsed ? item.label : void 0,
|
|
2279
|
+
children: [
|
|
2280
|
+
item.icon && /* @__PURE__ */ jsx("span", { className: collapsed ? "" : "mr-2", children: item.icon }),
|
|
2281
|
+
!collapsed && /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap overflow-hidden text-ellipsis", children: item.label })
|
|
2282
|
+
]
|
|
2283
|
+
}
|
|
2284
|
+
),
|
|
2285
|
+
collapsed && /* @__PURE__ */ jsxs("div", { className: "absolute left-full ml-2 px-3 py-2 bg-gray-900 text-white text-sm rounded-lg shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 pointer-events-none z-50 whitespace-nowrap", children: [
|
|
2286
|
+
item.label,
|
|
2287
|
+
/* @__PURE__ */ jsx("div", { className: "absolute right-full top-1/2 -translate-y-1/2 border-4 border-transparent border-r-gray-900" })
|
|
2288
|
+
] }),
|
|
2289
|
+
!collapsed && item.children && item.children.length > 0 && /* @__PURE__ */ jsx("ul", { className: "ml-4 mt-1 space-y-1", children: item.children.map((child) => {
|
|
2290
|
+
const isChildActive = currentPath === child.href || currentPath && currentPath.startsWith(child.href + "/");
|
|
2291
|
+
const childNavItemId = `nav-item-${child.href.replace(/[^a-zA-Z0-9]/g, "-")}`;
|
|
2292
|
+
return /* @__PURE__ */ jsx(
|
|
2293
|
+
"li",
|
|
2294
|
+
{
|
|
2295
|
+
id: childNavItemId,
|
|
2296
|
+
className: "relative group",
|
|
2297
|
+
children: /* @__PURE__ */ jsxs(
|
|
2298
|
+
"a",
|
|
2299
|
+
{
|
|
2300
|
+
href: child.href,
|
|
2301
|
+
className: `flex items-center px-4 py-2 rounded-lg text-sm transition-colors ${isChildActive ? "bg-blue-500 text-white" : "text-gray-400 hover:bg-gray-700 hover:text-white"}`,
|
|
2302
|
+
"hx-get": child.href,
|
|
2303
|
+
"hx-push-url": "true",
|
|
2304
|
+
children: [
|
|
2305
|
+
child.icon && /* @__PURE__ */ jsx("span", { className: "mr-2", children: child.icon }),
|
|
2306
|
+
/* @__PURE__ */ jsx("span", { className: "whitespace-nowrap overflow-hidden text-ellipsis", children: child.label })
|
|
2307
|
+
]
|
|
2308
|
+
}
|
|
2309
|
+
)
|
|
2310
|
+
},
|
|
2311
|
+
child.href
|
|
2312
|
+
);
|
|
2313
|
+
}) })
|
|
2314
|
+
] }, item.href);
|
|
2315
|
+
}
|
|
2316
|
+
var BaseLayout = (props) => {
|
|
2317
|
+
return /* @__PURE__ */ jsxs("html", { children: [
|
|
2318
|
+
/* @__PURE__ */ jsxs("head", { children: [
|
|
2319
|
+
/* @__PURE__ */ jsx("meta", { charset: "UTF-8" }),
|
|
2320
|
+
/* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
|
|
2321
|
+
/* @__PURE__ */ jsx("title", { children: props.title }),
|
|
2322
|
+
props.description && /* @__PURE__ */ jsx("meta", { name: "description", content: props.description }),
|
|
2323
|
+
/* @__PURE__ */ jsx("script", { src: "https://unpkg.com/htmx.org@latest" }),
|
|
2324
|
+
/* @__PURE__ */ jsx("script", { src: "https://unpkg.com/hyperscript.org@latest" }),
|
|
2325
|
+
/* @__PURE__ */ jsx("script", { src: "https://cdn.tailwindcss.com" }),
|
|
2326
|
+
/* @__PURE__ */ jsx(
|
|
2327
|
+
"style",
|
|
2328
|
+
{
|
|
2329
|
+
dangerouslySetInnerHTML: {
|
|
2330
|
+
__html: `
|
|
2331
|
+
@keyframes fadeIn {
|
|
2332
|
+
from { opacity: 0;}
|
|
2333
|
+
to { opacity: 1;}
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
@keyframes slideIn {
|
|
2337
|
+
from { opacity: 0; transform: scale(0.95) translateY(-10px); }
|
|
2338
|
+
to { opacity: 1; transform: scale(1) translateY(0); }
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
@keyframes slideInRight {
|
|
2342
|
+
from { opacity: 0; transform: translateX(100%); }
|
|
2343
|
+
to { opacity: 1; transform: translateX(0); }
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
@keyframes slideOutRight {
|
|
2347
|
+
from { opacity: 1; transform: translateX(0); }
|
|
2348
|
+
to { opacity: 0; transform: translateX(100%); }
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
@keyframes fadeOut {
|
|
2352
|
+
from { opacity: 1; }
|
|
2353
|
+
to { opacity: 0; }
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
@keyframes scaleOut {
|
|
2357
|
+
from { opacity: 1; transform: scale(1) translateY(0); }
|
|
2358
|
+
to { opacity: 0; transform: scale(0.95) translateY(-10px); }
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
/* Dialog \u9000\u51FA\u52A8\u753B */
|
|
2362
|
+
.dialog-exit {
|
|
2363
|
+
animation: fadeOut 0.2s ease-in forwards !important;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
.dialog-content-exit {
|
|
2367
|
+
animation: scaleOut 0.2s ease-in forwards !important;
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
/* ErrorAlert \u9000\u51FA\u52A8\u753B */
|
|
2371
|
+
.error-alert-exit {
|
|
2372
|
+
animation: slideOutRight 0.3s ease-in forwards, fadeOut 0.3s ease-in forwards;
|
|
2373
|
+
}
|
|
2374
|
+
`
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
)
|
|
2378
|
+
] }),
|
|
2379
|
+
/* @__PURE__ */ jsxs(
|
|
2380
|
+
"body",
|
|
2381
|
+
{
|
|
2382
|
+
className: "bg-gray-50",
|
|
2383
|
+
"hx-indicator": "#loading-bar",
|
|
2384
|
+
"hx-target": "#main-content",
|
|
2385
|
+
"hx-swap": "outerHTML",
|
|
2386
|
+
children: [
|
|
2387
|
+
/* @__PURE__ */ jsx(LoadingBar, {}),
|
|
2388
|
+
props.children,
|
|
2389
|
+
/* @__PURE__ */ jsx(
|
|
2390
|
+
"div",
|
|
2391
|
+
{
|
|
2392
|
+
id: "error-container",
|
|
2393
|
+
className: "fixed top-4 right-4 z-[200] w-full max-w-2xl px-4"
|
|
2394
|
+
}
|
|
2395
|
+
),
|
|
2396
|
+
/* @__PURE__ */ jsx("div", { id: "dialog-container" })
|
|
2397
|
+
]
|
|
2398
|
+
}
|
|
2399
|
+
)
|
|
2400
|
+
] });
|
|
2401
|
+
};
|
|
2402
|
+
var AdminLayout = (props) => {
|
|
2403
|
+
const logo = props.adminContext.pluginOptions.logo;
|
|
2404
|
+
const navItems = props.adminContext.pluginOptions.navigation;
|
|
2405
|
+
const referer = props.adminContext.ctx.req.header("Referer");
|
|
2406
|
+
let currentPath = props.adminContext.ctx.req.path;
|
|
2407
|
+
if (referer) {
|
|
2408
|
+
try {
|
|
2409
|
+
const refererUrl = new URL(referer);
|
|
2410
|
+
const method = props.adminContext.ctx.req.method;
|
|
2411
|
+
if (["POST", "PUT", "DELETE"].includes(method)) {
|
|
2412
|
+
currentPath = refererUrl.pathname;
|
|
2413
|
+
}
|
|
2414
|
+
} catch (e) {
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
const sidebarCollapsed = props.sidebarCollapsed || false;
|
|
2418
|
+
const breadcrumbs = props.adminContext.breadcrumbs;
|
|
2419
|
+
const userInfo = props.adminContext.userInfo;
|
|
2420
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex h-screen", id: "main-content", children: [
|
|
2421
|
+
/* @__PURE__ */ jsx(
|
|
2422
|
+
"aside",
|
|
2423
|
+
{
|
|
2424
|
+
className: `${props.sidebarCollapsed ? "w-16" : "w-64"} bg-gradient-to-b from-gray-900 to-gray-800 text-white shadow-lg transition-all duration-300 ease-in-out overflow-hidden`,
|
|
2425
|
+
children: /* @__PURE__ */ jsxs("div", { className: `${props.sidebarCollapsed ? "p-2" : "p-6"}`, children: [
|
|
2426
|
+
!props.sidebarCollapsed && /* @__PURE__ */ jsx(Fragment, { children: logo ? /* @__PURE__ */ jsx("img", { src: logo, alt: "Logo", className: "h-10 mb-6" }) : /* @__PURE__ */ jsx("h1", { className: "text-xl font-bold mb-6 text-white whitespace-nowrap overflow-hidden text-ellipsis", children: props.adminContext.pluginOptions.title }) }),
|
|
2427
|
+
/* @__PURE__ */ jsx("nav", { children: /* @__PURE__ */ jsx("ul", { className: "space-y-1", children: navItems && navItems.length > 0 ? navItems.map(
|
|
2428
|
+
(item) => renderNavItem(
|
|
2429
|
+
item,
|
|
2430
|
+
currentPath,
|
|
2431
|
+
props.sidebarCollapsed || false
|
|
2432
|
+
)
|
|
2433
|
+
) : /* @__PURE__ */ jsx(
|
|
2434
|
+
"li",
|
|
2435
|
+
{
|
|
2436
|
+
className: `${props.sidebarCollapsed ? "px-2" : "px-4"} py-2 text-gray-400 text-sm`,
|
|
2437
|
+
children: "\u6682\u65E0\u5BFC\u822A\u9879"
|
|
2438
|
+
}
|
|
2439
|
+
) }) })
|
|
2440
|
+
] })
|
|
2441
|
+
}
|
|
2442
|
+
),
|
|
2443
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col overflow-hidden", children: [
|
|
2444
|
+
/* @__PURE__ */ jsx(
|
|
2445
|
+
Header,
|
|
2446
|
+
{
|
|
2447
|
+
breadcrumbs,
|
|
2448
|
+
userInfo,
|
|
2449
|
+
sidebarCollapsed: sidebarCollapsed || false
|
|
2450
|
+
}
|
|
2451
|
+
),
|
|
2452
|
+
/* @__PURE__ */ jsx("main", { className: "flex-1 overflow-auto bg-gray-50", children: /* @__PURE__ */ jsx("div", { className: "p-6", children: props.children }) })
|
|
2453
|
+
] })
|
|
2454
|
+
] });
|
|
2455
|
+
};
|
|
2456
|
+
|
|
2457
|
+
// src/utils/context.tsx
|
|
2458
|
+
var HtmxAdminContext = class {
|
|
2459
|
+
/** 模块元数据 */
|
|
2460
|
+
moduleMetadata;
|
|
2461
|
+
/** 插件选项 */
|
|
2462
|
+
pluginOptions;
|
|
2463
|
+
/** Hono Context */
|
|
2464
|
+
ctx;
|
|
2465
|
+
/** 之前的模块名 */
|
|
2466
|
+
previousModuleName;
|
|
2467
|
+
/** 是否是片段请求(HTMX 请求) */
|
|
2468
|
+
isFragment;
|
|
2469
|
+
/** 是否是对话框请求 */
|
|
2470
|
+
isDialog;
|
|
2471
|
+
/** 用户信息 */
|
|
2472
|
+
userInfo;
|
|
2473
|
+
/** 通知队列 */
|
|
2474
|
+
notifications = [];
|
|
2475
|
+
/** 页面标题(用于 HX-Title 和页面展示) */
|
|
2476
|
+
title = "";
|
|
2477
|
+
/** 页面描述(用于SEO和页面展示) */
|
|
2478
|
+
description = "";
|
|
2479
|
+
/** 面包屑项 */
|
|
2480
|
+
breadcrumbs = [];
|
|
2481
|
+
/** 主要内容 */
|
|
2482
|
+
content = null;
|
|
2483
|
+
/** 需要重定向的 URL */
|
|
2484
|
+
redirectUrl;
|
|
2485
|
+
/** 是否需要刷新页面(用于 HX-Refresh) */
|
|
2486
|
+
refresh = false;
|
|
2487
|
+
constructor(ctx, userInfo, moduleMetadata, pluginOptions) {
|
|
2488
|
+
this.ctx = ctx;
|
|
2489
|
+
this.userInfo = userInfo;
|
|
2490
|
+
this.moduleMetadata = moduleMetadata;
|
|
2491
|
+
this.pluginOptions = pluginOptions;
|
|
2492
|
+
const url = new URL(ctx.req.url);
|
|
2493
|
+
this.isFragment = ctx.req.header("HX-Request") === "true";
|
|
2494
|
+
this.isDialog = url.searchParams.get("dialog") === "true" || ctx.req.header("HX-Target") === "#dialog-container" || (ctx.req.header("Referer") || "").includes("dialog=true");
|
|
2495
|
+
const referer = ctx.req.header("Referer");
|
|
2496
|
+
this.previousModuleName = referer ? new URL(referer).pathname.replace(pluginOptions.prefix, "").split("/").pop() : void 0;
|
|
2497
|
+
}
|
|
2498
|
+
/**
|
|
2499
|
+
* 发送通知
|
|
2500
|
+
* 通知将在响应时通过 OOB 更新到错误容器
|
|
2501
|
+
*/
|
|
2502
|
+
sendNotification(type, title, message) {
|
|
2503
|
+
this.notifications.push({ type, title, message });
|
|
2504
|
+
}
|
|
2505
|
+
/**
|
|
2506
|
+
* 发送错误通知(便捷方法)
|
|
2507
|
+
*/
|
|
2508
|
+
sendError(title, message) {
|
|
2509
|
+
this.sendNotification("error", title, message);
|
|
2510
|
+
}
|
|
2511
|
+
/**
|
|
2512
|
+
* 发送警告通知(便捷方法)
|
|
2513
|
+
*/
|
|
2514
|
+
sendWarning(title, message) {
|
|
2515
|
+
this.sendNotification("warning", title, message);
|
|
2516
|
+
}
|
|
2517
|
+
/**
|
|
2518
|
+
* 发送信息通知(便捷方法)
|
|
2519
|
+
*/
|
|
2520
|
+
sendInfo(title, message) {
|
|
2521
|
+
this.sendNotification("info", title, message);
|
|
2522
|
+
}
|
|
2523
|
+
/**
|
|
2524
|
+
* 发送成功通知(便捷方法)
|
|
2525
|
+
*/
|
|
2526
|
+
sendSuccess(title, message) {
|
|
2527
|
+
this.sendNotification("success", title, message);
|
|
2528
|
+
}
|
|
2529
|
+
/**
|
|
2530
|
+
* 设置需要推送的 URL
|
|
2531
|
+
* 用于 HX-Push-Url 响应头
|
|
2532
|
+
*/
|
|
2533
|
+
redirect(url) {
|
|
2534
|
+
this.redirectUrl = url;
|
|
2535
|
+
}
|
|
2536
|
+
/**
|
|
2537
|
+
* 设置需要刷新页面
|
|
2538
|
+
* 用于 HX-Refresh 响应头
|
|
2539
|
+
*/
|
|
2540
|
+
setRefresh(refresh = true) {
|
|
2541
|
+
this.refresh = refresh;
|
|
2542
|
+
}
|
|
2543
|
+
/**
|
|
2544
|
+
* 检查是否有列表页面
|
|
2545
|
+
* 封装 moduleMetadata 访问,避免直接访问内部属性
|
|
2546
|
+
*/
|
|
2547
|
+
hasList() {
|
|
2548
|
+
return this.moduleMetadata.hasList;
|
|
2549
|
+
}
|
|
2550
|
+
/**
|
|
2551
|
+
* 检查是否有详情页面
|
|
2552
|
+
* 封装 moduleMetadata 访问,避免直接访问内部属性
|
|
2553
|
+
*/
|
|
2554
|
+
hasDetail() {
|
|
2555
|
+
return this.moduleMetadata.hasDetail;
|
|
2556
|
+
}
|
|
2557
|
+
/**
|
|
2558
|
+
* 检查是否有表单页面
|
|
2559
|
+
* 封装 moduleMetadata 访问,避免直接访问内部属性
|
|
2560
|
+
*/
|
|
2561
|
+
hasForm() {
|
|
2562
|
+
return this.moduleMetadata.hasForm;
|
|
2563
|
+
}
|
|
2564
|
+
/**
|
|
2565
|
+
* 检查是否有自定义页面
|
|
2566
|
+
* 封装 moduleMetadata 访问,避免直接访问内部属性
|
|
2567
|
+
*/
|
|
2568
|
+
hasCustom() {
|
|
2569
|
+
return this.moduleMetadata.hasCustom;
|
|
2570
|
+
}
|
|
2571
|
+
};
|
|
2572
|
+
var RouteHandler = class {
|
|
2573
|
+
moduleClass;
|
|
2574
|
+
pluginOptions;
|
|
2575
|
+
moduleMetadata;
|
|
2576
|
+
moduleOptions;
|
|
2577
|
+
constructor(options) {
|
|
2578
|
+
this.moduleClass = options.moduleClass;
|
|
2579
|
+
this.pluginOptions = options.pluginOptions;
|
|
2580
|
+
this.moduleMetadata = options.moduleMetadata;
|
|
2581
|
+
this.moduleOptions = options.moduleOptions;
|
|
2582
|
+
}
|
|
2583
|
+
/**
|
|
2584
|
+
* 更新模块元数据
|
|
2585
|
+
*/
|
|
2586
|
+
setModuleMetadata(moduleMetadata) {
|
|
2587
|
+
this.moduleMetadata = moduleMetadata;
|
|
2588
|
+
}
|
|
2589
|
+
/**
|
|
2590
|
+
* 处理请求的唯一对外接口
|
|
2591
|
+
*/
|
|
2592
|
+
async handle(ctx) {
|
|
2593
|
+
try {
|
|
2594
|
+
const userInfo = await this.pluginOptions.getUserInfo(ctx);
|
|
2595
|
+
const adminContext = this.createAdminContext(ctx, userInfo);
|
|
2596
|
+
const moduleInstance = new this.moduleClass();
|
|
2597
|
+
moduleInstance.__init(adminContext);
|
|
2598
|
+
try {
|
|
2599
|
+
adminContext.content = await moduleInstance.__handle();
|
|
2600
|
+
adminContext.title = moduleInstance.getTitle();
|
|
2601
|
+
adminContext.description = moduleInstance.getDescription();
|
|
2602
|
+
adminContext.breadcrumbs = moduleInstance.getBreadcrumbs();
|
|
2603
|
+
} catch (error) {
|
|
2604
|
+
this.handleError(adminContext, error);
|
|
2605
|
+
}
|
|
2606
|
+
if (!adminContext.isFragment) {
|
|
2607
|
+
return this.renderFullPage(ctx, adminContext);
|
|
2608
|
+
} else {
|
|
2609
|
+
return this.renderFragment(ctx, adminContext);
|
|
2610
|
+
}
|
|
2611
|
+
} catch (error) {
|
|
2612
|
+
return ctx.html(
|
|
2613
|
+
/* @__PURE__ */ jsxs("div", { className: "text-red-500 p-4", children: [
|
|
2614
|
+
/* @__PURE__ */ jsx("p", { className: "font-semibold", children: "\u9519\u8BEF" }),
|
|
2615
|
+
/* @__PURE__ */ jsx("p", { children: error instanceof Error ? error.message : String(error) })
|
|
2616
|
+
] }),
|
|
2617
|
+
500
|
|
2618
|
+
);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
renderFullPage(ctx, adminContext) {
|
|
2622
|
+
const useAdminLayout = this.moduleOptions.useAdminLayout !== false;
|
|
2623
|
+
return ctx.html(
|
|
2624
|
+
/* @__PURE__ */ jsx(
|
|
2625
|
+
BaseLayout,
|
|
2626
|
+
{
|
|
2627
|
+
title: adminContext.title,
|
|
2628
|
+
description: adminContext.description,
|
|
2629
|
+
children: useAdminLayout ? /* @__PURE__ */ jsx(AdminLayout, { adminContext, sidebarCollapsed: false, children: adminContext.content }) : /* @__PURE__ */ jsx("div", { id: "main-content", children: adminContext.content })
|
|
2630
|
+
}
|
|
2631
|
+
)
|
|
2632
|
+
);
|
|
2633
|
+
}
|
|
2634
|
+
renderFragment(ctx, adminContext) {
|
|
2635
|
+
if (adminContext.redirectUrl) {
|
|
2636
|
+
return ctx.redirect(adminContext.redirectUrl);
|
|
2637
|
+
}
|
|
2638
|
+
const target = adminContext.isDialog ? "#dialog-container" : "#main-content";
|
|
2639
|
+
const swap = adminContext.isDialog ? "innerHTML" : "outerHTML";
|
|
2640
|
+
const fragments = [
|
|
2641
|
+
// 通知变动 - 使用 OOB swap 到 error-container
|
|
2642
|
+
adminContext.notifications.map((notification, index) => {
|
|
2643
|
+
return /* @__PURE__ */ jsx(
|
|
2644
|
+
"div",
|
|
2645
|
+
{
|
|
2646
|
+
id: "error-container",
|
|
2647
|
+
"hx-swap-oob": "beforeend",
|
|
2648
|
+
children: /* @__PURE__ */ jsx(
|
|
2649
|
+
ErrorAlert,
|
|
2650
|
+
{
|
|
2651
|
+
type: notification.type,
|
|
2652
|
+
title: notification.title,
|
|
2653
|
+
message: notification.message
|
|
2654
|
+
}
|
|
2655
|
+
)
|
|
2656
|
+
},
|
|
2657
|
+
`notification-${index}`
|
|
2658
|
+
);
|
|
2659
|
+
}),
|
|
2660
|
+
/* @__PURE__ */ jsx("title", { children: adminContext.title })
|
|
2661
|
+
];
|
|
2662
|
+
const headers = {
|
|
2663
|
+
"HX-Retarget": target,
|
|
2664
|
+
"HX-Reswap": swap
|
|
2665
|
+
};
|
|
2666
|
+
if (!adminContext.isDialog) {
|
|
2667
|
+
const url = new URL(adminContext.ctx.req.url);
|
|
2668
|
+
if (url.search) {
|
|
2669
|
+
headers["HX-Push-Url"] = url.pathname + "?" + url.search;
|
|
2670
|
+
} else {
|
|
2671
|
+
headers["HX-Push-Url"] = url.pathname;
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
if (adminContext.refresh) {
|
|
2675
|
+
headers["HX-Refresh"] = "true";
|
|
2676
|
+
if (adminContext.isDialog) {
|
|
2677
|
+
return ctx.html(
|
|
2678
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2679
|
+
/* @__PURE__ */ jsx(
|
|
2680
|
+
"div",
|
|
2681
|
+
{
|
|
2682
|
+
id: "dialog-container",
|
|
2683
|
+
"hx-swap-oob": "innerHTML"
|
|
2684
|
+
}
|
|
2685
|
+
),
|
|
2686
|
+
fragments
|
|
2687
|
+
] }),
|
|
2688
|
+
200,
|
|
2689
|
+
headers
|
|
2690
|
+
);
|
|
2691
|
+
}
|
|
2692
|
+
return ctx.html(/* @__PURE__ */ jsx("div", {}), 200, headers);
|
|
2693
|
+
}
|
|
2694
|
+
const useAdminLayout = this.moduleOptions.useAdminLayout !== false;
|
|
2695
|
+
return ctx.html(
|
|
2696
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2697
|
+
adminContext.isDialog ? (
|
|
2698
|
+
// Dialog 会被插入到 #dialog-container 中(使用 innerHTML)
|
|
2699
|
+
/* @__PURE__ */ jsx(Dialog, { title: adminContext.title, size: "lg", children: adminContext.content })
|
|
2700
|
+
) : useAdminLayout ? (
|
|
2701
|
+
// AdminLayout 会被替换整个 #main-content(使用 outerHTML)
|
|
2702
|
+
/* @__PURE__ */ jsx(AdminLayout, { adminContext, sidebarCollapsed: false, children: adminContext.content })
|
|
2703
|
+
) : (
|
|
2704
|
+
// 不使用 AdminLayout 时,替换 #main-content 容器(使用 outerHTML)
|
|
2705
|
+
/* @__PURE__ */ jsx("div", { id: "main-content", children: adminContext.content })
|
|
2706
|
+
),
|
|
2707
|
+
fragments
|
|
2708
|
+
] }),
|
|
2709
|
+
200,
|
|
2710
|
+
headers
|
|
2711
|
+
);
|
|
2712
|
+
}
|
|
2713
|
+
/**
|
|
2714
|
+
* 创建 HtmxAdminContext
|
|
2715
|
+
*/
|
|
2716
|
+
createAdminContext(ctx, userInfo) {
|
|
2717
|
+
return new HtmxAdminContext(
|
|
2718
|
+
ctx,
|
|
2719
|
+
userInfo,
|
|
2720
|
+
this.moduleMetadata,
|
|
2721
|
+
this.pluginOptions
|
|
2722
|
+
);
|
|
2723
|
+
}
|
|
2724
|
+
/**
|
|
2725
|
+
* 统一错误处理
|
|
2726
|
+
*/
|
|
2727
|
+
handleError(adminContext, error) {
|
|
2728
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2729
|
+
adminContext.content = /* @__PURE__ */ jsxs("div", { className: "text-red-500 p-4", children: [
|
|
2730
|
+
/* @__PURE__ */ jsx("p", { className: "font-semibold", children: "\u9519\u8BEF" }),
|
|
2731
|
+
/* @__PURE__ */ jsx("p", { children: errorMessage })
|
|
2732
|
+
] });
|
|
2733
|
+
adminContext.notifications.push({
|
|
2734
|
+
type: "error",
|
|
2735
|
+
title: "\u9519\u8BEF",
|
|
2736
|
+
message: errorMessage
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
};
|
|
2740
|
+
|
|
2741
|
+
// src/utils/module.ts
|
|
2742
|
+
function isListPageModule(moduleClass) {
|
|
2743
|
+
if (!moduleClass || !moduleClass.prototype) {
|
|
2744
|
+
return false;
|
|
2745
|
+
}
|
|
2746
|
+
let proto = moduleClass.prototype;
|
|
2747
|
+
while (proto) {
|
|
2748
|
+
if (proto.constructor === ListPageModule) {
|
|
2749
|
+
return true;
|
|
2750
|
+
}
|
|
2751
|
+
proto = Object.getPrototypeOf(proto);
|
|
2752
|
+
if (!proto) {
|
|
2753
|
+
break;
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
return false;
|
|
2757
|
+
}
|
|
2758
|
+
function isDetailPageModule(moduleClass) {
|
|
2759
|
+
if (!moduleClass || !moduleClass.prototype) {
|
|
2760
|
+
return false;
|
|
2761
|
+
}
|
|
2762
|
+
let proto = moduleClass.prototype;
|
|
2763
|
+
while (proto) {
|
|
2764
|
+
if (proto.constructor === DetailPageModule) {
|
|
2765
|
+
return true;
|
|
2766
|
+
}
|
|
2767
|
+
proto = Object.getPrototypeOf(proto);
|
|
2768
|
+
if (!proto) {
|
|
2769
|
+
break;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
return false;
|
|
2773
|
+
}
|
|
2774
|
+
function isFormPageModule(moduleClass) {
|
|
2775
|
+
if (!moduleClass || !moduleClass.prototype) {
|
|
2776
|
+
return false;
|
|
2777
|
+
}
|
|
2778
|
+
let proto = moduleClass.prototype;
|
|
2779
|
+
while (proto) {
|
|
2780
|
+
if (proto.constructor === FormPageModule) {
|
|
2781
|
+
return true;
|
|
2782
|
+
}
|
|
2783
|
+
proto = Object.getPrototypeOf(proto);
|
|
2784
|
+
if (!proto) {
|
|
2785
|
+
break;
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
return false;
|
|
2789
|
+
}
|
|
2790
|
+
function isPageModule(moduleClass) {
|
|
2791
|
+
if (!moduleClass || !moduleClass.prototype) {
|
|
2792
|
+
return false;
|
|
2793
|
+
}
|
|
2794
|
+
let proto = moduleClass.prototype;
|
|
2795
|
+
while (proto) {
|
|
2796
|
+
if (proto.constructor === PageModule) {
|
|
2797
|
+
return true;
|
|
2798
|
+
}
|
|
2799
|
+
proto = Object.getPrototypeOf(proto);
|
|
2800
|
+
if (!proto) {
|
|
2801
|
+
break;
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
return false;
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
// src/plugin.tsx
|
|
2808
|
+
var HtmxAdminPlugin = class {
|
|
2809
|
+
name = "htmx-admin-plugin";
|
|
2810
|
+
priority = PluginPriority.ROUTE;
|
|
2811
|
+
engine;
|
|
2812
|
+
hono;
|
|
2813
|
+
options;
|
|
2814
|
+
// 模块类映射(记录每个模块的类)
|
|
2815
|
+
moduleTypeMap = /* @__PURE__ */ new Map();
|
|
2816
|
+
constructor(options) {
|
|
2817
|
+
this.options = {
|
|
2818
|
+
title: options?.title || "\u7BA1\u7406\u540E\u53F0",
|
|
2819
|
+
logo: options?.logo || "",
|
|
2820
|
+
prefix: options?.prefix || "/admin",
|
|
2821
|
+
homePath: options?.homePath || "",
|
|
2822
|
+
navigation: options?.navigation ?? [],
|
|
2823
|
+
getUserInfo: options?.getUserInfo ?? (() => null)
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
/**
|
|
2827
|
+
* 声明Module配置Schema
|
|
2828
|
+
*/
|
|
2829
|
+
getModuleOptionsSchema() {
|
|
2830
|
+
return {
|
|
2831
|
+
_type: {},
|
|
2832
|
+
validate: (options) => {
|
|
2833
|
+
if (!options.type) {
|
|
2834
|
+
return "Module type is required";
|
|
2835
|
+
}
|
|
2836
|
+
return true;
|
|
2837
|
+
}
|
|
2838
|
+
};
|
|
2839
|
+
}
|
|
2840
|
+
/**
|
|
2841
|
+
* 引擎初始化钩子
|
|
2842
|
+
*/
|
|
2843
|
+
onInit(engine) {
|
|
2844
|
+
this.engine = engine;
|
|
2845
|
+
this.hono = engine.getHono();
|
|
2846
|
+
logger.info("HtmxAdminPlugin initialized");
|
|
2847
|
+
}
|
|
2848
|
+
/**
|
|
2849
|
+
* 检查并注册模块
|
|
2850
|
+
*/
|
|
2851
|
+
checkAndRegisterModules(modules) {
|
|
2852
|
+
modules = modules.filter(
|
|
2853
|
+
(module) => module.clazz instanceof PageModule || module.clazz.prototype instanceof PageModule
|
|
2854
|
+
);
|
|
2855
|
+
for (const module of modules) {
|
|
2856
|
+
const options = module.options;
|
|
2857
|
+
const moduleClass = module.clazz;
|
|
2858
|
+
if (!this.options.homePath) {
|
|
2859
|
+
this.options.homePath = `${this.options.prefix}${moduleNameToPath(module.name)}`;
|
|
2860
|
+
}
|
|
2861
|
+
if (options.type === "list") {
|
|
2862
|
+
if (!isListPageModule(moduleClass)) {
|
|
2863
|
+
throw new Error(
|
|
2864
|
+
`Module ${module.name} is of type "list" but does not extend ListPageModule. Please extend ListPageModule to customize list behavior.`
|
|
2865
|
+
);
|
|
2866
|
+
}
|
|
2867
|
+
} else if (options.type === "detail") {
|
|
2868
|
+
if (!isDetailPageModule(moduleClass)) {
|
|
2869
|
+
throw new Error(
|
|
2870
|
+
`Module ${module.name} is of type "detail" but does not extend DetailPageModule. Please extend DetailPageModule to customize detail behavior.`
|
|
2871
|
+
);
|
|
2872
|
+
}
|
|
2873
|
+
} else if (options.type === "form") {
|
|
2874
|
+
if (!isFormPageModule(moduleClass)) {
|
|
2875
|
+
throw new Error(
|
|
2876
|
+
`Module ${module.name} is of type "form" but does not extend FormPageModule. Please extend FormPageModule to customize form behavior.`
|
|
2877
|
+
);
|
|
2878
|
+
}
|
|
2879
|
+
} else if (options.type === "custom") {
|
|
2880
|
+
if (!isPageModule(moduleClass)) {
|
|
2881
|
+
throw new Error(
|
|
2882
|
+
`Module ${module.name} is of type "custom" but does not extend PageModule. Please extend PageModule to customize page rendering.`
|
|
2883
|
+
);
|
|
2884
|
+
}
|
|
2885
|
+
} else {
|
|
2886
|
+
throw new Error(
|
|
2887
|
+
`Module ${module.name} has an invalid type: ${options.type}. Please use one of the following types: "list", "detail", "form", "custom".`
|
|
2888
|
+
);
|
|
2889
|
+
}
|
|
2890
|
+
let metadata = this.moduleTypeMap.get(module.name);
|
|
2891
|
+
if (!metadata) {
|
|
2892
|
+
this.moduleTypeMap.set(module.name, {});
|
|
2893
|
+
metadata = this.moduleTypeMap.get(module.name);
|
|
2894
|
+
}
|
|
2895
|
+
if (!metadata[options.type]) {
|
|
2896
|
+
metadata[options.type] = {
|
|
2897
|
+
class: moduleClass,
|
|
2898
|
+
options
|
|
2899
|
+
};
|
|
2900
|
+
} else {
|
|
2901
|
+
throw new Error(
|
|
2902
|
+
`Module ${module.name} already has a ${options.type} module. Please use a different name.`
|
|
2903
|
+
);
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
/**
|
|
2908
|
+
* 注册路由
|
|
2909
|
+
*/
|
|
2910
|
+
registerRoutes() {
|
|
2911
|
+
const moduleNames = Array.from(this.moduleTypeMap.keys());
|
|
2912
|
+
for (const moduleName of moduleNames) {
|
|
2913
|
+
const moduleTypes = this.moduleTypeMap.get(moduleName);
|
|
2914
|
+
const basePath = `${this.options.prefix}${moduleNameToPath(moduleName)}`;
|
|
2915
|
+
for (const type in moduleTypes) {
|
|
2916
|
+
const moduleInfo = moduleTypes[type];
|
|
2917
|
+
const routeHandler = new RouteHandler({
|
|
2918
|
+
moduleClass: moduleInfo.class,
|
|
2919
|
+
moduleName,
|
|
2920
|
+
basePath,
|
|
2921
|
+
moduleOptions: moduleInfo.options,
|
|
2922
|
+
pluginOptions: this.options,
|
|
2923
|
+
moduleMetadata: {
|
|
2924
|
+
hasList: !!moduleTypes["list"],
|
|
2925
|
+
hasDetail: !!moduleTypes["detail"],
|
|
2926
|
+
hasForm: !!moduleTypes["form"],
|
|
2927
|
+
hasCustom: !!moduleTypes["custom"],
|
|
2928
|
+
basePath,
|
|
2929
|
+
title: moduleInfo.options.title ?? moduleName,
|
|
2930
|
+
description: moduleInfo.options.description ?? ""
|
|
2931
|
+
}
|
|
2932
|
+
});
|
|
2933
|
+
const routeConfig = {
|
|
2934
|
+
list: [{ method: "get", path: `${basePath}/list` }],
|
|
2935
|
+
detail: [{ method: "get", path: `${basePath}/detail/:id` }],
|
|
2936
|
+
form: [
|
|
2937
|
+
{ method: "get", path: `${basePath}/new` },
|
|
2938
|
+
{ method: "get", path: `${basePath}/edit/:id` },
|
|
2939
|
+
{ method: "post", path: basePath },
|
|
2940
|
+
{ method: "put", path: `${basePath}/:id` },
|
|
2941
|
+
{ method: "delete", path: `${basePath}/:id` }
|
|
2942
|
+
],
|
|
2943
|
+
custom: [{ method: "get", path: basePath }]
|
|
2944
|
+
};
|
|
2945
|
+
const routes = routeConfig[type];
|
|
2946
|
+
for (const route of routes) {
|
|
2947
|
+
logger.info(
|
|
2948
|
+
`[HtmxAdminPlugin] Registering ${type} route: ${route.method.toUpperCase()} ${route.path}`
|
|
2949
|
+
);
|
|
2950
|
+
const handler = async (ctx) => routeHandler.handle(ctx);
|
|
2951
|
+
this.hono[route.method](route.path, handler);
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
/**
|
|
2957
|
+
* 模块加载钩子:检查模块类型约束并注册路由
|
|
2958
|
+
*/
|
|
2959
|
+
onModuleLoad(modules) {
|
|
2960
|
+
this.checkAndRegisterModules(modules);
|
|
2961
|
+
this.registerRoutes();
|
|
2962
|
+
this.engine.getHono().get(this.options.prefix, async (ctx) => {
|
|
2963
|
+
return ctx.redirect(this.options.homePath);
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
};
|
|
2967
|
+
|
|
2968
|
+
// src/components/index.ts
|
|
2969
|
+
var components_exports = {};
|
|
2970
|
+
__export(components_exports, {
|
|
2971
|
+
ActionButton: () => ActionButton,
|
|
2972
|
+
ActivityCard: () => ActivityCard,
|
|
2973
|
+
Badge: () => Badge,
|
|
2974
|
+
Button: () => Button,
|
|
2975
|
+
Card: () => Card,
|
|
2976
|
+
DateInput: () => DateInput,
|
|
2977
|
+
Detail: () => Detail,
|
|
2978
|
+
DetailContent: () => DetailContent,
|
|
2979
|
+
Dialog: () => Dialog,
|
|
2980
|
+
EmptyState: () => EmptyState,
|
|
2981
|
+
ErrorAlert: () => ErrorAlert,
|
|
2982
|
+
FilterCard: () => FilterCard,
|
|
2983
|
+
Form: () => Form,
|
|
2984
|
+
FormField: () => FormField,
|
|
2985
|
+
Input: () => Input,
|
|
2986
|
+
ListContent: () => ListContent,
|
|
2987
|
+
PageHeader: () => PageHeader,
|
|
2988
|
+
Pagination: () => Pagination,
|
|
2989
|
+
Select: () => Select,
|
|
2990
|
+
StatCard: () => StatCard,
|
|
2991
|
+
SystemStatusCard: () => SystemStatusCard,
|
|
2992
|
+
Textarea: () => Textarea
|
|
2993
|
+
});
|
|
2994
|
+
var ActivityCard = (props) => {
|
|
2995
|
+
const { title, activities, className = "" } = props;
|
|
2996
|
+
const colorClasses = {
|
|
2997
|
+
blue: "bg-blue-500",
|
|
2998
|
+
green: "bg-green-500",
|
|
2999
|
+
purple: "bg-purple-500",
|
|
3000
|
+
gray: "bg-gray-500",
|
|
3001
|
+
yellow: "bg-yellow-500",
|
|
3002
|
+
red: "bg-red-500"
|
|
3003
|
+
};
|
|
3004
|
+
return /* @__PURE__ */ jsxs(
|
|
3005
|
+
"div",
|
|
3006
|
+
{
|
|
3007
|
+
className: `bg-white rounded-lg shadow-sm p-6 hover:shadow-md transition-shadow ${className}`,
|
|
3008
|
+
children: [
|
|
3009
|
+
/* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-gray-900 mb-4", children: title }),
|
|
3010
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-4", children: activities.length > 0 ? activities.map((activity, index) => /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
3011
|
+
/* @__PURE__ */ jsx(
|
|
3012
|
+
"div",
|
|
3013
|
+
{
|
|
3014
|
+
className: `flex-shrink-0 w-2 h-2 rounded-full mt-2 ${colorClasses[activity.color || "gray"]}`
|
|
3015
|
+
}
|
|
3016
|
+
),
|
|
3017
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
3018
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-gray-900", children: activity.description }),
|
|
3019
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: activity.time })
|
|
3020
|
+
] })
|
|
3021
|
+
] }, index)) : /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500 text-center py-4", children: "\u6682\u65E0\u6D3B\u52A8\u8BB0\u5F55" }) })
|
|
3022
|
+
]
|
|
3023
|
+
}
|
|
3024
|
+
);
|
|
3025
|
+
};
|
|
3026
|
+
var Badge = (props) => {
|
|
3027
|
+
const { children, variant = "gray", className = "" } = props;
|
|
3028
|
+
const variantClasses = {
|
|
3029
|
+
blue: "bg-blue-100 text-blue-800",
|
|
3030
|
+
green: "bg-green-100 text-green-800",
|
|
3031
|
+
yellow: "bg-yellow-100 text-yellow-800",
|
|
3032
|
+
red: "bg-red-100 text-red-800",
|
|
3033
|
+
gray: "bg-gray-100 text-gray-800",
|
|
3034
|
+
purple: "bg-purple-100 text-purple-800",
|
|
3035
|
+
indigo: "bg-indigo-100 text-indigo-800"
|
|
3036
|
+
};
|
|
3037
|
+
return /* @__PURE__ */ jsx(
|
|
3038
|
+
"span",
|
|
3039
|
+
{
|
|
3040
|
+
className: `inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${variantClasses[variant]} ${className}`,
|
|
3041
|
+
children
|
|
3042
|
+
}
|
|
3043
|
+
);
|
|
3044
|
+
};
|
|
3045
|
+
var SystemStatusCard = (props) => {
|
|
3046
|
+
const { title, statusItems, className = "" } = props;
|
|
3047
|
+
const progressColorClasses = {
|
|
3048
|
+
blue: "bg-blue-500",
|
|
3049
|
+
green: "bg-green-500",
|
|
3050
|
+
orange: "bg-orange-500",
|
|
3051
|
+
red: "bg-red-500",
|
|
3052
|
+
purple: "bg-purple-500",
|
|
3053
|
+
yellow: "bg-yellow-500"
|
|
3054
|
+
};
|
|
3055
|
+
return /* @__PURE__ */ jsxs(
|
|
3056
|
+
"div",
|
|
3057
|
+
{
|
|
3058
|
+
className: `bg-white rounded-lg shadow-sm p-6 hover:shadow-md transition-shadow ${className}`,
|
|
3059
|
+
children: [
|
|
3060
|
+
/* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-gray-900 mb-4", children: title }),
|
|
3061
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-4", children: statusItems.map((item, index) => /* @__PURE__ */ jsxs("div", { children: [
|
|
3062
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
3063
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-gray-700", children: item.label }),
|
|
3064
|
+
item.isText ? /* @__PURE__ */ jsx(
|
|
3065
|
+
"span",
|
|
3066
|
+
{
|
|
3067
|
+
className: `text-sm font-medium ${item.value === "\u6B63\u5E38" || item.value === "Normal" ? "text-green-600" : "text-gray-900"}`,
|
|
3068
|
+
children: item.value
|
|
3069
|
+
}
|
|
3070
|
+
) : /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-gray-900", children: typeof item.value === "number" ? `${item.value}%` : item.value })
|
|
3071
|
+
] }),
|
|
3072
|
+
!item.isText && item.progress !== void 0 && /* @__PURE__ */ jsx("div", { className: "w-full bg-gray-200 rounded-full h-2 overflow-hidden", children: /* @__PURE__ */ jsx(
|
|
3073
|
+
"div",
|
|
3074
|
+
{
|
|
3075
|
+
className: `h-full rounded-full transition-all duration-300 ${progressColorClasses[item.color || "blue"]}`,
|
|
3076
|
+
style: {
|
|
3077
|
+
width: `${Math.min(100, Math.max(0, item.progress))}%`
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
) })
|
|
3081
|
+
] }, index)) })
|
|
3082
|
+
]
|
|
3083
|
+
}
|
|
3084
|
+
);
|
|
3085
|
+
};
|
|
3086
|
+
|
|
3087
|
+
export { components_exports as Components, DetailPageModule, FormPageModule, HtmxAdminPlugin, ListPageModule, MemoryListDatasource, PageModule, PathHelper, generateFormFieldsFromSchema, safeRender, validateFormDataWithSchema };
|