imean-service-engine-htmx-plugin 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,11 +1,688 @@
1
- import { jsx, jsxs, Fragment } from 'hono/jsx/jsx-runtime';
1
+ import { jsxs, jsx, Fragment } from 'hono/jsx/jsx-runtime';
2
2
  import { PluginPriority, logger } from 'imean-service-engine';
3
+ import { getCookie } from 'hono/cookie';
3
4
 
4
5
  var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __esm = (fn, res) => function __init() {
8
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
9
+ };
5
10
  var __export = (target, all) => {
6
11
  for (var name in all)
7
12
  __defProp(target, name, { get: all[name], enumerable: true });
8
13
  };
14
+ var Button;
15
+ var init_Button = __esm({
16
+ "src/components/Button.tsx"() {
17
+ Button = (props) => {
18
+ const {
19
+ children,
20
+ variant = "primary",
21
+ size = "md",
22
+ disabled = false,
23
+ className = "",
24
+ hxGet,
25
+ hxPost,
26
+ hxPut,
27
+ hxDelete,
28
+ hxTarget,
29
+ hxSwap,
30
+ hxPushUrl,
31
+ hxIndicator,
32
+ hxConfirm,
33
+ hxHeaders,
34
+ ...rest
35
+ } = props;
36
+ const baseClasses = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2";
37
+ const variantClasses = {
38
+ primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
39
+ secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500",
40
+ danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
41
+ ghost: "bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-500"
42
+ };
43
+ const sizeClasses = {
44
+ sm: "px-3 py-1.5 text-sm",
45
+ md: "px-4 py-2 text-sm",
46
+ lg: "px-6 py-3 text-base"
47
+ };
48
+ const classes = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${disabled ? "opacity-50 cursor-not-allowed" : ""} ${className}`;
49
+ const htmxAttrs = {};
50
+ if (hxGet) htmxAttrs["hx-get"] = hxGet;
51
+ if (hxPost) htmxAttrs["hx-post"] = hxPost;
52
+ if (hxPut) htmxAttrs["hx-put"] = hxPut;
53
+ if (hxDelete) htmxAttrs["hx-delete"] = hxDelete;
54
+ if (hxTarget) htmxAttrs["hx-target"] = hxTarget;
55
+ if (hxSwap) htmxAttrs["hx-swap"] = hxSwap;
56
+ if (hxPushUrl !== void 0)
57
+ htmxAttrs["hx-push-url"] = hxPushUrl === true ? "true" : hxPushUrl;
58
+ if (hxIndicator) htmxAttrs["hx-indicator"] = hxIndicator;
59
+ if (hxConfirm) htmxAttrs["hx-confirm"] = hxConfirm;
60
+ if (hxHeaders) htmxAttrs["hx-headers"] = hxHeaders;
61
+ const Tag = hxGet || hxPost || hxPut || hxDelete ? "a" : "button";
62
+ const href = rest.href ?? hxGet ?? "#";
63
+ return /* @__PURE__ */ jsx(
64
+ Tag,
65
+ {
66
+ className: classes,
67
+ disabled,
68
+ href,
69
+ ...htmxAttrs,
70
+ ...rest,
71
+ children
72
+ }
73
+ );
74
+ };
75
+ }
76
+ });
77
+ var Card;
78
+ var init_Card = __esm({
79
+ "src/components/Card.tsx"() {
80
+ Card = (props) => {
81
+ const {
82
+ children,
83
+ title,
84
+ className = "",
85
+ shadow = true,
86
+ bordered = false,
87
+ noPadding = false
88
+ } = props;
89
+ const baseClasses = "bg-white rounded-lg";
90
+ const shadowClass = shadow ? "shadow-sm hover:shadow-md transition-shadow" : "";
91
+ const borderClass = bordered ? "border border-gray-200" : "";
92
+ const paddingClass = noPadding ? "" : "p-6";
93
+ return /* @__PURE__ */ jsxs(
94
+ "div",
95
+ {
96
+ className: `${baseClasses} ${shadowClass} ${borderClass} ${className}`,
97
+ children: [
98
+ 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 }) }),
99
+ /* @__PURE__ */ jsx("div", { className: noPadding ? "" : paddingClass, children })
100
+ ]
101
+ }
102
+ );
103
+ };
104
+ }
105
+ });
106
+ var Breadcrumb;
107
+ var init_Breadcrumb = __esm({
108
+ "src/components/Breadcrumb.tsx"() {
109
+ Breadcrumb = (props) => {
110
+ const { items } = props;
111
+ if (items.length === 0) {
112
+ return null;
113
+ }
114
+ 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: [
115
+ index > 0 && /* @__PURE__ */ jsx(
116
+ "svg",
117
+ {
118
+ className: "w-5 h-5 text-gray-400 mx-2",
119
+ fill: "currentColor",
120
+ viewBox: "0 0 20 20",
121
+ children: /* @__PURE__ */ jsx(
122
+ "path",
123
+ {
124
+ fillRule: "evenodd",
125
+ 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",
126
+ clipRule: "evenodd"
127
+ }
128
+ )
129
+ }
130
+ ),
131
+ item.href ? /* @__PURE__ */ jsx(
132
+ "a",
133
+ {
134
+ href: item.href,
135
+ className: "text-sm font-medium text-gray-500 hover:text-gray-700",
136
+ "hx-get": item.href,
137
+ "hx-push-url": "true",
138
+ children: item.label
139
+ }
140
+ ) : /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-gray-900", children: item.label })
141
+ ] }, index)) }) });
142
+ };
143
+ }
144
+ });
145
+ var Header;
146
+ var init_Header = __esm({
147
+ "src/components/Header.tsx"() {
148
+ init_Breadcrumb();
149
+ Header = (props) => {
150
+ const { breadcrumbs = [], userInfo, sidebarCollapsed = false } = props;
151
+ 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: [
152
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4 flex-1 min-w-0", children: [
153
+ /* @__PURE__ */ jsx(
154
+ "a",
155
+ {
156
+ className: "p-2 rounded-lg hover:bg-gray-100 transition-colors inline-block flex-shrink-0",
157
+ title: sidebarCollapsed ? "\u5C55\u5F00\u4FA7\u8FB9\u680F" : "\u6298\u53E0\u4FA7\u8FB9\u680F",
158
+ "hx-get": "",
159
+ children: sidebarCollapsed ? (
160
+ // 展开图标(向右箭头)
161
+ /* @__PURE__ */ jsx(
162
+ "svg",
163
+ {
164
+ className: "w-5 h-5 text-gray-600",
165
+ fill: "none",
166
+ stroke: "currentColor",
167
+ viewBox: "0 0 24 24",
168
+ children: /* @__PURE__ */ jsx(
169
+ "path",
170
+ {
171
+ strokeLinecap: "round",
172
+ strokeLinejoin: "round",
173
+ strokeWidth: 2,
174
+ d: "M9 5l7 7-7 7"
175
+ }
176
+ )
177
+ }
178
+ )
179
+ ) : (
180
+ // 折叠图标(三条横线)
181
+ /* @__PURE__ */ jsx(
182
+ "svg",
183
+ {
184
+ className: "w-5 h-5 text-gray-600",
185
+ fill: "none",
186
+ stroke: "currentColor",
187
+ viewBox: "0 0 24 24",
188
+ children: /* @__PURE__ */ jsx(
189
+ "path",
190
+ {
191
+ strokeLinecap: "round",
192
+ strokeLinejoin: "round",
193
+ strokeWidth: 2,
194
+ d: "M4 6h16M4 12h16M4 18h16"
195
+ }
196
+ )
197
+ }
198
+ )
199
+ )
200
+ }
201
+ ),
202
+ breadcrumbs.length > 0 && /* @__PURE__ */ jsx(Breadcrumb, { items: breadcrumbs })
203
+ ] }),
204
+ userInfo && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 flex-shrink-0", children: [
205
+ /* @__PURE__ */ jsxs("div", { className: "text-right hidden sm:block", children: [
206
+ userInfo.name && /* @__PURE__ */ jsx("div", { className: "text-sm font-medium text-gray-900", children: userInfo.name }),
207
+ userInfo.email && /* @__PURE__ */ jsx("div", { className: "text-xs text-gray-500", children: userInfo.email })
208
+ ] }),
209
+ userInfo.avatar ? /* @__PURE__ */ jsx(
210
+ "img",
211
+ {
212
+ src: userInfo.avatar,
213
+ alt: userInfo.name || "\u7528\u6237",
214
+ className: "w-8 h-8 rounded-full"
215
+ }
216
+ ) : /* @__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() })
217
+ ] })
218
+ ] }) }) });
219
+ };
220
+ }
221
+ });
222
+ var LoadingBar;
223
+ var init_LoadingBar = __esm({
224
+ "src/components/LoadingBar.tsx"() {
225
+ LoadingBar = () => {
226
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
227
+ /* @__PURE__ */ jsx(
228
+ "div",
229
+ {
230
+ id: "loading-bar",
231
+ className: "fixed top-0 left-0 right-0 h-1 bg-transparent z-50 opacity-0 transition-opacity duration-200",
232
+ children: /* @__PURE__ */ jsx("div", { className: "h-full bg-gradient-to-r from-blue-500 via-blue-600 to-blue-500 loading-bar-progress" })
233
+ }
234
+ ),
235
+ /* @__PURE__ */ jsx("style", { children: `
236
+ @keyframes loading-progress {
237
+ 0% { transform: translateX(-100%); width: 0%; }
238
+ 50% { width: 70%; }
239
+ 100% { transform: translateX(100%); width: 100%; }
240
+ }
241
+ #loading-bar.htmx-request {
242
+ opacity: 1 !important;
243
+ }
244
+ #loading-bar.htmx-request .loading-bar-progress {
245
+ animation: loading-progress 1.5s ease-in-out infinite;
246
+ }
247
+ ` })
248
+ ] });
249
+ };
250
+ }
251
+ });
252
+ function renderNavItem(item, currentPath, collapsed = false, isChild = false) {
253
+ const isActive = currentPath === item.href || currentPath && currentPath.startsWith(item.href + "/");
254
+ const hasActiveChild = item.children?.some(
255
+ (child) => currentPath === child.href || currentPath && currentPath.startsWith(child.href + "/")
256
+ );
257
+ const navItemId = `nav-item-${item.href.replace(/[^a-zA-Z0-9]/g, "-")}`;
258
+ return /* @__PURE__ */ jsxs("li", { id: navItemId, className: "relative group", children: [
259
+ /* @__PURE__ */ jsxs(
260
+ "a",
261
+ {
262
+ href: item.href,
263
+ 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"}`,
264
+ "hx-get": item.href,
265
+ "hx-push-url": "true",
266
+ title: collapsed ? item.label : void 0,
267
+ children: [
268
+ item.icon && /* @__PURE__ */ jsx("span", { className: collapsed ? "" : "mr-2", children: item.icon }),
269
+ !collapsed && /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap overflow-hidden text-ellipsis", children: item.label })
270
+ ]
271
+ }
272
+ ),
273
+ 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: [
274
+ item.label,
275
+ /* @__PURE__ */ jsx("div", { className: "absolute right-full top-1/2 -translate-y-1/2 border-4 border-transparent border-r-gray-900" })
276
+ ] }),
277
+ !collapsed && item.children && item.children.length > 0 && /* @__PURE__ */ jsx("ul", { className: "ml-4 mt-1 space-y-1", children: item.children.map((child) => {
278
+ const isChildActive = currentPath === child.href || currentPath && currentPath.startsWith(child.href + "/");
279
+ const childNavItemId = `nav-item-${child.href.replace(/[^a-zA-Z0-9]/g, "-")}`;
280
+ return /* @__PURE__ */ jsx(
281
+ "li",
282
+ {
283
+ id: childNavItemId,
284
+ className: "relative group",
285
+ children: /* @__PURE__ */ jsxs(
286
+ "a",
287
+ {
288
+ href: child.href,
289
+ 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"}`,
290
+ "hx-get": child.href,
291
+ "hx-push-url": "true",
292
+ children: [
293
+ child.icon && /* @__PURE__ */ jsx("span", { className: "mr-2", children: child.icon }),
294
+ /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap overflow-hidden text-ellipsis", children: child.label })
295
+ ]
296
+ }
297
+ )
298
+ },
299
+ child.href
300
+ );
301
+ }) })
302
+ ] }, item.href);
303
+ }
304
+ var init_NavItem = __esm({
305
+ "src/components/NavItem.tsx"() {
306
+ }
307
+ });
308
+ var BaseLayout, AdminLayout;
309
+ var init_Layout = __esm({
310
+ "src/components/Layout.tsx"() {
311
+ init_Header();
312
+ init_LoadingBar();
313
+ init_NavItem();
314
+ BaseLayout = (props) => {
315
+ return /* @__PURE__ */ jsxs("html", { children: [
316
+ /* @__PURE__ */ jsxs("head", { children: [
317
+ /* @__PURE__ */ jsx("meta", { charset: "UTF-8" }),
318
+ /* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
319
+ /* @__PURE__ */ jsx("title", { children: props.title }),
320
+ props.description && /* @__PURE__ */ jsx("meta", { name: "description", content: props.description }),
321
+ /* @__PURE__ */ jsx("script", { src: "https://unpkg.com/htmx.org@latest" }),
322
+ /* @__PURE__ */ jsx("script", { src: "https://unpkg.com/hyperscript.org@latest" }),
323
+ /* @__PURE__ */ jsx("script", { src: "https://cdn.tailwindcss.com" }),
324
+ /* @__PURE__ */ jsx(
325
+ "style",
326
+ {
327
+ dangerouslySetInnerHTML: {
328
+ __html: `
329
+ @keyframes fadeIn {
330
+ from { opacity: 0;}
331
+ to { opacity: 1;}
332
+ }
333
+
334
+ @keyframes slideIn {
335
+ from { opacity: 0; transform: scale(0.95) translateY(-10px); }
336
+ to { opacity: 1; transform: scale(1) translateY(0); }
337
+ }
338
+
339
+ @keyframes slideInRight {
340
+ from { opacity: 0; transform: translateX(100%); }
341
+ to { opacity: 1; transform: translateX(0); }
342
+ }
343
+
344
+ @keyframes slideOutRight {
345
+ from { opacity: 1; transform: translateX(0); }
346
+ to { opacity: 0; transform: translateX(100%); }
347
+ }
348
+
349
+ @keyframes fadeOut {
350
+ from { opacity: 1; }
351
+ to { opacity: 0; }
352
+ }
353
+
354
+ @keyframes scaleOut {
355
+ from { opacity: 1; transform: scale(1) translateY(0); }
356
+ to { opacity: 0; transform: scale(0.95) translateY(-10px); }
357
+ }
358
+
359
+ /* Dialog \u9000\u51FA\u52A8\u753B */
360
+ .dialog-exit {
361
+ animation: fadeOut 0.2s ease-in forwards !important;
362
+ }
363
+
364
+ .dialog-content-exit {
365
+ animation: scaleOut 0.2s ease-in forwards !important;
366
+ }
367
+
368
+ /* ErrorAlert \u9000\u51FA\u52A8\u753B */
369
+ .error-alert-exit {
370
+ animation: slideOutRight 0.3s ease-in forwards, fadeOut 0.3s ease-in forwards;
371
+ }
372
+ `
373
+ }
374
+ }
375
+ )
376
+ ] }),
377
+ /* @__PURE__ */ jsxs(
378
+ "body",
379
+ {
380
+ className: "bg-gray-50",
381
+ "hx-indicator": "#loading-bar",
382
+ "hx-target": "#main-content",
383
+ "hx-swap": "outerHTML",
384
+ children: [
385
+ /* @__PURE__ */ jsx(LoadingBar, {}),
386
+ props.children,
387
+ /* @__PURE__ */ jsx(
388
+ "div",
389
+ {
390
+ id: "error-container",
391
+ className: "fixed top-4 right-4 z-[200] w-full max-w-2xl px-4"
392
+ }
393
+ ),
394
+ /* @__PURE__ */ jsx("div", { id: "dialog-container" })
395
+ ]
396
+ }
397
+ )
398
+ ] });
399
+ };
400
+ AdminLayout = (props) => {
401
+ const logo = props.adminContext.pluginOptions.logo;
402
+ const navItems = props.adminContext.pluginOptions.navigation;
403
+ const referer = props.adminContext.ctx.req.header("Referer");
404
+ let currentPath = props.adminContext.ctx.req.path;
405
+ if (referer) {
406
+ try {
407
+ const refererUrl = new URL(referer);
408
+ const method = props.adminContext.ctx.req.method;
409
+ if (["POST", "PUT", "DELETE"].includes(method)) {
410
+ currentPath = refererUrl.pathname;
411
+ }
412
+ } catch (e) {
413
+ }
414
+ }
415
+ const sidebarCollapsed = props.sidebarCollapsed || false;
416
+ const breadcrumbs = props.adminContext.breadcrumbs;
417
+ const userInfo = props.adminContext.userInfo;
418
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-screen", id: "main-content", children: [
419
+ /* @__PURE__ */ jsx(
420
+ "aside",
421
+ {
422
+ 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`,
423
+ children: /* @__PURE__ */ jsxs("div", { className: `${props.sidebarCollapsed ? "p-2" : "p-6"}`, children: [
424
+ !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 }) }),
425
+ /* @__PURE__ */ jsx("nav", { children: /* @__PURE__ */ jsx("ul", { className: "space-y-1", children: navItems && navItems.length > 0 ? navItems.map(
426
+ (item) => renderNavItem(
427
+ item,
428
+ currentPath,
429
+ props.sidebarCollapsed || false
430
+ )
431
+ ) : /* @__PURE__ */ jsx(
432
+ "li",
433
+ {
434
+ className: `${props.sidebarCollapsed ? "px-2" : "px-4"} py-2 text-gray-400 text-sm`,
435
+ children: "\u6682\u65E0\u5BFC\u822A\u9879"
436
+ }
437
+ ) }) })
438
+ ] })
439
+ }
440
+ ),
441
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col overflow-hidden", children: [
442
+ /* @__PURE__ */ jsx(
443
+ Header,
444
+ {
445
+ breadcrumbs,
446
+ userInfo,
447
+ sidebarCollapsed: sidebarCollapsed || false
448
+ }
449
+ ),
450
+ /* @__PURE__ */ jsx("main", { className: "flex-1 overflow-auto bg-gray-50", children: /* @__PURE__ */ jsx("div", { className: "p-6", children: props.children }) })
451
+ ] })
452
+ ] });
453
+ };
454
+ }
455
+ });
456
+
457
+ // src/utils/context.tsx
458
+ var context_exports = {};
459
+ __export(context_exports, {
460
+ HtmxAdminContext: () => HtmxAdminContext
461
+ });
462
+ var HtmxAdminContext;
463
+ var init_context = __esm({
464
+ "src/utils/context.tsx"() {
465
+ HtmxAdminContext = class {
466
+ /** 模块元数据 */
467
+ moduleMetadata;
468
+ /** 插件选项 */
469
+ pluginOptions;
470
+ /** 服务名(用于生成权限ID) */
471
+ serviceName;
472
+ /** Hono Context */
473
+ ctx;
474
+ /** 之前的模块名 */
475
+ previousModuleName;
476
+ /** 是否是片段请求(HTMX 请求) */
477
+ isFragment;
478
+ /** 是否是对话框请求 */
479
+ isDialog;
480
+ /** 用户信息 */
481
+ userInfo;
482
+ /** 通知队列 */
483
+ notifications = [];
484
+ /** 页面标题(用于 HX-Title 和页面展示) */
485
+ title = "";
486
+ /** 页面描述(用于SEO和页面展示) */
487
+ description = "";
488
+ /** 面包屑项 */
489
+ breadcrumbs = [];
490
+ /** 主要内容 */
491
+ content = null;
492
+ /** 需要重定向的 URL */
493
+ redirectUrl;
494
+ /** 是否需要刷新页面(用于 HX-Refresh) */
495
+ refresh = false;
496
+ constructor(ctx, userInfo, moduleMetadata, pluginOptions, serviceName = "") {
497
+ this.ctx = ctx;
498
+ this.userInfo = userInfo;
499
+ this.moduleMetadata = moduleMetadata;
500
+ this.pluginOptions = pluginOptions;
501
+ this.serviceName = serviceName;
502
+ const url = new URL(ctx.req.url);
503
+ this.isFragment = ctx.req.header("HX-Request") === "true";
504
+ this.isDialog = url.searchParams.get("dialog") === "true" || ctx.req.header("HX-Target") === "#dialog-container" || (ctx.req.header("Referer") || "").includes("dialog=true");
505
+ const referer = ctx.req.header("Referer");
506
+ this.previousModuleName = referer ? new URL(referer).pathname.replace(pluginOptions.prefix, "").split("/").pop() : void 0;
507
+ }
508
+ /**
509
+ * 发送通知
510
+ * 通知将在响应时通过 OOB 更新到错误容器
511
+ */
512
+ sendNotification(type, title, message) {
513
+ this.notifications.push({ type, title, message });
514
+ }
515
+ /**
516
+ * 发送错误通知(便捷方法)
517
+ */
518
+ sendError(title, message) {
519
+ this.sendNotification("error", title, message);
520
+ }
521
+ /**
522
+ * 发送警告通知(便捷方法)
523
+ */
524
+ sendWarning(title, message) {
525
+ this.sendNotification("warning", title, message);
526
+ }
527
+ /**
528
+ * 发送信息通知(便捷方法)
529
+ */
530
+ sendInfo(title, message) {
531
+ this.sendNotification("info", title, message);
532
+ }
533
+ /**
534
+ * 发送成功通知(便捷方法)
535
+ */
536
+ sendSuccess(title, message) {
537
+ this.sendNotification("success", title, message);
538
+ }
539
+ /**
540
+ * 设置需要推送的 URL
541
+ * 用于 HX-Push-Url 响应头
542
+ */
543
+ redirect(url) {
544
+ this.redirectUrl = url;
545
+ }
546
+ /**
547
+ * 设置需要刷新页面
548
+ * 用于 HX-Refresh 响应头
549
+ */
550
+ setRefresh(refresh = true) {
551
+ this.refresh = refresh;
552
+ }
553
+ /**
554
+ * 检查是否有列表页面
555
+ * 封装 moduleMetadata 访问,避免直接访问内部属性
556
+ */
557
+ hasList() {
558
+ return this.moduleMetadata.hasList;
559
+ }
560
+ /**
561
+ * 检查是否有详情页面
562
+ * 封装 moduleMetadata 访问,避免直接访问内部属性
563
+ */
564
+ hasDetail() {
565
+ return this.moduleMetadata.hasDetail;
566
+ }
567
+ /**
568
+ * 检查是否有表单页面
569
+ * 封装 moduleMetadata 访问,避免直接访问内部属性
570
+ */
571
+ hasForm() {
572
+ return this.moduleMetadata.hasForm;
573
+ }
574
+ /**
575
+ * 检查是否有自定义页面
576
+ * 封装 moduleMetadata 访问,避免直接访问内部属性
577
+ */
578
+ hasCustom() {
579
+ return this.moduleMetadata.hasCustom;
580
+ }
581
+ };
582
+ }
583
+ });
584
+
585
+ // src/components/PermissionDenied.tsx
586
+ var PermissionDenied_exports = {};
587
+ __export(PermissionDenied_exports, {
588
+ PermissionDeniedContent: () => PermissionDeniedContent,
589
+ PermissionDeniedPage: () => PermissionDeniedPage
590
+ });
591
+ function PermissionDeniedContent(adminContext, operationId, fromPath, registeredOperations) {
592
+ let operationInfo = null;
593
+ if (operationId && registeredOperations) {
594
+ operationInfo = registeredOperations.find(
595
+ (op) => op.operationId === operationId
596
+ ) || null;
597
+ }
598
+ const parts = operationId?.split(".") || [];
599
+ const operationType = parts[parts.length - 1] || "";
600
+ const moduleName = parts.length >= 2 ? parts[parts.length - 2] : "";
601
+ return /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
602
+ /* @__PURE__ */ jsx("div", { className: "text-6xl mb-4", children: "\u{1F512}" }),
603
+ /* @__PURE__ */ jsx("h1", { className: "text-3xl font-bold text-gray-900 mb-2", children: "\u6743\u9650\u4E0D\u8DB3" }),
604
+ /* @__PURE__ */ 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" }),
605
+ operationId && /* @__PURE__ */ jsxs("div", { className: "mb-8 p-4 bg-red-50 border border-red-200 rounded-lg text-left", children: [
606
+ /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-red-900 mb-2", children: "\u88AB\u62D2\u7EDD\u7684\u64CD\u4F5C" }),
607
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2 text-sm", children: [
608
+ /* @__PURE__ */ jsxs("div", { children: [
609
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-gray-700", children: "\u64CD\u4F5CID:" }),
610
+ " ",
611
+ /* @__PURE__ */ jsx("code", { className: "px-2 py-1 bg-gray-100 rounded text-blue-600 font-mono", children: operationId })
612
+ ] }),
613
+ operationInfo && /* @__PURE__ */ jsxs(Fragment, { children: [
614
+ /* @__PURE__ */ jsxs("div", { children: [
615
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-gray-700", children: "\u6A21\u5757:" }),
616
+ " ",
617
+ /* @__PURE__ */ jsx("span", { className: "text-gray-900", children: operationInfo.moduleTitle || moduleName })
618
+ ] }),
619
+ /* @__PURE__ */ jsxs("div", { children: [
620
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-gray-700", children: "\u64CD\u4F5C\u7C7B\u578B:" }),
621
+ " ",
622
+ /* @__PURE__ */ jsx("span", { className: "px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-semibold", children: getOperationTypeLabel(operationType) })
623
+ ] }),
624
+ operationInfo.moduleDescription && /* @__PURE__ */ jsxs("div", { children: [
625
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-gray-700", children: "\u6A21\u5757\u63CF\u8FF0:" }),
626
+ " ",
627
+ /* @__PURE__ */ jsx("span", { className: "text-gray-600", children: operationInfo.moduleDescription })
628
+ ] }),
629
+ /* @__PURE__ */ jsxs("div", { children: [
630
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-gray-700", children: "\u8DEF\u7531\u8DEF\u5F84:" }),
631
+ " ",
632
+ /* @__PURE__ */ jsx("code", { className: "px-2 py-1 bg-gray-100 rounded text-gray-700 font-mono text-xs", children: operationInfo.routePath })
633
+ ] })
634
+ ] }),
635
+ fromPath && /* @__PURE__ */ jsxs("div", { children: [
636
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-gray-700", children: "\u8BF7\u6C42\u8DEF\u5F84:" }),
637
+ " ",
638
+ /* @__PURE__ */ jsx("code", { className: "px-2 py-1 bg-gray-100 rounded text-gray-700 font-mono text-xs", children: fromPath })
639
+ ] })
640
+ ] })
641
+ ] }),
642
+ /* @__PURE__ */ jsx("div", { className: "mt-8", children: /* @__PURE__ */ jsx(
643
+ Button,
644
+ {
645
+ variant: "secondary",
646
+ _: "on click set #dialog-container's innerHTML to ''",
647
+ children: "\u5173\u95ED"
648
+ }
649
+ ) })
650
+ ] });
651
+ }
652
+ function PermissionDeniedPage(adminContext, operationId, fromPath, registeredOperations) {
653
+ return /* @__PURE__ */ jsx(
654
+ BaseLayout,
655
+ {
656
+ title: `\u6743\u9650\u4E0D\u8DB3 - ${adminContext.pluginOptions.title}`,
657
+ description: "\u60A8\u6CA1\u6709\u6743\u9650\u8BBF\u95EE\u6B64\u8D44\u6E90",
658
+ children: /* @__PURE__ */ jsx("div", { id: "main-content", className: "min-h-screen flex items-center justify-center px-4", children: /* @__PURE__ */ jsx(Card, { className: "max-w-2xl w-full p-8", children: /* @__PURE__ */ jsx(
659
+ PermissionDeniedContent,
660
+ {
661
+ adminContext,
662
+ operationId,
663
+ fromPath,
664
+ registeredOperations
665
+ }
666
+ ) }) })
667
+ }
668
+ );
669
+ }
670
+ function getOperationTypeLabel(operationType) {
671
+ const labels = {
672
+ read: "\u67E5\u770B",
673
+ create: "\u521B\u5EFA",
674
+ edit: "\u7F16\u8F91",
675
+ delete: "\u5220\u9664"
676
+ };
677
+ return labels[operationType] || operationType;
678
+ }
679
+ var init_PermissionDenied = __esm({
680
+ "src/components/PermissionDenied.tsx"() {
681
+ init_Card();
682
+ init_Button();
683
+ init_Layout();
684
+ }
685
+ });
9
686
  function safeRender(value) {
10
687
  if (value === null || value === void 0) {
11
688
  return null;
@@ -21,88 +698,10 @@ function safeRender(value) {
21
698
  }
22
699
  return value;
23
700
  }
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
- };
701
+
702
+ // src/components/Detail.tsx
703
+ init_Button();
704
+ init_Card();
106
705
  var PageHeader = (props) => {
107
706
  const { title, description, actions, className = "" } = props;
108
707
  return /* @__PURE__ */ jsx("div", { className: `mb-6 ${className}`, children: /* @__PURE__ */ jsxs("div", { className: "flex justify-between items-start", children: [
@@ -230,6 +829,14 @@ function DetailContent(props) {
230
829
  );
231
830
  }
232
831
 
832
+ // src/utils/module-name.ts
833
+ function extractModuleNameFromPath(basePath, prefix) {
834
+ const pathWithoutPrefix = basePath.replace(prefix, "");
835
+ const pathSegments = pathWithoutPrefix.split("/").filter(Boolean);
836
+ const modulePath = pathSegments[0] || "";
837
+ return modulePath.toLowerCase();
838
+ }
839
+
233
840
  // src/utils/path.ts
234
841
  var PathHelper = class {
235
842
  basePath;
@@ -333,6 +940,22 @@ var PageModule = class {
333
940
  ];
334
941
  return breadcrumbs;
335
942
  }
943
+ /**
944
+ * 获取所需权限
945
+ * 子类可以重写此方法来声明页面所需的权限
946
+ *
947
+ * 返回值说明:
948
+ * - 返回空字符串 "" 或 null 或 undefined:表示开放访问(无需权限)
949
+ * - 返回权限字符串:表示需要该权限,如 "users.read", "users.*"
950
+ *
951
+ * 默认实现:返回空字符串(开放访问)
952
+ *
953
+ * @param ctx Hono Context(可选,用于根据请求动态决定权限)
954
+ * @returns 所需权限字符串,或空字符串/null/undefined 表示开放
955
+ */
956
+ getRequiredPermission(ctx) {
957
+ return "";
958
+ }
336
959
  /**
337
960
  * 处理请求的统一入口(由 RouteHandler 调用)
338
961
  * 默认实现:直接调用 render 方法
@@ -348,6 +971,22 @@ var PageModule = class {
348
971
  var DetailPageModule = class extends PageModule {
349
972
  /** ID 字段名(默认 "id") */
350
973
  idField = "id";
974
+ /**
975
+ * 获取所需权限
976
+ * 默认实现:返回 "{serviceName}.{moduleName}.read"
977
+ * 子类可以重写此方法来自定义权限要求
978
+ *
979
+ * @param ctx Hono Context(可选)
980
+ * @returns 所需权限字符串,或空字符串/null/undefined 表示开放
981
+ */
982
+ getRequiredPermission(ctx) {
983
+ const moduleName = extractModuleNameFromPath(
984
+ this.context.moduleMetadata.basePath,
985
+ this.context.pluginOptions.prefix
986
+ );
987
+ const servicePrefix = this.context.serviceName ? `${this.context.serviceName}.` : "";
988
+ return `${servicePrefix}${moduleName}.read`;
989
+ }
351
990
  /**
352
991
  * 获取字段标签(可选)
353
992
  * 子类可以重写此方法来自定义字段的中文标签
@@ -451,6 +1090,10 @@ var DetailPageModule = class extends PageModule {
451
1090
  );
452
1091
  }
453
1092
  };
1093
+
1094
+ // src/components/Form.tsx
1095
+ init_Button();
1096
+ init_Card();
454
1097
  var DateInput = (props) => {
455
1098
  const {
456
1099
  id,
@@ -952,6 +1595,33 @@ var FormPageModule = class extends PageModule {
952
1595
  super();
953
1596
  this.schema = schema;
954
1597
  }
1598
+ /**
1599
+ * 获取所需权限
1600
+ * 根据是新建还是编辑返回不同的权限要求
1601
+ * - 新建:{serviceName}.{moduleName}.create
1602
+ * - 编辑:{serviceName}.{moduleName}.edit
1603
+ * 子类可以重写此方法来自定义权限要求
1604
+ *
1605
+ * @param ctx Hono Context(可选)
1606
+ * @returns 所需权限字符串,或空字符串/null/undefined 表示开放
1607
+ */
1608
+ getRequiredPermission(ctx) {
1609
+ const moduleName = extractModuleNameFromPath(
1610
+ this.context.moduleMetadata.basePath,
1611
+ this.context.pluginOptions.prefix
1612
+ );
1613
+ const servicePrefix = this.context.serviceName ? `${this.context.serviceName}.` : "";
1614
+ if (ctx) {
1615
+ const url = new URL(ctx.req.url);
1616
+ const path = url.pathname;
1617
+ if (path.includes("/new")) {
1618
+ return `${servicePrefix}${moduleName}.create`;
1619
+ } else if (path.includes("/edit/")) {
1620
+ return `${servicePrefix}${moduleName}.edit`;
1621
+ }
1622
+ }
1623
+ return `${servicePrefix}${moduleName}.edit`;
1624
+ }
955
1625
  /**
956
1626
  * 获取单条数据(编辑时使用)
957
1627
  */
@@ -1216,6 +1886,13 @@ var FormPageModule = class extends PageModule {
1216
1886
  return await this.render(formData);
1217
1887
  }
1218
1888
  };
1889
+
1890
+ // src/components/ListContent.tsx
1891
+ init_Button();
1892
+
1893
+ // src/components/FilterCard.tsx
1894
+ init_Button();
1895
+ init_Card();
1219
1896
  var FilterCard = (props) => {
1220
1897
  const {
1221
1898
  title,
@@ -1360,6 +2037,9 @@ var StatCard = (props) => {
1360
2037
  }
1361
2038
  );
1362
2039
  };
2040
+
2041
+ // src/components/ActionButton.tsx
2042
+ init_Button();
1363
2043
  var ActionButton = (props) => {
1364
2044
  const { label, href, method, className = "", item } = props;
1365
2045
  const hrefValue = typeof href === "function" ? href(item || {}) : href;
@@ -1382,10 +2062,17 @@ var ActionButton = (props) => {
1382
2062
  }
1383
2063
  );
1384
2064
  };
2065
+
2066
+ // src/components/Table.tsx
2067
+ init_Button();
2068
+ init_Card();
1385
2069
  var EmptyState = (props) => {
1386
2070
  const { message = "\u6682\u65E0\u6570\u636E", children } = props;
1387
2071
  return /* @__PURE__ */ jsx("div", { className: "text-center py-12", children: children || /* @__PURE__ */ jsx("p", { className: "text-gray-500 text-sm", children: message }) });
1388
2072
  };
2073
+
2074
+ // src/components/Pagination.tsx
2075
+ init_Button();
1389
2076
  var Pagination = (props) => {
1390
2077
  const {
1391
2078
  page,
@@ -1716,6 +2403,22 @@ function parseListParams(ctx) {
1716
2403
  var ListPageModule = class extends PageModule {
1717
2404
  /** ID 字段名(默认 "id") */
1718
2405
  idField = "id";
2406
+ /**
2407
+ * 获取所需权限
2408
+ * 默认实现:返回 "{serviceName}.{moduleName}.read"
2409
+ * 子类可以重写此方法来自定义权限要求
2410
+ *
2411
+ * @param ctx Hono Context(可选)
2412
+ * @returns 所需权限字符串,或空字符串/null/undefined 表示开放
2413
+ */
2414
+ getRequiredPermission(ctx) {
2415
+ const moduleName = extractModuleNameFromPath(
2416
+ this.context.moduleMetadata.basePath,
2417
+ this.context.pluginOptions.prefix
2418
+ );
2419
+ const servicePrefix = this.context.serviceName ? `${this.context.serviceName}.` : "";
2420
+ return `${servicePrefix}${moduleName}.read`;
2421
+ }
1719
2422
  /**
1720
2423
  * 获取列表数据
1721
2424
  */
@@ -1965,74 +2668,6 @@ var MemoryListDatasource = class {
1965
2668
  return newItem;
1966
2669
  }
1967
2670
  };
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
2671
  var ErrorAlert = (props) => {
2037
2672
  const {
2038
2673
  title,
@@ -2117,468 +2752,333 @@ var ErrorAlert = (props) => {
2117
2752
  children: /* @__PURE__ */ jsx(
2118
2753
  "path",
2119
2754
  {
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
- `
2755
+ strokeLinecap: "round",
2756
+ strokeLinejoin: "round",
2757
+ strokeWidth: 2,
2758
+ d: "M6 18L18 6M6 6l12 12"
2759
+ }
2760
+ )
2761
+ }
2762
+ )
2375
2763
  }
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) {
2764
+ )
2765
+ ] })
2415
2766
  }
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,
2767
+ );
2768
+ };
2769
+
2770
+ // src/handler.tsx
2771
+ init_Layout();
2772
+ var Dialog = (props) => {
2773
+ const {
2774
+ title,
2775
+ children,
2776
+ showClose = true,
2777
+ closeUrl,
2778
+ className = "",
2779
+ size = "lg"
2780
+ } = props;
2781
+ const sizeClasses = {
2782
+ sm: "max-w-md",
2783
+ md: "max-w-lg",
2784
+ lg: "max-w-2xl",
2785
+ xl: "max-w-4xl",
2786
+ full: "max-w-7xl"
2787
+ };
2788
+ return /* @__PURE__ */ jsx(
2789
+ "div",
2790
+ {
2791
+ className: "fixed inset-0 bg-black bg-opacity-50 z-[100] flex items-center justify-center p-4 dialog-backdrop",
2792
+ style: {
2793
+ animation: "fadeIn 0.2s ease-out"
2794
+ },
2795
+ _: "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",
2796
+ children: /* @__PURE__ */ jsxs(
2797
+ "div",
2446
2798
  {
2447
- breadcrumbs,
2448
- userInfo,
2449
- sidebarCollapsed: sidebarCollapsed || false
2799
+ className: `bg-gray-50 rounded-lg shadow-xl ${sizeClasses[size]} w-full max-h-[90vh] overflow-hidden flex flex-col dialog-content ${className}`,
2800
+ style: {
2801
+ animation: "slideIn 0.3s ease-out"
2802
+ },
2803
+ _: "on click call event.stopPropagation()",
2804
+ children: [
2805
+ (title || showClose) && /* @__PURE__ */ jsxs("div", { className: "px-6 py-4 border-b border-gray-200 bg-white flex items-center justify-between", children: [
2806
+ title && /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title }),
2807
+ showClose && /* @__PURE__ */ jsx(
2808
+ "button",
2809
+ {
2810
+ className: "text-gray-400 hover:text-gray-600 transition-colors",
2811
+ _: "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",
2812
+ children: /* @__PURE__ */ jsx(
2813
+ "svg",
2814
+ {
2815
+ className: "w-6 h-6",
2816
+ fill: "none",
2817
+ stroke: "currentColor",
2818
+ viewBox: "0 0 24 24",
2819
+ children: /* @__PURE__ */ jsx(
2820
+ "path",
2821
+ {
2822
+ strokeLinecap: "round",
2823
+ strokeLinejoin: "round",
2824
+ strokeWidth: 2,
2825
+ d: "M6 18L18 6M6 6l12 12"
2826
+ }
2827
+ )
2828
+ }
2829
+ )
2830
+ }
2831
+ )
2832
+ ] }),
2833
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto p-6 bg-gray-50", children })
2834
+ ]
2450
2835
  }
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
- ] });
2836
+ )
2837
+ }
2838
+ );
2455
2839
  };
