imean-service-engine-htmx-plugin 1.0.1 → 1.2.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/dist/index.js CHANGED
@@ -2,12 +2,733 @@
2
2
 
3
3
  var jsxRuntime = require('hono/jsx/jsx-runtime');
4
4
  var imeanServiceEngine = require('imean-service-engine');
5
+ var cookie = require('hono/cookie');
5
6
 
6
7
  var __defProp = Object.defineProperty;
8
+ var __getOwnPropNames = Object.getOwnPropertyNames;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
7
12
  var __export = (target, all) => {
8
13
  for (var name in all)
9
14
  __defProp(target, name, { get: all[name], enumerable: true });
10
15
  };
16
+ var Button;
17
+ var init_Button = __esm({
18
+ "src/components/Button.tsx"() {
19
+ Button = (props) => {
20
+ const {
21
+ children,
22
+ variant = "primary",
23
+ size = "md",
24
+ disabled = false,
25
+ className = "",
26
+ hxGet,
27
+ hxPost,
28
+ hxPut,
29
+ hxDelete,
30
+ hxTarget,
31
+ hxSwap,
32
+ hxPushUrl,
33
+ hxIndicator,
34
+ hxConfirm,
35
+ hxHeaders,
36
+ ...rest
37
+ } = props;
38
+ const baseClasses = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2";
39
+ const variantClasses = {
40
+ primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
41
+ secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500",
42
+ danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
43
+ ghost: "bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-500"
44
+ };
45
+ const sizeClasses = {
46
+ sm: "px-3 py-1.5 text-sm",
47
+ md: "px-4 py-2 text-sm",
48
+ lg: "px-6 py-3 text-base"
49
+ };
50
+ const classes = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${disabled ? "opacity-50 cursor-not-allowed" : ""} ${className}`;
51
+ const htmxAttrs = {};
52
+ if (hxGet) htmxAttrs["hx-get"] = hxGet;
53
+ if (hxPost) htmxAttrs["hx-post"] = hxPost;
54
+ if (hxPut) htmxAttrs["hx-put"] = hxPut;
55
+ if (hxDelete) htmxAttrs["hx-delete"] = hxDelete;
56
+ if (hxTarget) htmxAttrs["hx-target"] = hxTarget;
57
+ if (hxSwap) htmxAttrs["hx-swap"] = hxSwap;
58
+ if (hxPushUrl !== void 0)
59
+ htmxAttrs["hx-push-url"] = hxPushUrl === true ? "true" : hxPushUrl;
60
+ if (hxIndicator) htmxAttrs["hx-indicator"] = hxIndicator;
61
+ if (hxConfirm) htmxAttrs["hx-confirm"] = hxConfirm;
62
+ if (hxHeaders) htmxAttrs["hx-headers"] = hxHeaders;
63
+ const Tag = hxGet || hxPost || hxPut || hxDelete ? "a" : "button";
64
+ const href = rest.href ?? hxGet ?? "#";
65
+ return /* @__PURE__ */ jsxRuntime.jsx(
66
+ Tag,
67
+ {
68
+ className: classes,
69
+ disabled,
70
+ href,
71
+ ...htmxAttrs,
72
+ ...rest,
73
+ children
74
+ }
75
+ );
76
+ };
77
+ }
78
+ });
79
+ var Card;
80
+ var init_Card = __esm({
81
+ "src/components/Card.tsx"() {
82
+ 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__ */ jsxRuntime.jsxs(
96
+ "div",
97
+ {
98
+ className: `${baseClasses} ${shadowClass} ${borderClass} ${className}`,
99
+ children: [
100
+ title && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 border-b border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title }) }),
101
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: noPadding ? "" : paddingClass, children })
102
+ ]
103
+ }
104
+ );
105
+ };
106
+ }
107
+ });
108
+ var Breadcrumb;
109
+ var init_Breadcrumb = __esm({
110
+ "src/components/Breadcrumb.tsx"() {
111
+ Breadcrumb = (props) => {
112
+ const { items } = props;
113
+ if (items.length === 0) {
114
+ return null;
115
+ }
116
+ return /* @__PURE__ */ jsxRuntime.jsx("nav", { className: "flex", "aria-label": "Breadcrumb", children: /* @__PURE__ */ jsxRuntime.jsx("ol", { className: "flex items-center space-x-2", children: items.map((item, index) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex items-center", children: [
117
+ index > 0 && /* @__PURE__ */ jsxRuntime.jsx(
118
+ "svg",
119
+ {
120
+ className: "w-5 h-5 text-gray-400 mx-2",
121
+ fill: "currentColor",
122
+ viewBox: "0 0 20 20",
123
+ children: /* @__PURE__ */ jsxRuntime.jsx(
124
+ "path",
125
+ {
126
+ fillRule: "evenodd",
127
+ 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",
128
+ clipRule: "evenodd"
129
+ }
130
+ )
131
+ }
132
+ ),
133
+ item.href ? /* @__PURE__ */ jsxRuntime.jsx(
134
+ "a",
135
+ {
136
+ href: item.href,
137
+ className: "text-sm font-medium text-gray-500 hover:text-gray-700",
138
+ "hx-get": item.href,
139
+ "hx-push-url": "true",
140
+ children: item.label
141
+ }
142
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-900", children: item.label })
143
+ ] }, index)) }) });
144
+ };
145
+ }
146
+ });
147
+ var Header;
148
+ var init_Header = __esm({
149
+ "src/components/Header.tsx"() {
150
+ init_Breadcrumb();
151
+ Header = (props) => {
152
+ const { breadcrumbs = [], userInfo, sidebarCollapsed = false, logoutUrl } = props;
153
+ return /* @__PURE__ */ jsxRuntime.jsx("header", { className: "bg-white border-b border-gray-200 shadow-sm sticky top-0 z-40", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
154
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 flex-1 min-w-0", children: [
155
+ /* @__PURE__ */ jsxRuntime.jsx(
156
+ "a",
157
+ {
158
+ className: "p-2 rounded-lg hover:bg-gray-100 transition-colors inline-block flex-shrink-0",
159
+ title: sidebarCollapsed ? "\u5C55\u5F00\u4FA7\u8FB9\u680F" : "\u6298\u53E0\u4FA7\u8FB9\u680F",
160
+ "hx-get": "",
161
+ children: sidebarCollapsed ? (
162
+ // 展开图标(向右箭头)
163
+ /* @__PURE__ */ jsxRuntime.jsx(
164
+ "svg",
165
+ {
166
+ className: "w-5 h-5 text-gray-600",
167
+ fill: "none",
168
+ stroke: "currentColor",
169
+ viewBox: "0 0 24 24",
170
+ children: /* @__PURE__ */ jsxRuntime.jsx(
171
+ "path",
172
+ {
173
+ strokeLinecap: "round",
174
+ strokeLinejoin: "round",
175
+ strokeWidth: 2,
176
+ d: "M9 5l7 7-7 7"
177
+ }
178
+ )
179
+ }
180
+ )
181
+ ) : (
182
+ // 折叠图标(三条横线)
183
+ /* @__PURE__ */ jsxRuntime.jsx(
184
+ "svg",
185
+ {
186
+ className: "w-5 h-5 text-gray-600",
187
+ fill: "none",
188
+ stroke: "currentColor",
189
+ viewBox: "0 0 24 24",
190
+ children: /* @__PURE__ */ jsxRuntime.jsx(
191
+ "path",
192
+ {
193
+ strokeLinecap: "round",
194
+ strokeLinejoin: "round",
195
+ strokeWidth: 2,
196
+ d: "M4 6h16M4 12h16M4 18h16"
197
+ }
198
+ )
199
+ }
200
+ )
201
+ )
202
+ }
203
+ ),
204
+ breadcrumbs.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(Breadcrumb, { items: breadcrumbs })
205
+ ] }),
206
+ userInfo && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 flex-shrink-0", children: [
207
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-right hidden sm:block", children: [
208
+ userInfo.name && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-medium text-gray-900", children: userInfo.name }),
209
+ userInfo.email && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-500", children: userInfo.email })
210
+ ] }),
211
+ userInfo.avatar ? /* @__PURE__ */ jsxRuntime.jsx(
212
+ "img",
213
+ {
214
+ src: userInfo.avatar,
215
+ alt: userInfo.name || "\u7528\u6237",
216
+ className: "w-8 h-8 rounded-full"
217
+ }
218
+ ) : /* @__PURE__ */ jsxRuntime.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() }),
219
+ logoutUrl && /* @__PURE__ */ jsxRuntime.jsx(
220
+ "a",
221
+ {
222
+ href: logoutUrl,
223
+ "hx-get": logoutUrl,
224
+ "hx-target": "body",
225
+ "hx-swap": "outerHTML",
226
+ className: "p-2 rounded-lg hover:bg-gray-100 transition-colors",
227
+ title: "\u9000\u51FA\u767B\u5F55",
228
+ children: /* @__PURE__ */ jsxRuntime.jsx(
229
+ "svg",
230
+ {
231
+ className: "w-5 h-5 text-gray-600",
232
+ fill: "none",
233
+ stroke: "currentColor",
234
+ viewBox: "0 0 24 24",
235
+ children: /* @__PURE__ */ jsxRuntime.jsx(
236
+ "path",
237
+ {
238
+ strokeLinecap: "round",
239
+ strokeLinejoin: "round",
240
+ strokeWidth: 2,
241
+ d: "M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
242
+ }
243
+ )
244
+ }
245
+ )
246
+ }
247
+ )
248
+ ] })
249
+ ] }) }) });
250
+ };
251
+ }
252
+ });
253
+ var LoadingBar;
254
+ var init_LoadingBar = __esm({
255
+ "src/components/LoadingBar.tsx"() {
256
+ LoadingBar = () => {
257
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
258
+ /* @__PURE__ */ jsxRuntime.jsx(
259
+ "div",
260
+ {
261
+ id: "loading-bar",
262
+ className: "fixed top-0 left-0 right-0 h-1 bg-transparent z-50 opacity-0 transition-opacity duration-200",
263
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full bg-gradient-to-r from-blue-500 via-blue-600 to-blue-500 loading-bar-progress" })
264
+ }
265
+ ),
266
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `
267
+ @keyframes loading-progress {
268
+ 0% { transform: translateX(-100%); width: 0%; }
269
+ 50% { width: 70%; }
270
+ 100% { transform: translateX(100%); width: 100%; }
271
+ }
272
+ #loading-bar.htmx-request {
273
+ opacity: 1 !important;
274
+ }
275
+ #loading-bar.htmx-request .loading-bar-progress {
276
+ animation: loading-progress 1.5s ease-in-out infinite;
277
+ }
278
+ ` })
279
+ ] });
280
+ };
281
+ }
282
+ });
283
+ function renderNavItem(item, currentPath, collapsed = false, isChild = false) {
284
+ const isActive = currentPath === item.href || currentPath && currentPath.startsWith(item.href + "/");
285
+ const hasActiveChild = item.children?.some(
286
+ (child) => currentPath === child.href || currentPath && currentPath.startsWith(child.href + "/")
287
+ );
288
+ const navItemId = `nav-item-${item.href.replace(/[^a-zA-Z0-9]/g, "-")}`;
289
+ return /* @__PURE__ */ jsxRuntime.jsxs("li", { id: navItemId, className: "relative group", children: [
290
+ /* @__PURE__ */ jsxRuntime.jsxs(
291
+ "a",
292
+ {
293
+ href: item.href,
294
+ 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"}`,
295
+ "hx-get": item.href,
296
+ "hx-push-url": "true",
297
+ title: collapsed ? item.label : void 0,
298
+ children: [
299
+ item.icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: collapsed ? "" : "mr-2", children: item.icon }),
300
+ !collapsed && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "whitespace-nowrap overflow-hidden text-ellipsis", children: item.label })
301
+ ]
302
+ }
303
+ ),
304
+ collapsed && /* @__PURE__ */ jsxRuntime.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: [
305
+ item.label,
306
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute right-full top-1/2 -translate-y-1/2 border-4 border-transparent border-r-gray-900" })
307
+ ] }),
308
+ !collapsed && item.children && item.children.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "ml-4 mt-1 space-y-1", children: item.children.map((child) => {
309
+ const isChildActive = currentPath === child.href || currentPath && currentPath.startsWith(child.href + "/");
310
+ const childNavItemId = `nav-item-${child.href.replace(/[^a-zA-Z0-9]/g, "-")}`;
311
+ return /* @__PURE__ */ jsxRuntime.jsx(
312
+ "li",
313
+ {
314
+ id: childNavItemId,
315
+ className: "relative group",
316
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
317
+ "a",
318
+ {
319
+ href: child.href,
320
+ 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"}`,
321
+ "hx-get": child.href,
322
+ "hx-push-url": "true",
323
+ children: [
324
+ child.icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mr-2", children: child.icon }),
325
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "whitespace-nowrap overflow-hidden text-ellipsis", children: child.label })
326
+ ]
327
+ }
328
+ )
329
+ },
330
+ child.href
331
+ );
332
+ }) })
333
+ ] }, item.href);
334
+ }
335
+ var init_NavItem = __esm({
336
+ "src/components/NavItem.tsx"() {
337
+ }
338
+ });
339
+ var BaseLayout, AdminLayout;
340
+ var init_Layout = __esm({
341
+ "src/components/Layout.tsx"() {
342
+ init_Header();
343
+ init_LoadingBar();
344
+ init_NavItem();
345
+ BaseLayout = (props) => {
346
+ return /* @__PURE__ */ jsxRuntime.jsxs("html", { children: [
347
+ /* @__PURE__ */ jsxRuntime.jsxs("head", { children: [
348
+ /* @__PURE__ */ jsxRuntime.jsx("meta", { charset: "UTF-8" }),
349
+ /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
350
+ /* @__PURE__ */ jsxRuntime.jsx("title", { children: props.title }),
351
+ props.description && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "description", content: props.description }),
352
+ /* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://unpkg.com/htmx.org@latest" }),
353
+ /* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://unpkg.com/hyperscript.org@latest" }),
354
+ /* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.tailwindcss.com" }),
355
+ /* @__PURE__ */ jsxRuntime.jsx(
356
+ "style",
357
+ {
358
+ dangerouslySetInnerHTML: {
359
+ __html: `
360
+ @keyframes fadeIn {
361
+ from { opacity: 0;}
362
+ to { opacity: 1;}
363
+ }
364
+
365
+ @keyframes slideIn {
366
+ from { opacity: 0; transform: scale(0.95) translateY(-10px); }
367
+ to { opacity: 1; transform: scale(1) translateY(0); }
368
+ }
369
+
370
+ @keyframes slideInRight {
371
+ from { opacity: 0; transform: translateX(100%); }
372
+ to { opacity: 1; transform: translateX(0); }
373
+ }
374
+
375
+ @keyframes slideOutRight {
376
+ from { opacity: 1; transform: translateX(0); }
377
+ to { opacity: 0; transform: translateX(100%); }
378
+ }
379
+
380
+ @keyframes fadeOut {
381
+ from { opacity: 1; }
382
+ to { opacity: 0; }
383
+ }
384
+
385
+ @keyframes scaleOut {
386
+ from { opacity: 1; transform: scale(1) translateY(0); }
387
+ to { opacity: 0; transform: scale(0.95) translateY(-10px); }
388
+ }
389
+
390
+ /* Dialog \u9000\u51FA\u52A8\u753B */
391
+ .dialog-exit {
392
+ animation: fadeOut 0.2s ease-in forwards !important;
393
+ }
394
+
395
+ .dialog-content-exit {
396
+ animation: scaleOut 0.2s ease-in forwards !important;
397
+ }
398
+
399
+ /* ErrorAlert \u9000\u51FA\u52A8\u753B */
400
+ .error-alert-exit {
401
+ animation: slideOutRight 0.3s ease-in forwards, fadeOut 0.3s ease-in forwards;
402
+ }
403
+ `
404
+ }
405
+ }
406
+ )
407
+ ] }),
408
+ /* @__PURE__ */ jsxRuntime.jsxs(
409
+ "body",
410
+ {
411
+ className: "bg-gray-50",
412
+ "hx-indicator": "#loading-bar",
413
+ "hx-target": "#main-content",
414
+ "hx-swap": "outerHTML",
415
+ children: [
416
+ /* @__PURE__ */ jsxRuntime.jsx(LoadingBar, {}),
417
+ props.children,
418
+ /* @__PURE__ */ jsxRuntime.jsx(
419
+ "div",
420
+ {
421
+ id: "error-container",
422
+ className: "fixed top-4 right-4 z-[200] w-full max-w-2xl px-4"
423
+ }
424
+ ),
425
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "dialog-container" })
426
+ ]
427
+ }
428
+ )
429
+ ] });
430
+ };
431
+ AdminLayout = (props) => {
432
+ const logo = props.adminContext.pluginOptions.logo;
433
+ const navItems = props.adminContext.pluginOptions.navigation;
434
+ const referer = props.adminContext.ctx.req.header("Referer");
435
+ let currentPath = props.adminContext.ctx.req.path;
436
+ if (referer) {
437
+ try {
438
+ const refererUrl = new URL(referer);
439
+ const method = props.adminContext.ctx.req.method;
440
+ if (["POST", "PUT", "DELETE"].includes(method)) {
441
+ currentPath = refererUrl.pathname;
442
+ }
443
+ } catch (e) {
444
+ }
445
+ }
446
+ const sidebarCollapsed = props.sidebarCollapsed || false;
447
+ const breadcrumbs = props.adminContext.breadcrumbs;
448
+ const userInfo = props.adminContext.userInfo;
449
+ const logoutUrl = props.adminContext.pluginOptions.authProvider?.logoutUrl;
450
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-screen", id: "main-content", children: [
451
+ /* @__PURE__ */ jsxRuntime.jsx(
452
+ "aside",
453
+ {
454
+ 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`,
455
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${props.sidebarCollapsed ? "p-2" : "p-6"}`, children: [
456
+ !props.sidebarCollapsed && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: logo ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: logo, alt: "Logo", className: "h-10 mb-6" }) : /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-xl font-bold mb-6 text-white whitespace-nowrap overflow-hidden text-ellipsis", children: props.adminContext.pluginOptions.title }) }),
457
+ /* @__PURE__ */ jsxRuntime.jsx("nav", { children: /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "space-y-1", children: navItems && navItems.length > 0 ? navItems.map(
458
+ (item) => renderNavItem(
459
+ item,
460
+ currentPath,
461
+ props.sidebarCollapsed || false
462
+ )
463
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
464
+ "li",
465
+ {
466
+ className: `${props.sidebarCollapsed ? "px-2" : "px-4"} py-2 text-gray-400 text-sm`,
467
+ children: "\u6682\u65E0\u5BFC\u822A\u9879"
468
+ }
469
+ ) }) })
470
+ ] })
471
+ }
472
+ ),
473
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col overflow-hidden", children: [
474
+ /* @__PURE__ */ jsxRuntime.jsx(
475
+ Header,
476
+ {
477
+ breadcrumbs,
478
+ userInfo,
479
+ sidebarCollapsed: sidebarCollapsed || false,
480
+ logoutUrl
481
+ }
482
+ ),
483
+ /* @__PURE__ */ jsxRuntime.jsx("main", { className: "flex-1 overflow-auto bg-gray-50", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6", children: props.children }) })
484
+ ] })
485
+ ] });
486
+ };
487
+ }
488
+ });
489
+
490
+ // src/utils/context.tsx
491
+ var context_exports = {};
492
+ __export(context_exports, {
493
+ HtmxAdminContext: () => HtmxAdminContext
494
+ });
495
+ var HtmxAdminContext;
496
+ var init_context = __esm({
497
+ "src/utils/context.tsx"() {
498
+ HtmxAdminContext = class {
499
+ /** 模块元数据 */
500
+ moduleMetadata;
501
+ /** 插件选项 */
502
+ pluginOptions;
503
+ /** 服务名(用于生成权限ID) */
504
+ serviceName;
505
+ /** Hono Context */
506
+ ctx;
507
+ /** 之前的模块名 */
508
+ previousModuleName;
509
+ /** 是否是片段请求(HTMX 请求) */
510
+ isFragment;
511
+ /** 是否是对话框请求 */
512
+ isDialog;
513
+ /** 用户信息 */
514
+ userInfo;
515
+ /** 通知队列 */
516
+ notifications = [];
517
+ /** 页面标题(用于 HX-Title 和页面展示) */
518
+ title = "";
519
+ /** 页面描述(用于SEO和页面展示) */
520
+ description = "";
521
+ /** 面包屑项 */
522
+ breadcrumbs = [];
523
+ /** 主要内容 */
524
+ content = null;
525
+ /** 需要重定向的 URL */
526
+ redirectUrl;
527
+ /** 是否需要刷新页面(用于 HX-Refresh) */
528
+ refresh = false;
529
+ constructor(ctx, userInfo, moduleMetadata, pluginOptions, serviceName = "") {
530
+ this.ctx = ctx;
531
+ this.userInfo = userInfo;
532
+ this.moduleMetadata = moduleMetadata;
533
+ this.pluginOptions = pluginOptions;
534
+ this.serviceName = serviceName;
535
+ const url = new URL(ctx.req.url);
536
+ this.isFragment = ctx.req.header("HX-Request") === "true";
537
+ this.isDialog = url.searchParams.get("dialog") === "true" || ctx.req.header("HX-Target") === "#dialog-container" || (ctx.req.header("Referer") || "").includes("dialog=true");
538
+ const referer = ctx.req.header("Referer");
539
+ this.previousModuleName = referer ? new URL(referer).pathname.replace(pluginOptions.prefix, "").split("/").pop() : void 0;
540
+ }
541
+ /**
542
+ * 发送通知
543
+ * 通知将在响应时通过 OOB 更新到错误容器
544
+ */
545
+ sendNotification(type, title, message) {
546
+ this.notifications.push({ type, title, message });
547
+ }
548
+ /**
549
+ * 发送错误通知(便捷方法)
550
+ */
551
+ sendError(title, message) {
552
+ this.sendNotification("error", title, message);
553
+ }
554
+ /**
555
+ * 发送警告通知(便捷方法)
556
+ */
557
+ sendWarning(title, message) {
558
+ this.sendNotification("warning", title, message);
559
+ }
560
+ /**
561
+ * 发送信息通知(便捷方法)
562
+ */
563
+ sendInfo(title, message) {
564
+ this.sendNotification("info", title, message);
565
+ }
566
+ /**
567
+ * 发送成功通知(便捷方法)
568
+ */
569
+ sendSuccess(title, message) {
570
+ this.sendNotification("success", title, message);
571
+ }
572
+ /**
573
+ * 设置需要推送的 URL
574
+ * 用于 HX-Push-Url 响应头
575
+ */
576
+ redirect(url) {
577
+ this.redirectUrl = url;
578
+ }
579
+ /**
580
+ * 设置需要刷新页面
581
+ * 用于 HX-Refresh 响应头
582
+ */
583
+ setRefresh(refresh = true) {
584
+ this.refresh = refresh;
585
+ }
586
+ /**
587
+ * 检查是否有列表页面
588
+ * 封装 moduleMetadata 访问,避免直接访问内部属性
589
+ */
590
+ hasList() {
591
+ return this.moduleMetadata.hasList;
592
+ }
593
+ /**
594
+ * 检查是否有详情页面
595
+ * 封装 moduleMetadata 访问,避免直接访问内部属性
596
+ */
597
+ hasDetail() {
598
+ return this.moduleMetadata.hasDetail;
599
+ }
600
+ /**
601
+ * 检查是否有表单页面
602
+ * 封装 moduleMetadata 访问,避免直接访问内部属性
603
+ */
604
+ hasForm() {
605
+ return this.moduleMetadata.hasForm;
606
+ }
607
+ /**
608
+ * 检查是否有自定义页面
609
+ * 封装 moduleMetadata 访问,避免直接访问内部属性
610
+ */
611
+ hasCustom() {
612
+ return this.moduleMetadata.hasCustom;
613
+ }
614
+ };
615
+ }
616
+ });
617
+
618
+ // src/components/PermissionDenied.tsx
619
+ var PermissionDenied_exports = {};
620
+ __export(PermissionDenied_exports, {
621
+ PermissionDeniedContent: () => PermissionDeniedContent,
622
+ PermissionDeniedPage: () => PermissionDeniedPage
623
+ });
624
+ function PermissionDeniedContent(adminContext, operationId, fromPath, registeredOperations) {
625
+ let operationInfo = null;
626
+ if (operationId && registeredOperations) {
627
+ operationInfo = registeredOperations.find(
628
+ (op) => op.operationId === operationId
629
+ ) || null;
630
+ }
631
+ const parts = operationId?.split(".") || [];
632
+ const operationType = parts[parts.length - 1] || "";
633
+ const moduleName = parts.length >= 2 ? parts[parts.length - 2] : "";
634
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
635
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-6xl mb-4", children: "\u{1F512}" }),
636
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-3xl font-bold text-gray-900 mb-2", children: "\u6743\u9650\u4E0D\u8DB3" }),
637
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600 mb-8", children: "\u60A8\u5F53\u524D\u6CA1\u6709\u6743\u9650\u8BBF\u95EE\u6B64\u8D44\u6E90\u3002\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u83B7\u53D6\u76F8\u5E94\u6743\u9650\u3002" }),
638
+ operationId && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-8 p-4 bg-red-50 border border-red-200 rounded-lg text-left", children: [
639
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-red-900 mb-2", children: "\u88AB\u62D2\u7EDD\u7684\u64CD\u4F5C" }),
640
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2 text-sm", children: [
641
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
642
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-gray-700", children: "\u64CD\u4F5CID:" }),
643
+ " ",
644
+ /* @__PURE__ */ jsxRuntime.jsx("code", { className: "px-2 py-1 bg-gray-100 rounded text-blue-600 font-mono", children: operationId })
645
+ ] }),
646
+ operationInfo && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
647
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
648
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-gray-700", children: "\u6A21\u5757:" }),
649
+ " ",
650
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-900", children: operationInfo.moduleTitle || moduleName })
651
+ ] }),
652
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
653
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-gray-700", children: "\u64CD\u4F5C\u7C7B\u578B:" }),
654
+ " ",
655
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-semibold", children: getOperationTypeLabel(operationType) })
656
+ ] }),
657
+ operationInfo.moduleDescription && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
658
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-gray-700", children: "\u6A21\u5757\u63CF\u8FF0:" }),
659
+ " ",
660
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-600", children: operationInfo.moduleDescription })
661
+ ] }),
662
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
663
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-gray-700", children: "\u8DEF\u7531\u8DEF\u5F84:" }),
664
+ " ",
665
+ /* @__PURE__ */ jsxRuntime.jsx("code", { className: "px-2 py-1 bg-gray-100 rounded text-gray-700 font-mono text-xs", children: operationInfo.routePath })
666
+ ] })
667
+ ] }),
668
+ fromPath && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
669
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-gray-700", children: "\u8BF7\u6C42\u8DEF\u5F84:" }),
670
+ " ",
671
+ /* @__PURE__ */ jsxRuntime.jsx("code", { className: "px-2 py-1 bg-gray-100 rounded text-gray-700 font-mono text-xs", children: fromPath })
672
+ ] })
673
+ ] })
674
+ ] }),
675
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-8", children: /* @__PURE__ */ jsxRuntime.jsx(
676
+ Button,
677
+ {
678
+ variant: "secondary",
679
+ _: "on click set #dialog-container's innerHTML to ''",
680
+ children: "\u5173\u95ED"
681
+ }
682
+ ) })
683
+ ] });
684
+ }
685
+ function PermissionDeniedPage(adminContext, operationId, fromPath, registeredOperations) {
686
+ return /* @__PURE__ */ jsxRuntime.jsx(
687
+ BaseLayout,
688
+ {
689
+ title: `\u6743\u9650\u4E0D\u8DB3 - ${adminContext.pluginOptions.title}`,
690
+ description: "\u60A8\u6CA1\u6709\u6743\u9650\u8BBF\u95EE\u6B64\u8D44\u6E90",
691
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { id: "main-content", className: "min-h-screen flex items-center justify-center px-4", children: /* @__PURE__ */ jsxRuntime.jsxs(Card, { className: "max-w-2xl w-full p-8", children: [
692
+ /* @__PURE__ */ jsxRuntime.jsx(
693
+ PermissionDeniedContent,
694
+ {
695
+ adminContext,
696
+ operationId,
697
+ fromPath,
698
+ registeredOperations
699
+ }
700
+ ),
701
+ adminContext.pluginOptions.authProvider?.loginUrl && !adminContext.userInfo && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-8 text-center", children: /* @__PURE__ */ jsxRuntime.jsx(
702
+ Button,
703
+ {
704
+ variant: "primary",
705
+ href: adminContext.pluginOptions.authProvider.loginUrl,
706
+ hxGet: adminContext.pluginOptions.authProvider.loginUrl,
707
+ hxTarget: "body",
708
+ hxSwap: "outerHTML",
709
+ children: "\u524D\u5F80\u767B\u5F55"
710
+ }
711
+ ) })
712
+ ] }) })
713
+ }
714
+ );
715
+ }
716
+ function getOperationTypeLabel(operationType) {
717
+ const labels = {
718
+ read: "\u67E5\u770B",
719
+ create: "\u521B\u5EFA",
720
+ edit: "\u7F16\u8F91",
721
+ delete: "\u5220\u9664"
722
+ };
723
+ return labels[operationType] || operationType;
724
+ }
725
+ var init_PermissionDenied = __esm({
726
+ "src/components/PermissionDenied.tsx"() {
727
+ init_Card();
728
+ init_Button();
729
+ init_Layout();
730
+ }
731
+ });
11
732
  function safeRender(value) {
12
733
  if (value === null || value === void 0) {
13
734
  return null;
@@ -23,88 +744,10 @@ function safeRender(value) {
23
744
  }
24
745
  return value;
25
746
  }
26
- var Button = (props) => {
27
- const {
28
- children,
29
- variant = "primary",
30
- size = "md",
31
- disabled = false,
32
- className = "",
33
- hxGet,
34
- hxPost,
35
- hxPut,
36
- hxDelete,
37
- hxTarget,
38
- hxSwap,
39
- hxPushUrl,
40
- hxIndicator,
41
- hxConfirm,
42
- hxHeaders,
43
- ...rest
44
- } = props;
45
- const baseClasses = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2";
46
- const variantClasses = {
47
- primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
48
- secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500",
49
- danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
50
- ghost: "bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-500"
51
- };
52
- const sizeClasses = {
53
- sm: "px-3 py-1.5 text-sm",
54
- md: "px-4 py-2 text-sm",
55
- lg: "px-6 py-3 text-base"
56
- };
57
- const classes = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${disabled ? "opacity-50 cursor-not-allowed" : ""} ${className}`;
58
- const htmxAttrs = {};
59
- if (hxGet) htmxAttrs["hx-get"] = hxGet;
60
- if (hxPost) htmxAttrs["hx-post"] = hxPost;
61
- if (hxPut) htmxAttrs["hx-put"] = hxPut;
62
- if (hxDelete) htmxAttrs["hx-delete"] = hxDelete;
63
- if (hxTarget) htmxAttrs["hx-target"] = hxTarget;
64
- if (hxSwap) htmxAttrs["hx-swap"] = hxSwap;
65
- if (hxPushUrl !== void 0)
66
- htmxAttrs["hx-push-url"] = hxPushUrl === true ? "true" : hxPushUrl;
67
- if (hxIndicator) htmxAttrs["hx-indicator"] = hxIndicator;
68
- if (hxConfirm) htmxAttrs["hx-confirm"] = hxConfirm;
69
- if (hxHeaders) htmxAttrs["hx-headers"] = hxHeaders;
70
- const Tag = hxGet || hxPost || hxPut || hxDelete ? "a" : "button";
71
- const href = rest.href ?? hxGet ?? "#";
72
- return /* @__PURE__ */ jsxRuntime.jsx(
73
- Tag,
74
- {
75
- className: classes,
76
- disabled,
77
- href,
78
- ...htmxAttrs,
79
- ...rest,
80
- children
81
- }
82
- );
83
- };
84
- var Card = (props) => {
85
- const {
86
- children,
87
- title,
88
- className = "",
89
- shadow = true,
90
- bordered = false,
91
- noPadding = false
92
- } = props;
93
- const baseClasses = "bg-white rounded-lg";
94
- const shadowClass = shadow ? "shadow-sm hover:shadow-md transition-shadow" : "";
95
- const borderClass = bordered ? "border border-gray-200" : "";
96
- const paddingClass = noPadding ? "" : "p-6";
97
- return /* @__PURE__ */ jsxRuntime.jsxs(
98
- "div",
99
- {
100
- className: `${baseClasses} ${shadowClass} ${borderClass} ${className}`,
101
- children: [
102
- title && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 border-b border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title }) }),
103
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: noPadding ? "" : paddingClass, children })
104
- ]
105
- }
106
- );
107
- };
747
+
748
+ // src/components/Detail.tsx
749
+ init_Button();
750
+ init_Card();
108
751
  var PageHeader = (props) => {
109
752
  const { title, description, actions, className = "" } = props;
110
753
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `mb-6 ${className}`, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between items-start", children: [
@@ -232,6 +875,14 @@ function DetailContent(props) {
232
875
  );
233
876
  }
234
877
 
878
+ // src/utils/module-name.ts
879
+ function extractModuleNameFromPath(basePath, prefix) {
880
+ const pathWithoutPrefix = basePath.replace(prefix, "");
881
+ const pathSegments = pathWithoutPrefix.split("/").filter(Boolean);
882
+ const modulePath = pathSegments[0] || "";
883
+ return modulePath.toLowerCase();
884
+ }
885
+
235
886
  // src/utils/path.ts
236
887
  var PathHelper = class {
237
888
  basePath;
@@ -335,6 +986,22 @@ var PageModule = class {
335
986
  ];
336
987
  return breadcrumbs;
337
988
  }
989
+ /**
990
+ * 获取所需权限
991
+ * 子类可以重写此方法来声明页面所需的权限
992
+ *
993
+ * 返回值说明:
994
+ * - 返回空字符串 "" 或 null 或 undefined:表示开放访问(无需权限)
995
+ * - 返回权限字符串:表示需要该权限,如 "users.read", "users.*"
996
+ *
997
+ * 默认实现:返回空字符串(开放访问)
998
+ *
999
+ * @param ctx Hono Context(可选,用于根据请求动态决定权限)
1000
+ * @returns 所需权限字符串,或空字符串/null/undefined 表示开放
1001
+ */
1002
+ getRequiredPermission(ctx) {
1003
+ return "";
1004
+ }
338
1005
  /**
339
1006
  * 处理请求的统一入口(由 RouteHandler 调用)
340
1007
  * 默认实现:直接调用 render 方法
@@ -350,6 +1017,22 @@ var PageModule = class {
350
1017
  var DetailPageModule = class extends PageModule {
351
1018
  /** ID 字段名(默认 "id") */
352
1019
  idField = "id";
1020
+ /**
1021
+ * 获取所需权限
1022
+ * 默认实现:返回 "{serviceName}.{moduleName}.read"
1023
+ * 子类可以重写此方法来自定义权限要求
1024
+ *
1025
+ * @param ctx Hono Context(可选)
1026
+ * @returns 所需权限字符串,或空字符串/null/undefined 表示开放
1027
+ */
1028
+ getRequiredPermission(ctx) {
1029
+ const moduleName = extractModuleNameFromPath(
1030
+ this.context.moduleMetadata.basePath,
1031
+ this.context.pluginOptions.prefix
1032
+ );
1033
+ const servicePrefix = this.context.serviceName ? `${this.context.serviceName}.` : "";
1034
+ return `${servicePrefix}${moduleName}.read`;
1035
+ }
353
1036
  /**
354
1037
  * 获取字段标签(可选)
355
1038
  * 子类可以重写此方法来自定义字段的中文标签
@@ -453,6 +1136,10 @@ var DetailPageModule = class extends PageModule {
453
1136
  );
454
1137
  }
455
1138
  };
1139
+
1140
+ // src/components/Form.tsx
1141
+ init_Button();
1142
+ init_Card();
456
1143
  var DateInput = (props) => {
457
1144
  const {
458
1145
  id,
@@ -954,6 +1641,33 @@ var FormPageModule = class extends PageModule {
954
1641
  super();
955
1642
  this.schema = schema;
956
1643
  }
1644
+ /**
1645
+ * 获取所需权限
1646
+ * 根据是新建还是编辑返回不同的权限要求
1647
+ * - 新建:{serviceName}.{moduleName}.create
1648
+ * - 编辑:{serviceName}.{moduleName}.edit
1649
+ * 子类可以重写此方法来自定义权限要求
1650
+ *
1651
+ * @param ctx Hono Context(可选)
1652
+ * @returns 所需权限字符串,或空字符串/null/undefined 表示开放
1653
+ */
1654
+ getRequiredPermission(ctx) {
1655
+ const moduleName = extractModuleNameFromPath(
1656
+ this.context.moduleMetadata.basePath,
1657
+ this.context.pluginOptions.prefix
1658
+ );
1659
+ const servicePrefix = this.context.serviceName ? `${this.context.serviceName}.` : "";
1660
+ if (ctx) {
1661
+ const url = new URL(ctx.req.url);
1662
+ const path = url.pathname;
1663
+ if (path.includes("/new")) {
1664
+ return `${servicePrefix}${moduleName}.create`;
1665
+ } else if (path.includes("/edit/")) {
1666
+ return `${servicePrefix}${moduleName}.edit`;
1667
+ }
1668
+ }
1669
+ return `${servicePrefix}${moduleName}.edit`;
1670
+ }
957
1671
  /**
958
1672
  * 获取单条数据(编辑时使用)
959
1673
  */
@@ -1218,6 +1932,13 @@ var FormPageModule = class extends PageModule {
1218
1932
  return await this.render(formData);
1219
1933
  }
1220
1934
  };
1935
+
1936
+ // src/components/ListContent.tsx
1937
+ init_Button();
1938
+
1939
+ // src/components/FilterCard.tsx
1940
+ init_Button();
1941
+ init_Card();
1221
1942
  var FilterCard = (props) => {
1222
1943
  const {
1223
1944
  title,
@@ -1362,6 +2083,9 @@ var StatCard = (props) => {
1362
2083
  }
1363
2084
  );
1364
2085
  };
2086
+
2087
+ // src/components/ActionButton.tsx
2088
+ init_Button();
1365
2089
  var ActionButton = (props) => {
1366
2090
  const { label, href, method, className = "", item } = props;
1367
2091
  const hrefValue = typeof href === "function" ? href(item || {}) : href;
@@ -1384,10 +2108,17 @@ var ActionButton = (props) => {
1384
2108
  }
1385
2109
  );
1386
2110
  };
2111
+
2112
+ // src/components/Table.tsx
2113
+ init_Button();
2114
+ init_Card();
1387
2115
  var EmptyState = (props) => {
1388
2116
  const { message = "\u6682\u65E0\u6570\u636E", children } = props;
1389
2117
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-12", children: children || /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-500 text-sm", children: message }) });
1390
2118
  };
2119
+
2120
+ // src/components/Pagination.tsx
2121
+ init_Button();
1391
2122
  var Pagination = (props) => {
1392
2123
  const {
1393
2124
  page,
@@ -1718,6 +2449,22 @@ function parseListParams(ctx) {
1718
2449
  var ListPageModule = class extends PageModule {
1719
2450
  /** ID 字段名(默认 "id") */
1720
2451
  idField = "id";
2452
+ /**
2453
+ * 获取所需权限
2454
+ * 默认实现:返回 "{serviceName}.{moduleName}.read"
2455
+ * 子类可以重写此方法来自定义权限要求
2456
+ *
2457
+ * @param ctx Hono Context(可选)
2458
+ * @returns 所需权限字符串,或空字符串/null/undefined 表示开放
2459
+ */
2460
+ getRequiredPermission(ctx) {
2461
+ const moduleName = extractModuleNameFromPath(
2462
+ this.context.moduleMetadata.basePath,
2463
+ this.context.pluginOptions.prefix
2464
+ );
2465
+ const servicePrefix = this.context.serviceName ? `${this.context.serviceName}.` : "";
2466
+ return `${servicePrefix}${moduleName}.read`;
2467
+ }
1721
2468
  /**
1722
2469
  * 获取列表数据
1723
2470
  */
@@ -1967,74 +2714,6 @@ var MemoryListDatasource = class {
1967
2714
  return newItem;
1968
2715
  }
1969
2716
  };
1970
- var Dialog = (props) => {
1971
- const {
1972
- title,
1973
- children,
1974
- showClose = true,
1975
- closeUrl,
1976
- className = "",
1977
- size = "lg"
1978
- } = props;
1979
- const sizeClasses = {
1980
- sm: "max-w-md",
1981
- md: "max-w-lg",
1982
- lg: "max-w-2xl",
1983
- xl: "max-w-4xl",
1984
- full: "max-w-7xl"
1985
- };
1986
- return /* @__PURE__ */ jsxRuntime.jsx(
1987
- "div",
1988
- {
1989
- className: "fixed inset-0 bg-black bg-opacity-50 z-[100] flex items-center justify-center p-4 dialog-backdrop",
1990
- style: {
1991
- animation: "fadeIn 0.2s ease-out"
1992
- },
1993
- _: "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",
1994
- children: /* @__PURE__ */ jsxRuntime.jsxs(
1995
- "div",
1996
- {
1997
- className: `bg-gray-50 rounded-lg shadow-xl ${sizeClasses[size]} w-full max-h-[90vh] overflow-hidden flex flex-col dialog-content ${className}`,
1998
- style: {
1999
- animation: "slideIn 0.3s ease-out"
2000
- },
2001
- _: "on click call event.stopPropagation()",
2002
- children: [
2003
- (title || showClose) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 border-b border-gray-200 bg-white flex items-center justify-between", children: [
2004
- title && /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title }),
2005
- showClose && /* @__PURE__ */ jsxRuntime.jsx(
2006
- "button",
2007
- {
2008
- className: "text-gray-400 hover:text-gray-600 transition-colors",
2009
- _: "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",
2010
- children: /* @__PURE__ */ jsxRuntime.jsx(
2011
- "svg",
2012
- {
2013
- className: "w-6 h-6",
2014
- fill: "none",
2015
- stroke: "currentColor",
2016
- viewBox: "0 0 24 24",
2017
- children: /* @__PURE__ */ jsxRuntime.jsx(
2018
- "path",
2019
- {
2020
- strokeLinecap: "round",
2021
- strokeLinejoin: "round",
2022
- strokeWidth: 2,
2023
- d: "M6 18L18 6M6 6l12 12"
2024
- }
2025
- )
2026
- }
2027
- )
2028
- }
2029
- )
2030
- ] }),
2031
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 overflow-y-auto p-6 bg-gray-50", children })
2032
- ]
2033
- }
2034
- )
2035
- }
2036
- );
2037
- };
2038
2717
  var ErrorAlert = (props) => {
2039
2718
  const {
2040
2719
  title,
@@ -2121,466 +2800,343 @@ var ErrorAlert = (props) => {
2121
2800
  {
2122
2801
  strokeLinecap: "round",
2123
2802
  strokeLinejoin: "round",
2124
- strokeWidth: 2,
2125
- d: "M6 18L18 6M6 6l12 12"
2126
- }
2127
- )
2128
- }
2129
- )
2130
- }
2131
- )
2132
- ] })
2133
- }
2134
- );
2135
- };
2136
- var Breadcrumb = (props) => {
2137
- const { items } = props;
2138
- if (items.length === 0) {
2139
- return null;
2140
- }
2141
- return /* @__PURE__ */ jsxRuntime.jsx("nav", { className: "flex", "aria-label": "Breadcrumb", children: /* @__PURE__ */ jsxRuntime.jsx("ol", { className: "flex items-center space-x-2", children: items.map((item, index) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex items-center", children: [
2142
- index > 0 && /* @__PURE__ */ jsxRuntime.jsx(
2143
- "svg",
2144
- {
2145
- className: "w-5 h-5 text-gray-400 mx-2",
2146
- fill: "currentColor",
2147
- viewBox: "0 0 20 20",
2148
- children: /* @__PURE__ */ jsxRuntime.jsx(
2149
- "path",
2150
- {
2151
- fillRule: "evenodd",
2152
- 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",
2153
- clipRule: "evenodd"
2154
- }
2155
- )
2156
- }
2157
- ),
2158
- item.href ? /* @__PURE__ */ jsxRuntime.jsx(
2159
- "a",
2160
- {
2161
- href: item.href,
2162
- className: "text-sm font-medium text-gray-500 hover:text-gray-700",
2163
- "hx-get": item.href,
2164
- "hx-push-url": "true",
2165
- children: item.label
2166
- }
2167
- ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-900", children: item.label })
2168
- ] }, index)) }) });
2169
- };
2170
- var Header = (props) => {
2171
- const { breadcrumbs = [], userInfo, sidebarCollapsed = false } = props;
2172
- return /* @__PURE__ */ jsxRuntime.jsx("header", { className: "bg-white border-b border-gray-200 shadow-sm sticky top-0 z-40", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
2173
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 flex-1 min-w-0", children: [
2174
- /* @__PURE__ */ jsxRuntime.jsx(
2175
- "a",
2176
- {
2177
- className: "p-2 rounded-lg hover:bg-gray-100 transition-colors inline-block flex-shrink-0",
2178
- title: sidebarCollapsed ? "\u5C55\u5F00\u4FA7\u8FB9\u680F" : "\u6298\u53E0\u4FA7\u8FB9\u680F",
2179
- "hx-get": "",
2180
- children: sidebarCollapsed ? (
2181
- // 展开图标(向右箭头)
2182
- /* @__PURE__ */ jsxRuntime.jsx(
2183
- "svg",
2184
- {
2185
- className: "w-5 h-5 text-gray-600",
2186
- fill: "none",
2187
- stroke: "currentColor",
2188
- viewBox: "0 0 24 24",
2189
- children: /* @__PURE__ */ jsxRuntime.jsx(
2190
- "path",
2191
- {
2192
- strokeLinecap: "round",
2193
- strokeLinejoin: "round",
2194
- strokeWidth: 2,
2195
- d: "M9 5l7 7-7 7"
2196
- }
2197
- )
2198
- }
2199
- )
2200
- ) : (
2201
- // 折叠图标(三条横线)
2202
- /* @__PURE__ */ jsxRuntime.jsx(
2203
- "svg",
2204
- {
2205
- className: "w-5 h-5 text-gray-600",
2206
- fill: "none",
2207
- stroke: "currentColor",
2208
- viewBox: "0 0 24 24",
2209
- children: /* @__PURE__ */ jsxRuntime.jsx(
2210
- "path",
2211
- {
2212
- strokeLinecap: "round",
2213
- strokeLinejoin: "round",
2214
- strokeWidth: 2,
2215
- d: "M4 6h16M4 12h16M4 18h16"
2216
- }
2217
- )
2218
- }
2219
- )
2220
- )
2221
- }
2222
- ),
2223
- breadcrumbs.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(Breadcrumb, { items: breadcrumbs })
2224
- ] }),
2225
- userInfo && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 flex-shrink-0", children: [
2226
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-right hidden sm:block", children: [
2227
- userInfo.name && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-medium text-gray-900", children: userInfo.name }),
2228
- userInfo.email && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-500", children: userInfo.email })
2229
- ] }),
2230
- userInfo.avatar ? /* @__PURE__ */ jsxRuntime.jsx(
2231
- "img",
2232
- {
2233
- src: userInfo.avatar,
2234
- alt: userInfo.name || "\u7528\u6237",
2235
- className: "w-8 h-8 rounded-full"
2236
- }
2237
- ) : /* @__PURE__ */ jsxRuntime.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() })
2238
- ] })
2239
- ] }) }) });
2240
- };
2241
- var LoadingBar = () => {
2242
- return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2243
- /* @__PURE__ */ jsxRuntime.jsx(
2244
- "div",
2245
- {
2246
- id: "loading-bar",
2247
- className: "fixed top-0 left-0 right-0 h-1 bg-transparent z-50 opacity-0 transition-opacity duration-200",
2248
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full bg-gradient-to-r from-blue-500 via-blue-600 to-blue-500 loading-bar-progress" })
2249
- }
2250
- ),
2251
- /* @__PURE__ */ jsxRuntime.jsx("style", { children: `
2252
- @keyframes loading-progress {
2253
- 0% { transform: translateX(-100%); width: 0%; }
2254
- 50% { width: 70%; }
2255
- 100% { transform: translateX(100%); width: 100%; }
2256
- }
2257
- #loading-bar.htmx-request {
2258
- opacity: 1 !important;
2259
- }
2260
- #loading-bar.htmx-request .loading-bar-progress {
2261
- animation: loading-progress 1.5s ease-in-out infinite;
2262
- }
2263
- ` })
2264
- ] });
2265
- };
2266
- function renderNavItem(item, currentPath, collapsed = false, isChild = false) {
2267
- const isActive = currentPath === item.href || currentPath && currentPath.startsWith(item.href + "/");
2268
- const hasActiveChild = item.children?.some(
2269
- (child) => currentPath === child.href || currentPath && currentPath.startsWith(child.href + "/")
2270
- );
2271
- const navItemId = `nav-item-${item.href.replace(/[^a-zA-Z0-9]/g, "-")}`;
2272
- return /* @__PURE__ */ jsxRuntime.jsxs("li", { id: navItemId, className: "relative group", children: [
2273
- /* @__PURE__ */ jsxRuntime.jsxs(
2274
- "a",
2275
- {
2276
- href: item.href,
2277
- 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"}`,
2278
- "hx-get": item.href,
2279
- "hx-push-url": "true",
2280
- title: collapsed ? item.label : void 0,
2281
- children: [
2282
- item.icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: collapsed ? "" : "mr-2", children: item.icon }),
2283
- !collapsed && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "whitespace-nowrap overflow-hidden text-ellipsis", children: item.label })
2284
- ]
2285
- }
2286
- ),
2287
- collapsed && /* @__PURE__ */ jsxRuntime.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: [
2288
- item.label,
2289
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute right-full top-1/2 -translate-y-1/2 border-4 border-transparent border-r-gray-900" })
2290
- ] }),
2291
- !collapsed && item.children && item.children.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "ml-4 mt-1 space-y-1", children: item.children.map((child) => {
2292
- const isChildActive = currentPath === child.href || currentPath && currentPath.startsWith(child.href + "/");
2293
- const childNavItemId = `nav-item-${child.href.replace(/[^a-zA-Z0-9]/g, "-")}`;
2294
- return /* @__PURE__ */ jsxRuntime.jsx(
2295
- "li",
2296
- {
2297
- id: childNavItemId,
2298
- className: "relative group",
2299
- children: /* @__PURE__ */ jsxRuntime.jsxs(
2300
- "a",
2301
- {
2302
- href: child.href,
2303
- 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"}`,
2304
- "hx-get": child.href,
2305
- "hx-push-url": "true",
2306
- children: [
2307
- child.icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mr-2", children: child.icon }),
2308
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "whitespace-nowrap overflow-hidden text-ellipsis", children: child.label })
2309
- ]
2310
- }
2311
- )
2312
- },
2313
- child.href
2314
- );
2315
- }) })
2316
- ] }, item.href);
2317
- }
2318
- var BaseLayout = (props) => {
2319
- return /* @__PURE__ */ jsxRuntime.jsxs("html", { children: [
2320
- /* @__PURE__ */ jsxRuntime.jsxs("head", { children: [
2321
- /* @__PURE__ */ jsxRuntime.jsx("meta", { charset: "UTF-8" }),
2322
- /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
2323
- /* @__PURE__ */ jsxRuntime.jsx("title", { children: props.title }),
2324
- props.description && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "description", content: props.description }),
2325
- /* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://unpkg.com/htmx.org@latest" }),
2326
- /* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://unpkg.com/hyperscript.org@latest" }),
2327
- /* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.tailwindcss.com" }),
2328
- /* @__PURE__ */ jsxRuntime.jsx(
2329
- "style",
2330
- {
2331
- dangerouslySetInnerHTML: {
2332
- __html: `
2333
- @keyframes fadeIn {
2334
- from { opacity: 0;}
2335
- to { opacity: 1;}
2336
- }
2337
-
2338
- @keyframes slideIn {
2339
- from { opacity: 0; transform: scale(0.95) translateY(-10px); }
2340
- to { opacity: 1; transform: scale(1) translateY(0); }
2341
- }
2342
-
2343
- @keyframes slideInRight {
2344
- from { opacity: 0; transform: translateX(100%); }
2345
- to { opacity: 1; transform: translateX(0); }
2346
- }
2347
-
2348
- @keyframes slideOutRight {
2349
- from { opacity: 1; transform: translateX(0); }
2350
- to { opacity: 0; transform: translateX(100%); }
2351
- }
2352
-
2353
- @keyframes fadeOut {
2354
- from { opacity: 1; }
2355
- to { opacity: 0; }
2356
- }
2357
-
2358
- @keyframes scaleOut {
2359
- from { opacity: 1; transform: scale(1) translateY(0); }
2360
- to { opacity: 0; transform: scale(0.95) translateY(-10px); }
2361
- }
2362
-
2363
- /* Dialog \u9000\u51FA\u52A8\u753B */
2364
- .dialog-exit {
2365
- animation: fadeOut 0.2s ease-in forwards !important;
2366
- }
2367
-
2368
- .dialog-content-exit {
2369
- animation: scaleOut 0.2s ease-in forwards !important;
2370
- }
2371
-
2372
- /* ErrorAlert \u9000\u51FA\u52A8\u753B */
2373
- .error-alert-exit {
2374
- animation: slideOutRight 0.3s ease-in forwards, fadeOut 0.3s ease-in forwards;
2375
- }
2376
- `
2803
+ strokeWidth: 2,
2804
+ d: "M6 18L18 6M6 6l12 12"
2805
+ }
2806
+ )
2807
+ }
2808
+ )
2377
2809
  }
2378
- }
2379
- )
2380
- ] }),
2381
- /* @__PURE__ */ jsxRuntime.jsxs(
2382
- "body",
2383
- {
2384
- className: "bg-gray-50",
2385
- "hx-indicator": "#loading-bar",
2386
- "hx-target": "#main-content",
2387
- "hx-swap": "outerHTML",
2388
- children: [
2389
- /* @__PURE__ */ jsxRuntime.jsx(LoadingBar, {}),
2390
- props.children,
2391
- /* @__PURE__ */ jsxRuntime.jsx(
2392
- "div",
2393
- {
2394
- id: "error-container",
2395
- className: "fixed top-4 right-4 z-[200] w-full max-w-2xl px-4"
2396
- }
2397
- ),
2398
- /* @__PURE__ */ jsxRuntime.jsx("div", { id: "dialog-container" })
2399
- ]
2400
- }
2401
- )
2402
- ] });
2403
- };
2404
- var AdminLayout = (props) => {
2405
- const logo = props.adminContext.pluginOptions.logo;
2406
- const navItems = props.adminContext.pluginOptions.navigation;
2407
- const referer = props.adminContext.ctx.req.header("Referer");
2408
- let currentPath = props.adminContext.ctx.req.path;
2409
- if (referer) {
2410
- try {
2411
- const refererUrl = new URL(referer);
2412
- const method = props.adminContext.ctx.req.method;
2413
- if (["POST", "PUT", "DELETE"].includes(method)) {
2414
- currentPath = refererUrl.pathname;
2415
- }
2416
- } catch (e) {
2810
+ )
2811
+ ] })
2417
2812
  }