2456
2840
 
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;
2841
+ // src/handler.tsx
2842
+ init_context();
2843
+
2844
+ // src/utils/permissions.ts
2845
+ function checkUserPermission(requiredPermission, userPermissions) {
2846
+ if (!requiredPermission) {
2847
+ return { allowed: true, reason: "\u65E0\u9700\u6743\u9650" };
2497
2848
  }
2498
- /**
2499
- * 发送通知
2500
- * 通知将在响应时通过 OOB 更新到错误容器
2501
- */
2502
- sendNotification(type, title, message) {
2503
- this.notifications.push({ type, title, message });
2849
+ if (!userPermissions || userPermissions.length === 0) {
2850
+ return {
2851
+ allowed: false,
2852
+ reason: "\u7528\u6237\u65E0\u4EFB\u4F55\u6743\u9650",
2853
+ matchedPermission: "none"
2854
+ };
2504
2855
  }
2505
- /**
2506
- * 发送错误通知(便捷方法)
2507
- */
2508
- sendError(title, message) {
2509
- this.sendNotification("error", title, message);
2856
+ const denyResult = checkDenyPermissions(requiredPermission, userPermissions);
2857
+ if (!denyResult.allowed) {
2858
+ return denyResult;
2510
2859
  }
2511
- /**
2512
- * 发送警告通知(便捷方法)
2513
- */
2514
- sendWarning(title, message) {
2515
- this.sendNotification("warning", title, message);
2860
+ const allowResult = checkAllowPermissions(
2861
+ requiredPermission,
2862
+ userPermissions
2863
+ );
2864
+ return allowResult;
2865
+ }
2866
+ function checkDenyPermissions(requiredPermission, userPermissions) {
2867
+ const denyPermissions = userPermissions.filter((p) => p.startsWith("deny:"));
2868
+ for (const denyPermission of denyPermissions) {
2869
+ const denyPattern = denyPermission.substring(5);
2870
+ if (matchPermission(requiredPermission, denyPattern)) {
2871
+ return {
2872
+ allowed: false,
2873
+ reason: `\u6743\u9650\u88AB\u7981\u6B62: ${denyPermission}`,
2874
+ matchedPermission: denyPermission
2875
+ };
2876
+ }
2877
+ }
2878
+ return { allowed: true };
2879
+ }
2880
+ function checkAllowPermissions(requiredPermission, userPermissions) {
2881
+ const allowPermissions = userPermissions.filter(
2882
+ (p) => !p.startsWith("deny:")
2883
+ );
2884
+ if (allowPermissions.length === 0) {
2885
+ return {
2886
+ allowed: false,
2887
+ reason: "\u7528\u6237\u65E0\u5141\u8BB8\u6743\u9650",
2888
+ matchedPermission: "none"
2889
+ };
2516
2890
  }
2517
- /**
2518
- * 发送信息通知(便捷方法)
2519
- */
2520
- sendInfo(title, message) {
2521
- this.sendNotification("info", title, message);
2891
+ for (const allowPermission of allowPermissions) {
2892
+ const allowPattern = allowPermission.startsWith("allow:") ? allowPermission.substring(6) : allowPermission;
2893
+ if (matchPermission(requiredPermission, allowPattern)) {
2894
+ return {
2895
+ allowed: true,
2896
+ reason: `\u6743\u9650\u5339\u914D: ${allowPermission}`,
2897
+ matchedPermission: allowPermission
2898
+ };
2899
+ }
2522
2900
  }
2523
- /**
2524
- * 发送成功通知(便捷方法)
2525
- */
2526
- sendSuccess(title, message) {
2527
- this.sendNotification("success", title, message);
2901
+ return {
2902
+ allowed: false,
2903
+ reason: "\u65E0\u5339\u914D\u7684\u5141\u8BB8\u6743\u9650",
2904
+ matchedPermission: "none"
2905
+ };
2906
+ }
2907
+ function matchPermission(requiredPermission, pattern) {
2908
+ const required = requiredPermission.startsWith("allow:") ? requiredPermission.substring(6) : requiredPermission;
2909
+ if (pattern.includes("*")) {
2910
+ return matchWithWildcard(required, pattern);
2528
2911
  }
2529
- /**
2530
- * 设置需要推送的 URL
2531
- * 用于 HX-Push-Url 响应头
2532
- */
2533
- redirect(url) {
2534
- this.redirectUrl = url;
2912
+ return required === pattern;
2913
+ }
2914
+ function matchWithWildcard(required, pattern) {
2915
+ const requiredParts = required.split(".");
2916
+ const patternParts = pattern.split(".");
2917
+ if (requiredParts.length !== patternParts.length && !pattern.endsWith("*")) {
2918
+ return false;
2535
2919
  }
2536
- /**
2537
- * 设置需要刷新页面
2538
- * 用于 HX-Refresh 响应头
2539
- */
2540
- setRefresh(refresh = true) {
2541
- this.refresh = refresh;
2920
+ const maxLength = Math.max(requiredParts.length, patternParts.length);
2921
+ for (let i = 0; i < maxLength; i++) {
2922
+ const requiredPart = requiredParts[i];
2923
+ const patternPart = patternParts[i];
2924
+ if (patternPart === "*") {
2925
+ continue;
2926
+ }
2927
+ if (!patternPart && pattern.endsWith("*")) {
2928
+ return true;
2929
+ }
2930
+ if (!requiredPart) {
2931
+ return false;
2932
+ }
2933
+ if (requiredPart !== patternPart) {
2934
+ return false;
2935
+ }
2542
2936
  }
2543
- /**
2544
- * 检查是否有列表页面
2545
- * 封装 moduleMetadata 访问,避免直接访问内部属性
2546
- */
2547
- hasList() {
2548
- return this.moduleMetadata.hasList;
2937
+ return true;
2938
+ }
2939
+
2940
+ // src/utils/auth.ts
2941
+ async function getUserInfo(ctx, authProvider) {
2942
+ if (!authProvider) {
2943
+ return null;
2549
2944
  }
2550
- /**
2551
- * 检查是否有详情页面
2552
- * 封装 moduleMetadata 访问,避免直接访问内部属性
2553
- */
2554
- hasDetail() {
2555
- return this.moduleMetadata.hasDetail;
2945
+ const cookieKey = authProvider.cookieKey || "auth_token";
2946
+ const token = getCookie(ctx, cookieKey);
2947
+ if (!token) {
2948
+ return null;
2556
2949
  }
2557
- /**
2558
- * 检查是否有表单页面
2559
- * 封装 moduleMetadata 访问,避免直接访问内部属性
2560
- */
2561
- hasForm() {
2562
- return this.moduleMetadata.hasForm;
2950
+ return await authProvider.tokenToUser(token, ctx);
2951
+ }
2952
+ function checkPermissionDefault(userInfo, operation) {
2953
+ if (!operation) {
2954
+ return { allowed: true };
2563
2955
  }
2564
- /**
2565
- * 检查是否有自定义页面
2566
- * 封装 moduleMetadata 访问,避免直接访问内部属性
2567
- */
2568
- hasCustom() {
2569
- return this.moduleMetadata.hasCustom;
2956
+ if (!userInfo) {
2957
+ return {
2958
+ allowed: false,
2959
+ message: "\u672A\u767B\u5F55\uFF0C\u65E0\u6CD5\u8BBF\u95EE\u6B64\u8D44\u6E90",
2960
+ operationId: operation
2961
+ };
2570
2962
  }
2571
- };
2963
+ const userPermissions = userInfo.permissions || [];
2964
+ const result = checkUserPermission(operation, userPermissions);
2965
+ if (result.allowed) {
2966
+ return { allowed: true };
2967
+ } else {
2968
+ return {
2969
+ allowed: false,
2970
+ message: result.reason || "\u60A8\u6CA1\u6709\u6743\u9650\u8BBF\u95EE\u6B64\u8D44\u6E90",
2971
+ operationId: operation
2972
+ };
2973
+ }
2974
+ }
2975
+
2976
+ // src/utils/operation.ts
2977
+ function generateOperationId(ctx, moduleName, moduleOptions, serviceName) {
2978
+ const method = ctx.req.method.toLowerCase();
2979
+ const url = new URL(ctx.req.url);
2980
+ const path = url.pathname;
2981
+ const lowerModuleName = moduleName.toLowerCase();
2982
+ const servicePrefix = serviceName ? `${serviceName}.` : "";
2983
+ if (moduleOptions.type === "list") {
2984
+ return `${servicePrefix}${lowerModuleName}.read`;
2985
+ } else if (moduleOptions.type === "detail") {
2986
+ if (method === "delete") {
2987
+ return `${servicePrefix}${lowerModuleName}.delete`;
2988
+ }
2989
+ return `${servicePrefix}${lowerModuleName}.read`;
2990
+ } else if (moduleOptions.type === "form") {
2991
+ if (path.includes("/new")) {
2992
+ if (method === "get") {
2993
+ return `${servicePrefix}${lowerModuleName}.create`;
2994
+ }
2995
+ return `${servicePrefix}${lowerModuleName}.create`;
2996
+ } else if (path.includes("/edit/")) {
2997
+ if (method === "get") {
2998
+ return `${servicePrefix}${lowerModuleName}.edit`;
2999
+ }
3000
+ return `${servicePrefix}${lowerModuleName}.edit`;
3001
+ } else if (method === "post") {
3002
+ return `${servicePrefix}${lowerModuleName}.create`;
3003
+ } else if (method === "put") {
3004
+ return `${servicePrefix}${lowerModuleName}.edit`;
3005
+ } else if (method === "delete") {
3006
+ return `${servicePrefix}${lowerModuleName}.delete`;
3007
+ }
3008
+ return `${servicePrefix}${lowerModuleName}.read`;
3009
+ } else if (moduleOptions.type === "custom") {
3010
+ return `${servicePrefix}${lowerModuleName}.read`;
3011
+ }
3012
+ return `${servicePrefix}${lowerModuleName}.read`;
3013
+ }
3014
+
3015
+ // src/utils/permission-handler.tsx
3016
+ init_PermissionDenied();
3017
+ async function handlePermissionDenied(ctx, adminContext, permissionResult, getOperationId, pluginPrefix, getRegisteredOperations) {
3018
+ const operationId = permissionResult.operationId || getOperationId();
3019
+ const fromPath = ctx.req.path;
3020
+ if (adminContext.isFragment) {
3021
+ return renderPermissionDeniedDialog(
3022
+ ctx,
3023
+ adminContext,
3024
+ operationId,
3025
+ fromPath,
3026
+ getRegisteredOperations
3027
+ );
3028
+ } else {
3029
+ return redirectToPermissionDeniedPage(
3030
+ ctx,
3031
+ operationId,
3032
+ fromPath,
3033
+ pluginPrefix,
3034
+ permissionResult.redirectUrl
3035
+ );
3036
+ }
3037
+ }
3038
+ function renderPermissionDeniedDialog(ctx, adminContext, operationId, fromPath, getRegisteredOperations) {
3039
+ const registeredOperations = getRegisteredOperations ? getRegisteredOperations() : [];
3040
+ const permissionContent = PermissionDeniedContent(
3041
+ adminContext,
3042
+ operationId,
3043
+ fromPath,
3044
+ registeredOperations
3045
+ );
3046
+ return ctx.html(
3047
+ /* @__PURE__ */ jsx(Dialog, { title: "\u6743\u9650\u4E0D\u8DB3", size: "lg", children: permissionContent }),
3048
+ 200,
3049
+ {
3050
+ "HX-Retarget": "#dialog-container",
3051
+ "HX-Reswap": "innerHTML"
3052
+ }
3053
+ );
3054
+ }
3055
+ function redirectToPermissionDeniedPage(ctx, operationId, fromPath, pluginPrefix, customRedirectUrl) {
3056
+ const permissionDeniedUrl = `${pluginPrefix}/permission-denied`;
3057
+ const url = new URL(permissionDeniedUrl, ctx.req.url);
3058
+ url.searchParams.set("operation", operationId);
3059
+ url.searchParams.set("from", fromPath);
3060
+ const redirectUrl = url.pathname + url.search;
3061
+ const finalRedirectUrl = customRedirectUrl || redirectUrl;
3062
+ return ctx.redirect(finalRedirectUrl);
3063
+ }
2572
3064
  var RouteHandler = class {
2573
3065
  moduleClass;
2574
3066
  pluginOptions;
2575
3067
  moduleMetadata;
2576
3068
  moduleOptions;
3069
+ moduleName;
3070
+ basePath;
3071
+ serviceName;
3072
+ getRegisteredOperations;
2577
3073
  constructor(options) {
2578
3074
  this.moduleClass = options.moduleClass;
2579
3075
  this.pluginOptions = options.pluginOptions;
2580
3076
  this.moduleMetadata = options.moduleMetadata;
2581
3077
  this.moduleOptions = options.moduleOptions;
3078
+ this.moduleName = options.moduleName;
3079
+ this.basePath = options.basePath;
3080
+ this.serviceName = options.serviceName;
3081
+ this.getRegisteredOperations = options.getRegisteredOperations;
2582
3082
  }
2583
3083
  /**
2584
3084
  * 更新模块元数据
@@ -2591,10 +3091,40 @@ var RouteHandler = class {
2591
3091
  */
2592
3092
  async handle(ctx) {
2593
3093
  try {
2594
- const userInfo = await this.pluginOptions.getUserInfo(ctx);
3094
+ const userInfo = await getUserInfo(ctx, this.pluginOptions.authProvider);
2595
3095
  const adminContext = this.createAdminContext(ctx, userInfo);
2596
3096
  const moduleInstance = new this.moduleClass();
2597
3097
  moduleInstance.__init(adminContext);
3098
+ const requiredPermission = moduleInstance.getRequiredPermission?.(ctx);
3099
+ const operation = this.getOperationId(ctx);
3100
+ if (this.pluginOptions.authProvider && requiredPermission !== void 0 && requiredPermission !== null && requiredPermission !== "") {
3101
+ let permissionResult;
3102
+ if (this.pluginOptions.authProvider.checkPermission) {
3103
+ permissionResult = await this.pluginOptions.authProvider.checkPermission(
3104
+ userInfo,
3105
+ operation,
3106
+ adminContext
3107
+ );
3108
+ } else {
3109
+ permissionResult = checkPermissionDefault(
3110
+ userInfo,
3111
+ operation
3112
+ );
3113
+ }
3114
+ if (!permissionResult.allowed) {
3115
+ if (!permissionResult.operationId) {
3116
+ permissionResult.operationId = operation;
3117
+ }
3118
+ return await handlePermissionDenied(
3119
+ ctx,
3120
+ adminContext,
3121
+ permissionResult,
3122
+ () => this.getOperationId(ctx),
3123
+ this.pluginOptions.prefix,
3124
+ this.getRegisteredOperations
3125
+ );
3126
+ }
3127
+ }
2598
3128
  try {
2599
3129
  adminContext.content = await moduleInstance.__handle();
2600
3130
  adminContext.title = moduleInstance.getTitle();
@@ -2618,7 +3148,21 @@ var RouteHandler = class {
2618
3148
  );
2619
3149
  }
2620
3150
  }
3151
+ /**
3152
+ * 获取操作ID(内部方法,用于传递给工具函数)
3153
+ */
3154
+ getOperationId(ctx) {
3155
+ return generateOperationId(
3156
+ ctx,
3157
+ this.moduleName,
3158
+ this.moduleOptions,
3159
+ this.serviceName
3160
+ );
3161
+ }
2621
3162
  renderFullPage(ctx, adminContext) {
3163
+ if (adminContext.redirectUrl) {
3164
+ return ctx.redirect(adminContext.redirectUrl);
3165
+ }
2622
3166
  const useAdminLayout = this.moduleOptions.useAdminLayout !== false;
2623
3167
  return ctx.html(
2624
3168
  /* @__PURE__ */ jsx(
@@ -2633,7 +3177,9 @@ var RouteHandler = class {
2633
3177
  }
2634
3178
  renderFragment(ctx, adminContext) {
2635
3179
  if (adminContext.redirectUrl) {
2636
- return ctx.redirect(adminContext.redirectUrl);
3180
+ return ctx.html(/* @__PURE__ */ jsx("div", {}), 200, {
3181
+ "HX-Redirect": adminContext.redirectUrl
3182
+ });
2637
3183
  }
2638
3184
  const target = adminContext.isDialog ? "#dialog-container" : "#main-content";
2639
3185
  const swap = adminContext.isDialog ? "innerHTML" : "outerHTML";
@@ -2718,7 +3264,8 @@ var RouteHandler = class {
2718
3264
  ctx,
2719
3265
  userInfo,
2720
3266
  this.moduleMetadata,
2721
- this.pluginOptions
3267
+ this.pluginOptions,
3268
+ this.serviceName
2722
3269
  );
2723
3270
  }
2724
3271
  /**
@@ -2811,8 +3358,12 @@ var HtmxAdminPlugin = class {
2811
3358
  engine;
2812
3359
  hono;
2813
3360
  options;
3361
+ // 服务名(从引擎中获取)
3362
+ serviceName = "";
2814
3363
  // 模块类映射(记录每个模块的类)
2815
3364
  moduleTypeMap = /* @__PURE__ */ new Map();
3365
+ // 注册的操作列表(权限列表)
3366
+ registeredOperations = [];
2816
3367
  constructor(options) {
2817
3368
  this.options = {
2818
3369
  title: options?.title || "\u7BA1\u7406\u540E\u53F0",
@@ -2820,7 +3371,7 @@ var HtmxAdminPlugin = class {
2820
3371
  prefix: options?.prefix || "/admin",
2821
3372
  homePath: options?.homePath || "",
2822
3373
  navigation: options?.navigation ?? [],
2823
- getUserInfo: options?.getUserInfo ?? (() => null)
3374
+ authProvider: options?.authProvider
2824
3375
  };
2825
3376
  }
2826
3377
  /**
@@ -2843,7 +3394,8 @@ var HtmxAdminPlugin = class {
2843
3394
  onInit(engine) {
2844
3395
  this.engine = engine;
2845
3396
  this.hono = engine.getHono();
2846
- logger.info("HtmxAdminPlugin initialized");
3397
+ this.serviceName = engine.options.name;
3398
+ logger.info(`HtmxAdminPlugin initialized${this.serviceName ? ` (service: ${this.serviceName})` : ""}`);
2847
3399
  }
2848
3400
  /**
2849
3401
  * 检查并注册模块
@@ -2904,10 +3456,127 @@ var HtmxAdminPlugin = class {
2904
3456
  }
2905
3457
  }
2906
3458
  }
3459
+ /**
3460
+ * 根据模块类型和路由信息生成操作ID
3461
+ */
3462
+ generateOperationId(moduleName, moduleType, method, path) {
3463
+ const lowerModuleName = moduleName.toLowerCase();
3464
+ const lowerMethod = method.toLowerCase();
3465
+ if (moduleType === "list") {
3466
+ return {
3467
+ operationId: `${lowerModuleName}.read`,
3468
+ operationType: "read"
3469
+ };
3470
+ } else if (moduleType === "detail") {
3471
+ if (lowerMethod === "delete") {
3472
+ return {
3473
+ operationId: `${lowerModuleName}.delete`,
3474
+ operationType: "delete"
3475
+ };
3476
+ }
3477
+ return {
3478
+ operationId: `${lowerModuleName}.read`,
3479
+ operationType: "read"
3480
+ };
3481
+ } else if (moduleType === "form") {
3482
+ if (path.includes("/new")) {
3483
+ if (lowerMethod === "get") {
3484
+ return {
3485
+ operationId: `${lowerModuleName}.create`,
3486
+ operationType: "create"
3487
+ };
3488
+ }
3489
+ return {
3490
+ operationId: `${lowerModuleName}.create`,
3491
+ operationType: "create"
3492
+ };
3493
+ } else if (path.includes("/edit/")) {
3494
+ if (lowerMethod === "get") {
3495
+ return {
3496
+ operationId: `${lowerModuleName}.edit`,
3497
+ operationType: "edit"
3498
+ };
3499
+ }
3500
+ return {
3501
+ operationId: `${lowerModuleName}.edit`,
3502
+ operationType: "edit"
3503
+ };
3504
+ } else if (lowerMethod === "post") {
3505
+ return {
3506
+ operationId: `${lowerModuleName}.create`,
3507
+ operationType: "create"
3508
+ };
3509
+ } else if (lowerMethod === "put") {
3510
+ return {
3511
+ operationId: `${lowerModuleName}.edit`,
3512
+ operationType: "edit"
3513
+ };
3514
+ } else if (lowerMethod === "delete") {
3515
+ return {
3516
+ operationId: `${lowerModuleName}.delete`,
3517
+ operationType: "delete"
3518
+ };
3519
+ }
3520
+ return {
3521
+ operationId: `${lowerModuleName}.read`,
3522
+ operationType: "read"
3523
+ };
3524
+ } else if (moduleType === "custom") {
3525
+ return {
3526
+ operationId: `${lowerModuleName}.read`,
3527
+ operationType: "read"
3528
+ };
3529
+ }
3530
+ return {
3531
+ operationId: `${lowerModuleName}.read`,
3532
+ operationType: "read"
3533
+ };
3534
+ }
3535
+ /**
3536
+ * 注册内置路由(权限提示页面)
3537
+ */
3538
+ registerBuiltinRoutes() {
3539
+ const permissionDeniedPath = `${this.options.prefix}/permission-denied`;
3540
+ this.hono.get(permissionDeniedPath, async (ctx) => {
3541
+ const url = new URL(ctx.req.url);
3542
+ const operationId = url.searchParams.get("operation") || "";
3543
+ const fromPath = url.searchParams.get("from") || "";
3544
+ const { PermissionDeniedPage: PermissionDeniedPage2 } = await Promise.resolve().then(() => (init_PermissionDenied(), PermissionDenied_exports));
3545
+ const { HtmxAdminContext: HtmxAdminContext2 } = await Promise.resolve().then(() => (init_context(), context_exports));
3546
+ const adminContext = new HtmxAdminContext2(
3547
+ ctx,
3548
+ null,
3549
+ // 不需要用户信息
3550
+ {
3551
+ hasList: false,
3552
+ hasDetail: false,
3553
+ hasForm: false,
3554
+ hasCustom: true,
3555
+ basePath: permissionDeniedPath,
3556
+ title: "\u6743\u9650\u4E0D\u8DB3",
3557
+ description: "\u60A8\u6CA1\u6709\u6743\u9650\u8BBF\u95EE\u6B64\u8D44\u6E90"
3558
+ },
3559
+ this.options,
3560
+ this.serviceName
3561
+ );
3562
+ return ctx.html(
3563
+ PermissionDeniedPage2(
3564
+ adminContext,
3565
+ operationId,
3566
+ fromPath,
3567
+ this.registeredOperations
3568
+ )
3569
+ );
3570
+ });
3571
+ logger.info(
3572
+ `[HtmxAdminPlugin] Registered builtin route: GET ${permissionDeniedPath}`
3573
+ );
3574
+ }
2907
3575
  /**
2908
3576
  * 注册路由
2909
3577
  */
2910
3578
  registerRoutes() {
3579
+ this.registerBuiltinRoutes();
2911
3580
  const moduleNames = Array.from(this.moduleTypeMap.keys());
2912
3581
  for (const moduleName of moduleNames) {
2913
3582
  const moduleTypes = this.moduleTypeMap.get(moduleName);
@@ -2920,6 +3589,8 @@ var HtmxAdminPlugin = class {
2920
3589
  basePath,
2921
3590
  moduleOptions: moduleInfo.options,
2922
3591
  pluginOptions: this.options,
3592
+ serviceName: this.serviceName,
3593
+ // 传递服务名
2923
3594
  moduleMetadata: {
2924
3595
  hasList: !!moduleTypes["list"],
2925
3596
  hasDetail: !!moduleTypes["detail"],
@@ -2928,7 +3599,9 @@ var HtmxAdminPlugin = class {
2928
3599
  basePath,
2929
3600
  title: moduleInfo.options.title ?? moduleName,
2930
3601
  description: moduleInfo.options.description ?? ""
2931
- }
3602
+ },
3603
+ getRegisteredOperations: () => this.registeredOperations
3604
+ // 传递获取操作列表的方法
2932
3605
  });
2933
3606
  const routeConfig = {
2934
3607
  list: [{ method: "get", path: `${basePath}/list` }],
@@ -2947,17 +3620,74 @@ var HtmxAdminPlugin = class {
2947
3620
  logger.info(
2948
3621
  `[HtmxAdminPlugin] Registering ${type} route: ${route.method.toUpperCase()} ${route.path}`
2949
3622
  );
2950
- const handler = async (ctx) => routeHandler.handle(ctx);
3623
+ const { operationId, operationType } = this.generateOperationId(
3624
+ moduleName,
3625
+ type,
3626
+ route.method,
3627
+ route.path
3628
+ );
3629
+ const existingOperation = this.registeredOperations.find(
3630
+ (op) => op.operationId === operationId && op.httpMethod === route.method.toUpperCase()
3631
+ );
3632
+ if (!existingOperation) {
3633
+ this.registeredOperations.push({
3634
+ operationId,
3635
+ moduleName: moduleName.toLowerCase(),
3636
+ operationType,
3637
+ moduleType: type,
3638
+ moduleTitle: moduleInfo.options.title,
3639
+ moduleDescription: moduleInfo.options.description,
3640
+ httpMethod: route.method.toUpperCase(),
3641
+ routePath: route.path
3642
+ });
3643
+ }
3644
+ const handler = async (ctx) => {
3645
+ return routeHandler.handle(ctx);
3646
+ };
2951
3647
  this.hono[route.method](route.path, handler);
2952
3648
  }
2953
3649
  }
2954
3650
  }
2955
3651
  }
3652
+ /**
3653
+ * 获取所有注册的操作(权限)列表
3654
+ * 可用于权限管理模块作为数据源,或用于确定管理员的权限
3655
+ *
3656
+ * @returns 所有注册的操作信息列表
3657
+ */
3658
+ getRegisteredOperations() {
3659
+ return [...this.registeredOperations];
3660
+ }
3661
+ /**
3662
+ * 获取指定模块的所有操作
3663
+ *
3664
+ * @param moduleName 模块名
3665
+ * @returns 该模块的所有操作信息列表
3666
+ */
3667
+ getModuleOperations(moduleName) {
3668
+ const lowerModuleName = moduleName.toLowerCase();
3669
+ return this.registeredOperations.filter(
3670
+ (op) => op.moduleName === lowerModuleName
3671
+ );
3672
+ }
3673
+ /**
3674
+ * 获取所有唯一的操作ID列表(去重)
3675
+ *
3676
+ * @returns 所有唯一的操作ID列表
3677
+ */
3678
+ getOperationIds() {
3679
+ const uniqueIds = /* @__PURE__ */ new Set();
3680
+ for (const op of this.registeredOperations) {
3681
+ uniqueIds.add(op.operationId);
3682
+ }
3683
+ return Array.from(uniqueIds).sort();
3684
+ }
2956
3685
  /**
2957
3686
  * 模块加载钩子:检查模块类型约束并注册路由
2958
3687
  */
2959
3688
  onModuleLoad(modules) {
2960
3689
  this.checkAndRegisterModules(modules);
3690
+ this.registerBuiltinRoutes();
2961
3691
  this.registerRoutes();
2962
3692
  this.engine.getHono().get(this.options.prefix, async (ctx) => {
2963
3693
  return ctx.redirect(this.options.homePath);
@@ -3042,6 +3772,10 @@ var Badge = (props) => {
3042
3772
  }
3043
3773
  );
3044
3774
  };
3775
+
3776
+ // src/components/index.ts
3777
+ init_Button();
3778
+ init_Card();
3045
3779
  var SystemStatusCard = (props) => {
3046
3780
  const { title, statusItems, className = "" } = props;
3047
3781
  const progressColorClasses = {