2418
- }
2419
- const sidebarCollapsed = props.sidebarCollapsed || false;
2420
- const breadcrumbs = props.adminContext.breadcrumbs;
2421
- const userInfo = props.adminContext.userInfo;
2422
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-screen", id: "main-content", children: [
2423
- /* @__PURE__ */ jsxRuntime.jsx(
2424
- "aside",
2425
- {
2426
- 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`,
2427
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${props.sidebarCollapsed ? "p-2" : "p-6"}`, children: [
2428
- !props.sidebarCollapsed && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: logo ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: logo, alt: "Logo", className: "h-10 mb-6" }) : /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-xl font-bold mb-6 text-white whitespace-nowrap overflow-hidden text-ellipsis", children: props.adminContext.pluginOptions.title }) }),
2429
- /* @__PURE__ */ jsxRuntime.jsx("nav", { children: /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "space-y-1", children: navItems && navItems.length > 0 ? navItems.map(
2430
- (item) => renderNavItem(
2431
- item,
2432
- currentPath,
2433
- props.sidebarCollapsed || false
2434
- )
2435
- ) : /* @__PURE__ */ jsxRuntime.jsx(
2436
- "li",
2437
- {
2438
- className: `${props.sidebarCollapsed ? "px-2" : "px-4"} py-2 text-gray-400 text-sm`,
2439
- children: "\u6682\u65E0\u5BFC\u822A\u9879"
2440
- }
2441
- ) }) })
2442
- ] })
2443
- }
2444
- ),
2445
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col overflow-hidden", children: [
2446
- /* @__PURE__ */ jsxRuntime.jsx(
2447
- Header,
2813
+ );
2814
+ };
2815
+
2816
+ // src/handler.tsx
2817
+ init_Layout();
2818
+ var Dialog = (props) => {
2819
+ const {
2820
+ title,
2821
+ children,
2822
+ showClose = true,
2823
+ closeUrl,
2824
+ className = "",
2825
+ size = "lg"
2826
+ } = props;
2827
+ const sizeClasses = {
2828
+ sm: "max-w-md",
2829
+ md: "max-w-lg",
2830
+ lg: "max-w-2xl",
2831
+ xl: "max-w-4xl",
2832
+ full: "max-w-7xl"
2833
+ };
2834
+ return /* @__PURE__ */ jsxRuntime.jsx(
2835
+ "div",
2836
+ {
2837
+ className: "fixed inset-0 bg-black bg-opacity-50 z-[100] flex items-center justify-center p-4 dialog-backdrop",
2838
+ style: {
2839
+ animation: "fadeIn 0.2s ease-out"
2840
+ },
2841
+ _: "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",
2842
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
2843
+ "div",
2448
2844
  {
2449
- breadcrumbs,
2450
- userInfo,
2451
- sidebarCollapsed: sidebarCollapsed || false
2845
+ className: `bg-gray-50 rounded-lg shadow-xl ${sizeClasses[size]} w-full max-h-[90vh] overflow-hidden flex flex-col dialog-content ${className}`,
2846
+ style: {
2847
+ animation: "slideIn 0.3s ease-out"
2848
+ },
2849
+ _: "on click call event.stopPropagation()",
2850
+ children: [
2851
+ (title || showClose) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 border-b border-gray-200 bg-white flex items-center justify-between", children: [
2852
+ title && /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title }),
2853
+ showClose && /* @__PURE__ */ jsxRuntime.jsx(
2854
+ "button",
2855
+ {
2856
+ className: "text-gray-400 hover:text-gray-600 transition-colors",
2857
+ _: "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",
2858
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2859
+ "svg",
2860
+ {
2861
+ className: "w-6 h-6",
2862
+ fill: "none",
2863
+ stroke: "currentColor",
2864
+ viewBox: "0 0 24 24",
2865
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2866
+ "path",
2867
+ {
2868
+ strokeLinecap: "round",
2869
+ strokeLinejoin: "round",
2870
+ strokeWidth: 2,
2871
+ d: "M6 18L18 6M6 6l12 12"
2872
+ }
2873
+ )
2874
+ }
2875
+ )
2876
+ }
2877
+ )
2878
+ ] }),
2879
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 overflow-y-auto p-6 bg-gray-50", children })
2880
+ ]
2452
2881
  }
2453
- ),
2454
- /* @__PURE__ */ jsxRuntime.jsx("main", { className: "flex-1 overflow-auto bg-gray-50", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6", children: props.children }) })
2455
- ] })
2456
- ] });
2882
+ )
2883
+ }
2884
+ );
2457
2885
  };
2458
2886
 
2459
- // src/utils/context.tsx
2460
- var HtmxAdminContext = class {
2461
- /** 模块元数据 */
2462
- moduleMetadata;
2463
- /** 插件选项 */
2464
- pluginOptions;
2465
- /** Hono Context */
2466
- ctx;
2467
- /** 之前的模块名 */
2468
- previousModuleName;
2469
- /** 是否是片段请求(HTMX 请求) */
2470
- isFragment;
2471
- /** 是否是对话框请求 */
2472
- isDialog;
2473
- /** 用户信息 */
2474
- userInfo;
2475
- /** 通知队列 */
2476
- notifications = [];
2477
- /** 页面标题(用于 HX-Title 和页面展示) */
2478
- title = "";
2479
- /** 页面描述(用于SEO和页面展示) */
2480
- description = "";
2481
- /** 面包屑项 */
2482
- breadcrumbs = [];
2483
- /** 主要内容 */
2484
- content = null;
2485
- /** 需要重定向的 URL */
2486
- redirectUrl;
2487
- /** 是否需要刷新页面(用于 HX-Refresh) */
2488
- refresh = false;
2489
- constructor(ctx, userInfo, moduleMetadata, pluginOptions) {
2490
- this.ctx = ctx;
2491
- this.userInfo = userInfo;
2492
- this.moduleMetadata = moduleMetadata;
2493
- this.pluginOptions = pluginOptions;
2494
- const url = new URL(ctx.req.url);
2495
- this.isFragment = ctx.req.header("HX-Request") === "true";
2496
- this.isDialog = url.searchParams.get("dialog") === "true" || ctx.req.header("HX-Target") === "#dialog-container" || (ctx.req.header("Referer") || "").includes("dialog=true");
2497
- const referer = ctx.req.header("Referer");
2498
- this.previousModuleName = referer ? new URL(referer).pathname.replace(pluginOptions.prefix, "").split("/").pop() : void 0;
2887
+ // src/handler.tsx
2888
+ init_context();
2889
+
2890
+ // src/utils/permissions.ts
2891
+ function checkUserPermission(requiredPermission, userPermissions) {
2892
+ if (!requiredPermission) {
2893
+ return { allowed: true, reason: "\u65E0\u9700\u6743\u9650" };
2499
2894
  }
2500
- /**
2501
- * 发送通知
2502
- * 通知将在响应时通过 OOB 更新到错误容器
2503
- */
2504
- sendNotification(type, title, message) {
2505
- this.notifications.push({ type, title, message });
2895
+ if (!userPermissions || userPermissions.length === 0) {
2896
+ return {
2897
+ allowed: false,
2898
+ reason: "\u7528\u6237\u65E0\u4EFB\u4F55\u6743\u9650",
2899
+ matchedPermission: "none"
2900
+ };
2506
2901
  }
2507
- /**
2508
- * 发送错误通知(便捷方法)
2509
- */
2510
- sendError(title, message) {
2511
- this.sendNotification("error", title, message);
2902
+ const denyResult = checkDenyPermissions(requiredPermission, userPermissions);
2903
+ if (!denyResult.allowed) {
2904
+ return denyResult;
2512
2905
  }
2513
- /**
2514
- * 发送警告通知(便捷方法)
2515
- */
2516
- sendWarning(title, message) {
2517
- this.sendNotification("warning", title, message);
2906
+ const allowResult = checkAllowPermissions(
2907
+ requiredPermission,
2908
+ userPermissions
2909
+ );
2910
+ return allowResult;
2911
+ }
2912
+ function checkDenyPermissions(requiredPermission, userPermissions) {
2913
+ const denyPermissions = userPermissions.filter((p) => p.startsWith("deny:"));
2914
+ for (const denyPermission of denyPermissions) {
2915
+ const denyPattern = denyPermission.substring(5);
2916
+ if (matchPermission(requiredPermission, denyPattern)) {
2917
+ return {
2918
+ allowed: false,
2919
+ reason: `\u6743\u9650\u88AB\u7981\u6B62: ${denyPermission}`,
2920
+ matchedPermission: denyPermission
2921
+ };
2922
+ }
2923
+ }
2924
+ return { allowed: true };
2925
+ }
2926
+ function checkAllowPermissions(requiredPermission, userPermissions) {
2927
+ const allowPermissions = userPermissions.filter(
2928
+ (p) => !p.startsWith("deny:")
2929
+ );
2930
+ if (allowPermissions.length === 0) {
2931
+ return {
2932
+ allowed: false,
2933
+ reason: "\u7528\u6237\u65E0\u5141\u8BB8\u6743\u9650",
2934
+ matchedPermission: "none"
2935
+ };
2518
2936
  }
2519
- /**
2520
- * 发送信息通知(便捷方法)
2521
- */
2522
- sendInfo(title, message) {
2523
- this.sendNotification("info", title, message);
2937
+ for (const allowPermission of allowPermissions) {
2938
+ const allowPattern = allowPermission.startsWith("allow:") ? allowPermission.substring(6) : allowPermission;
2939
+ if (matchPermission(requiredPermission, allowPattern)) {
2940
+ return {
2941
+ allowed: true,
2942
+ reason: `\u6743\u9650\u5339\u914D: ${allowPermission}`,
2943
+ matchedPermission: allowPermission
2944
+ };
2945
+ }
2524
2946
  }
2525
- /**
2526
- * 发送成功通知(便捷方法)
2527
- */
2528
- sendSuccess(title, message) {
2529
- this.sendNotification("success", title, message);
2947
+ return {
2948
+ allowed: false,
2949
+ reason: "\u65E0\u5339\u914D\u7684\u5141\u8BB8\u6743\u9650",
2950
+ matchedPermission: "none"
2951
+ };
2952
+ }
2953
+ function matchPermission(requiredPermission, pattern) {
2954
+ const required = requiredPermission.startsWith("allow:") ? requiredPermission.substring(6) : requiredPermission;
2955
+ if (pattern.includes("*")) {
2956
+ return matchWithWildcard(required, pattern);
2530
2957
  }
2531
- /**
2532
- * 设置需要推送的 URL
2533
- * 用于 HX-Push-Url 响应头
2534
- */
2535
- redirect(url) {
2536
- this.redirectUrl = url;
2958
+ return required === pattern;
2959
+ }
2960
+ function matchWithWildcard(required, pattern) {
2961
+ const requiredParts = required.split(".");
2962
+ const patternParts = pattern.split(".");
2963
+ if (requiredParts.length !== patternParts.length && !pattern.endsWith("*")) {
2964
+ return false;
2537
2965
  }
2538
- /**
2539
- * 设置需要刷新页面
2540
- * 用于 HX-Refresh 响应头
2541
- */
2542
- setRefresh(refresh = true) {
2543
- this.refresh = refresh;
2966
+ const maxLength = Math.max(requiredParts.length, patternParts.length);
2967
+ for (let i = 0; i < maxLength; i++) {
2968
+ const requiredPart = requiredParts[i];
2969
+ const patternPart = patternParts[i];
2970
+ if (patternPart === "*") {
2971
+ continue;
2972
+ }
2973
+ if (!patternPart && pattern.endsWith("*")) {
2974
+ return true;
2975
+ }
2976
+ if (!requiredPart) {
2977
+ return false;
2978
+ }
2979
+ if (requiredPart !== patternPart) {
2980
+ return false;
2981
+ }
2544
2982
  }
2545
- /**
2546
- * 检查是否有列表页面
2547
- * 封装 moduleMetadata 访问,避免直接访问内部属性
2548
- */
2549
- hasList() {
2550
- return this.moduleMetadata.hasList;
2983
+ return true;
2984
+ }
2985
+
2986
+ // src/utils/auth.ts
2987
+ async function getUserInfo(ctx, authProvider) {
2988
+ if (!authProvider) {
2989
+ return null;
2551
2990
  }
2552
- /**
2553
- * 检查是否有详情页面
2554
- * 封装 moduleMetadata 访问,避免直接访问内部属性
2555
- */
2556
- hasDetail() {
2557
- return this.moduleMetadata.hasDetail;
2991
+ const cookieKey = authProvider.cookieKey || "auth_token";
2992
+ const token = cookie.getCookie(ctx, cookieKey);
2993
+ if (!token) {
2994
+ return null;
2558
2995
  }
2559
- /**
2560
- * 检查是否有表单页面
2561
- * 封装 moduleMetadata 访问,避免直接访问内部属性
2562
- */
2563
- hasForm() {
2564
- return this.moduleMetadata.hasForm;
2996
+ return await authProvider.tokenToUser(token, ctx);
2997
+ }
2998
+ function checkPermissionDefault(userInfo, operation, loginUrl) {
2999
+ if (!operation) {
3000
+ return { allowed: true };
2565
3001
  }
2566
- /**
2567
- * 检查是否有自定义页面
2568
- * 封装 moduleMetadata 访问,避免直接访问内部属性
2569
- */
2570
- hasCustom() {
2571
- return this.moduleMetadata.hasCustom;
3002
+ if (!userInfo) {
3003
+ return {
3004
+ allowed: false,
3005
+ message: "\u672A\u767B\u5F55\uFF0C\u65E0\u6CD5\u8BBF\u95EE\u6B64\u8D44\u6E90",
3006
+ operationId: operation,
3007
+ redirectUrl: loginUrl
3008
+ // 如果提供了 loginUrl,则重定向到登录页面
3009
+ };
2572
3010
  }
2573
- };
3011
+ const userPermissions = userInfo.permissions || [];
3012
+ const result = checkUserPermission(operation, userPermissions);
3013
+ if (result.allowed) {
3014
+ return { allowed: true };
3015
+ } else {
3016
+ return {
3017
+ allowed: false,
3018
+ message: result.reason || "\u60A8\u6CA1\u6709\u6743\u9650\u8BBF\u95EE\u6B64\u8D44\u6E90",
3019
+ operationId: operation
3020
+ };
3021
+ }
3022
+ }
3023
+
3024
+ // src/utils/operation.ts
3025
+ function generateOperationId(ctx, moduleName, moduleOptions, serviceName) {
3026
+ const method = ctx.req.method.toLowerCase();
3027
+ const url = new URL(ctx.req.url);
3028
+ const path = url.pathname;
3029
+ const lowerModuleName = moduleName.toLowerCase();
3030
+ const servicePrefix = serviceName ? `${serviceName}.` : "";
3031
+ if (moduleOptions.type === "list") {
3032
+ return `${servicePrefix}${lowerModuleName}.read`;
3033
+ } else if (moduleOptions.type === "detail") {
3034
+ if (method === "delete") {
3035
+ return `${servicePrefix}${lowerModuleName}.delete`;
3036
+ }
3037
+ return `${servicePrefix}${lowerModuleName}.read`;
3038
+ } else if (moduleOptions.type === "form") {
3039
+ if (path.includes("/new")) {
3040
+ if (method === "get") {
3041
+ return `${servicePrefix}${lowerModuleName}.create`;
3042
+ }
3043
+ return `${servicePrefix}${lowerModuleName}.create`;
3044
+ } else if (path.includes("/edit/")) {
3045
+ if (method === "get") {
3046
+ return `${servicePrefix}${lowerModuleName}.edit`;
3047
+ }
3048
+ return `${servicePrefix}${lowerModuleName}.edit`;
3049
+ } else if (method === "post") {
3050
+ return `${servicePrefix}${lowerModuleName}.create`;
3051
+ } else if (method === "put") {
3052
+ return `${servicePrefix}${lowerModuleName}.edit`;
3053
+ } else if (method === "delete") {
3054
+ return `${servicePrefix}${lowerModuleName}.delete`;
3055
+ }
3056
+ return `${servicePrefix}${lowerModuleName}.read`;
3057
+ } else if (moduleOptions.type === "custom") {
3058
+ if (method === "post") {
3059
+ return `${servicePrefix}${lowerModuleName}.create`;
3060
+ } else if (method === "put") {
3061
+ return `${servicePrefix}${lowerModuleName}.edit`;
3062
+ } else if (method === "delete") {
3063
+ return `${servicePrefix}${lowerModuleName}.delete`;
3064
+ }
3065
+ return `${servicePrefix}${lowerModuleName}.read`;
3066
+ }
3067
+ return `${servicePrefix}${lowerModuleName}.read`;
3068
+ }
3069
+
3070
+ // src/utils/permission-handler.tsx
3071
+ init_PermissionDenied();
3072
+ async function handlePermissionDenied(ctx, adminContext, permissionResult, getOperationId, pluginPrefix, getRegisteredOperations) {
3073
+ const operationId = permissionResult.operationId || getOperationId();
3074
+ const fromPath = ctx.req.path;
3075
+ if (adminContext.isFragment) {
3076
+ return renderPermissionDeniedDialog(
3077
+ ctx,
3078
+ adminContext,
3079
+ operationId,
3080
+ fromPath,
3081
+ getRegisteredOperations
3082
+ );
3083
+ } else {
3084
+ if (permissionResult.redirectUrl) {
3085
+ return ctx.redirect(permissionResult.redirectUrl);
3086
+ }
3087
+ return redirectToPermissionDeniedPage(
3088
+ ctx,
3089
+ operationId,
3090
+ fromPath,
3091
+ pluginPrefix,
3092
+ permissionResult.redirectUrl
3093
+ );
3094
+ }
3095
+ }
3096
+ function renderPermissionDeniedDialog(ctx, adminContext, operationId, fromPath, getRegisteredOperations) {
3097
+ const registeredOperations = getRegisteredOperations ? getRegisteredOperations() : [];
3098
+ const permissionContent = PermissionDeniedContent(
3099
+ adminContext,
3100
+ operationId,
3101
+ fromPath,
3102
+ registeredOperations
3103
+ );
3104
+ return ctx.html(
3105
+ /* @__PURE__ */ jsxRuntime.jsx(Dialog, { title: "\u6743\u9650\u4E0D\u8DB3", size: "lg", children: permissionContent }),
3106
+ 200,
3107
+ {
3108
+ "HX-Retarget": "#dialog-container",
3109
+ "HX-Reswap": "innerHTML"
3110
+ }
3111
+ );
3112
+ }
3113
+ function redirectToPermissionDeniedPage(ctx, operationId, fromPath, pluginPrefix, customRedirectUrl) {
3114
+ const permissionDeniedUrl = `${pluginPrefix}/permission-denied`;
3115
+ const url = new URL(permissionDeniedUrl, ctx.req.url);
3116
+ url.searchParams.set("operation", operationId);
3117
+ url.searchParams.set("from", fromPath);
3118
+ const redirectUrl = url.pathname + url.search;
3119
+ const finalRedirectUrl = customRedirectUrl || redirectUrl;
3120
+ return ctx.redirect(finalRedirectUrl);
3121
+ }
2574
3122
  var RouteHandler = class {
2575
3123
  moduleClass;
2576
3124
  pluginOptions;
2577
3125
  moduleMetadata;
2578
3126
  moduleOptions;
3127
+ moduleName;
3128
+ basePath;
3129
+ serviceName;
3130
+ getRegisteredOperations;
2579
3131
  constructor(options) {
2580
3132
  this.moduleClass = options.moduleClass;
2581
3133
  this.pluginOptions = options.pluginOptions;
2582
3134
  this.moduleMetadata = options.moduleMetadata;
2583
3135
  this.moduleOptions = options.moduleOptions;
3136
+ this.moduleName = options.moduleName;
3137
+ this.basePath = options.basePath;
3138
+ this.serviceName = options.serviceName;
3139
+ this.getRegisteredOperations = options.getRegisteredOperations;
2584
3140
  }
2585
3141
  /**
2586
3142
  * 更新模块元数据
@@ -2593,10 +3149,41 @@ var RouteHandler = class {
2593
3149
  */
2594
3150
  async handle(ctx) {
2595
3151
  try {
2596
- const userInfo = await this.pluginOptions.getUserInfo(ctx);
3152
+ const userInfo = await getUserInfo(ctx, this.pluginOptions.authProvider);
2597
3153
  const adminContext = this.createAdminContext(ctx, userInfo);
2598
3154
  const moduleInstance = new this.moduleClass();
2599
3155
  moduleInstance.__init(adminContext);
3156
+ const requiredPermission = moduleInstance.getRequiredPermission?.(ctx);
3157
+ const operation = this.getOperationId(ctx);
3158
+ if (this.pluginOptions.authProvider && requiredPermission !== void 0 && requiredPermission !== null && requiredPermission !== "") {
3159
+ let permissionResult;
3160
+ if (this.pluginOptions.authProvider.checkPermission) {
3161
+ permissionResult = await this.pluginOptions.authProvider.checkPermission(
3162
+ userInfo,
3163
+ operation,
3164
+ adminContext
3165
+ );
3166
+ } else {
3167
+ permissionResult = checkPermissionDefault(
3168
+ userInfo,
3169
+ operation,
3170
+ this.pluginOptions.authProvider?.loginUrl
3171
+ );
3172
+ }
3173
+ if (!permissionResult.allowed) {
3174
+ if (!permissionResult.operationId) {
3175
+ permissionResult.operationId = operation;
3176
+ }
3177
+ return await handlePermissionDenied(
3178
+ ctx,
3179
+ adminContext,
3180
+ permissionResult,
3181
+ () => this.getOperationId(ctx),
3182
+ this.pluginOptions.prefix,
3183
+ this.getRegisteredOperations
3184
+ );
3185
+ }
3186
+ }
2600
3187
  try {
2601
3188
  adminContext.content = await moduleInstance.__handle();
2602
3189
  adminContext.title = moduleInstance.getTitle();
@@ -2620,6 +3207,17 @@ var RouteHandler = class {
2620
3207
  );
2621
3208
  }
2622
3209
  }
3210
+ /**
3211
+ * 获取操作ID(内部方法,用于传递给工具函数)
3212
+ */
3213
+ getOperationId(ctx) {
3214
+ return generateOperationId(
3215
+ ctx,
3216
+ this.moduleName,
3217
+ this.moduleOptions,
3218
+ this.serviceName
3219
+ );
3220
+ }
2623
3221
  renderFullPage(ctx, adminContext) {
2624
3222
  if (adminContext.redirectUrl) {
2625
3223
  return ctx.redirect(adminContext.redirectUrl);
@@ -2725,7 +3323,8 @@ var RouteHandler = class {
2725
3323
  ctx,
2726
3324
  userInfo,
2727
3325
  this.moduleMetadata,
2728
- this.pluginOptions
3326
+ this.pluginOptions,
3327
+ this.serviceName
2729
3328
  );
2730
3329
  }
2731
3330
  /**
@@ -2818,8 +3417,12 @@ var HtmxAdminPlugin = class {
2818
3417
  engine;
2819
3418
  hono;
2820
3419
  options;
3420
+ // 服务名(从引擎中获取)
3421
+ serviceName = "";
2821
3422
  // 模块类映射(记录每个模块的类)
2822
3423
  moduleTypeMap = /* @__PURE__ */ new Map();
3424
+ // 注册的操作列表(权限列表)
3425
+ registeredOperations = [];
2823
3426
  constructor(options) {
2824
3427
  this.options = {
2825
3428
  title: options?.title || "\u7BA1\u7406\u540E\u53F0",
@@ -2827,7 +3430,7 @@ var HtmxAdminPlugin = class {
2827
3430
  prefix: options?.prefix || "/admin",
2828
3431
  homePath: options?.homePath || "",
2829
3432
  navigation: options?.navigation ?? [],
2830
- getUserInfo: options?.getUserInfo ?? (() => null)
3433
+ authProvider: options?.authProvider
2831
3434
  };
2832
3435
  }
2833
3436
  /**
@@ -2850,7 +3453,8 @@ var HtmxAdminPlugin = class {
2850
3453
  onInit(engine) {
2851
3454
  this.engine = engine;
2852
3455
  this.hono = engine.getHono();
2853
- imeanServiceEngine.logger.info("HtmxAdminPlugin initialized");
3456
+ this.serviceName = engine.options.name;
3457
+ imeanServiceEngine.logger.info(`HtmxAdminPlugin initialized${this.serviceName ? ` (service: ${this.serviceName})` : ""}`);
2854
3458
  }
2855
3459
  /**
2856
3460
  * 检查并注册模块
@@ -2911,10 +3515,144 @@ var HtmxAdminPlugin = class {
2911
3515
  }
2912
3516
  }
2913
3517
  }
3518
+ /**
3519
+ * 根据模块类型和路由信息生成操作ID
3520
+ */
3521
+ generateOperationId(moduleName, moduleType, method, path) {
3522
+ const lowerModuleName = moduleName.toLowerCase();
3523
+ const lowerMethod = method.toLowerCase();
3524
+ const servicePrefix = this.serviceName ? `${this.serviceName}.` : "";
3525
+ if (moduleType === "list") {
3526
+ return {
3527
+ operationId: `${servicePrefix}${lowerModuleName}.read`,
3528
+ operationType: "read"
3529
+ };
3530
+ } else if (moduleType === "detail") {
3531
+ if (lowerMethod === "delete") {
3532
+ return {
3533
+ operationId: `${servicePrefix}${lowerModuleName}.delete`,
3534
+ operationType: "delete"
3535
+ };
3536
+ }
3537
+ return {
3538
+ operationId: `${servicePrefix}${lowerModuleName}.read`,
3539
+ operationType: "read"
3540
+ };
3541
+ } else if (moduleType === "form") {
3542
+ if (path.includes("/new")) {
3543
+ if (lowerMethod === "get") {
3544
+ return {
3545
+ operationId: `${servicePrefix}${lowerModuleName}.create`,
3546
+ operationType: "create"
3547
+ };
3548
+ }
3549
+ return {
3550
+ operationId: `${servicePrefix}${lowerModuleName}.create`,
3551
+ operationType: "create"
3552
+ };
3553
+ } else if (path.includes("/edit/")) {
3554
+ if (lowerMethod === "get") {
3555
+ return {
3556
+ operationId: `${servicePrefix}${lowerModuleName}.edit`,
3557
+ operationType: "edit"
3558
+ };
3559
+ }
3560
+ return {
3561
+ operationId: `${servicePrefix}${lowerModuleName}.edit`,
3562
+ operationType: "edit"
3563
+ };
3564
+ } else if (lowerMethod === "post") {
3565
+ return {
3566
+ operationId: `${servicePrefix}${lowerModuleName}.create`,
3567
+ operationType: "create"
3568
+ };
3569
+ } else if (lowerMethod === "put") {
3570
+ return {
3571
+ operationId: `${servicePrefix}${lowerModuleName}.edit`,
3572
+ operationType: "edit"
3573
+ };
3574
+ } else if (lowerMethod === "delete") {
3575
+ return {
3576
+ operationId: `${servicePrefix}${lowerModuleName}.delete`,
3577
+ operationType: "delete"
3578
+ };
3579
+ }
3580
+ return {
3581
+ operationId: `${servicePrefix}${lowerModuleName}.read`,
3582
+ operationType: "read"
3583
+ };
3584
+ } else if (moduleType === "custom") {
3585
+ if (lowerMethod === "post") {
3586
+ return {
3587
+ operationId: `${servicePrefix}${lowerModuleName}.create`,
3588
+ operationType: "create"
3589
+ };
3590
+ } else if (lowerMethod === "put") {
3591
+ return {
3592
+ operationId: `${servicePrefix}${lowerModuleName}.edit`,
3593
+ operationType: "edit"
3594
+ };
3595
+ } else if (lowerMethod === "delete") {
3596
+ return {
3597
+ operationId: `${servicePrefix}${lowerModuleName}.delete`,
3598
+ operationType: "delete"
3599
+ };
3600
+ }
3601
+ return {
3602
+ operationId: `${servicePrefix}${lowerModuleName}.read`,
3603
+ operationType: "read"
3604
+ };
3605
+ }
3606
+ return {
3607
+ operationId: `${servicePrefix}${lowerModuleName}.read`,
3608
+ operationType: "read"
3609
+ };
3610
+ }
3611
+ /**
3612
+ * 注册内置路由(权限提示页面)
3613
+ */
3614
+ registerBuiltinRoutes() {
3615
+ const permissionDeniedPath = `${this.options.prefix}/permission-denied`;
3616
+ this.hono.get(permissionDeniedPath, async (ctx) => {
3617
+ const url = new URL(ctx.req.url);
3618
+ const operationId = url.searchParams.get("operation") || "";
3619
+ const fromPath = url.searchParams.get("from") || "";
3620
+ const { PermissionDeniedPage: PermissionDeniedPage2 } = await Promise.resolve().then(() => (init_PermissionDenied(), PermissionDenied_exports));
3621
+ const { HtmxAdminContext: HtmxAdminContext2 } = await Promise.resolve().then(() => (init_context(), context_exports));
3622
+ const adminContext = new HtmxAdminContext2(
3623
+ ctx,
3624
+ null,
3625
+ // 不需要用户信息
3626
+ {
3627
+ hasList: false,
3628
+ hasDetail: false,
3629
+ hasForm: false,
3630
+ hasCustom: true,
3631
+ basePath: permissionDeniedPath,
3632
+ title: "\u6743\u9650\u4E0D\u8DB3",
3633
+ description: "\u60A8\u6CA1\u6709\u6743\u9650\u8BBF\u95EE\u6B64\u8D44\u6E90"
3634
+ },
3635
+ this.options,
3636
+ this.serviceName
3637
+ );
3638
+ return ctx.html(
3639
+ PermissionDeniedPage2(
3640
+ adminContext,
3641
+ operationId,
3642
+ fromPath,
3643
+ this.registeredOperations
3644
+ )
3645
+ );
3646
+ });
3647
+ imeanServiceEngine.logger.info(
3648
+ `[HtmxAdminPlugin] Registered builtin route: GET ${permissionDeniedPath}`
3649
+ );
3650
+ }
2914
3651
  /**
2915
3652
  * 注册路由
2916
3653
  */
2917
3654
  registerRoutes() {
3655
+ this.registerBuiltinRoutes();
2918
3656
  const moduleNames = Array.from(this.moduleTypeMap.keys());
2919
3657
  for (const moduleName of moduleNames) {
2920
3658
  const moduleTypes = this.moduleTypeMap.get(moduleName);
@@ -2927,6 +3665,8 @@ var HtmxAdminPlugin = class {
2927
3665
  basePath,
2928
3666
  moduleOptions: moduleInfo.options,
2929
3667
  pluginOptions: this.options,
3668
+ serviceName: this.serviceName,
3669
+ // 传递服务名
2930
3670
  moduleMetadata: {
2931
3671
  hasList: !!moduleTypes["list"],
2932
3672
  hasDetail: !!moduleTypes["detail"],
@@ -2935,7 +3675,9 @@ var HtmxAdminPlugin = class {
2935
3675
  basePath,
2936
3676
  title: moduleInfo.options.title ?? moduleName,
2937
3677
  description: moduleInfo.options.description ?? ""
2938
- }
3678
+ },
3679
+ getRegisteredOperations: () => this.registeredOperations
3680
+ // 传递获取操作列表的方法
2939
3681
  });
2940
3682
  const routeConfig = {
2941
3683
  list: [{ method: "get", path: `${basePath}/list` }],
@@ -2947,24 +3689,87 @@ var HtmxAdminPlugin = class {
2947
3689
  { method: "put", path: `${basePath}/:id` },
2948
3690
  { method: "delete", path: `${basePath}/:id` }
2949
3691
  ],
2950
- custom: [{ method: "get", path: basePath }]
3692
+ // custom 类型注册所有 HTTP 方法,允许模块在 render 方法中根据请求方法进行扩展
3693
+ custom: [
3694
+ { method: "get", path: basePath },
3695
+ { method: "post", path: basePath },
3696
+ { method: "put", path: basePath },
3697
+ { method: "delete", path: basePath }
3698
+ ]
2951
3699
  };
2952
3700
  const routes = routeConfig[type];
2953
3701
  for (const route of routes) {
2954
3702
  imeanServiceEngine.logger.info(
2955
3703
  `[HtmxAdminPlugin] Registering ${type} route: ${route.method.toUpperCase()} ${route.path}`
2956
3704
  );
2957
- const handler = async (ctx) => routeHandler.handle(ctx);
3705
+ const { operationId, operationType } = this.generateOperationId(
3706
+ moduleName,
3707
+ type,
3708
+ route.method,
3709
+ route.path
3710
+ );
3711
+ const existingOperation = this.registeredOperations.find(
3712
+ (op) => op.operationId === operationId && op.httpMethod === route.method.toUpperCase()
3713
+ );
3714
+ if (!existingOperation) {
3715
+ this.registeredOperations.push({
3716
+ operationId,
3717
+ moduleName: moduleName.toLowerCase(),
3718
+ operationType,
3719
+ moduleType: type,
3720
+ moduleTitle: moduleInfo.options.title,
3721
+ moduleDescription: moduleInfo.options.description,
3722
+ httpMethod: route.method.toUpperCase(),
3723
+ routePath: route.path
3724
+ });
3725
+ }
3726
+ const handler = async (ctx) => {
3727
+ return routeHandler.handle(ctx);
3728
+ };
2958
3729
  this.hono[route.method](route.path, handler);
2959
3730
  }
2960
3731
  }
2961
3732
  }
2962
3733
  }
3734
+ /**
3735
+ * 获取所有注册的操作(权限)列表
3736
+ * 可用于权限管理模块作为数据源,或用于确定管理员的权限
3737
+ *
3738
+ * @returns 所有注册的操作信息列表
3739
+ */
3740
+ getRegisteredOperations() {
3741
+ return [...this.registeredOperations];
3742
+ }
3743
+ /**
3744
+ * 获取指定模块的所有操作
3745
+ *
3746
+ * @param moduleName 模块名
3747
+ * @returns 该模块的所有操作信息列表
3748
+ */
3749
+ getModuleOperations(moduleName) {
3750
+ const lowerModuleName = moduleName.toLowerCase();
3751
+ return this.registeredOperations.filter(
3752
+ (op) => op.moduleName === lowerModuleName
3753
+ );
3754
+ }
3755
+ /**
3756
+ * 获取所有唯一的操作ID列表(去重)
3757
+ *
3758
+ * @returns 所有唯一的操作ID列表
3759
+ */
3760
+ getOperationIds() {
3761
+ const uniqueIds = /* @__PURE__ */ new Set();
3762
+ for (const op of this.registeredOperations) {
3763
+ uniqueIds.add(op.operationId);
3764
+ }
3765
+ return Array.from(uniqueIds).sort();
3766
+ }
2963
3767
  /**
2964
3768
  * 模块加载钩子:检查模块类型约束并注册路由
2965
3769
  */
2966
3770
  onModuleLoad(modules) {
2967
3771
  this.checkAndRegisterModules(modules);
3772
+ this.registerBuiltinRoutes();
2968
3773
  this.registerRoutes();
2969
3774
  this.engine.getHono().get(this.options.prefix, async (ctx) => {
2970
3775
  return ctx.redirect(this.options.homePath);
@@ -3049,6 +3854,10 @@ var Badge = (props) => {
3049
3854
  }
3050
3855
  );
3051
3856
  };
3857
+
3858
+ // src/components/index.ts
3859
+ init_Button();
3860
+ init_Card();
3052
3861
  var SystemStatusCard = (props) => {
3053
3862
  const { title, statusItems, className = "" } = props;
3054
3863
  const progressColorClasses = {