imean-service-engine-htmx-plugin 2.3.1 → 2.5.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
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
- var imeanServiceEngine = require('imean-service-engine');
3
+ var jsxRuntime = require('hono/jsx/jsx-runtime');
4
4
  var fs = require('fs');
5
+ var imeanServiceEngine = require('imean-service-engine');
5
6
  var path = require('path');
6
- var jsxRuntime = require('hono/jsx/jsx-runtime');
7
7
  var cookie = require('hono/cookie');
8
8
  var html = require('hono/html');
9
9
 
@@ -16,6 +16,170 @@ var __export = (target, all) => {
16
16
  for (var name in all)
17
17
  __defProp(target, name, { get: all[name], enumerable: true });
18
18
  };
19
+ function Button(props) {
20
+ const {
21
+ children,
22
+ variant = "primary",
23
+ size = "md",
24
+ disabled = false,
25
+ className = "",
26
+ ...rest
27
+ } = props;
28
+ const baseClasses = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2";
29
+ const variantClasses = {
30
+ primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
31
+ secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500",
32
+ danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
33
+ ghost: "bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-500"
34
+ };
35
+ const sizeClasses = {
36
+ sm: "px-3 py-1.5 text-sm",
37
+ md: "px-4 py-2 text-sm",
38
+ lg: "px-6 py-3 text-base"
39
+ };
40
+ const classes = [
41
+ baseClasses,
42
+ variantClasses[variant],
43
+ sizeClasses[size],
44
+ disabled ? "opacity-50 cursor-not-allowed" : "",
45
+ className
46
+ ].filter(Boolean).join(" ");
47
+ const href = rest.href ?? "#";
48
+ return /* @__PURE__ */ jsxRuntime.jsx(
49
+ "a",
50
+ {
51
+ className: classes,
52
+ disabled,
53
+ href,
54
+ ...rest,
55
+ children
56
+ }
57
+ );
58
+ }
59
+ var init_button = __esm({
60
+ "src/components/button.tsx"() {
61
+ }
62
+ });
63
+
64
+ // src/utils/action-button-renderer.tsx
65
+ var action_button_renderer_exports = {};
66
+ __export(action_button_renderer_exports, {
67
+ renderActionButton: () => renderActionButton,
68
+ renderActionButtons: () => renderActionButtons
69
+ });
70
+ function renderActionButton(action, index) {
71
+ const {
72
+ label,
73
+ href,
74
+ hxGet,
75
+ hxPost,
76
+ hxPut,
77
+ hxDelete,
78
+ variant = "primary",
79
+ confirm,
80
+ close,
81
+ submit,
82
+ formId,
83
+ onClick,
84
+ className = "",
85
+ target
86
+ } = action;
87
+ if (submit && formId) {
88
+ const variantStyles = {
89
+ primary: "bg-blue-600 text-white hover:bg-blue-700",
90
+ secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
91
+ danger: "bg-red-600 text-white hover:bg-red-700",
92
+ ghost: "bg-transparent text-gray-700 hover:bg-gray-100"
93
+ };
94
+ const buttonStyle = variantStyles[variant] || variantStyles.primary;
95
+ const testId2 = label === "\u521B\u5EFA" || label === "\u66F4\u65B0" ? "submit-button" : `action-${label}`;
96
+ return /* @__PURE__ */ jsxRuntime.jsx(
97
+ "button",
98
+ {
99
+ type: "submit",
100
+ form: formId,
101
+ className: `px-4 py-2 rounded transition-colors font-medium ${buttonStyle} ${className}`,
102
+ "data-testid": testId2,
103
+ ...confirm && { "data-confirm": confirm },
104
+ children: label
105
+ },
106
+ index
107
+ );
108
+ }
109
+ const finalOnClick = close ? CLOSE_DIALOG_SCRIPT : onClick;
110
+ if (finalOnClick) {
111
+ const variantStyles = {
112
+ primary: "bg-blue-600 text-white hover:bg-blue-700",
113
+ secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
114
+ danger: "bg-red-600 text-white hover:bg-red-700",
115
+ ghost: "bg-transparent text-gray-700 hover:bg-gray-100"
116
+ };
117
+ const buttonStyle = variantStyles[variant] || variantStyles.secondary;
118
+ let testId2 = `action-${label}`;
119
+ if (label === "\u53D6\u6D88") {
120
+ testId2 = "cancel-button";
121
+ } else if (label === "\u5173\u95ED") {
122
+ testId2 = "close-button";
123
+ }
124
+ return /* @__PURE__ */ jsxRuntime.jsx(
125
+ "button",
126
+ {
127
+ type: "button",
128
+ _: finalOnClick,
129
+ className: `px-4 py-2 rounded transition-colors font-medium ${buttonStyle} ${className}`,
130
+ "data-testid": testId2,
131
+ ...confirm && { "data-confirm": confirm },
132
+ children: label
133
+ },
134
+ index
135
+ );
136
+ }
137
+ let testId = `action-${label}`;
138
+ if (label === "\u65B0\u5EFA" || label === "\u521B\u5EFA") {
139
+ testId = "create-button";
140
+ } else if (label === "\u53D6\u6D88") {
141
+ testId = "cancel-button";
142
+ } else if (label === "\u5173\u95ED") {
143
+ testId = "close-button";
144
+ }
145
+ const isNewWindow = target === "_blank";
146
+ const htmxAttrs = {};
147
+ if (!isNewWindow) {
148
+ if (hxGet) htmxAttrs["hx-get"] = hxGet;
149
+ if (hxPost) htmxAttrs["hx-post"] = hxPost;
150
+ if (hxPut) htmxAttrs["hx-put"] = hxPut;
151
+ if (hxDelete) htmxAttrs["hx-delete"] = hxDelete;
152
+ }
153
+ if (confirm) htmxAttrs["hx-confirm"] = confirm;
154
+ return /* @__PURE__ */ jsxRuntime.jsx(
155
+ Button,
156
+ {
157
+ variant,
158
+ href,
159
+ className,
160
+ "data-testid": testId,
161
+ target,
162
+ rel: target === "_blank" ? "noopener noreferrer" : void 0,
163
+ ...htmxAttrs,
164
+ children: label
165
+ },
166
+ index
167
+ );
168
+ }
169
+ function renderActionButtons(actions) {
170
+ return actions.map((action, index) => renderActionButton(action, index));
171
+ }
172
+ var CLOSE_DIALOG_SCRIPT;
173
+ var init_action_button_renderer = __esm({
174
+ "src/utils/action-button-renderer.tsx"() {
175
+ init_button();
176
+ CLOSE_DIALOG_SCRIPT = `on click
177
+ add .dialog-exit to .dialog-backdrop
178
+ add .dialog-content-exit to .dialog-content
179
+ wait 200ms
180
+ set #dialog-container's innerHTML to '' end`;
181
+ }
182
+ });
19
183
 
20
184
  // src/utils/cdn-cache.ts
21
185
  var cdn_cache_exports = {};
@@ -62,7 +226,11 @@ async function saveToFileCache(name, content, mimeType) {
62
226
  fs.promises.writeFile(contentPath, content, "utf-8"),
63
227
  fs.promises.writeFile(
64
228
  metadataPath,
65
- JSON.stringify({ mimeType, cachedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
229
+ JSON.stringify(
230
+ { mimeType, cachedAt: (/* @__PURE__ */ new Date()).toISOString() },
231
+ null,
232
+ 2
233
+ ),
66
234
  "utf-8"
67
235
  )
68
236
  ]);
@@ -75,14 +243,18 @@ async function fetchAndCacheResource(resource) {
75
243
  try {
76
244
  const fileCache = await readFromFileCache(resource.name);
77
245
  if (fileCache) {
78
- imeanServiceEngine.logger.info(`[CDNCache] \u4ECE\u6587\u4EF6\u7F13\u5B58\u52A0\u8F7D\u8D44\u6E90: ${resource.name} (${fileCache.content.length} \u5B57\u8282)`);
246
+ imeanServiceEngine.logger.info(
247
+ `[CDNCache] \u4ECE\u6587\u4EF6\u7F13\u5B58\u52A0\u8F7D\u8D44\u6E90: ${resource.name} (${fileCache.content.length} \u5B57\u8282)`
248
+ );
79
249
  cache.set(resource.name, fileCache);
80
250
  return;
81
251
  }
82
252
  imeanServiceEngine.logger.info(`[CDNCache] \u6B63\u5728\u4E0B\u8F7D\u8D44\u6E90: ${resource.name} (${resource.url})`);
83
253
  const response = await fetch(resource.url);
84
254
  if (!response.ok) {
85
- throw new Error(`Failed to fetch ${resource.url}: ${response.statusText}`);
255
+ throw new Error(
256
+ `Failed to fetch ${resource.url}: ${response.statusText}`
257
+ );
86
258
  }
87
259
  const content = await response.text();
88
260
  const cached = {
@@ -90,10 +262,14 @@ async function fetchAndCacheResource(resource) {
90
262
  mimeType: resource.mimeType
91
263
  };
92
264
  cache.set(resource.name, cached);
93
- saveToFileCache(resource.name, content, resource.mimeType).catch((error) => {
94
- imeanServiceEngine.logger.warn(`[CDNCache] \u4FDD\u5B58\u6587\u4EF6\u7F13\u5B58\u5931\u8D25: ${resource.name}`, error);
95
- });
96
- imeanServiceEngine.logger.info(`[CDNCache] \u8D44\u6E90\u5DF2\u4E0B\u8F7D\u5E76\u7F13\u5B58: ${resource.name} (${content.length} \u5B57\u8282)`);
265
+ saveToFileCache(resource.name, content, resource.mimeType).catch(
266
+ (error) => {
267
+ imeanServiceEngine.logger.warn(`[CDNCache] \u4FDD\u5B58\u6587\u4EF6\u7F13\u5B58\u5931\u8D25: ${resource.name}`, error);
268
+ }
269
+ );
270
+ imeanServiceEngine.logger.info(
271
+ `[CDNCache] \u8D44\u6E90\u5DF2\u4E0B\u8F7D\u5E76\u7F13\u5B58: ${resource.name} (${content.length} \u5B57\u8282)`
272
+ );
97
273
  } catch (error) {
98
274
  imeanServiceEngine.logger.error(`[CDNCache] \u4E0B\u8F7D\u8D44\u6E90\u5931\u8D25: ${resource.name}`, error);
99
275
  }
@@ -145,6 +321,16 @@ var init_cdn_cache = __esm({
145
321
  name: "alpinejs",
146
322
  url: "https://unpkg.com/alpinejs@latest/dist/cdn.min.js",
147
323
  mimeType: "application/javascript"
324
+ },
325
+ {
326
+ name: "idiomorph",
327
+ url: "https://unpkg.com/idiomorph@0.7.4/dist/idiomorph-ext.min.js",
328
+ mimeType: "application/javascript"
329
+ },
330
+ {
331
+ name: "sortablejs",
332
+ url: "https://unpkg.com/sortablejs@latest/Sortable.min.js",
333
+ mimeType: "application/javascript"
148
334
  }
149
335
  ];
150
336
  cache = /* @__PURE__ */ new Map();
@@ -283,11 +469,17 @@ var BaseFeature = class {
283
469
  return metadata.description;
284
470
  }
285
471
  /**
286
- * 获取操作按钮(默认实现:返回空数组)
287
- * 子类可以覆盖此方法以提供操作按钮
472
+ * 获取操作按钮(默认实现:返回 null)
473
+ * 子类可以覆盖此方法以提供操作按钮(返回 JSX)
288
474
  */
289
475
  async getActions(context) {
290
- return [];
476
+ return null;
477
+ }
478
+ /**
479
+ * 处理请求
480
+ */
481
+ async handle(context) {
482
+ return await this.render?.(context) ?? null;
291
483
  }
292
484
  };
293
485
  function getFieldValue(field, initialData) {
@@ -439,7 +631,7 @@ function renderFormField(field, initialData, formFieldRenderers) {
439
631
  name: field.name,
440
632
  required: field.required,
441
633
  placeholder: field.placeholder || (isJsonString(value) ? "JSON \u683C\u5F0F\u6570\u636E" : ""),
442
- rows: isJsonString(value) ? 10 : 4,
634
+ rows: isJsonString(value) ? Math.max(10, value.split("\n").length) : Math.max(5, Math.ceil(value.length / 100)),
443
635
  className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 resize-y font-mono text-sm",
444
636
  "data-testid": `input-${field.name}`,
445
637
  children: isJsonString(value) ? formatJsonString(value) : value
@@ -590,10 +782,10 @@ function FormPage(props) {
590
782
  id: finalFormId,
591
783
  method: method === "put" ? "post" : method,
592
784
  action: submitUrl,
593
- "hx-boost": "true",
594
785
  ...method === "put" ? { "hx-put": submitUrl } : {},
595
786
  "hx-indicator": "#form-loading-indicator",
596
787
  "data-testid": "form",
788
+ "hx-sync": "this:abort",
597
789
  className: isDialog ? "p-6" : "mt-6",
598
790
  children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6", children: groups.map((group, index) => /* @__PURE__ */ jsxRuntime.jsx(
599
791
  "div",
@@ -618,6 +810,7 @@ function FormPage(props) {
618
810
  method: method === "put" ? "post" : method,
619
811
  action: submitUrl,
620
812
  "hx-boost": "true",
813
+ "hx-trigger": "click from:#form-submit-button",
621
814
  ...method === "put" ? { "hx-put": submitUrl } : {},
622
815
  "hx-indicator": "#form-loading-indicator",
623
816
  className: "space-y-6",
@@ -633,13 +826,195 @@ function FormPage(props) {
633
826
  );
634
827
  }
635
828
 
829
+ // src/utils/json-form-parser.ts
830
+ function parseNestedFormData(formData) {
831
+ const result = {};
832
+ const arrayIndicesMap = /* @__PURE__ */ new Map();
833
+ const arrayIndicesSet = /* @__PURE__ */ new Map();
834
+ for (const [key] of Object.entries(formData)) {
835
+ const parts = parseKey(key);
836
+ collectArrayIndices(parts, arrayIndicesSet);
837
+ }
838
+ for (const [pathKey, indices] of arrayIndicesSet.entries()) {
839
+ const sortedIndices = Array.from(indices).sort((a, b) => a - b);
840
+ const indexMap = /* @__PURE__ */ new Map();
841
+ for (let i = 0; i < sortedIndices.length; i++) {
842
+ indexMap.set(sortedIndices[i], i);
843
+ }
844
+ arrayIndicesMap.set(pathKey, indexMap);
845
+ }
846
+ for (const [key, value] of Object.entries(formData)) {
847
+ const parts = parseKey(key);
848
+ buildNestedObject(result, parts, value, arrayIndicesMap);
849
+ }
850
+ compressArrays(result);
851
+ return result;
852
+ }
853
+ function collectArrayIndices(parts, arrayIndicesSet) {
854
+ for (let i = 0; i < parts.length; i++) {
855
+ const part = parts[i];
856
+ if (typeof part === "number") {
857
+ const pathKey = getArrayPathKey(parts, i);
858
+ if (!arrayIndicesSet.has(pathKey)) {
859
+ arrayIndicesSet.set(pathKey, /* @__PURE__ */ new Set());
860
+ }
861
+ arrayIndicesSet.get(pathKey).add(part);
862
+ }
863
+ }
864
+ }
865
+ function parseKey(key) {
866
+ const result = [];
867
+ let current = "";
868
+ let inBrackets = false;
869
+ let bracketContent = "";
870
+ for (let i = 0; i < key.length; i++) {
871
+ const char = key[i];
872
+ if (char === "[" && !inBrackets) {
873
+ if (current) {
874
+ result.push(current);
875
+ current = "";
876
+ }
877
+ inBrackets = true;
878
+ } else if (char === "]" && inBrackets) {
879
+ inBrackets = false;
880
+ const index = parseInt(bracketContent, 10);
881
+ if (!isNaN(index)) {
882
+ result.push(index);
883
+ }
884
+ bracketContent = "";
885
+ } else if (char === "." && !inBrackets) {
886
+ if (current) {
887
+ result.push(current);
888
+ current = "";
889
+ }
890
+ } else if (inBrackets) {
891
+ bracketContent += char;
892
+ } else {
893
+ current += char;
894
+ }
895
+ }
896
+ if (current) {
897
+ result.push(current);
898
+ }
899
+ return result;
900
+ }
901
+ function getArrayPathKey(parts, endIndex) {
902
+ const pathParts = [];
903
+ for (let i = 0; i < endIndex; i++) {
904
+ const part = parts[i];
905
+ if (typeof part === "string") {
906
+ pathParts.push(part);
907
+ } else {
908
+ pathParts.push(`[${part}]`);
909
+ }
910
+ }
911
+ return pathParts.join(".");
912
+ }
913
+ function getCompressedIndex(pathKey, originalIndex, arrayIndicesMap) {
914
+ if (!arrayIndicesMap.has(pathKey)) {
915
+ arrayIndicesMap.set(pathKey, /* @__PURE__ */ new Map());
916
+ }
917
+ const indexMap = arrayIndicesMap.get(pathKey);
918
+ let compressedIndex = indexMap.get(originalIndex);
919
+ if (compressedIndex === void 0) {
920
+ compressedIndex = indexMap.size;
921
+ indexMap.set(originalIndex, compressedIndex);
922
+ }
923
+ return compressedIndex;
924
+ }
925
+ function buildNestedObject(obj, parts, value, arrayIndicesMap) {
926
+ let current = obj;
927
+ for (let i = 0; i < parts.length - 1; i++) {
928
+ const part = parts[i];
929
+ const nextPart = parts[i + 1];
930
+ if (typeof part === "number") {
931
+ if (!Array.isArray(current)) {
932
+ throw new Error(`Expected array at index ${i}, but got ${typeof current}`);
933
+ }
934
+ const pathKey = getArrayPathKey(parts, i);
935
+ const compressedIndex = getCompressedIndex(
936
+ pathKey,
937
+ part,
938
+ arrayIndicesMap
939
+ );
940
+ while (current.length <= compressedIndex) {
941
+ if (typeof nextPart === "number") {
942
+ current.push([]);
943
+ } else {
944
+ current.push({});
945
+ }
946
+ }
947
+ current = current[compressedIndex];
948
+ } else {
949
+ if (typeof nextPart === "number") {
950
+ if (!current.hasOwnProperty(part) || !Array.isArray(current[part])) {
951
+ current[part] = [];
952
+ }
953
+ } else {
954
+ if (!current.hasOwnProperty(part) || (typeof current[part] !== "object" || current[part] === null || Array.isArray(current[part]))) {
955
+ current[part] = {};
956
+ }
957
+ }
958
+ current = current[part];
959
+ }
960
+ }
961
+ const lastPart = parts[parts.length - 1];
962
+ if (typeof lastPart === "number") {
963
+ const pathKey = getArrayPathKey(parts, parts.length - 1);
964
+ const compressedIndex = getCompressedIndex(
965
+ pathKey,
966
+ lastPart,
967
+ arrayIndicesMap
968
+ );
969
+ if (!Array.isArray(current)) {
970
+ throw new Error(`Expected array for last part, but got ${typeof current}`);
971
+ }
972
+ while (current.length <= compressedIndex) {
973
+ current.push(void 0);
974
+ }
975
+ current[compressedIndex] = value;
976
+ } else {
977
+ if (Array.isArray(current)) {
978
+ throw new Error(`Cannot set property on array: ${lastPart}`);
979
+ }
980
+ if (typeof current !== "object" || current === null) {
981
+ current = {};
982
+ }
983
+ current[lastPart] = value;
984
+ }
985
+ }
986
+ function compressArrays(obj, arrayIndicesMap) {
987
+ if (Array.isArray(obj)) {
988
+ const newArray = [];
989
+ for (let i = 0; i < obj.length; i++) {
990
+ if (obj[i] !== void 0) {
991
+ newArray.push(obj[i]);
992
+ if (typeof obj[i] === "object" && obj[i] !== null) {
993
+ compressArrays(obj[i]);
994
+ }
995
+ }
996
+ }
997
+ obj.length = 0;
998
+ obj.push(...newArray);
999
+ } else if (typeof obj === "object" && obj !== null) {
1000
+ for (const key in obj) {
1001
+ if (obj.hasOwnProperty(key)) {
1002
+ compressArrays(obj[key]);
1003
+ }
1004
+ }
1005
+ }
1006
+ }
1007
+
636
1008
  // src/utils/form-data-processor.ts
1009
+ function parseNestedFormData2(flatData) {
1010
+ return parseNestedFormData(flatData);
1011
+ }
637
1012
  function preprocessFormData(data, zodSchema) {
638
1013
  if (!zodSchema) {
639
1014
  return data;
640
1015
  }
641
1016
  const processed = { ...data };
642
- const def = zodSchema._def;
1017
+ const def = zodSchema._zod?.def || zodSchema._def;
643
1018
  const shape = typeof def.shape === "function" ? def.shape() : def.shape;
644
1019
  if (!shape || typeof shape !== "object") {
645
1020
  return processed;
@@ -657,16 +1032,18 @@ function preprocessFormData(data, zodSchema) {
657
1032
  processed[fieldName] = void 0;
658
1033
  continue;
659
1034
  }
660
- const fieldDef = fieldSchema._def;
661
- let typeName = fieldDef?.type || fieldDef?.typeName;
662
- if (typeName === "optional" || typeName === "ZodOptional") {
663
- const innerType = fieldDef.innerType;
1035
+ const fieldDef = fieldSchema._zod?.def || fieldSchema._def;
1036
+ let typeName = fieldDef?.type;
1037
+ let actualSchema = fieldSchema;
1038
+ if (typeName === "optional") {
1039
+ const innerType = fieldDef?.innerType;
664
1040
  if (innerType) {
665
- const innerDef = innerType._def;
666
- typeName = innerDef?.type || innerDef?.typeName;
1041
+ actualSchema = innerType;
1042
+ const innerDef = innerType._zod?.def || innerType._def;
1043
+ typeName = innerDef?.type;
667
1044
  }
668
1045
  }
669
- if (typeName === "number" || typeName === "ZodNumber") {
1046
+ if (typeName === "number" || typeName === "int") {
670
1047
  if (typeof value === "string") {
671
1048
  const trimmed = value.trim();
672
1049
  if (trimmed !== "") {
@@ -679,7 +1056,7 @@ function preprocessFormData(data, zodSchema) {
679
1056
  processed[fieldName] = value;
680
1057
  }
681
1058
  }
682
- if (typeName === "boolean" || typeName === "ZodBoolean") {
1059
+ if (typeName === "boolean") {
683
1060
  if (typeof value === "string") {
684
1061
  const trimmed = value.trim().toLowerCase();
685
1062
  if (trimmed === "true" || trimmed === "1" || trimmed === "on") {
@@ -691,8 +1068,64 @@ function preprocessFormData(data, zodSchema) {
691
1068
  processed[fieldName] = value;
692
1069
  }
693
1070
  }
694
- if (typeName === "array" || typeName === "ZodArray") {
695
- if (typeof value === "string") {
1071
+ if (typeName === "array") {
1072
+ if (Array.isArray(value)) {
1073
+ const arraySchema = actualSchema;
1074
+ const arrayDef = arraySchema._zod?.def || arraySchema._def;
1075
+ let elementType = arrayDef?.element;
1076
+ if (!elementType) {
1077
+ processed[fieldName] = value;
1078
+ } else {
1079
+ const elementDef = elementType._zod?.def || elementType._def;
1080
+ let elementTypeName = elementDef?.type;
1081
+ if (elementTypeName === "optional") {
1082
+ const innerElementType = elementDef?.innerType;
1083
+ if (innerElementType) {
1084
+ elementType = innerElementType;
1085
+ const innerElementDef = innerElementType._zod?.def || innerElementType._def;
1086
+ elementTypeName = innerElementDef?.type;
1087
+ }
1088
+ }
1089
+ processed[fieldName] = value.map((item) => {
1090
+ if (typeof item === "object" && item !== null && !Array.isArray(item)) {
1091
+ if (elementTypeName === "object") {
1092
+ const processedItem = preprocessFormData(
1093
+ item,
1094
+ elementType
1095
+ );
1096
+ return { ...item, ...processedItem };
1097
+ }
1098
+ return item;
1099
+ }
1100
+ if (typeof item === "string") {
1101
+ return convertValueByType(item, elementType);
1102
+ }
1103
+ if (Array.isArray(item)) {
1104
+ const elementDef2 = elementType._zod?.def || elementType._def;
1105
+ const nestedInnerType = elementDef2?.element;
1106
+ if (nestedInnerType) {
1107
+ return item.map((subItem) => {
1108
+ if (typeof subItem === "string") {
1109
+ return convertValueByType(subItem, nestedInnerType);
1110
+ }
1111
+ if (typeof subItem === "object" && subItem !== null) {
1112
+ const subItemDef = nestedInnerType._zod?.def || nestedInnerType._def;
1113
+ if (subItemDef?.type === "object") {
1114
+ return preprocessFormData(
1115
+ subItem,
1116
+ nestedInnerType
1117
+ );
1118
+ }
1119
+ }
1120
+ return subItem;
1121
+ });
1122
+ }
1123
+ return item;
1124
+ }
1125
+ return item;
1126
+ });
1127
+ }
1128
+ } else if (typeof value === "string") {
696
1129
  const trimmed = value.trim();
697
1130
  if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
698
1131
  try {
@@ -706,8 +1139,11 @@ function preprocessFormData(data, zodSchema) {
706
1139
  }
707
1140
  }
708
1141
  }
709
- if (typeName === "object" || typeName === "ZodObject") {
710
- if (typeof value === "string" && value.trim() !== "") {
1142
+ if (typeName === "object") {
1143
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
1144
+ const objectSchema = actualSchema;
1145
+ processed[fieldName] = preprocessFormData(value, objectSchema);
1146
+ } else if (typeof value === "string" && value.trim() !== "") {
711
1147
  try {
712
1148
  const trimmed = value.trim();
713
1149
  if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
@@ -718,7 +1154,7 @@ function preprocessFormData(data, zodSchema) {
718
1154
  }
719
1155
  }
720
1156
  }
721
- if (typeName === "any" || typeName === "ZodAny") {
1157
+ if (typeName === "any" || typeName === "unknown") {
722
1158
  if (typeof value === "string" && value.trim() !== "") {
723
1159
  try {
724
1160
  const trimmed = value.trim();
@@ -733,6 +1169,36 @@ function preprocessFormData(data, zodSchema) {
733
1169
  }
734
1170
  return processed;
735
1171
  }
1172
+ function convertValueByType(value, schema) {
1173
+ const def = schema._zod?.def || schema._def;
1174
+ let typeName = def?.type;
1175
+ if (typeName === "optional") {
1176
+ const innerType = def?.innerType;
1177
+ if (innerType) {
1178
+ const innerDef = innerType._zod?.def || innerType._def;
1179
+ typeName = innerDef?.type;
1180
+ }
1181
+ }
1182
+ if (typeName === "number" || typeName === "int") {
1183
+ const trimmed = value.trim();
1184
+ if (trimmed !== "") {
1185
+ const numValue = Number(trimmed);
1186
+ if (!isNaN(numValue)) {
1187
+ return numValue;
1188
+ }
1189
+ }
1190
+ return value;
1191
+ } else if (typeName === "boolean") {
1192
+ const trimmed = value.trim().toLowerCase();
1193
+ if (trimmed === "true" || trimmed === "1" || trimmed === "on") {
1194
+ return true;
1195
+ } else if (trimmed === "false" || trimmed === "0" || trimmed === "off" || trimmed === "") {
1196
+ return false;
1197
+ }
1198
+ return value;
1199
+ }
1200
+ return value;
1201
+ }
736
1202
 
737
1203
  // src/utils/schema-utils.ts
738
1204
  function parseSchemaToFields(schema) {
@@ -1023,22 +1489,9 @@ var BaseFormFeature = class extends BaseFeature {
1023
1489
  if (ctx.req.method === "GET") {
1024
1490
  return this.render(context);
1025
1491
  } else if (ctx.req.method === "POST" || ctx.req.method === "PUT" || ctx.req.method === "PATCH") {
1026
- const methodOverride = ctx.req.header("X-HTTP-Method-Override");
1027
- const actualMethod = methodOverride || ctx.req.method;
1028
- const expectedMethod = this.getFormAction() === "edit" ? "PUT" : "POST";
1029
- if (actualMethod.toUpperCase() !== expectedMethod) {
1030
- imeanServiceEngine.logger.warn(
1031
- `[BaseFormFeature] Method mismatch: expected ${expectedMethod}, got ${actualMethod} (request method: ${ctx.req.method}, X-HTTP-Method-Override: ${methodOverride || "none"})`
1032
- );
1033
- }
1034
1492
  const originalData = { ...context.body };
1035
- imeanServiceEngine.logger.info(
1036
- `[BaseFormFeature] Original body data: ${JSON.stringify(originalData)}`
1037
- );
1038
- let data = this.preprocessFormData(context.body);
1039
- imeanServiceEngine.logger.info(
1040
- `[BaseFormFeature] Preprocessed data: ${JSON.stringify(data)}`
1041
- );
1493
+ const nestedData = parseNestedFormData2(context.body);
1494
+ let data = this.preprocessFormData(nestedData);
1042
1495
  if (!this.schema) {
1043
1496
  throw new Error("Schema is required for form validation");
1044
1497
  }
@@ -1050,9 +1503,6 @@ var BaseFormFeature = class extends BaseFeature {
1050
1503
  const errorMessage = firstError.message;
1051
1504
  const errorText = fieldName ? `${fieldName}: ${errorMessage}` : errorMessage;
1052
1505
  context.sendError("\u9A8C\u8BC1\u5931\u8D25", errorText);
1053
- imeanServiceEngine.logger.info(
1054
- `[BaseFormFeature] Validation failed, returning form with originalData: ${JSON.stringify(originalData)}`
1055
- );
1056
1506
  return this.render(context, originalData);
1057
1507
  }
1058
1508
  const item = await this.handleSubmit(
@@ -1072,9 +1522,6 @@ var BaseFormFeature = class extends BaseFeature {
1072
1522
  `${context.model.getMetadata().title}\u5DF2\u6210\u529F${actionText}`
1073
1523
  );
1074
1524
  if (context.isDialog) {
1075
- imeanServiceEngine.logger.info(
1076
- `[BaseFormFeature] Dialog mode: setting refresh to close dialog and refresh list`
1077
- );
1078
1525
  context.setRefresh(true);
1079
1526
  if (context.redirectUrl) {
1080
1527
  context.redirectUrl = void 0;
@@ -1082,9 +1529,6 @@ var BaseFormFeature = class extends BaseFeature {
1082
1529
  return null;
1083
1530
  } else {
1084
1531
  const redirectUrl = this.getSuccessRedirectUrl(context, item);
1085
- imeanServiceEngine.logger.info(
1086
- `[BaseFormFeature] Page mode: Setting redirect URL: ${redirectUrl} (isHtmxRequest: ${context.isHtmxRequest})`
1087
- );
1088
1532
  context.redirect(redirectUrl);
1089
1533
  return null;
1090
1534
  }
@@ -1106,9 +1550,6 @@ var BaseFormFeature = class extends BaseFeature {
1106
1550
  } else {
1107
1551
  formData = initialData;
1108
1552
  }
1109
- imeanServiceEngine.logger.info(
1110
- `[BaseFormFeature] render: initialData=${JSON.stringify(initialData)}, formData=${JSON.stringify(formData)}`
1111
- );
1112
1553
  let submitUrl = this.getSubmitUrl(context);
1113
1554
  if (context.isDialog) {
1114
1555
  const url = new URL(submitUrl, "http://localhost");
@@ -1175,7 +1616,7 @@ var BaseFormFeature = class extends BaseFeature {
1175
1616
  );
1176
1617
  }
1177
1618
  /**
1178
- * 获取操作按钮
1619
+ * 获取操作按钮(返回 JSX)
1179
1620
  */
1180
1621
  async getActions(context) {
1181
1622
  const actions = [];
@@ -1203,7 +1644,8 @@ var BaseFormFeature = class extends BaseFeature {
1203
1644
  });
1204
1645
  }
1205
1646
  }
1206
- return actions;
1647
+ const { renderActionButtons: renderActionButtons2 } = await Promise.resolve().then(() => (init_action_button_renderer(), action_button_renderer_exports));
1648
+ return renderActionButtons2(actions);
1207
1649
  }
1208
1650
  };
1209
1651
 
@@ -1252,7 +1694,7 @@ var CustomFeature = class extends BaseFeature {
1252
1694
  if (this.handlerFn) {
1253
1695
  return await this.handlerFn(context);
1254
1696
  }
1255
- return void 0;
1697
+ return await this.render(context);
1256
1698
  }
1257
1699
  async render(context) {
1258
1700
  if (this.renderFn) {
@@ -1395,7 +1837,13 @@ function renderDefaultValue(value) {
1395
1837
  return /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "bg-gray-50 border border-gray-200 rounded p-3 text-xs overflow-x-auto", children: JSON.stringify(value, null, 2) });
1396
1838
  }
1397
1839
  if (typeof value === "boolean") {
1398
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `px-2 py-1 rounded text-sm font-medium ${value ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-600"}`, children: value ? "\u662F" : "\u5426" });
1840
+ return /* @__PURE__ */ jsxRuntime.jsx(
1841
+ "span",
1842
+ {
1843
+ className: `px-2 py-1 rounded text-sm font-medium ${value ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-600"}`,
1844
+ children: value ? "\u662F" : "\u5426"
1845
+ }
1846
+ );
1399
1847
  }
1400
1848
  if (typeof value === "number") {
1401
1849
  return /* @__PURE__ */ jsxRuntime.jsx("span", { children: value.toLocaleString() });
@@ -1421,8 +1869,20 @@ function renderField(field, value, item) {
1421
1869
  {
1422
1870
  className: "\r\n px-4 py-4 sm:px-6 sm:py-5\r\n flex flex-col\r\n hover:bg-gray-50/50 transition-colors duration-150\r\n group\r\n gap-1.5\r\n ",
1423
1871
  children: [
1424
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "\r\n text-xs sm:text-sm font-semibold text-gray-600 sm:text-gray-700\r\n flex items-start\r\n leading-tight sm:leading-5\r\n tracking-wide\r\n ", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 uppercase sm:normal-case", children: field.label }) }),
1425
- /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "\r\n text-sm sm:text-base text-gray-900\r\n break-words\r\n leading-relaxed\r\n min-w-0\r\n ", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0", children: content }) })
1872
+ /* @__PURE__ */ jsxRuntime.jsx(
1873
+ "dt",
1874
+ {
1875
+ className: "\r\n text-xs sm:text-sm font-semibold text-gray-600 sm:text-gray-700\r\n flex items-start\r\n leading-tight sm:leading-5\r\n tracking-wide\r\n ",
1876
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 uppercase sm:normal-case", children: field.label })
1877
+ }
1878
+ ),
1879
+ /* @__PURE__ */ jsxRuntime.jsx(
1880
+ "dd",
1881
+ {
1882
+ className: "\r\n text-sm sm:text-base text-gray-900\r\n break-words\r\n leading-relaxed\r\n min-w-0\r\n ",
1883
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0", children: content })
1884
+ }
1885
+ )
1426
1886
  ]
1427
1887
  },
1428
1888
  field.key
@@ -1456,7 +1916,6 @@ function DetailPage(props) {
1456
1916
  }
1457
1917
  var DefaultDetailFeature = class extends BaseFeature {
1458
1918
  getItem;
1459
- deleteItem;
1460
1919
  titleGetter;
1461
1920
  descriptionGetter;
1462
1921
  detailFieldNames;
@@ -1473,7 +1932,6 @@ var DefaultDetailFeature = class extends BaseFeature {
1473
1932
  this.schema = options.schema;
1474
1933
  this.fields = parseSchemaToFields(options.schema);
1475
1934
  this.getItem = options.getItem;
1476
- this.deleteItem = options.deleteItem;
1477
1935
  this.titleGetter = options.getTitle;
1478
1936
  this.descriptionGetter = options.getDescription;
1479
1937
  this.detailFieldNames = options.detailFieldNames;
@@ -1515,32 +1973,40 @@ var DefaultDetailFeature = class extends BaseFeature {
1515
1973
  }
1516
1974
  const schema = this.schema;
1517
1975
  const groupSchemas = this.groups.map((group) => {
1518
- const pickObject = group.fields.reduce((acc, fieldName) => {
1519
- acc[fieldName] = true;
1520
- return acc;
1521
- }, {});
1976
+ const pickObject = group.fields.reduce(
1977
+ (acc, fieldName) => {
1978
+ acc[fieldName] = true;
1979
+ return acc;
1980
+ },
1981
+ {}
1982
+ );
1522
1983
  return {
1523
1984
  label: group.label,
1524
1985
  schema: schema.pick(pickObject),
1525
1986
  fields: group.fields
1526
1987
  };
1527
1988
  });
1528
- const groupFields = groupSchemas.map(({ label, schema: schema2, fields: fieldNames }) => {
1529
- const groupFields2 = parseSchemaToFields(schema2);
1530
- const detailFields2 = groupFields2.map((field) => ({
1531
- key: field.name,
1532
- label: field.label,
1533
- render: this.fieldRenderers?.[field.name]
1534
- }));
1535
- return {
1536
- label,
1537
- fields: detailFields2,
1538
- values: fieldNames.reduce((acc, fieldName) => {
1539
- acc[fieldName] = item[fieldName];
1540
- return acc;
1541
- }, {})
1542
- };
1543
- });
1989
+ const groupFields = groupSchemas.map(
1990
+ ({ label, schema: schema2, fields: fieldNames }) => {
1991
+ const groupFields2 = parseSchemaToFields(schema2);
1992
+ const detailFields2 = groupFields2.map((field) => ({
1993
+ key: field.name,
1994
+ label: field.label,
1995
+ render: this.fieldRenderers?.[field.name]
1996
+ }));
1997
+ return {
1998
+ label,
1999
+ fields: detailFields2,
2000
+ values: fieldNames.reduce(
2001
+ (acc, fieldName) => {
2002
+ acc[fieldName] = item[fieldName];
2003
+ return acc;
2004
+ },
2005
+ {}
2006
+ )
2007
+ };
2008
+ }
2009
+ );
1544
2010
  return /* @__PURE__ */ jsxRuntime.jsx(DetailPage, { item, groups: groupFields });
1545
2011
  }
1546
2012
  const detailFields = this.detailFieldNames ? filterFieldsByNames(this.fields || [], this.detailFieldNames) : this.fields || [];
@@ -1571,7 +2037,7 @@ var DefaultDetailFeature = class extends BaseFeature {
1571
2037
  const id = context.params.id;
1572
2038
  const item = await this.getItem(id);
1573
2039
  if (!item) {
1574
- return [];
2040
+ return null;
1575
2041
  }
1576
2042
  const model = context.model;
1577
2043
  const prefix = context.prefix || "";
@@ -1608,7 +2074,8 @@ var DefaultDetailFeature = class extends BaseFeature {
1608
2074
  });
1609
2075
  }
1610
2076
  }
1611
- return actions;
2077
+ const { renderActionButtons: renderActionButtons2 } = await Promise.resolve().then(() => (init_action_button_renderer(), action_button_renderer_exports));
2078
+ return renderActionButtons2(actions);
1612
2079
  }
1613
2080
  };
1614
2081
 
@@ -1814,77 +2281,16 @@ function FilterForm(props) {
1814
2281
  }
1815
2282
  ) });
1816
2283
  }
1817
- function Button(props) {
1818
- const {
1819
- children,
1820
- variant = "primary",
1821
- size = "md",
1822
- disabled = false,
1823
- className = "",
1824
- hxGet,
1825
- hxPost,
1826
- hxPut,
1827
- hxDelete,
1828
- hxTarget,
1829
- hxSwap,
1830
- hxPushUrl,
1831
- hxIndicator,
1832
- hxConfirm,
1833
- hxHeaders,
1834
- ...rest
1835
- } = props;
1836
- const baseClasses = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2";
1837
- const variantClasses = {
1838
- primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
1839
- secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500",
1840
- danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
1841
- ghost: "bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-500"
1842
- };
1843
- const sizeClasses = {
1844
- sm: "px-3 py-1.5 text-sm",
1845
- md: "px-4 py-2 text-sm",
1846
- lg: "px-6 py-3 text-base"
1847
- };
1848
- const classes = [
1849
- baseClasses,
1850
- variantClasses[variant],
1851
- sizeClasses[size],
1852
- disabled ? "opacity-50 cursor-not-allowed" : "",
1853
- className
1854
- ].filter(Boolean).join(" ");
1855
- const isNewWindow = rest.target === "_blank";
1856
- const htmxAttrs = {};
1857
- if (!isNewWindow) {
1858
- if (hxGet) htmxAttrs["hx-get"] = hxGet;
1859
- if (hxPost) htmxAttrs["hx-post"] = hxPost;
1860
- if (hxPut) htmxAttrs["hx-put"] = hxPut;
1861
- if (hxDelete) htmxAttrs["hx-delete"] = hxDelete;
1862
- if (hxTarget) htmxAttrs["hx-target"] = hxTarget;
1863
- if (hxSwap) htmxAttrs["hx-swap"] = hxSwap;
1864
- if (hxPushUrl !== void 0)
1865
- htmxAttrs["hx-push-url"] = hxPushUrl === true ? "true" : hxPushUrl;
1866
- if (hxIndicator) htmxAttrs["hx-indicator"] = hxIndicator;
1867
- if (hxConfirm) htmxAttrs["hx-confirm"] = hxConfirm;
1868
- if (hxHeaders) htmxAttrs["hx-headers"] = hxHeaders;
1869
- }
1870
- const href = rest.href ?? hxGet ?? "#";
1871
- const { className: _, ...otherRest } = rest;
1872
- return /* @__PURE__ */ jsxRuntime.jsx(
1873
- "a",
1874
- {
1875
- className: classes,
1876
- disabled,
1877
- href,
1878
- ...htmxAttrs,
1879
- ...otherRest,
1880
- children
1881
- }
1882
- );
1883
- }
2284
+
2285
+ // src/components/table.tsx
2286
+ init_button();
1884
2287
  function EmptyState(props) {
1885
2288
  const { message = "\u6682\u65E0\u6570\u636E", children } = props;
1886
2289
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-12", children: children || /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-500 text-sm", children: message }) });
1887
2290
  }
2291
+
2292
+ // src/components/pagination.tsx
2293
+ init_button();
1888
2294
  function Pagination(props) {
1889
2295
  const {
1890
2296
  page,
@@ -1927,8 +2333,8 @@ function Pagination(props) {
1927
2333
  {
1928
2334
  variant: "secondary",
1929
2335
  size: "sm",
1930
- hxGet: buildUrl(page - 1),
1931
2336
  href: buildUrl(page - 1),
2337
+ "hx-get": buildUrl(page - 1),
1932
2338
  "data-testid": "pagination-prev",
1933
2339
  "aria-label": "\u4E0A\u4E00\u9875",
1934
2340
  children: "\u4E0A\u4E00\u9875"
@@ -1939,8 +2345,8 @@ function Pagination(props) {
1939
2345
  {
1940
2346
  variant: "secondary",
1941
2347
  size: "sm",
1942
- hxGet: buildUrl(page + 1),
1943
2348
  href: buildUrl(page + 1),
2349
+ "hx-get": buildUrl(page + 1),
1944
2350
  "data-testid": "pagination-next",
1945
2351
  "aria-label": "\u4E0B\u4E00\u9875",
1946
2352
  children: "\u4E0B\u4E00\u9875"
@@ -1998,10 +2404,10 @@ function TableHeader(props) {
1998
2404
  {
1999
2405
  variant: action.variant || "secondary",
2000
2406
  href: action.href,
2001
- hxGet: action.hxGet,
2002
- hxPost: action.hxPost,
2003
- hxDelete: action.hxDelete,
2004
- hxConfirm: action.hxConfirm,
2407
+ "hx-get": action.hxGet,
2408
+ "hx-post": action.hxPost,
2409
+ "hx-delete": action.hxDelete,
2410
+ "hx-confirm": action.hxConfirm,
2005
2411
  "data-testid": testId,
2006
2412
  children: action.label
2007
2413
  },
@@ -2398,7 +2804,288 @@ var DefaultListFeature = class extends BaseFeature {
2398
2804
  });
2399
2805
  }
2400
2806
  }
2401
- return actions;
2807
+ const { renderActionButtons: renderActionButtons2 } = await Promise.resolve().then(() => (init_action_button_renderer(), action_button_renderer_exports));
2808
+ return renderActionButtons2(actions);
2809
+ }
2810
+ };
2811
+
2812
+ // src/component-system/store.ts
2813
+ var STATE_EXPIRATION_TIME = 864e5;
2814
+ var StateStore = class _StateStore {
2815
+ static instance;
2816
+ state;
2817
+ constructor() {
2818
+ this.state = /* @__PURE__ */ new Map();
2819
+ setInterval(() => {
2820
+ this.state.forEach((state) => {
2821
+ if (state.lastUpdated < Date.now() - STATE_EXPIRATION_TIME) {
2822
+ this.state.delete(state.instanceId);
2823
+ }
2824
+ });
2825
+ }, 3e4);
2826
+ }
2827
+ static get() {
2828
+ if (!_StateStore.instance) {
2829
+ _StateStore.instance = new _StateStore();
2830
+ }
2831
+ return _StateStore.instance;
2832
+ }
2833
+ /** 获取实例状态 */
2834
+ getState(instanceId) {
2835
+ const state = this.state.get(instanceId);
2836
+ if (state) {
2837
+ return state;
2838
+ }
2839
+ const newState = {
2840
+ instanceId,
2841
+ data: {
2842
+ props: {},
2843
+ state: {}
2844
+ },
2845
+ lastUpdated: Date.now()
2846
+ };
2847
+ this.state.set(instanceId, newState);
2848
+ return newState;
2849
+ }
2850
+ /** 设置实例状态 */
2851
+ setState(instanceId, state) {
2852
+ const instanceState = this.getState(instanceId);
2853
+ instanceState.data = Object.assign(instanceState.data.state, state);
2854
+ instanceState.lastUpdated = Date.now();
2855
+ }
2856
+ };
2857
+
2858
+ // src/component-system/utils.ts
2859
+ var globalIdCounter = 0;
2860
+ function generateUniqueId() {
2861
+ return `htmx-cid-${globalIdCounter++}`;
2862
+ }
2863
+ var HTMX_COMPONENT_PREFIX = "/_htmx_components";
2864
+
2865
+ // src/component-system/context.tsx
2866
+ var RenderContext = class {
2867
+ constructor(prefix, instanceId, componentName) {
2868
+ this.prefix = prefix;
2869
+ this.instanceId = instanceId;
2870
+ this.componentName = componentName;
2871
+ }
2872
+ // 生成唯一 ID(使用全局共享计数器,避免冲突)
2873
+ $id() {
2874
+ return generateUniqueId();
2875
+ }
2876
+ setState(state) {
2877
+ StateStore.get().setState(this.instanceId, state);
2878
+ }
2879
+ get state() {
2880
+ return StateStore.get().getState(this.instanceId).data.state;
2881
+ }
2882
+ get props() {
2883
+ return StateStore.get().getState(this.instanceId).data.props;
2884
+ }
2885
+ // 生成方法 URL(使用当前 instanceId)
2886
+ url(methodName, params) {
2887
+ const baseUrl = `${this.prefix}/${HTMX_COMPONENT_PREFIX}/${this.componentName}/${this.instanceId}/${methodName}`;
2888
+ if (params && Object.keys(params).length > 0) {
2889
+ const queryString = new URLSearchParams(
2890
+ Object.entries(params).reduce(
2891
+ (acc, [key, value]) => {
2892
+ acc[key] = String(value);
2893
+ return acc;
2894
+ },
2895
+ {}
2896
+ )
2897
+ ).toString();
2898
+ return `${baseUrl}?${queryString}`;
2899
+ }
2900
+ return baseUrl;
2901
+ }
2902
+ callMethod(methodName, params) {
2903
+ const selectors = Object.entries(params).map(([name, expression]) => `${name}:${expression}`).join(",");
2904
+ return {
2905
+ "hx-post": this.url(methodName),
2906
+ "hx-vals": `js:{_params_:{${selectors}}}`,
2907
+ "hx-params": "_state_,_params_,_this_value_"
2908
+ };
2909
+ }
2910
+ };
2911
+ var ComponentContext = class extends RenderContext {
2912
+ constructor(prefix, ctx, componentName) {
2913
+ const routeParams = ctx.req.param();
2914
+ const instanceId = String(routeParams.instanceId || "");
2915
+ super(prefix, instanceId, componentName);
2916
+ this.ctx = ctx;
2917
+ }
2918
+ // 获取所有参数(统一接口:聚合 query string 和 body)
2919
+ async params() {
2920
+ const params = {};
2921
+ const routeParams = this.ctx.req.param();
2922
+ Object.assign(params, routeParams);
2923
+ const url = new URL(this.ctx.req.url);
2924
+ for (const [key, value] of url.searchParams.entries()) {
2925
+ params[key] = value;
2926
+ }
2927
+ const contentType = this.ctx.req.header("Content-Type") || "";
2928
+ if (contentType.includes("application/json")) {
2929
+ try {
2930
+ const body = await this.ctx.req.json();
2931
+ Object.assign(params, body);
2932
+ } catch (e) {
2933
+ console.warn("[ComponentContext] Failed to parse JSON body:", e);
2934
+ }
2935
+ } else if (this.ctx.req.method === "POST" || this.ctx.req.method === "PUT" || this.ctx.req.method === "PATCH" || this.ctx.req.method === "DELETE") {
2936
+ try {
2937
+ const formData = await this.ctx.req.formData();
2938
+ for (const [key, value] of formData.entries()) {
2939
+ params[key] = value instanceof File ? value : value.toString();
2940
+ }
2941
+ } catch (e) {
2942
+ console.warn("[ComponentContext] Failed to parse form data:", e);
2943
+ }
2944
+ }
2945
+ return params;
2946
+ }
2947
+ // 获取查询参数
2948
+ query() {
2949
+ const url = new URL(this.ctx.req.url);
2950
+ return Object.fromEntries(url.searchParams.entries());
2951
+ }
2952
+ // 获取请求体
2953
+ async body() {
2954
+ const contentType = this.ctx.req.header("Content-Type") || "";
2955
+ if (contentType.includes("application/json")) {
2956
+ try {
2957
+ return await this.ctx.req.json();
2958
+ } catch (e) {
2959
+ console.warn("[ComponentContext] Failed to parse JSON body:", e);
2960
+ return {};
2961
+ }
2962
+ }
2963
+ try {
2964
+ const formData = await this.ctx.req.formData();
2965
+ const body = {};
2966
+ for (const [key, value] of formData.entries()) {
2967
+ body[key] = value instanceof File ? value : value.toString();
2968
+ }
2969
+ return body;
2970
+ } catch (e) {
2971
+ console.warn("[ComponentContext] Failed to parse form data:", e);
2972
+ return {};
2973
+ }
2974
+ }
2975
+ };
2976
+
2977
+ // src/component-system/component.tsx
2978
+ var METHOD_METADATA_KEY = /* @__PURE__ */ Symbol("htmx:method");
2979
+ function Method(config) {
2980
+ return function(target, propertyKey, _descriptor) {
2981
+ if (!target[METHOD_METADATA_KEY]) {
2982
+ target[METHOD_METADATA_KEY] = /* @__PURE__ */ new Map();
2983
+ }
2984
+ target[METHOD_METADATA_KEY].set(propertyKey, {
2985
+ method: config?.method || "get",
2986
+ path: config?.path
2987
+ });
2988
+ };
2989
+ }
2990
+ var HtmxComponent = class {
2991
+ constructor(name) {
2992
+ this.name = name;
2993
+ }
2994
+ prefix;
2995
+ // 组件函数
2996
+ Component = (props) => {
2997
+ const instanceId = generateUniqueId();
2998
+ const state = StateStore.get().getState(instanceId).data;
2999
+ state.props = props;
3000
+ state.state = {};
3001
+ const renderCtx = new RenderContext(
3002
+ this.prefix,
3003
+ instanceId,
3004
+ this.name
3005
+ );
3006
+ return this.render(renderCtx, props);
3007
+ };
3008
+ // 返回 JSX script 元素
3009
+ // 获取所有标记为 @Method() 的方法
3010
+ // 注意:handler 不再绑定 this,因为方法会接收 ComponentContext 作为第一个参数
3011
+ static getMethods(component) {
3012
+ const methods = /* @__PURE__ */ new Map();
3013
+ const metadata = component[METHOD_METADATA_KEY];
3014
+ if (!metadata) return methods;
3015
+ for (const [methodName, config] of metadata.entries()) {
3016
+ const handler = component[methodName];
3017
+ methods.set(methodName, {
3018
+ method: config.method,
3019
+ path: config.path,
3020
+ handler
3021
+ });
3022
+ }
3023
+ return methods;
3024
+ }
3025
+ };
3026
+ var HtmxComponentHandler = class {
3027
+ constructor(hono, prefix, components) {
3028
+ this.hono = hono;
3029
+ this.prefix = prefix;
3030
+ this.components = components;
3031
+ for (const component of this.components) {
3032
+ component.prefix = this.prefix;
3033
+ this.registerHandler(component);
3034
+ }
3035
+ }
3036
+ registerHandler(component) {
3037
+ const methods = HtmxComponent.getMethods(component);
3038
+ for (const [methodName, methodConfig] of methods) {
3039
+ const routePath = `${this.prefix}/${HTMX_COMPONENT_PREFIX}/${component.name}/:instanceId/${methodName}`;
3040
+ imeanServiceEngine.logger.info(
3041
+ `[HtmxComponent] Registering handler ${methodConfig.method} ${routePath}`
3042
+ );
3043
+ this.hono[methodConfig.method](routePath, async (ctx) => {
3044
+ return this.handleComponentMethod(
3045
+ ctx,
3046
+ component,
3047
+ methodConfig.handler.bind(component)
3048
+ );
3049
+ });
3050
+ }
3051
+ }
3052
+ async handleComponentMethod(ctx, component, handler) {
3053
+ const componentContext = new ComponentContext(
3054
+ this.prefix,
3055
+ ctx,
3056
+ component.name
3057
+ );
3058
+ const result = await handler(componentContext);
3059
+ if (result instanceof Object && ("target" in result || "swap" in result || "body" in result || "oobs" in result)) {
3060
+ const { target, swap, body, oobs, trigger } = result;
3061
+ const headers = {};
3062
+ let bodyContent = body;
3063
+ if (target) headers["HX-Retarget"] = target;
3064
+ if (swap) headers["HX-Reswap"] = swap;
3065
+ if (trigger) headers["HX-Trigger"] = trigger;
3066
+ if (oobs) {
3067
+ oobs.forEach((oob) => {
3068
+ oob.props["hx-swap-oob"] = "true";
3069
+ });
3070
+ if (!body) {
3071
+ headers["HX-Reswap"] = "delete";
3072
+ }
3073
+ bodyContent = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3074
+ body,
3075
+ oobs
3076
+ ] });
3077
+ }
3078
+ return ctx.html(bodyContent, 200, headers);
3079
+ }
3080
+ if (result === null) {
3081
+ return ctx.html(/* @__PURE__ */ jsxRuntime.jsx("div", {}), 200, {
3082
+ "HX-Reswap": "none"
3083
+ });
3084
+ }
3085
+ if (result instanceof Response) {
3086
+ return result;
3087
+ }
3088
+ return ctx.html(result, 200);
2402
3089
  }
2403
3090
  };
2404
3091
 
@@ -2698,131 +3385,35 @@ async function createFeatureContext(ctx, page, feature, user, options) {
2698
3385
  };
2699
3386
  return featureContext;
2700
3387
  }
2701
- var CLOSE_DIALOG_SCRIPT = `on click
2702
- add .dialog-exit to .dialog-backdrop
2703
- add .dialog-content-exit to .dialog-content
2704
- wait 200ms
2705
- set #dialog-container's innerHTML to '' end`;
2706
- function renderActionButton(action, index) {
3388
+ function Dialog(props) {
2707
3389
  const {
2708
- label,
2709
- href,
2710
- hxGet,
2711
- hxPost,
2712
- hxPut,
2713
- hxDelete,
2714
- variant = "primary",
2715
- confirm,
2716
- close,
2717
- submit,
2718
- formId,
2719
- onClick,
2720
- className = ""
2721
- } = action;
2722
- if (submit && formId) {
2723
- const variantStyles = {
2724
- primary: "bg-blue-600 text-white hover:bg-blue-700",
2725
- secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
2726
- danger: "bg-red-600 text-white hover:bg-red-700",
2727
- ghost: "bg-transparent text-gray-700 hover:bg-gray-100"
2728
- };
2729
- const buttonStyle = variantStyles[variant] || variantStyles.primary;
2730
- const testId2 = label === "\u521B\u5EFA" || label === "\u66F4\u65B0" ? "submit-button" : `action-${label}`;
2731
- return /* @__PURE__ */ jsxRuntime.jsx(
2732
- "button",
2733
- {
2734
- type: "submit",
2735
- form: formId,
2736
- className: `px-4 py-2 rounded transition-colors font-medium ${buttonStyle} ${className}`,
2737
- "data-testid": testId2,
2738
- ...confirm && { "data-confirm": confirm },
2739
- children: label
2740
- },
2741
- index
2742
- );
2743
- }
2744
- const finalOnClick = close ? CLOSE_DIALOG_SCRIPT : onClick;
2745
- if (finalOnClick) {
2746
- const variantStyles = {
2747
- primary: "bg-blue-600 text-white hover:bg-blue-700",
2748
- secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
2749
- danger: "bg-red-600 text-white hover:bg-red-700",
2750
- ghost: "bg-transparent text-gray-700 hover:bg-gray-100"
2751
- };
2752
- const buttonStyle = variantStyles[variant] || variantStyles.secondary;
2753
- let testId2 = `action-${label}`;
2754
- if (label === "\u53D6\u6D88") {
2755
- testId2 = "cancel-button";
2756
- } else if (label === "\u5173\u95ED") {
2757
- testId2 = "close-button";
2758
- }
2759
- return /* @__PURE__ */ jsxRuntime.jsx(
2760
- "button",
2761
- {
2762
- type: "button",
2763
- _: finalOnClick,
2764
- className: `px-4 py-2 rounded transition-colors font-medium ${buttonStyle} ${className}`,
2765
- "data-testid": testId2,
2766
- ...confirm && { "data-confirm": confirm },
2767
- children: label
2768
- },
2769
- index
2770
- );
2771
- }
2772
- let testId = `action-${label}`;
2773
- if (label === "\u65B0\u5EFA" || label === "\u521B\u5EFA") {
2774
- testId = "create-button";
2775
- } else if (label === "\u53D6\u6D88") {
2776
- testId = "cancel-button";
2777
- } else if (label === "\u5173\u95ED") {
2778
- testId = "close-button";
2779
- }
2780
- return /* @__PURE__ */ jsxRuntime.jsx(
2781
- Button,
2782
- {
2783
- variant,
2784
- href,
2785
- hxGet,
2786
- hxPost,
2787
- hxPut,
2788
- hxDelete,
2789
- hxConfirm: confirm,
2790
- className,
2791
- "data-testid": testId,
2792
- children: label
2793
- },
2794
- index
2795
- );
2796
- }
2797
- function Dialog(props) {
2798
- const {
2799
- title,
2800
- children,
2801
- showClose = true,
2802
- className = "",
2803
- size = "lg",
2804
- closeOnBackdropClick = true,
2805
- actions = [],
2806
- fixedContentHeight = false
2807
- } = props;
2808
- const sizeClasses = {
2809
- sm: "max-w-md",
2810
- md: "max-w-lg",
2811
- lg: "max-w-2xl",
2812
- xl: "max-w-4xl",
2813
- full: "max-w-7xl"
2814
- };
2815
- const backdropClickHandler = closeOnBackdropClick ? `on click if event.target is me
2816
- add .dialog-exit to me
2817
- add .dialog-content-exit to .dialog-content
2818
- wait 200ms
2819
- set #dialog-container's innerHTML to '' end` : "";
2820
- return /* @__PURE__ */ jsxRuntime.jsx(
2821
- "div",
2822
- {
2823
- className: "fixed inset-0 bg-black bg-opacity-50 z-[100] flex items-center justify-center p-4 dialog-backdrop",
2824
- style: {
2825
- animation: "fadeIn 0.2s ease-out"
3390
+ title,
3391
+ children,
3392
+ showClose = true,
3393
+ className = "",
3394
+ size = "lg",
3395
+ closeOnBackdropClick = true,
3396
+ actions = [],
3397
+ fixedContentHeight = false
3398
+ } = props;
3399
+ const sizeClasses = {
3400
+ sm: "max-w-md",
3401
+ md: "max-w-lg",
3402
+ lg: "max-w-2xl",
3403
+ xl: "max-w-4xl",
3404
+ full: "max-w-7xl"
3405
+ };
3406
+ const backdropClickHandler = closeOnBackdropClick ? `on click if event.target is me
3407
+ add .dialog-exit to me
3408
+ add .dialog-content-exit to .dialog-content
3409
+ wait 200ms
3410
+ set #dialog-container's innerHTML to '' end` : "";
3411
+ return /* @__PURE__ */ jsxRuntime.jsx(
3412
+ "div",
3413
+ {
3414
+ className: "fixed inset-0 bg-black bg-opacity-50 z-[100] flex items-center justify-center p-4 dialog-backdrop",
3415
+ style: {
3416
+ animation: "fadeIn 0.2s ease-out"
2826
3417
  },
2827
3418
  _: backdropClickHandler,
2828
3419
  children: /* @__PURE__ */ jsxRuntime.jsxs(
@@ -2869,13 +3460,16 @@ function Dialog(props) {
2869
3460
  children
2870
3461
  }
2871
3462
  ),
2872
- actions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 border-t border-gray-200 bg-white flex justify-end gap-2", children: actions.map((action, index) => renderActionButton(action, index)) })
3463
+ actions && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 border-t border-gray-200 bg-white flex justify-end gap-2", children: actions })
2873
3464
  ]
2874
3465
  }
2875
3466
  )
2876
3467
  }
2877
3468
  );
2878
3469
  }
3470
+
3471
+ // src/components/permission-denied.tsx
3472
+ init_button();
2879
3473
  function PermissionDeniedContent(props) {
2880
3474
  const {
2881
3475
  operationId,
@@ -2953,6 +3547,178 @@ function PermissionDeniedPage(props) {
2953
3547
  }
2954
3548
  ) }) });
2955
3549
  }
3550
+ var getResourceUrl = (prefix, name) => {
3551
+ return `${prefix}/_cdn/${name}`;
3552
+ };
3553
+ function globalScripts(prefix) {
3554
+ return html.html`
3555
+ <script src=${getResourceUrl(prefix, "htmx")}></script>
3556
+ <script src=${getResourceUrl(prefix, "htmx-ext-form-json")}></script>
3557
+ <script src=${getResourceUrl(prefix, "hyperscript")}></script>
3558
+ <script src=${getResourceUrl(prefix, "tailwindcss")}></script>
3559
+ <script src=${getResourceUrl(prefix, "alpinejs")} defer></script>
3560
+ <script src=${getResourceUrl(prefix, "sortablejs")}></script>
3561
+ <script src=${getResourceUrl(prefix, "idiomorph")}></script>
3562
+ <script type="module" src=${getResourceUrl(prefix, "datastar")}></script>
3563
+ `;
3564
+ }
3565
+ function globalStyles() {
3566
+ return html.html`<style>
3567
+ @keyframes fadeIn {
3568
+ from {
3569
+ opacity: 0;
3570
+ }
3571
+ to {
3572
+ opacity: 1;
3573
+ }
3574
+ }
3575
+
3576
+ @keyframes slideIn {
3577
+ from {
3578
+ opacity: 0;
3579
+ transform: scale(0.95) translateY(-10px);
3580
+ }
3581
+ to {
3582
+ opacity: 1;
3583
+ transform: scale(1) translateY(0);
3584
+ }
3585
+ }
3586
+
3587
+ @keyframes slideInRight {
3588
+ from {
3589
+ opacity: 0;
3590
+ transform: translateX(100%);
3591
+ }
3592
+ to {
3593
+ opacity: 1;
3594
+ transform: translateX(0);
3595
+ }
3596
+ }
3597
+
3598
+ @keyframes slideOutRight {
3599
+ from {
3600
+ opacity: 1;
3601
+ transform: translateX(0);
3602
+ }
3603
+ to {
3604
+ opacity: 0;
3605
+ transform: translateX(100%);
3606
+ }
3607
+ }
3608
+
3609
+ @keyframes fadeOut {
3610
+ from {
3611
+ opacity: 1;
3612
+ }
3613
+ to {
3614
+ opacity: 0;
3615
+ }
3616
+ }
3617
+
3618
+ @keyframes scaleOut {
3619
+ from {
3620
+ opacity: 1;
3621
+ transform: scale(1) translateY(0);
3622
+ }
3623
+ to {
3624
+ opacity: 0;
3625
+ transform: scale(0.95) translateY(-10px);
3626
+ }
3627
+ }
3628
+
3629
+ /* Dialog 退出动画 */
3630
+ .dialog-exit {
3631
+ animation: fadeOut 0.2s ease-in forwards !important;
3632
+ }
3633
+
3634
+ .dialog-content-exit {
3635
+ animation: scaleOut 0.2s ease-in forwards !important;
3636
+ }
3637
+
3638
+ .error-alert-exit {
3639
+ animation:
3640
+ slideOutRight 0.3s ease-in forwards,
3641
+ fadeOut 0.3s ease-in forwards;
3642
+ }
3643
+
3644
+ /* SortableList 拖拽样式 */
3645
+ /* 使用伪元素创建覆盖层,不影响原始布局 */
3646
+ .sortable-dragging {
3647
+ position: relative !important;
3648
+ opacity: 0.6 !important;
3649
+ transform: scale(0.98) !important;
3650
+ transition: all 0.2s ease-in-out !important;
3651
+ outline: 2px solid rgb(96, 165, 250) !important; /* blue-400 */
3652
+ outline-offset: 2px !important;
3653
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important;
3654
+ }
3655
+
3656
+ .sortable-dragging::before {
3657
+ content: '' !important;
3658
+ position: absolute !important;
3659
+ top: 0 !important;
3660
+ left: 0 !important;
3661
+ right: 0 !important;
3662
+ bottom: 0 !important;
3663
+ background-color: rgba(59, 130, 246, 0.1) !important; /* blue-500 with opacity */
3664
+ border-radius: 0.375rem !important;
3665
+ z-index: 1 !important;
3666
+ pointer-events: none !important;
3667
+ }
3668
+
3669
+ /* 拖拽悬停样式 */
3670
+ .sortable-drag-over {
3671
+ position: relative !important;
3672
+ transition: all 0.2s ease-in-out !important;
3673
+ outline: 2px solid rgb(59, 130, 246) !important; /* blue-500 */
3674
+ outline-offset: 2px !important;
3675
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
3676
+ }
3677
+
3678
+ .sortable-drag-over::before {
3679
+ content: '' !important;
3680
+ position: absolute !important;
3681
+ top: 0 !important;
3682
+ left: 0 !important;
3683
+ right: 0 !important;
3684
+ bottom: 0 !important;
3685
+ background-color: rgba(59, 130, 246, 0.15) !important; /* blue-500 with more opacity */
3686
+ border-radius: 0.375rem !important;
3687
+ z-index: 1 !important;
3688
+ pointer-events: none !important;
3689
+ }
3690
+ </style>`;
3691
+ }
3692
+ function sortableScript() {
3693
+ html.html`<script>
3694
+ if (typeof htmx !== "undefined" && typeof Sortable !== "undefined") {
3695
+ htmx.onLoad(function (content) {
3696
+ var sortables = content.querySelectorAll(".sortable");
3697
+ for (var i = 0; i < sortables.length; i++) {
3698
+ var sortable = sortables[i];
3699
+ // 检查是否已经初始化
3700
+ if (sortable.sortableInstance) {
3701
+ continue;
3702
+ }
3703
+ var sortableInstance = new Sortable(sortable, {
3704
+ animation: 150,
3705
+ ghostClass: "sortable-ghost",
3706
+ filter: ".htmx-indicator",
3707
+ onMove: function (evt) {
3708
+ return evt.related.className.indexOf("htmx-indicator") === -1;
3709
+ },
3710
+ });
3711
+ sortable.sortableInstance = sortableInstance;
3712
+ sortable.addEventListener("htmx:afterSwap", function () {
3713
+ if (sortable.sortableInstance) {
3714
+ sortable.sortableInstance.option("disabled", false);
3715
+ }
3716
+ });
3717
+ }
3718
+ });
3719
+ }
3720
+ </script>`;
3721
+ }
2956
3722
  function Breadcrumb(props) {
2957
3723
  const { items } = props;
2958
3724
  if (items.length === 0) {
@@ -3080,316 +3846,1061 @@ function LoadingBar() {
3080
3846
  ` })
3081
3847
  ] });
3082
3848
  }
3083
- function BaseLayout(props) {
3084
- const { cdnProxyPrefix = "/admin/_cdn" } = props;
3085
- const getResourceUrl = (name, cdnUrl) => {
3086
- return `${cdnProxyPrefix}/${name}`;
3087
- };
3088
- const htmxUrl = "https://unpkg.com/htmx.org@latest";
3089
- const hyperscriptUrl = "https://unpkg.com/hyperscript.org@latest";
3090
- const tailwindUrl = "https://cdn.tailwindcss.com";
3091
- const alpinejsUrl = "https://unpkg.com/alpinejs@latest/dist/cdn.min.js";
3092
- return /* @__PURE__ */ jsxRuntime.jsxs("html", { children: [
3093
- /* @__PURE__ */ jsxRuntime.jsxs("head", { children: [
3094
- /* @__PURE__ */ jsxRuntime.jsx("meta", { charSet: "UTF-8" }),
3095
- /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
3096
- /* @__PURE__ */ jsxRuntime.jsx("title", { children: props.title }),
3097
- props.description && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "description", content: props.description }),
3098
- /* @__PURE__ */ jsxRuntime.jsx(
3099
- "script",
3100
- {
3101
- src: getResourceUrl("htmx"),
3102
- onerror: `this.onerror=null;this.src='${htmxUrl}'`
3103
- }
3104
- ),
3105
- /* @__PURE__ */ jsxRuntime.jsx(
3106
- "script",
3107
- {
3108
- src: getResourceUrl("hyperscript"),
3109
- onerror: `this.onerror=null;this.src='${hyperscriptUrl}'`
3849
+
3850
+ // src/components/index.ts
3851
+ init_button();
3852
+ function SortableList(props) {
3853
+ const {
3854
+ children,
3855
+ className = "",
3856
+ handle,
3857
+ draggingClass = "sortable-dragging",
3858
+ dragOverClass = "sortable-drag-over",
3859
+ onSortableChange,
3860
+ ...rest
3861
+ } = props;
3862
+ const xDataContent = `{
3863
+ draggedIndex: null,
3864
+ dragOverIndex: null,
3865
+ handleSelector: ${JSON.stringify(handle || null)},
3866
+ draggingClass: ${JSON.stringify(draggingClass)},
3867
+ dragOverClass: ${JSON.stringify(dragOverClass)},
3868
+ handleDragStart(event) {
3869
+ // \u4E8B\u4EF6\u59D4\u6258\uFF1A\u5904\u7406\u6240\u6709\u5B50\u5143\u7D20\u7684\u62D6\u62FD\u5F00\u59CB\u4E8B\u4EF6
3870
+ var target = event.target;
3871
+ // \u5982\u679C\u70B9\u51FB\u7684\u662F\u62D6\u62FD\u624B\u67C4\uFF0C\u9700\u8981\u627E\u5230\u7236\u5143\u7D20
3872
+ var item = target.closest('[data-sortable-index]');
3873
+ // \u5982\u679C\u6CA1\u627E\u5230\uFF0C\u53EF\u80FD\u662F\u70B9\u51FB\u4E86\u624B\u67C4\uFF0C\u9700\u8981\u5411\u4E0A\u67E5\u627E
3874
+ if (!item && target.hasAttribute('data-sortable-handle')) {
3875
+ item = target.closest('[data-sortable-index]') || target.parentElement;
3876
+ }
3877
+ if (item && item.hasAttribute('data-sortable-index')) {
3878
+ var index = parseInt(item.getAttribute('data-sortable-index'));
3879
+ this.draggedIndex = index;
3880
+ event.dataTransfer.effectAllowed = 'move';
3881
+ event.dataTransfer.setData('text/html', '');
3882
+ // \u6DFB\u52A0\u62D6\u62FD\u6837\u5F0F\u7C7B\uFF08\u652F\u6301\u591A\u4E2A\u7C7B\u540D\uFF09
3883
+ if (this.draggingClass) {
3884
+ var classes = this.draggingClass.split(' ').filter(function(c) { return c.trim(); });
3885
+ classes.forEach(function(cls) {
3886
+ if (cls) {
3887
+ item.classList.add(cls);
3888
+ console.log('Added dragging class:', cls, 'to element:', item);
3889
+ }
3890
+ });
3110
3891
  }
3111
- ),
3112
- /* @__PURE__ */ jsxRuntime.jsx(
3113
- "script",
3114
- {
3115
- src: getResourceUrl("tailwindcss"),
3116
- onerror: `this.onerror=null;this.src='${tailwindUrl}'`
3892
+ }
3893
+ },
3894
+ handleDragEnd(event) {
3895
+ // \u4E8B\u4EF6\u59D4\u6258\uFF1A\u5904\u7406\u6240\u6709\u5B50\u5143\u7D20\u7684\u62D6\u62FD\u7ED3\u675F\u4E8B\u4EF6
3896
+ var target = event.target;
3897
+ var item = target.closest('[data-sortable-index]');
3898
+ if (item) {
3899
+ // \u79FB\u9664\u62D6\u62FD\u6837\u5F0F\u7C7B\uFF08\u652F\u6301\u591A\u4E2A\u7C7B\u540D\uFF09
3900
+ if (this.draggingClass) {
3901
+ var draggingClasses = this.draggingClass.split(' ').filter(function(c) { return c.trim(); });
3902
+ draggingClasses.forEach(function(cls) {
3903
+ item.classList.remove(cls);
3904
+ });
3117
3905
  }
3118
- ),
3119
- /* @__PURE__ */ jsxRuntime.jsx(
3120
- "script",
3121
- {
3122
- src: getResourceUrl("alpinejs"),
3123
- defer: true,
3124
- onerror: `this.onerror=null;this.src='${alpinejsUrl}'`
3906
+ this.draggedIndex = null;
3907
+ this.dragOverIndex = null;
3908
+ // \u6E05\u9664\u6240\u6709\u62D6\u62FD\u60AC\u505C\u6837\u5F0F
3909
+ if (this.dragOverClass) {
3910
+ var dragOverClasses = this.dragOverClass.split(' ').filter(function(c) { return c.trim(); });
3911
+ Array.from($el.children).forEach(function(c) {
3912
+ dragOverClasses.forEach(function(cls) {
3913
+ c.classList.remove(cls);
3914
+ });
3915
+ });
3125
3916
  }
3126
- ),
3127
- /* @__PURE__ */ jsxRuntime.jsx(
3128
- "style",
3129
- {
3130
- dangerouslySetInnerHTML: {
3131
- __html: `
3132
- /* \u5BB9\u5668\u67E5\u8BE2\u652F\u6301 - \u5982\u679C Tailwind CDN \u4E0D\u652F\u6301\uFF0C\u4F7F\u7528\u539F\u751F CSS \u5BB9\u5668\u67E5\u8BE2 */
3133
- @supports (container-type: inline-size) {
3134
- .\\@container {
3135
- container-type: inline-size;
3917
+ }
3918
+ },
3919
+ handleDragOver(event) {
3920
+ // \u4E8B\u4EF6\u59D4\u6258\uFF1A\u5904\u7406\u6240\u6709\u5B50\u5143\u7D20\u7684\u62D6\u62FD\u60AC\u505C\u4E8B\u4EF6
3921
+ var target = event.target;
3922
+ var item = target.closest('[data-sortable-index]');
3923
+ if (item && this.draggedIndex !== null) {
3924
+ var index = parseInt(item.getAttribute('data-sortable-index'));
3925
+ if (this.draggedIndex !== index) {
3926
+ event.preventDefault();
3927
+ event.dataTransfer.dropEffect = 'move';
3928
+ // \u6E05\u9664\u4E4B\u524D\u60AC\u505C\u5143\u7D20\u7684\u6837\u5F0F
3929
+ if (this.dragOverIndex !== null && this.dragOverIndex !== index && this.dragOverClass) {
3930
+ var self = this;
3931
+ var prevItem = Array.from($el.children).find(function(c) {
3932
+ return c.getAttribute('data-sortable-index') === String(self.dragOverIndex);
3933
+ });
3934
+ if (prevItem) {
3935
+ var dragOverClasses = this.dragOverClass.split(' ').filter(function(c) { return c.trim(); });
3936
+ dragOverClasses.forEach(function(cls) {
3937
+ prevItem.classList.remove(cls);
3938
+ });
3136
3939
  }
3137
3940
  }
3138
-
3139
- @keyframes fadeIn {
3140
- from { opacity: 0;}
3141
- to { opacity: 1;}
3142
- }
3143
-
3144
- @keyframes slideIn {
3145
- from { opacity: 0; transform: scale(0.95) translateY(-10px); }
3146
- to { opacity: 1; transform: scale(1) translateY(0); }
3147
- }
3148
-
3149
- @keyframes slideInRight {
3150
- from { opacity: 0; transform: translateX(100%); }
3151
- to { opacity: 1; transform: translateX(0); }
3152
- }
3153
-
3154
- @keyframes slideOutRight {
3155
- from { opacity: 1; transform: translateX(0); }
3156
- to { opacity: 0; transform: translateX(100%); }
3157
- }
3158
-
3159
- @keyframes fadeOut {
3160
- from { opacity: 1; }
3161
- to { opacity: 0; }
3162
- }
3163
-
3164
- @keyframes scaleOut {
3165
- from { opacity: 1; transform: scale(1) translateY(0); }
3166
- to { opacity: 0; transform: scale(0.95) translateY(-10px); }
3941
+ this.dragOverIndex = index;
3942
+ // \u6DFB\u52A0\u62D6\u62FD\u60AC\u505C\u6837\u5F0F\u7C7B\uFF08\u652F\u6301\u591A\u4E2A\u7C7B\u540D\uFF09
3943
+ if (this.dragOverClass) {
3944
+ var dragOverClasses = this.dragOverClass.split(' ').filter(function(c) { return c.trim(); });
3945
+ dragOverClasses.forEach(function(cls) {
3946
+ item.classList.add(cls);
3947
+ });
3167
3948
  }
3168
-
3169
- /* Dialog \u9000\u51FA\u52A8\u753B */
3170
- .dialog-exit {
3171
- animation: fadeOut 0.2s ease-in forwards !important;
3949
+ }
3950
+ }
3951
+ },
3952
+ handleDragLeave(event) {
3953
+ // \u4E8B\u4EF6\u59D4\u6258\uFF1A\u5904\u7406\u6240\u6709\u5B50\u5143\u7D20\u7684\u62D6\u62FD\u79BB\u5F00\u4E8B\u4EF6
3954
+ // \u53EA\u6709\u5F53\u79BB\u5F00\u7684\u5143\u7D20\u662F\u5F53\u524D\u60AC\u505C\u7684\u5143\u7D20\u65F6\u624D\u79FB\u9664\u6837\u5F0F
3955
+ var target = event.target;
3956
+ var item = target.closest('[data-sortable-index]');
3957
+ if (item && this.dragOverIndex !== null) {
3958
+ var index = parseInt(item.getAttribute('data-sortable-index'));
3959
+ if (index === this.dragOverIndex) {
3960
+ // \u68C0\u67E5\u662F\u5426\u771F\u7684\u79BB\u5F00\u4E86\u5143\u7D20\uFF08\u800C\u4E0D\u662F\u8FDB\u5165\u5B50\u5143\u7D20\uFF09
3961
+ var relatedTarget = event.relatedTarget;
3962
+ if (!item.contains(relatedTarget)) {
3963
+ if (this.dragOverClass) {
3964
+ var dragOverClasses = this.dragOverClass.split(' ').filter(function(c) { return c.trim(); });
3965
+ dragOverClasses.forEach(function(cls) {
3966
+ item.classList.remove(cls);
3967
+ });
3968
+ }
3969
+ this.dragOverIndex = null;
3172
3970
  }
3173
-
3174
- .dialog-content-exit {
3175
- animation: scaleOut 0.2s ease-in forwards !important;
3971
+ }
3972
+ }
3973
+ },
3974
+ handleDrop(event) {
3975
+ // \u4E8B\u4EF6\u59D4\u6258\uFF1A\u5904\u7406\u6240\u6709\u5B50\u5143\u7D20\u7684\u653E\u7F6E\u4E8B\u4EF6
3976
+ var target = event.target;
3977
+ var item = target.closest('[data-sortable-index]');
3978
+ if (item && this.draggedIndex !== null) {
3979
+ event.preventDefault();
3980
+ var index = parseInt(item.getAttribute('data-sortable-index'));
3981
+ if (this.draggedIndex !== index) {
3982
+ // \u4E0D\u79FB\u52A8 DOM\uFF0C\u53EA\u53D1\u51FA\u4EA4\u6362\u4E8B\u4EF6\uFF0C\u8BA9\u7236\u7EA7\uFF08Alpine.js\uFF09\u5904\u7406\u6570\u7EC4\u4EA4\u6362
3983
+ // \u89E6\u53D1\u81EA\u5B9A\u4E49\u4E8B\u4EF6\uFF0C\u901A\u77E5\u7236\u7EC4\u4EF6\u9700\u8981\u4EA4\u6362\u4F4D\u7F6E
3984
+ var changeEvent = new CustomEvent('sortable:change', {
3985
+ bubbles: true,
3986
+ cancelable: true,
3987
+ detail: {
3988
+ oldIndex: this.draggedIndex,
3989
+ newIndex: index,
3990
+ }
3991
+ });
3992
+ $el.dispatchEvent(changeEvent);
3993
+ }
3994
+
3995
+ if (this.dragOverClass) {
3996
+ var dragOverClasses = this.dragOverClass.split(' ').filter(function(c) { return c.trim(); });
3997
+ dragOverClasses.forEach(function(cls) {
3998
+ item.classList.remove(cls);
3999
+ });
4000
+ }
4001
+ this.draggedIndex = null;
4002
+ this.dragOverIndex = null;
4003
+ }
4004
+ },
4005
+ init() {
4006
+ // \u4E3A\u6240\u6709\u76F4\u63A5\u5B50\u5143\u7D20\u6DFB\u52A0\u62D6\u62FD\u652F\u6301
4007
+ var container = $el;
4008
+ var self = this;
4009
+
4010
+ var initSortable = function() {
4011
+ // \u53EA\u5904\u7406\u5B9E\u9645\u6E32\u67D3\u7684\u5143\u7D20\uFF0C\u6392\u9664 template \u5143\u7D20
4012
+ // Alpine.js \u7684 x-for \u4F1A\u5728 template \u7684\u4F4D\u7F6E\u63D2\u5165\u5B9E\u9645\u6E32\u67D3\u7684\u5143\u7D20
4013
+ var children = Array.from(container.children).filter(function(c) {
4014
+ return c.tagName !== 'TEMPLATE';
4015
+ });
4016
+ children.forEach(function(child, index) {
4017
+ child.setAttribute('data-sortable-index', index);
4018
+ // \u6DFB\u52A0\u8FC7\u6E21\u52A8\u753B\uFF0C\u8BA9\u62D6\u653E\u6548\u679C\u66F4\u5E73\u6ED1
4019
+ if (!child.style.transition) {
4020
+ child.style.transition = 'all 0.2s ease-in-out';
3176
4021
  }
3177
4022
 
3178
- /* ErrorAlert \u9000\u51FA\u52A8\u753B */
3179
- .error-alert-exit {
3180
- animation: slideOutRight 0.3s ease-in forwards, fadeOut 0.3s ease-in forwards;
3181
- }
3182
- `
4023
+ // \u5982\u679C\u6307\u5B9A\u4E86\u62D6\u62FD\u624B\u67C4\uFF0C\u53EA\u6709\u624B\u67C4\u53EF\u62D6\u62FD
4024
+ if (self.handleSelector) {
4025
+ var handleElement = child.querySelector(self.handleSelector);
4026
+ if (handleElement) {
4027
+ child.setAttribute('draggable', 'false');
4028
+ handleElement.setAttribute('draggable', 'true');
4029
+ handleElement.setAttribute('data-sortable-handle', 'true');
4030
+ handleElement.style.cursor = 'move';
4031
+ }
4032
+ } else {
4033
+ // \u6574\u4E2A\u5143\u7D20\u53EF\u62D6\u62FD
4034
+ child.setAttribute('draggable', 'true');
4035
+ child.style.cursor = 'move';
3183
4036
  }
3184
- }
3185
- )
3186
- ] }),
3187
- /* @__PURE__ */ jsxRuntime.jsxs("body", { className: "bg-gray-50", "hx-indicator": "#loading-bar", children: [
3188
- /* @__PURE__ */ jsxRuntime.jsx(LoadingBar, {}),
3189
- props.children,
3190
- /* @__PURE__ */ jsxRuntime.jsx(
3191
- "div",
3192
- {
3193
- id: "error-container",
3194
- className: "fixed top-4 right-4 z-[200] w-full max-w-2xl px-4"
3195
- }
3196
- ),
3197
- /* @__PURE__ */ jsxRuntime.jsx("div", { id: "dialog-container" })
3198
- ] })
3199
- ] });
3200
- }
3201
- function renderNavItem(item, currentPath, index) {
3202
- const isActive = currentPath === item.href || currentPath && currentPath.startsWith(item.href + "/");
3203
- const hasActiveChild = item.children?.some(
3204
- (child) => currentPath === child.href || currentPath && currentPath.startsWith(child.href + "/")
4037
+ });
4038
+ };
4039
+
4040
+ // \u521D\u59CB\u8BBE\u7F6E
4041
+ initSortable();
4042
+
4043
+ // \u4F7F\u7528 MutationObserver \u76D1\u542C\u5B50\u5143\u7D20\u53D8\u5316
4044
+ var observer = new MutationObserver(function() {
4045
+ initSortable();
4046
+ });
4047
+
4048
+ observer.observe(container, {
4049
+ childList: true,
4050
+ subtree: false,
4051
+ });
4052
+ }
4053
+ }`;
4054
+ return /* @__PURE__ */ jsxRuntime.jsx(
4055
+ "div",
4056
+ {
4057
+ className,
4058
+ ...{
4059
+ "x-data": xDataContent
4060
+ },
4061
+ ...{
4062
+ "x-init": "init()",
4063
+ "x-on:dragstart": "handleDragStart($event)",
4064
+ "x-on:dragend": "handleDragEnd($event)",
4065
+ "x-on:dragover": "handleDragOver($event)",
4066
+ "x-on:dragleave": "handleDragLeave($event)",
4067
+ "x-on:drop": "handleDrop($event)"
4068
+ },
4069
+ ...rest,
4070
+ children
4071
+ }
3205
4072
  );
3206
- return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "relative group", "data-testid": `nav-item-${index}`, children: [
3207
- /* @__PURE__ */ jsxRuntime.jsxs(
3208
- "a",
3209
- {
3210
- href: item.href,
3211
- "hx-get": item.href,
3212
- className: `flex items-center px-4 py-2.5 rounded-lg transition-all duration-200 ${isActive || hasActiveChild ? "bg-blue-600 text-white shadow-md font-medium" : "text-gray-300 hover:bg-gray-700 hover:text-white"}`,
3213
- "data-testid": `nav-link-${item.label}`,
3214
- "aria-current": isActive ? "page" : void 0,
3215
- "aria-label": `\u5BFC\u822A\u5230 ${item.label}`,
3216
- children: [
3217
- item.icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mr-2.5 text-lg", "aria-hidden": "true", children: item.icon }),
3218
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "whitespace-nowrap overflow-hidden text-ellipsis", children: item.label })
3219
- ]
3220
- }
3221
- ),
3222
- item.children && item.children.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "ml-4 mt-1 space-y-1", "data-testid": `nav-submenu-${index}`, children: item.children.map((child, childIndex) => {
3223
- const isChildActive = currentPath === child.href || currentPath && currentPath.startsWith(child.href + "/");
3224
- return /* @__PURE__ */ jsxRuntime.jsx("li", { "data-testid": `nav-subitem-${index}-${childIndex}`, children: /* @__PURE__ */ jsxRuntime.jsxs(
3225
- "a",
3226
- {
3227
- href: child.href,
3228
- "hx-get": child.href,
3229
- className: `flex items-center px-4 py-2 rounded-lg text-sm transition-all duration-200 ${isChildActive ? "bg-blue-500 text-white font-medium" : "text-gray-400 hover:bg-gray-700 hover:text-white"}`,
3230
- "data-testid": `nav-sublink-${child.label}`,
3231
- "aria-current": isChildActive ? "page" : void 0,
3232
- "aria-label": `\u5BFC\u822A\u5230 ${child.label}`,
3233
- children: [
3234
- child.icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mr-2", "aria-hidden": "true", children: child.icon }),
3235
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "whitespace-nowrap overflow-hidden text-ellipsis", children: child.label })
3236
- ]
3237
- }
3238
- ) }, childIndex);
3239
- }) })
3240
- ] }, index);
3241
4073
  }
3242
- function renderActionButton2(action, index) {
4074
+ function StringArrayEditor(props) {
3243
4075
  const {
3244
- label,
3245
- href,
3246
- hxGet,
3247
- hxPost,
3248
- hxPut,
3249
- hxDelete,
3250
- variant = "primary",
3251
- confirm,
3252
- submit,
3253
- formId,
3254
- onClick,
3255
- className = "",
3256
- target
3257
- } = action;
3258
- if (submit && formId) {
3259
- const variantStyles = {
3260
- primary: "bg-blue-600 text-white hover:bg-blue-700",
3261
- secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
3262
- danger: "bg-red-600 text-white hover:bg-red-700",
3263
- ghost: "bg-transparent text-gray-700 hover:bg-gray-100"
3264
- };
3265
- const buttonStyle = variantStyles[variant] || variantStyles.primary;
3266
- const testId2 = label === "\u521B\u5EFA" || label === "\u66F4\u65B0" ? "submit-button" : `action-${label}`;
3267
- return /* @__PURE__ */ jsxRuntime.jsx(
3268
- "button",
3269
- {
3270
- type: "submit",
3271
- form: formId,
3272
- className: `px-4 py-2 rounded transition-colors font-medium ${buttonStyle} ${className}`,
3273
- "data-testid": testId2,
3274
- ...confirm && { "data-confirm": confirm },
3275
- children: label
3276
- },
3277
- index
3278
- );
3279
- }
3280
- if (onClick) {
3281
- const variantStyles = {
3282
- primary: "bg-blue-600 text-white hover:bg-blue-700",
3283
- secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
3284
- danger: "bg-red-600 text-white hover:bg-red-700",
3285
- ghost: "bg-transparent text-gray-700 hover:bg-gray-100"
3286
- };
3287
- const buttonStyle = variantStyles[variant] || variantStyles.secondary;
3288
- const testId2 = label === "\u53D6\u6D88" ? "cancel-button" : `action-${label}`;
3289
- return /* @__PURE__ */ jsxRuntime.jsx(
3290
- "button",
3291
- {
3292
- type: "button",
3293
- _: onClick,
3294
- className: `px-4 py-2 rounded transition-colors font-medium ${buttonStyle} ${className}`,
3295
- "data-testid": testId2,
3296
- ...confirm && { "data-confirm": confirm },
3297
- children: label
3298
- },
3299
- index
3300
- );
3301
- }
3302
- let testId = `action-${label}`;
3303
- if (label === "\u65B0\u5EFA" || label === "\u521B\u5EFA") {
3304
- testId = "create-button";
3305
- } else if (label === "\u53D6\u6D88") {
3306
- testId = "cancel-button";
3307
- }
3308
- const isNewWindow = target === "_blank";
3309
- return /* @__PURE__ */ jsxRuntime.jsx(
3310
- Button,
4076
+ value,
4077
+ fieldName,
4078
+ placeholder = "\u8BF7\u8F93\u5165\u5185\u5BB9",
4079
+ allowEmpty = false,
4080
+ rows = 1
4081
+ } = props;
4082
+ const initialItems = value || [];
4083
+ const initialDataJson = JSON.stringify({
4084
+ items: initialItems.map((item) => item || ""),
4085
+ fieldName,
4086
+ placeholder,
4087
+ allowEmpty,
4088
+ rows
4089
+ });
4090
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4091
+ "div",
3311
4092
  {
3312
- variant,
3313
- href,
3314
- hxGet: !isNewWindow ? hxGet : void 0,
3315
- hxPost: !isNewWindow ? hxPost : void 0,
3316
- hxPut: !isNewWindow ? hxPut : void 0,
3317
- hxDelete: !isNewWindow ? hxDelete : void 0,
3318
- hxConfirm: !isNewWindow ? confirm : void 0,
3319
- className,
3320
- "data-testid": testId,
3321
- target,
3322
- rel: target === "_blank" ? "noopener noreferrer" : void 0,
3323
- children: label
3324
- },
3325
- index
4093
+ className: "space-y-3",
4094
+ "x-data": initialDataJson,
4095
+ children: [
4096
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-end", children: /* @__PURE__ */ jsxRuntime.jsxs(
4097
+ "button",
4098
+ {
4099
+ type: "button",
4100
+ ...{
4101
+ "x-on:click": `
4102
+ items.push('');
4103
+ `
4104
+ },
4105
+ className: "px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium flex items-center gap-2",
4106
+ "data-testid": `${fieldName}-add-button`,
4107
+ children: [
4108
+ /* @__PURE__ */ jsxRuntime.jsx(
4109
+ "svg",
4110
+ {
4111
+ className: "w-4 h-4",
4112
+ fill: "none",
4113
+ stroke: "currentColor",
4114
+ viewBox: "0 0 24 24",
4115
+ children: /* @__PURE__ */ jsxRuntime.jsx(
4116
+ "path",
4117
+ {
4118
+ strokeLinecap: "round",
4119
+ strokeLinejoin: "round",
4120
+ strokeWidth: "2",
4121
+ d: "M12 4v16m8-8H4"
4122
+ }
4123
+ )
4124
+ }
4125
+ ),
4126
+ "\u6DFB\u52A0\u9879"
4127
+ ]
4128
+ }
4129
+ ) }),
4130
+ /* @__PURE__ */ jsxRuntime.jsx(
4131
+ "div",
4132
+ {
4133
+ "x-show": "items.length > 0",
4134
+ "data-testid": `${fieldName}-list-container`,
4135
+ ...{
4136
+ "@sortable:change.stop": `
4137
+ (function() {
4138
+ const { oldIndex, newIndex } = $event.detail;
4139
+ [items[oldIndex], items[newIndex]] = [items[newIndex], items[oldIndex]];
4140
+ })();
4141
+ `
4142
+ },
4143
+ children: /* @__PURE__ */ jsxRuntime.jsx(SortableList, { className: "space-y-2", handle: "[data-drag-handle]", children: /* @__PURE__ */ jsxRuntime.jsx("template", { "x-for": "(item, index) in items", "x-bind:key": "index", children: /* @__PURE__ */ jsxRuntime.jsx(ArrayItem, { fieldName, rows }) }) })
4144
+ }
4145
+ ),
4146
+ /* @__PURE__ */ jsxRuntime.jsx(
4147
+ "div",
4148
+ {
4149
+ className: "empty-state text-center py-8 text-gray-400 text-sm border border-dashed border-gray-300 rounded-lg",
4150
+ "x-show": "items.length === 0",
4151
+ "data-testid": `${fieldName}-empty-state`,
4152
+ children: '\u6682\u65E0\u9879\uFF0C\u70B9\u51FB"\u6DFB\u52A0\u9879"\u6309\u94AE\u6DFB\u52A0'
4153
+ }
4154
+ )
4155
+ ]
4156
+ }
3326
4157
  );
3327
4158
  }
3328
- function AdminLayout(props) {
3329
- const {
3330
- title,
3331
- description,
3332
- options,
3333
- children,
3334
- currentPath,
3335
- userInfo,
3336
- breadcrumbs,
3337
- actions = []
3338
- } = props;
3339
- const navItems = options.navigation || [];
3340
- const logoutUrl = options.authProvider?.logoutUrl;
3341
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-screen", id: "main-content", children: [
3342
- /* @__PURE__ */ jsxRuntime.jsx("aside", { className: "w-64 bg-gradient-to-b from-gray-900 to-gray-800 text-white shadow-xl", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-6 h-full flex flex-col", children: [
3343
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-8", children: options.logo ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: options.logo, alt: "Logo", className: "h-10 mb-2" }) : /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-xl font-bold text-white whitespace-nowrap overflow-hidden text-ellipsis", children: options.title || "\u7BA1\u7406\u540E\u53F0" }) }),
3344
- /* @__PURE__ */ jsxRuntime.jsx("nav", { className: "flex-1 overflow-y-auto", "data-testid": "main-navigation", "aria-label": "\u4E3B\u5BFC\u822A", children: /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "space-y-1", "data-testid": "nav-list", children: navItems.length > 0 ? navItems.map(
3345
- (item, index) => renderNavItem(item, currentPath, index)
3346
- ) : /* @__PURE__ */ jsxRuntime.jsx("li", { className: "px-4 py-2 text-gray-400 text-sm", "data-testid": "nav-empty", children: "\u6682\u65E0\u5BFC\u822A\u9879" }) }) })
3347
- ] }) }),
3348
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col overflow-hidden", children: [
4159
+ function ArrayItem({ fieldName, rows = 1 }) {
4160
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4161
+ "div",
4162
+ {
4163
+ "data-array-item": true,
4164
+ className: "flex items-center gap-2 group",
4165
+ children: [
4166
+ /* @__PURE__ */ jsxRuntime.jsx(
4167
+ "div",
4168
+ {
4169
+ className: "flex-shrink-0 cursor-move text-gray-400 hover:text-gray-600 transition-colors p-1",
4170
+ "data-drag-handle": true,
4171
+ "data-testid": `${fieldName}-drag-handle`,
4172
+ title: "\u62D6\u62FD\u6392\u5E8F",
4173
+ children: /* @__PURE__ */ jsxRuntime.jsx(
4174
+ "svg",
4175
+ {
4176
+ className: "w-5 h-5",
4177
+ fill: "none",
4178
+ stroke: "currentColor",
4179
+ viewBox: "0 0 24 24",
4180
+ children: /* @__PURE__ */ jsxRuntime.jsx(
4181
+ "path",
4182
+ {
4183
+ strokeLinecap: "round",
4184
+ strokeLinejoin: "round",
4185
+ strokeWidth: "2",
4186
+ d: "M4 8h16M4 16h16"
4187
+ }
4188
+ )
4189
+ }
4190
+ )
4191
+ }
4192
+ ),
4193
+ rows === 1 ? /* @__PURE__ */ jsxRuntime.jsx(
4194
+ "input",
4195
+ {
4196
+ type: "text",
4197
+ "x-model": "items[index]",
4198
+ "x-bind:placeholder": "placeholder + ' ' + (index + 1)",
4199
+ className: "flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent",
4200
+ "data-testid": `${fieldName}-input`,
4201
+ "x-bind:required": "!allowEmpty"
4202
+ }
4203
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
4204
+ "textarea",
4205
+ {
4206
+ "x-model": "items[index]",
4207
+ "x-bind:placeholder": "placeholder + ' ' + (index + 1)",
4208
+ rows,
4209
+ className: "flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-y",
4210
+ "data-testid": `${fieldName}-input`,
4211
+ "x-bind:required": "!allowEmpty"
4212
+ }
4213
+ ),
4214
+ /* @__PURE__ */ jsxRuntime.jsx(
4215
+ "input",
4216
+ {
4217
+ type: "hidden",
4218
+ "x-bind:name": "fieldName + '[' + index + ']'",
4219
+ "x-bind:value": "item"
4220
+ }
4221
+ ),
4222
+ /* @__PURE__ */ jsxRuntime.jsx(
4223
+ "button",
4224
+ {
4225
+ type: "button",
4226
+ ...{
4227
+ "x-on:click": `
4228
+ items.splice(index, 1);
4229
+ `
4230
+ },
4231
+ className: "flex-shrink-0 px-3 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors",
4232
+ "data-testid": `${fieldName}-remove-button`,
4233
+ title: "\u5220\u9664\u6B64\u9879",
4234
+ children: /* @__PURE__ */ jsxRuntime.jsx(
4235
+ "svg",
4236
+ {
4237
+ className: "w-5 h-5",
4238
+ fill: "none",
4239
+ stroke: "currentColor",
4240
+ viewBox: "0 0 24 24",
4241
+ children: /* @__PURE__ */ jsxRuntime.jsx(
4242
+ "path",
4243
+ {
4244
+ strokeLinecap: "round",
4245
+ strokeLinejoin: "round",
4246
+ strokeWidth: "2",
4247
+ d: "M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
4248
+ }
4249
+ )
4250
+ }
4251
+ )
4252
+ }
4253
+ )
4254
+ ]
4255
+ }
4256
+ );
4257
+ }
4258
+ function TagsEditor(props) {
4259
+ const { value, fieldName, placeholder = "\u8F93\u5165\u6807\u7B7E\u540E\u6309\u56DE\u8F66\u6DFB\u52A0" } = props;
4260
+ const initialTags = value || [];
4261
+ const initialDataJson = JSON.stringify({
4262
+ tags: initialTags.map((tag) => tag || ""),
4263
+ fieldName,
4264
+ newTag: "",
4265
+ editingIndex: null,
4266
+ editingValue: "",
4267
+ error: ""
4268
+ });
4269
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", "x-data": initialDataJson, children: [
4270
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3349
4271
  /* @__PURE__ */ jsxRuntime.jsx(
3350
- Header,
4272
+ "input",
3351
4273
  {
3352
- title,
3353
- breadcrumbs,
3354
- userInfo,
3355
- logoutUrl
4274
+ type: "text",
4275
+ "x-model": "newTag",
4276
+ ...{
4277
+ "x-on:keydown.enter.prevent": `
4278
+ if (newTag.trim()) {
4279
+ tags.push(newTag.trim());
4280
+ newTag = '';
4281
+ error = '';
4282
+ } else {
4283
+ error = '\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A';
4284
+ }
4285
+ `
4286
+ },
4287
+ placeholder,
4288
+ autocomplete: "off",
4289
+ className: "w-full px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent",
4290
+ "data-testid": `${fieldName}-input`
3356
4291
  }
3357
4292
  ),
3358
- (title || description || actions.length > 0) && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white border-b border-gray-200 px-6 py-3", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between items-center gap-4", children: [
3359
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
3360
- title && /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-semibold text-gray-900 mb-0.5 leading-tight", children: title }),
3361
- description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600 text-xs leading-snug", children: description })
3362
- ] }),
3363
- actions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-2 flex-shrink-0", "data-testid": "page-actions", children: actions.map(
3364
- (action, index) => renderActionButton2(action, index)
3365
- ) })
3366
- ] }) }),
3367
- /* @__PURE__ */ jsxRuntime.jsx("main", { className: "flex-1 overflow-auto bg-gray-50", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6", children }) })
3368
- ] })
4293
+ /* @__PURE__ */ jsxRuntime.jsx(
4294
+ "div",
4295
+ {
4296
+ "x-show": "error && editingIndex === null",
4297
+ "x-text": "error",
4298
+ className: "text-red-600 text-sm p-2",
4299
+ id: `${fieldName}-error`
4300
+ }
4301
+ )
4302
+ ] }),
4303
+ /* @__PURE__ */ jsxRuntime.jsx(
4304
+ "div",
4305
+ {
4306
+ "x-show": "tags.length > 0",
4307
+ "data-testid": `${fieldName}-tags-container`,
4308
+ ...{
4309
+ "@sortable:change.stop": `
4310
+ (function() {
4311
+ const { oldIndex, newIndex } = $event.detail;
4312
+ [tags[oldIndex], tags[newIndex]] = [tags[newIndex], tags[oldIndex]];
4313
+ })();
4314
+ `
4315
+ },
4316
+ children: /* @__PURE__ */ jsxRuntime.jsx(
4317
+ SortableList,
4318
+ {
4319
+ className: "flex flex-wrap gap-2",
4320
+ handle: "[data-drag-handle]",
4321
+ children: /* @__PURE__ */ jsxRuntime.jsx("template", { "x-for": "(tag, index) in tags", "x-bind:key": "index", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "inline-flex", children: [
4322
+ /* @__PURE__ */ jsxRuntime.jsx(
4323
+ "input",
4324
+ {
4325
+ type: "hidden",
4326
+ "x-bind:name": `fieldName + '[' + index + ']'`,
4327
+ "x-bind:value": "editingIndex === index ? editingValue : tag"
4328
+ }
4329
+ ),
4330
+ /* @__PURE__ */ jsxRuntime.jsx(TagItem, { fieldName }),
4331
+ /* @__PURE__ */ jsxRuntime.jsx(TagItemEdit, { fieldName })
4332
+ ] }) })
4333
+ }
4334
+ )
4335
+ }
4336
+ ),
4337
+ /* @__PURE__ */ jsxRuntime.jsx(
4338
+ "div",
4339
+ {
4340
+ className: "empty-state text-center py-4 text-gray-400 text-sm border border-dashed border-gray-300 rounded-md",
4341
+ "x-show": "tags.length === 0",
4342
+ "data-testid": `${fieldName}-empty-state`,
4343
+ children: "\u6682\u65E0\u6807\u7B7E\uFF0C\u5728\u4E0A\u65B9\u8F93\u5165\u6846\u4E2D\u8F93\u5165\u6807\u7B7E\u540E\u6309\u56DE\u8F66\u6DFB\u52A0"
4344
+ }
4345
+ )
3369
4346
  ] });
3370
4347
  }
3371
- function NoLayout(props) {
3372
- return /* @__PURE__ */ jsxRuntime.jsx("div", { id: "main-content", children: props.children });
4348
+ function TagItem({ fieldName }) {
4349
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4350
+ "div",
4351
+ {
4352
+ className: "inline-flex items-center gap-1 px-2 py-1 bg-blue-50 border border-blue-200 rounded-md text-sm group",
4353
+ "x-show": "editingIndex !== index",
4354
+ children: [
4355
+ /* @__PURE__ */ jsxRuntime.jsx(
4356
+ "div",
4357
+ {
4358
+ className: "flex-shrink-0 cursor-move text-blue-400 hover:text-blue-600 transition-colors",
4359
+ "data-drag-handle": true,
4360
+ "data-testid": `${fieldName}-drag-handle`,
4361
+ title: "\u62D6\u62FD\u6392\u5E8F",
4362
+ children: /* @__PURE__ */ jsxRuntime.jsx(
4363
+ "svg",
4364
+ {
4365
+ className: "w-3 h-3",
4366
+ fill: "none",
4367
+ stroke: "currentColor",
4368
+ viewBox: "0 0 24 24",
4369
+ children: /* @__PURE__ */ jsxRuntime.jsx(
4370
+ "path",
4371
+ {
4372
+ strokeLinecap: "round",
4373
+ strokeLinejoin: "round",
4374
+ strokeWidth: "2",
4375
+ d: "M4 8h16M4 16h16"
4376
+ }
4377
+ )
4378
+ }
4379
+ )
4380
+ }
4381
+ ),
4382
+ /* @__PURE__ */ jsxRuntime.jsx(
4383
+ "span",
4384
+ {
4385
+ className: "flex-1 text-blue-900 cursor-pointer",
4386
+ "data-testid": `${fieldName}-tag-text`,
4387
+ ...{
4388
+ "x-on:click.stop": `
4389
+ editingIndex = index;
4390
+ editingValue = tag;
4391
+ `
4392
+ },
4393
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "x-text": "tag" })
4394
+ }
4395
+ ),
4396
+ /* @__PURE__ */ jsxRuntime.jsx(
4397
+ "button",
4398
+ {
4399
+ type: "button",
4400
+ ...{ "x-on:click.stop": "tags.splice(index, 1)" },
4401
+ className: "flex-shrink-0 text-blue-600 hover:text-red-600 hover:bg-red-50 rounded transition-colors p-0.5 delete-tag-button",
4402
+ "data-testid": `${fieldName}-tag-remove`,
4403
+ title: "\u5220\u9664\u6807\u7B7E",
4404
+ children: /* @__PURE__ */ jsxRuntime.jsx(
4405
+ "svg",
4406
+ {
4407
+ className: "w-3.5 h-3.5",
4408
+ fill: "none",
4409
+ stroke: "currentColor",
4410
+ viewBox: "0 0 24 24",
4411
+ children: /* @__PURE__ */ jsxRuntime.jsx(
4412
+ "path",
4413
+ {
4414
+ strokeLinecap: "round",
4415
+ strokeLinejoin: "round",
4416
+ strokeWidth: "2",
4417
+ d: "M6 18L18 6M6 6l12 12"
4418
+ }
4419
+ )
4420
+ }
4421
+ )
4422
+ }
4423
+ )
4424
+ ]
4425
+ }
4426
+ );
3373
4427
  }
3374
-
3375
- // src/utils/permissions.ts
3376
- function checkUserPermission(requiredPermission, userPermissions) {
3377
- if (!requiredPermission) {
3378
- return { allowed: true, reason: "\u65E0\u9700\u6743\u9650" };
3379
- }
3380
- if (!userPermissions || userPermissions.length === 0) {
3381
- return {
3382
- allowed: false,
3383
- reason: "\u7528\u6237\u65E0\u4EFB\u4F55\u6743\u9650",
3384
- matchedPermission: "none"
3385
- };
3386
- }
3387
- const denyResult = checkDenyPermissions(requiredPermission, userPermissions);
3388
- if (!denyResult.allowed) {
3389
- return denyResult;
3390
- }
3391
- const allowResult = checkAllowPermissions(
3392
- requiredPermission,
4428
+ function TagItemEdit({ fieldName }) {
4429
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4430
+ "div",
4431
+ {
4432
+ className: "inline-flex items-center gap-1 px-2 py-1 bg-blue-50 border border-blue-300 rounded-md text-sm",
4433
+ "x-show": "editingIndex === index",
4434
+ children: [
4435
+ /* @__PURE__ */ jsxRuntime.jsx(
4436
+ "div",
4437
+ {
4438
+ className: "flex-shrink-0 cursor-move text-blue-400 hover:text-blue-600 transition-colors",
4439
+ "data-drag-handle": true,
4440
+ "data-testid": `${fieldName}-drag-handle`,
4441
+ title: "\u62D6\u62FD\u6392\u5E8F",
4442
+ children: /* @__PURE__ */ jsxRuntime.jsx(
4443
+ "svg",
4444
+ {
4445
+ className: "w-3 h-3",
4446
+ fill: "none",
4447
+ stroke: "currentColor",
4448
+ viewBox: "0 0 24 24",
4449
+ children: /* @__PURE__ */ jsxRuntime.jsx(
4450
+ "path",
4451
+ {
4452
+ strokeLinecap: "round",
4453
+ strokeLinejoin: "round",
4454
+ strokeWidth: "2",
4455
+ d: "M4 8h16M4 16h16"
4456
+ }
4457
+ )
4458
+ }
4459
+ )
4460
+ }
4461
+ ),
4462
+ /* @__PURE__ */ jsxRuntime.jsx(
4463
+ "input",
4464
+ {
4465
+ type: "text",
4466
+ "x-model": "editingValue",
4467
+ ...{
4468
+ "x-on:keydown.enter.prevent": `
4469
+ if (editingValue.trim()) {
4470
+ tags[index] = editingValue.trim();
4471
+ editingIndex = null;
4472
+ editingValue = '';
4473
+ error = '';
4474
+ } else {
4475
+ error = '\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A';
4476
+ }
4477
+ `,
4478
+ "x-on:blur": `
4479
+ if (editingValue.trim()) {
4480
+ tags[index] = editingValue.trim();
4481
+ editingIndex = null;
4482
+ editingValue = '';
4483
+ error = '';
4484
+ } else {
4485
+ error = '\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A';
4486
+ }
4487
+ `
4488
+ },
4489
+ "x-bind:required": "editingIndex === index",
4490
+ className: "flex-1 px-1 py-0.5 border border-blue-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-500 min-w-[60px]",
4491
+ "data-testid": `${fieldName}-tag-edit-input`
4492
+ }
4493
+ ),
4494
+ /* @__PURE__ */ jsxRuntime.jsx(
4495
+ "button",
4496
+ {
4497
+ type: "button",
4498
+ ...{
4499
+ "x-on:click.stop": `
4500
+ editingIndex = null;
4501
+ editingValue = '';
4502
+ error = '';
4503
+ `
4504
+ },
4505
+ className: "px-1.5 py-0.5 text-xs bg-gray-300 text-gray-700 rounded hover:bg-gray-400 transition-colors",
4506
+ "data-testid": `${fieldName}-tag-cancel`,
4507
+ children: "\u53D6\u6D88"
4508
+ }
4509
+ ),
4510
+ /* @__PURE__ */ jsxRuntime.jsx(
4511
+ "div",
4512
+ {
4513
+ "x-show": "error && editingIndex === index",
4514
+ "x-text": "error",
4515
+ className: "text-red-600 text-xs mt-1"
4516
+ }
4517
+ )
4518
+ ]
4519
+ }
4520
+ );
4521
+ }
4522
+ function ObjectEditor(props) {
4523
+ const { value, fieldName, objectSchema } = props;
4524
+ if (!objectSchema) {
4525
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 border border-yellow-300 rounded-lg bg-yellow-50 text-yellow-800 text-sm", children: "\u8BF7\u63D0\u4F9B objectSchema \u53C2\u6570\u4EE5\u4F7F\u7528\u5BF9\u8C61\u7F16\u8F91\u5668" });
4526
+ }
4527
+ const fields = parseSchemaToFields(objectSchema);
4528
+ const initialObject = value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
4529
+ fields.forEach((field) => {
4530
+ if (!(field.name in initialObject)) {
4531
+ if (field.type === "number") {
4532
+ initialObject[field.name] = field.required ? 0 : void 0;
4533
+ } else if (field.type === "checkbox") {
4534
+ initialObject[field.name] = field.required ? false : void 0;
4535
+ } else {
4536
+ initialObject[field.name] = field.required ? "" : void 0;
4537
+ }
4538
+ }
4539
+ });
4540
+ const initialValueJson = JSON.stringify(initialObject);
4541
+ JSON.stringify(fields.map((f) => f.name));
4542
+ const generateField = (field) => {
4543
+ const fieldId = `${fieldName}-${field.name}`;
4544
+ const fieldValue = initialObject[field.name];
4545
+ const fieldValueStr = fieldValue === void 0 || fieldValue === null ? "" : typeof fieldValue === "object" ? JSON.stringify(fieldValue) : String(fieldValue);
4546
+ const requiredAttr = field.required ? "required" : "";
4547
+ let inputElement;
4548
+ if (field.type === "text") {
4549
+ inputElement = html.html`
4550
+ <input
4551
+ type="text"
4552
+ id="${fieldId}"
4553
+ name="${fieldName}.${field.name}"
4554
+ value="${fieldValueStr}"
4555
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
4556
+ data-testid="${fieldName}-input-${field.name}"
4557
+ oninput="updateObjectField('${fieldName}', '${field.name}', this.value, 'text', ${field.required})"
4558
+ ${requiredAttr}
4559
+ />
4560
+ `;
4561
+ } else if (field.type === "textarea") {
4562
+ inputElement = html.html`
4563
+ <textarea
4564
+ id="${fieldId}"
4565
+ name="${fieldName}.${field.name}"
4566
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-y"
4567
+ rows="4"
4568
+ data-testid="${fieldName}-input-${field.name}"
4569
+ oninput="updateObjectField('${fieldName}', '${field.name}', this.value, 'text', ${field.required})"
4570
+ ${requiredAttr}
4571
+ >${fieldValueStr}</textarea>
4572
+ `;
4573
+ } else if (field.type === "number") {
4574
+ const step = field.step || (field.step === void 0 ? "1" : "any");
4575
+ inputElement = html.html`
4576
+ <input
4577
+ type="number"
4578
+ id="${fieldId}"
4579
+ name="${fieldName}.${field.name}"
4580
+ value="${fieldValueStr}"
4581
+ step="${step}"
4582
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
4583
+ data-testid="${fieldName}-input-${field.name}"
4584
+ oninput="updateObjectField('${fieldName}', '${field.name}', this.value, 'number', ${field.required})"
4585
+ ${requiredAttr}
4586
+ />
4587
+ `;
4588
+ } else if (field.type === "date") {
4589
+ inputElement = html.html`
4590
+ <input
4591
+ type="date"
4592
+ id="${fieldId}"
4593
+ name="${fieldName}.${field.name}"
4594
+ value="${fieldValueStr}"
4595
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
4596
+ data-testid="${fieldName}-input-${field.name}"
4597
+ oninput="updateObjectField('${fieldName}', '${field.name}', this.value, 'date', ${field.required})"
4598
+ ${requiredAttr}
4599
+ />
4600
+ `;
4601
+ } else if (field.type === "email") {
4602
+ inputElement = html.html`
4603
+ <input
4604
+ type="email"
4605
+ id="${fieldId}"
4606
+ name="${fieldName}.${field.name}"
4607
+ value="${fieldValueStr}"
4608
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
4609
+ data-testid="${fieldName}-input-${field.name}"
4610
+ oninput="updateObjectField('${fieldName}', '${field.name}', this.value, 'text', ${field.required})"
4611
+ ${requiredAttr}
4612
+ />
4613
+ `;
4614
+ } else if (field.type === "select" && field.options) {
4615
+ inputElement = html.html`
4616
+ <select
4617
+ id="${fieldId}"
4618
+ name="${fieldName}.${field.name}"
4619
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
4620
+ data-testid="${fieldName}-select-${field.name}"
4621
+ onchange="updateObjectField('${fieldName}', '${field.name}', this.value, 'text', ${field.required})"
4622
+ ${requiredAttr}
4623
+ >
4624
+ ${!field.required ? html.html`<option value="">请选择</option>` : ""}
4625
+ ${field.options.map(
4626
+ (option) => html.html`
4627
+ <option
4628
+ value="${String(option.value)}"
4629
+ ${fieldValueStr === String(option.value) ? "selected" : ""}
4630
+ >
4631
+ ${option.label}
4632
+ </option>
4633
+ `
4634
+ )}
4635
+ </select>
4636
+ `;
4637
+ } else if (field.type === "checkbox") {
4638
+ const checked = fieldValue === true || fieldValue === "true" || fieldValue === 1 || fieldValue === "1";
4639
+ inputElement = html.html`
4640
+ <div class="flex items-center">
4641
+ <input
4642
+ type="checkbox"
4643
+ id="${fieldId}"
4644
+ name="${fieldName}.${field.name}"
4645
+ ${checked ? "checked" : ""}
4646
+ class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
4647
+ data-testid="${fieldName}-checkbox-${field.name}"
4648
+ onchange="updateObjectField('${fieldName}', '${field.name}', this.checked, 'checkbox', ${field.required}')"
4649
+ />
4650
+ <label for="${fieldId}" class="ml-2 text-sm text-gray-700">
4651
+ ${field.label}
4652
+ </label>
4653
+ </div>
4654
+ `;
4655
+ }
4656
+ return html.html`
4657
+ <div class="space-y-2" data-testid="${fieldName}-field-${field.name}">
4658
+ ${field.type !== "checkbox" ? html.html`
4659
+ <label
4660
+ for="${fieldId}"
4661
+ class="block text-sm font-semibold text-gray-700"
4662
+ data-testid="${fieldName}-label-${field.name}"
4663
+ >
4664
+ ${field.label}
4665
+ ${field.required ? html.html`<span class="text-red-500 ml-1">*</span>` : ""}
4666
+ </label>
4667
+ ` : ""}
4668
+ ${inputElement}
4669
+ </div>
4670
+ `;
4671
+ };
4672
+ return html.html`
4673
+ <div
4674
+ id="object-editor-${fieldName}"
4675
+ class="space-y-4"
4676
+ data-initial-value="${initialValueJson}"
4677
+ >
4678
+ <input
4679
+ type="hidden"
4680
+ name="${fieldName}"
4681
+ value="${initialValueJson}"
4682
+ data-testid="hidden-${fieldName}"
4683
+ />
4684
+ <div class="space-y-4">
4685
+ ${fields.map((field) => generateField(field))}
4686
+ </div>
4687
+ </div>
4688
+ <script>
4689
+ (function() {
4690
+ // 更新对象字段
4691
+ function updateObjectField(fieldName, subFieldName, value, fieldType, required) {
4692
+ const container = document.getElementById('object-editor-' + fieldName);
4693
+ if (!container) return;
4694
+
4695
+ const hiddenInput = container.querySelector('input[name="' + fieldName + '"][type="hidden"]');
4696
+ if (!hiddenInput) return;
4697
+
4698
+ try {
4699
+ const obj = JSON.parse(hiddenInput.value || '{}');
4700
+
4701
+ // 类型转换
4702
+ let convertedValue = value;
4703
+ if (fieldType === 'number') {
4704
+ convertedValue = value === '' ? (required ? 0 : undefined) : Number(value);
4705
+ if (isNaN(convertedValue)) convertedValue = required ? 0 : undefined;
4706
+ } else if (fieldType === 'checkbox') {
4707
+ convertedValue = value === 'true' || value === true || value === '1' || value === 1;
4708
+ } else {
4709
+ convertedValue = value || (required ? '' : undefined);
4710
+ }
4711
+
4712
+ // 更新对象
4713
+ if (convertedValue === undefined && !required) {
4714
+ delete obj[subFieldName];
4715
+ } else {
4716
+ obj[subFieldName] = convertedValue;
4717
+ }
4718
+
4719
+ // 更新隐藏字段
4720
+ hiddenInput.value = JSON.stringify(obj);
4721
+ } catch (e) {
4722
+ console.error('Failed to update object field:', e);
4723
+ }
4724
+ }
4725
+
4726
+ // 将函数暴露到全局作用域
4727
+ if (typeof window !== 'undefined') {
4728
+ window.updateObjectField = updateObjectField;
4729
+ }
4730
+ })();
4731
+ </script>
4732
+ `;
4733
+ }
4734
+ function BaseLayout(props) {
4735
+ return html.html`
4736
+ <html>
4737
+ <head>
4738
+ <meta charset="UTF-8" />
4739
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
4740
+ <title>${props.title}</title>
4741
+ <meta name="description" content="${props.description || ""}" />
4742
+ ${globalScripts(props.prefix)} ${globalStyles()}
4743
+ </head>
4744
+ <body hx-ext="morph" className="bg-gray-50" hx-indicator="#loading-bar">
4745
+ ${LoadingBar()} ${props.children}
4746
+ <div
4747
+ id="error-container"
4748
+ className="fixed top-4 right-4 z-[200] w-full max-w-2xl px-4"
4749
+ ></div>
4750
+ <div id="dialog-container"></div>
4751
+ ${sortableScript()}
4752
+ </body>
4753
+ </html>
4754
+ `;
4755
+ }
4756
+ function renderNavItem(item, currentPath, index) {
4757
+ const isActive = currentPath === item.href || currentPath && currentPath.startsWith(item.href + "/");
4758
+ const hasActiveChild = item.children?.some(
4759
+ (child) => currentPath === child.href || currentPath && currentPath.startsWith(child.href + "/")
4760
+ );
4761
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4762
+ "li",
4763
+ {
4764
+ className: "relative group",
4765
+ "data-testid": `nav-item-${index}`,
4766
+ children: [
4767
+ /* @__PURE__ */ jsxRuntime.jsxs(
4768
+ "a",
4769
+ {
4770
+ href: item.href,
4771
+ "hx-get": item.href,
4772
+ className: `flex items-center px-4 py-2.5 rounded-lg transition-all duration-200 ${isActive || hasActiveChild ? "bg-blue-600 text-white shadow-md font-medium" : "text-gray-300 hover:bg-gray-700 hover:text-white"}`,
4773
+ "data-testid": `nav-link-${item.label}`,
4774
+ "aria-current": isActive ? "page" : void 0,
4775
+ "aria-label": `\u5BFC\u822A\u5230 ${item.label}`,
4776
+ children: [
4777
+ item.icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mr-2.5 text-lg", "aria-hidden": "true", children: item.icon }),
4778
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "whitespace-nowrap overflow-hidden text-ellipsis", children: item.label })
4779
+ ]
4780
+ }
4781
+ ),
4782
+ item.children && item.children.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
4783
+ "ul",
4784
+ {
4785
+ className: "ml-4 mt-1 space-y-1",
4786
+ "data-testid": `nav-submenu-${index}`,
4787
+ children: item.children.map((child, childIndex) => {
4788
+ const isChildActive = currentPath === child.href || currentPath && currentPath.startsWith(child.href + "/");
4789
+ return /* @__PURE__ */ jsxRuntime.jsx(
4790
+ "li",
4791
+ {
4792
+ "data-testid": `nav-subitem-${index}-${childIndex}`,
4793
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
4794
+ "a",
4795
+ {
4796
+ href: child.href,
4797
+ "hx-get": child.href,
4798
+ className: `flex items-center px-4 py-2 rounded-lg text-sm transition-all duration-200 ${isChildActive ? "bg-blue-500 text-white font-medium" : "text-gray-400 hover:bg-gray-700 hover:text-white"}`,
4799
+ "data-testid": `nav-sublink-${child.label}`,
4800
+ "aria-current": isChildActive ? "page" : void 0,
4801
+ "aria-label": `\u5BFC\u822A\u5230 ${child.label}`,
4802
+ children: [
4803
+ child.icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mr-2", "aria-hidden": "true", children: child.icon }),
4804
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "whitespace-nowrap overflow-hidden text-ellipsis", children: child.label })
4805
+ ]
4806
+ }
4807
+ )
4808
+ },
4809
+ childIndex
4810
+ );
4811
+ })
4812
+ }
4813
+ )
4814
+ ]
4815
+ },
4816
+ index
4817
+ );
4818
+ }
4819
+ function AdminLayout(props) {
4820
+ const {
4821
+ title,
4822
+ description,
4823
+ options,
4824
+ children,
4825
+ currentPath,
4826
+ userInfo,
4827
+ breadcrumbs,
4828
+ actions
4829
+ } = props;
4830
+ const navItems = options.navigation || [];
4831
+ const logoutUrl = options.authProvider?.logoutUrl;
4832
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-screen", id: "main-content", children: [
4833
+ /* @__PURE__ */ jsxRuntime.jsx("aside", { className: "w-64 bg-gradient-to-b from-gray-900 to-gray-800 text-white shadow-xl", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-6 h-full flex flex-col", children: [
4834
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-8", children: options.logo ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: options.logo, alt: "Logo", className: "h-10 mb-2" }) : /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-xl font-bold text-white whitespace-nowrap overflow-hidden text-ellipsis", children: options.title || "\u7BA1\u7406\u540E\u53F0" }) }),
4835
+ /* @__PURE__ */ jsxRuntime.jsx(
4836
+ "nav",
4837
+ {
4838
+ className: "flex-1 overflow-y-auto",
4839
+ "data-testid": "main-navigation",
4840
+ "aria-label": "\u4E3B\u5BFC\u822A",
4841
+ children: /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "space-y-1", "data-testid": "nav-list", children: navItems.length > 0 ? navItems.map(
4842
+ (item, index) => renderNavItem(item, currentPath, index)
4843
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
4844
+ "li",
4845
+ {
4846
+ className: "px-4 py-2 text-gray-400 text-sm",
4847
+ "data-testid": "nav-empty",
4848
+ children: "\u6682\u65E0\u5BFC\u822A\u9879"
4849
+ }
4850
+ ) })
4851
+ }
4852
+ )
4853
+ ] }) }),
4854
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col overflow-hidden", children: [
4855
+ /* @__PURE__ */ jsxRuntime.jsx(
4856
+ Header,
4857
+ {
4858
+ title,
4859
+ breadcrumbs,
4860
+ userInfo,
4861
+ logoutUrl
4862
+ }
4863
+ ),
4864
+ (title || description || actions) && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white border-b border-gray-200 px-6 py-3", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between items-center gap-4", children: [
4865
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
4866
+ title && /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-semibold text-gray-900 mb-0.5 leading-tight", children: title }),
4867
+ description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600 text-xs leading-snug", children: description })
4868
+ ] }),
4869
+ actions && /* @__PURE__ */ jsxRuntime.jsx(
4870
+ "div",
4871
+ {
4872
+ className: "flex gap-2 flex-shrink-0",
4873
+ "data-testid": "page-actions",
4874
+ children: actions
4875
+ }
4876
+ )
4877
+ ] }) }),
4878
+ /* @__PURE__ */ jsxRuntime.jsx("main", { className: "flex-1 overflow-auto bg-gray-50", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6", children }) })
4879
+ ] })
4880
+ ] });
4881
+ }
4882
+ function NoLayout(props) {
4883
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { id: "main-content", children: props.children });
4884
+ }
4885
+
4886
+ // src/utils/permissions.ts
4887
+ function checkUserPermission(requiredPermission, userPermissions) {
4888
+ if (!requiredPermission) {
4889
+ return { allowed: true, reason: "\u65E0\u9700\u6743\u9650" };
4890
+ }
4891
+ if (!userPermissions || userPermissions.length === 0) {
4892
+ return {
4893
+ allowed: false,
4894
+ reason: "\u7528\u6237\u65E0\u4EFB\u4F55\u6743\u9650",
4895
+ matchedPermission: "none"
4896
+ };
4897
+ }
4898
+ const denyResult = checkDenyPermissions(requiredPermission, userPermissions);
4899
+ if (!denyResult.allowed) {
4900
+ return denyResult;
4901
+ }
4902
+ const allowResult = checkAllowPermissions(
4903
+ requiredPermission,
3393
4904
  userPermissions
3394
4905
  );
3395
4906
  return allowResult;
@@ -3455,1370 +4966,665 @@ function matchWithWildcard(required, pattern) {
3455
4966
  if (patternPart === "*") {
3456
4967
  continue;
3457
4968
  }
3458
- if (!patternPart && pattern.endsWith("*")) {
3459
- return true;
3460
- }
3461
- if (!requiredPart) {
3462
- return false;
3463
- }
3464
- if (requiredPart !== patternPart) {
3465
- return false;
3466
- }
3467
- }
3468
- return true;
3469
- }
3470
- async function checkPermission(user, permission, ctx, options) {
3471
- if (options.authProvider?.checkPermission) {
3472
- return await options.authProvider.checkPermission(user, permission, ctx);
3473
- }
3474
- const userPermissions = user?.permissions || [];
3475
- const result = checkUserPermission(permission, userPermissions);
3476
- return {
3477
- allowed: result.allowed,
3478
- message: result.reason,
3479
- operationId: permission
3480
- };
3481
- }
3482
- async function handlePermissionDenied(ctx, result, options) {
3483
- const isHtmxRequest = ctx.req.header("HX-Request") === "true";
3484
- const loginUrl = options.authProvider?.loginUrl || "/admin/login";
3485
- const user = await getUserInfo(ctx, options.authProvider);
3486
- if (!user && !isHtmxRequest) {
3487
- return ctx.redirect(loginUrl);
3488
- }
3489
- if (isHtmxRequest) {
3490
- const headers = {
3491
- "HX-Retarget": "#dialog-container",
3492
- "HX-Reswap": "innerHTML",
3493
- "X-Permission-Denied": "true"
3494
- };
3495
- return ctx.html(
3496
- /* @__PURE__ */ jsxRuntime.jsx(Dialog, { title: "\u6743\u9650\u4E0D\u8DB3", size: "lg", children: /* @__PURE__ */ jsxRuntime.jsx(
3497
- PermissionDeniedContent,
3498
- {
3499
- operationId: result.operationId,
3500
- fromPath: ctx.req.path,
3501
- message: result.message,
3502
- userInfo: user,
3503
- pluginOptions: options,
3504
- isDialog: true
3505
- }
3506
- ) }),
3507
- 200,
3508
- headers
3509
- );
3510
- }
3511
- const cdnProxyPrefix = `${options.prefix}/_cdn`;
3512
- return ctx.html(
3513
- /* @__PURE__ */ jsxRuntime.jsx(
3514
- BaseLayout,
3515
- {
3516
- title: `\u6743\u9650\u4E0D\u8DB3 - ${options.title}`,
3517
- description: "\u60A8\u6CA1\u6709\u6743\u9650\u8BBF\u95EE\u6B64\u8D44\u6E90",
3518
- cdnProxyPrefix,
3519
- children: /* @__PURE__ */ jsxRuntime.jsx(
3520
- PermissionDeniedPage,
3521
- {
3522
- operationId: result.operationId,
3523
- fromPath: ctx.req.path,
3524
- message: result.message,
3525
- userInfo: user,
3526
- pluginOptions: options
3527
- }
3528
- )
3529
- }
3530
- ),
3531
- 403
3532
- );
3533
- }
3534
- function buildNotificationFragments(notifications) {
3535
- if (notifications.length === 0) {
3536
- return null;
3537
- }
3538
- return notifications.map((notification, index) => /* @__PURE__ */ jsxRuntime.jsx(
3539
- "div",
3540
- {
3541
- id: "error-container",
3542
- "hx-swap-oob": "beforeend",
3543
- children: /* @__PURE__ */ jsxRuntime.jsx(
3544
- ErrorAlert,
3545
- {
3546
- type: notification.type,
3547
- title: notification.title,
3548
- message: notification.message
3549
- }
3550
- )
3551
- },
3552
- `notification-${index}`
3553
- ));
3554
- }
3555
- function isEmptyContent(result) {
3556
- if (result === null || result === void 0) return true;
3557
- if (typeof result === "string" && result.trim() === "") return true;
3558
- if (result && typeof result === "object" && "type" in result) {
3559
- if (result.type === "div") {
3560
- const props = result.props || {};
3561
- const children = props.children;
3562
- if (!children) return true;
3563
- if (Array.isArray(children) && children.length === 0) return true;
3564
- if (typeof children === "string" && children.trim() === "") return true;
3565
- }
3566
- }
3567
- return false;
3568
- }
3569
- async function isEmptyResponse(response) {
3570
- const clonedResponse = response.clone();
3571
- const text = await clonedResponse.text();
3572
- return !text || text.trim() === "" || text.trim() === "<div></div>" || text.trim().match(/^<div[^>]*>\s*<\/div>$/) !== null;
3573
- }
3574
- async function getDynamicMetadata(feature, context, defaultMetadata) {
3575
- if (feature?.getTitle) {
3576
- const title = await feature.getTitle(context);
3577
- const description = feature.getDescription ? await feature.getDescription(context) : defaultMetadata.description;
3578
- return { title, description };
3579
- }
3580
- return defaultMetadata;
3581
- }
3582
- async function getActions(feature, context) {
3583
- if (feature?.getActions) {
3584
- return await feature.getActions(context);
3585
- }
3586
- return [];
3587
- }
3588
- async function renderResult(ctx, context, result, renderOptions) {
3589
- const { options, metadata, currentPath, breadcrumbs, user, feature } = renderOptions;
3590
- const { notifications } = context;
3591
- const dynamicMetadata = await getDynamicMetadata(feature, context, metadata);
3592
- if (result instanceof Response) {
3593
- const status = result.status;
3594
- if (status === 302 || status === 301) {
3595
- const location = result.headers.get("Location");
3596
- if (location) {
3597
- if (context.isHtmxRequest) {
3598
- return ctx.html(/* @__PURE__ */ jsxRuntime.jsx("div", {}), 200, {
3599
- "HX-Redirect": location
3600
- });
3601
- }
3602
- return result;
3603
- }
3604
- }
3605
- if (status >= 300 && status < 400) {
3606
- const location = result.headers.get("Location");
3607
- if (location) {
3608
- if (context.isHtmxRequest) {
3609
- return ctx.html(/* @__PURE__ */ jsxRuntime.jsx("div", {}), 200, {
3610
- "HX-Redirect": location
3611
- });
3612
- }
3613
- return result;
3614
- }
4969
+ if (!patternPart && pattern.endsWith("*")) {
4970
+ return true;
3615
4971
  }
3616
- if (notifications.length > 0) {
3617
- const empty = await isEmptyResponse(result);
3618
- if (empty) {
3619
- const notificationFragments = buildNotificationFragments(notifications);
3620
- const titleFragment = /* @__PURE__ */ jsxRuntime.jsx("title", { children: dynamicMetadata.title });
3621
- return ctx.html(
3622
- /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3623
- notificationFragments,
3624
- titleFragment
3625
- ] }),
3626
- 200,
3627
- {
3628
- "HX-Reswap": "none"
3629
- // 关键:阻止 HTMX 替换任何内容
3630
- }
3631
- );
3632
- }
4972
+ if (!requiredPart) {
4973
+ return false;
4974
+ }
4975
+ if (requiredPart !== patternPart) {
4976
+ return false;
3633
4977
  }
3634
- return result;
3635
4978
  }
3636
- if (context.refresh) {
3637
- imeanServiceEngine.logger.info(
3638
- `[ResponseRenderer] Refresh requested (isDialog: ${context.isDialog}, isHtmxRequest: ${context.isHtmxRequest})`
3639
- );
4979
+ return true;
4980
+ }
4981
+ async function checkPermission(user, permission, ctx, options) {
4982
+ if (options.authProvider?.checkPermission) {
4983
+ return await options.authProvider.checkPermission(user, permission, ctx);
4984
+ }
4985
+ const userPermissions = user?.permissions || [];
4986
+ const result = checkUserPermission(permission, userPermissions);
4987
+ return {
4988
+ allowed: result.allowed,
4989
+ message: result.reason,
4990
+ operationId: permission
4991
+ };
4992
+ }
4993
+ async function handlePermissionDenied(ctx, result, options) {
4994
+ const isHtmxRequest = ctx.req.header("HX-Request") === "true";
4995
+ const loginUrl = options.authProvider?.loginUrl || "/admin/login";
4996
+ const user = await getUserInfo(ctx, options.authProvider);
4997
+ if (!user && !isHtmxRequest) {
4998
+ return ctx.redirect(loginUrl);
4999
+ }
5000
+ if (isHtmxRequest) {
3640
5001
  const headers = {
3641
- "HX-Refresh": "true"
5002
+ "HX-Retarget": "#dialog-container",
5003
+ "HX-Reswap": "innerHTML",
5004
+ "X-Permission-Denied": "true"
3642
5005
  };
3643
- const notificationFragments = buildNotificationFragments(notifications);
3644
- if (context.isDialog) {
3645
- return ctx.html(
3646
- /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3647
- /* @__PURE__ */ jsxRuntime.jsx("div", { id: "dialog-container", "hx-swap-oob": "innerHTML" }),
3648
- notificationFragments,
3649
- /* @__PURE__ */ jsxRuntime.jsx("title", { children: metadata.title })
3650
- ] }),
3651
- 200,
3652
- headers
3653
- );
3654
- }
3655
5006
  return ctx.html(
3656
- /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3657
- /* @__PURE__ */ jsxRuntime.jsx("div", {}),
3658
- notificationFragments,
3659
- /* @__PURE__ */ jsxRuntime.jsx("title", { children: metadata.title })
3660
- ] }),
5007
+ /* @__PURE__ */ jsxRuntime.jsx(Dialog, { title: "\u6743\u9650\u4E0D\u8DB3", size: "lg", children: /* @__PURE__ */ jsxRuntime.jsx(
5008
+ PermissionDeniedContent,
5009
+ {
5010
+ operationId: result.operationId,
5011
+ fromPath: ctx.req.path,
5012
+ message: result.message,
5013
+ userInfo: user,
5014
+ pluginOptions: options,
5015
+ isDialog: true
5016
+ }
5017
+ ) }),
3661
5018
  200,
3662
5019
  headers
3663
5020
  );
3664
5021
  }
3665
- if (context.redirectUrl && !context.refresh) {
3666
- imeanServiceEngine.logger.info(
3667
- `[ResponseRenderer] Redirect URL found: ${context.redirectUrl} (isHtmxRequest: ${context.isHtmxRequest}, isDialog: ${context.isDialog})`
3668
- );
3669
- if (context.isHtmxRequest) {
3670
- return ctx.html(/* @__PURE__ */ jsxRuntime.jsx("div", {}), 200, {
3671
- "HX-Redirect": context.redirectUrl
3672
- });
3673
- } else {
3674
- return ctx.redirect(context.redirectUrl);
3675
- }
3676
- } else if (context.redirectUrl && context.refresh) {
3677
- imeanServiceEngine.logger.info(
3678
- `[ResponseRenderer] Both redirect URL and refresh are set, using refresh (isDialog: ${context.isDialog})`
3679
- );
3680
- } else if (!context.redirectUrl) {
3681
- imeanServiceEngine.logger.info(
3682
- `[ResponseRenderer] No redirect URL found (result: ${result === null ? "null" : typeof result}, isHtmxRequest: ${context.isHtmxRequest})`
3683
- );
3684
- }
3685
- if (context.isHtmxRequest) {
3686
- const notificationFragments = buildNotificationFragments(notifications);
3687
- const titleFragment = /* @__PURE__ */ jsxRuntime.jsx("title", { children: metadata.title });
3688
- const empty = isEmptyContent(result);
3689
- if (empty && notifications.length > 0 && !context.redirectUrl) {
3690
- return ctx.html(
3691
- /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3692
- notificationFragments,
3693
- titleFragment
3694
- ] }),
3695
- 200,
3696
- {
3697
- "HX-Reswap": "none"
3698
- // 关键:阻止 HTMX 替换任何内容,即使 body 上有 hx-target
3699
- }
3700
- );
3701
- }
3702
- const target = context.isDialog ? "#dialog-container" : "#main-content";
3703
- const swap = context.isDialog ? "innerHTML" : "outerHTML";
3704
- const headers = {
3705
- "HX-Retarget": target,
3706
- "HX-Reswap": swap
3707
- };
3708
- if (!context.isDialog) {
3709
- const hasError = notifications.some((n) => n.type === "error");
3710
- if (hasError) {
3711
- const referer = ctx.req.header("Referer");
3712
- if (referer) {
3713
- try {
3714
- const refererUrl = new URL(referer);
3715
- headers["HX-Push-Url"] = refererUrl.pathname + refererUrl.search;
3716
- } catch {
3717
- headers["HX-Push-Url"] = "false";
3718
- }
3719
- } else {
3720
- headers["HX-Push-Url"] = "false";
3721
- }
3722
- } else {
3723
- const url = new URL(ctx.req.url);
3724
- headers["HX-Push-Url"] = url.pathname + url.search;
3725
- }
3726
- }
3727
- const actions = await getActions(renderOptions.feature, context);
3728
- if (context.isDialog) {
3729
- const dialogSize = renderOptions.feature?.dialogSize || "lg";
3730
- const closeOnBackdropClick = renderOptions.feature?.closeOnBackdropClick ?? true;
3731
- const isFormFeature = renderOptions.feature?.type === "create" || renderOptions.feature?.type === "edit";
3732
- const fixedContentHeight = isFormFeature;
3733
- return ctx.html(
3734
- /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3735
- /* @__PURE__ */ jsxRuntime.jsx(
3736
- Dialog,
3737
- {
3738
- title: dynamicMetadata.title,
3739
- size: dialogSize,
3740
- closeOnBackdropClick,
3741
- actions,
3742
- fixedContentHeight,
3743
- children: result
3744
- }
3745
- ),
3746
- notificationFragments,
3747
- titleFragment
3748
- ] }),
3749
- 200,
3750
- headers
3751
- );
3752
- } else {
3753
- const useAdminLayout = metadata.useAdminLayout !== false;
3754
- if (useAdminLayout) {
3755
- return ctx.html(
3756
- /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3757
- /* @__PURE__ */ jsxRuntime.jsx(
3758
- AdminLayout,
3759
- {
3760
- title: dynamicMetadata.title,
3761
- description: dynamicMetadata.description,
3762
- options,
3763
- useAdminLayout,
3764
- currentPath,
3765
- userInfo: user,
3766
- breadcrumbs,
3767
- actions,
3768
- children: result
3769
- }
3770
- ),
3771
- notificationFragments,
3772
- titleFragment
3773
- ] }),
3774
- 200,
3775
- headers
3776
- );
3777
- } else {
3778
- return ctx.html(
3779
- /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3780
- /* @__PURE__ */ jsxRuntime.jsx(NoLayout, { children: result }),
3781
- notificationFragments,
3782
- titleFragment
3783
- ] }),
3784
- 200,
3785
- headers
3786
- );
3787
- }
3788
- }
3789
- } else {
3790
- const cdnProxyPrefix = `${options.prefix}/_cdn`;
3791
- const useAdminLayout = metadata.useAdminLayout !== false;
3792
- const actions = await getActions(renderOptions.feature, context);
3793
- if (useAdminLayout) {
3794
- return ctx.html(
3795
- /* @__PURE__ */ jsxRuntime.jsx(
3796
- BaseLayout,
3797
- {
3798
- title: dynamicMetadata.title,
3799
- description: dynamicMetadata.description,
3800
- cdnProxyPrefix,
3801
- children: /* @__PURE__ */ jsxRuntime.jsx(
3802
- AdminLayout,
3803
- {
3804
- title: dynamicMetadata.title,
3805
- description: dynamicMetadata.description,
3806
- options,
3807
- useAdminLayout,
3808
- currentPath,
3809
- userInfo: user,
3810
- breadcrumbs,
3811
- actions,
3812
- children: result
3813
- }
3814
- )
3815
- }
3816
- )
3817
- );
3818
- } else {
3819
- return ctx.html(
3820
- /* @__PURE__ */ jsxRuntime.jsx(
3821
- BaseLayout,
5022
+ return ctx.html(
5023
+ /* @__PURE__ */ jsxRuntime.jsx(
5024
+ BaseLayout,
5025
+ {
5026
+ prefix: options.prefix,
5027
+ title: `\u6743\u9650\u4E0D\u8DB3 - ${options.title}`,
5028
+ description: "\u60A8\u6CA1\u6709\u6743\u9650\u8BBF\u95EE\u6B64\u8D44\u6E90",
5029
+ children: /* @__PURE__ */ jsxRuntime.jsx(
5030
+ PermissionDeniedPage,
3822
5031
  {
3823
- title: dynamicMetadata.title,
3824
- description: dynamicMetadata.description,
3825
- cdnProxyPrefix,
3826
- children: /* @__PURE__ */ jsxRuntime.jsx(NoLayout, { children: result })
5032
+ operationId: result.operationId,
5033
+ fromPath: ctx.req.path,
5034
+ message: result.message,
5035
+ userInfo: user,
5036
+ pluginOptions: options
3827
5037
  }
3828
5038
  )
3829
- );
3830
- }
5039
+ }
5040
+ ),
5041
+ 403
5042
+ );
5043
+ }
5044
+ function buildNotificationFragments(notifications) {
5045
+ if (notifications.length === 0) {
5046
+ return null;
3831
5047
  }
5048
+ return notifications.map((notification, index) => /* @__PURE__ */ jsxRuntime.jsx(
5049
+ "div",
5050
+ {
5051
+ id: "error-container",
5052
+ "hx-swap-oob": "beforeend",
5053
+ children: /* @__PURE__ */ jsxRuntime.jsx(
5054
+ ErrorAlert,
5055
+ {
5056
+ type: notification.type,
5057
+ title: notification.title,
5058
+ message: notification.message
5059
+ }
5060
+ )
5061
+ },
5062
+ `notification-${index}`
5063
+ ));
3832
5064
  }
3833
- function buildBreadcrumbs(currentPath, homePath, pageTitle) {
3834
- const breadcrumbs = [];
3835
- if (homePath) {
3836
- if (currentPath === homePath) {
3837
- breadcrumbs.push({ label: pageTitle, href: void 0 });
3838
- } else {
3839
- breadcrumbs.push({ label: "\u9996\u9875", href: homePath });
3840
- breadcrumbs.push({ label: pageTitle, href: void 0 });
5065
+ function isEmptyContent(result) {
5066
+ if (result === null || result === void 0) return true;
5067
+ if (typeof result === "string" && result.trim() === "") return true;
5068
+ if (result && typeof result === "object" && "type" in result) {
5069
+ if (result.type === "div") {
5070
+ const props = result.props || {};
5071
+ const children = props.children;
5072
+ if (!children) return true;
5073
+ if (Array.isArray(children) && children.length === 0) return true;
5074
+ if (typeof children === "string" && children.trim() === "") return true;
3841
5075
  }
3842
- } else {
3843
- breadcrumbs.push({ label: pageTitle, href: void 0 });
3844
5076
  }
3845
- return breadcrumbs;
5077
+ return false;
3846
5078
  }
3847
- function getCurrentPath(ctx) {
3848
- const referer = ctx.req.header("Referer");
3849
- let currentPath = ctx.req.path;
3850
- if (referer) {
3851
- try {
3852
- const refererUrl = new URL(referer);
3853
- const method = ctx.req.method;
3854
- if (["POST", "PUT", "DELETE"].includes(method)) {
3855
- currentPath = refererUrl.pathname;
3856
- }
3857
- } catch (e) {
3858
- }
5079
+ async function isEmptyResponse(response) {
5080
+ const clonedResponse = response.clone();
5081
+ const text = await clonedResponse.text();
5082
+ return !text || text.trim() === "" || text.trim() === "<div></div>" || text.trim().match(/^<div[^>]*>\s*<\/div>$/) !== null;
5083
+ }
5084
+ async function getDynamicMetadata(feature, context, defaultMetadata) {
5085
+ if (feature?.getTitle) {
5086
+ const title = await feature.getTitle(context);
5087
+ const description = feature.getDescription ? await feature.getDescription(context) : defaultMetadata.description;
5088
+ return { title, description };
3859
5089
  }
3860
- return currentPath;
5090
+ return defaultMetadata;
3861
5091
  }
3862
- async function handleRequest(ctx, page, feature, handlerOptions) {
3863
- try {
3864
- imeanServiceEngine.logger.info(
3865
- `[HtmxAdminPlugin] Handling request: ${ctx.req.method} ${ctx.req.path} (feature: ${feature.name}, page: ${page.modelName})`
3866
- );
3867
- const user = await getUserInfo(ctx, handlerOptions.options.authProvider);
3868
- if (feature.permission !== null && handlerOptions.options.authProvider) {
3869
- const permissionResult = await checkPermission(
3870
- user,
3871
- feature.permission,
3872
- ctx,
3873
- {
3874
- authProvider: handlerOptions.options.authProvider,
3875
- prefix: handlerOptions.options.prefix,
3876
- title: handlerOptions.options.title
5092
+ async function getActions(feature, context) {
5093
+ if (feature?.getActions) {
5094
+ return await feature.getActions(context);
5095
+ }
5096
+ return null;
5097
+ }
5098
+ async function renderResult(ctx, context, result, renderOptions) {
5099
+ const { options, metadata, currentPath, breadcrumbs, user, feature } = renderOptions;
5100
+ const { notifications } = context;
5101
+ const dynamicMetadata = await getDynamicMetadata(feature, context, metadata);
5102
+ if (result instanceof Response) {
5103
+ const status = result.status;
5104
+ if (status === 302 || status === 301) {
5105
+ const location = result.headers.get("Location");
5106
+ if (location) {
5107
+ if (context.isHtmxRequest) {
5108
+ return ctx.html(/* @__PURE__ */ jsxRuntime.jsx("div", {}), 200, {
5109
+ "HX-Redirect": location
5110
+ });
3877
5111
  }
3878
- );
3879
- if (!permissionResult.allowed) {
3880
- return await handlePermissionDenied(ctx, permissionResult, {
3881
- authProvider: handlerOptions.options.authProvider,
3882
- prefix: handlerOptions.options.prefix,
3883
- title: handlerOptions.options.title
3884
- });
3885
- }
3886
- }
3887
- const context = await createFeatureContext(ctx, page, feature, user, {
3888
- prefix: handlerOptions.options.prefix
3889
- });
3890
- const metadata = page.getMetadata();
3891
- const currentPath = getCurrentPath(ctx);
3892
- const breadcrumbs = buildBreadcrumbs(
3893
- currentPath,
3894
- handlerOptions.options.homePath,
3895
- metadata.title
3896
- );
3897
- if (feature.handle) {
3898
- const result = await feature.handle(context);
3899
- if (result !== void 0) {
3900
- return await renderResult(ctx, context, result, {
3901
- options: handlerOptions.options,
3902
- metadata,
3903
- feature,
3904
- currentPath,
3905
- breadcrumbs,
3906
- user
3907
- });
5112
+ return result;
3908
5113
  }
3909
5114
  }
3910
- if (feature.render) {
3911
- const result = await feature.render(context);
3912
- return await renderResult(ctx, context, result, {
3913
- options: handlerOptions.options,
3914
- metadata,
3915
- feature,
3916
- currentPath,
3917
- breadcrumbs,
3918
- user
3919
- });
3920
- }
3921
- const isHtmxRequest = ctx.req.header("HX-Request") === "true";
3922
- if (isHtmxRequest) {
3923
- return ctx.html(
3924
- /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3925
- /* @__PURE__ */ jsxRuntime.jsx("div", { id: "error-container", "hx-swap-oob": "beforeend", children: /* @__PURE__ */ jsxRuntime.jsx(
3926
- ErrorAlert,
3927
- {
3928
- type: "error",
3929
- title: "\u8BF7\u6C42\u5931\u8D25",
3930
- message: "Feature has no handler or render method"
3931
- }
3932
- ) }),
3933
- /* @__PURE__ */ jsxRuntime.jsx("title", { children: "\u9519\u8BEF" })
3934
- ] }),
3935
- 500,
3936
- {
3937
- "HX-Reswap": "none"
3938
- // 关键:不替换任何内容
3939
- }
3940
- );
3941
- }
3942
- return ctx.json({ error: "Feature has no handler or render method" }, 500);
3943
- } catch (error) {
3944
- imeanServiceEngine.logger.error(`[HtmxAdminPlugin] Error handling request:`, error);
3945
- const isHtmxRequest = ctx.req.header("HX-Request") === "true";
3946
- if (isHtmxRequest) {
3947
- const errorMessage = error instanceof Error ? error.message : "Internal server error";
3948
- return ctx.html(
3949
- /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3950
- /* @__PURE__ */ jsxRuntime.jsx("div", { id: "error-container", "hx-swap-oob": "beforeend", children: /* @__PURE__ */ jsxRuntime.jsx(ErrorAlert, { type: "error", title: "\u8BF7\u6C42\u5931\u8D25", message: errorMessage }) }),
3951
- /* @__PURE__ */ jsxRuntime.jsx("title", { children: "\u9519\u8BEF" })
3952
- ] }),
3953
- 500,
3954
- {
3955
- "HX-Reswap": "none"
3956
- // 关键:不替换任何内容,只显示通知
5115
+ if (status >= 300 && status < 400) {
5116
+ const location = result.headers.get("Location");
5117
+ if (location) {
5118
+ if (context.isHtmxRequest) {
5119
+ return ctx.html(/* @__PURE__ */ jsxRuntime.jsx("div", {}), 200, {
5120
+ "HX-Redirect": location
5121
+ });
3957
5122
  }
3958
- );
3959
- }
3960
- return ctx.json(
3961
- {
3962
- error: error instanceof Error ? error.message : "Internal server error"
3963
- },
3964
- 500
3965
- );
3966
- }
3967
- }
3968
-
3969
- // src/internal/route-registrar.ts
3970
- function registerCdnCacheRoutes(options) {
3971
- const { prefix } = options.options;
3972
- options.hono.get(`${prefix}/_cdn/:name`, async (ctx) => {
3973
- const name = ctx.req.param("name");
3974
- const { getCachedResource: getCachedResource2 } = await Promise.resolve().then(() => (init_cdn_cache(), cdn_cache_exports));
3975
- const cached = getCachedResource2(name);
3976
- if (cached) {
3977
- return ctx.body(cached.content, 200, {
3978
- "Content-Type": cached.mimeType,
3979
- "Cache-Control": "public, max-age=31536000, immutable"
3980
- // 缓存 1 年
3981
- });
5123
+ return result;
5124
+ }
3982
5125
  }
3983
- return ctx.text(`Resource "${name}" not cached`, 404);
3984
- });
3985
- imeanServiceEngine.logger.info(`[HtmxAdminPlugin] CDN \u7F13\u5B58\u4EE3\u7406\u8DEF\u7531\u5DF2\u6CE8\u518C: ${prefix}/_cdn/:name`);
3986
- }
3987
- function registerPageRoutes(page, options) {
3988
- const basePath = `${options.options.prefix}${modelNameToPath(page.modelName)}`;
3989
- const features = page.features.getAll();
3990
- for (const feature of features) {
3991
- const routes = feature.getRoutes();
3992
- for (const route of routes) {
3993
- const fullPath = `${basePath}${route.path}`;
3994
- imeanServiceEngine.logger.info(
3995
- `[HtmxAdminPlugin] Registering route: ${route.method.toUpperCase()} ${fullPath} (feature: ${feature.name}, permission: ${feature.permission || "none"})`
3996
- );
3997
- const handler = async (ctx) => {
3998
- return handleRequest(ctx, page, feature, {
3999
- options: options.options
4000
- });
4001
- };
4002
- options.hono[route.method](fullPath, handler);
4003
- if (route.method === "put" || route.method === "delete") {
4004
- const postHandler = async (ctx) => {
4005
- const methodOverride = ctx.req.header("X-HTTP-Method-Override");
4006
- const expectedMethod = route.method.toUpperCase();
4007
- if (methodOverride === expectedMethod) {
4008
- imeanServiceEngine.logger.info(
4009
- `[HtmxAdminPlugin] Method override detected: POST ${fullPath} -> ${expectedMethod} (feature: ${feature.name})`
4010
- );
4011
- return handleRequest(ctx, page, feature, {
4012
- options: options.options
4013
- });
5126
+ if (notifications.length > 0) {
5127
+ const empty = await isEmptyResponse(result);
5128
+ if (empty) {
5129
+ const notificationFragments = buildNotificationFragments(notifications);
5130
+ const titleFragment = /* @__PURE__ */ jsxRuntime.jsx("title", { children: dynamicMetadata.title });
5131
+ return ctx.html(
5132
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5133
+ notificationFragments,
5134
+ titleFragment
5135
+ ] }),
5136
+ 200,
5137
+ {
5138
+ "HX-Reswap": "none"
5139
+ // 关键:阻止 HTMX 替换任何内容
4014
5140
  }
4015
- imeanServiceEngine.logger.warn(
4016
- `[HtmxAdminPlugin] POST request to ${fullPath} without matching X-HTTP-Method-Override header (got: ${methodOverride || "none"}, expected: ${expectedMethod})`
4017
- );
4018
- return ctx.text("Method Not Allowed", 405);
4019
- };
4020
- imeanServiceEngine.logger.info(
4021
- `[HtmxAdminPlugin] Registering POST route for method override: POST ${fullPath} (actual method: ${route.method.toUpperCase()}, feature: ${feature.name})`
4022
5141
  );
4023
- options.hono.post(fullPath, postHandler);
4024
5142
  }
4025
5143
  }
5144
+ return result;
4026
5145
  }
4027
- }
4028
- function registerHomeRedirect(pages, options) {
4029
- const { prefix, homePath } = options.options;
4030
- if (homePath) {
4031
- options.hono.get(prefix, async (ctx) => {
4032
- return ctx.redirect(homePath);
4033
- });
4034
- } else if (pages.size > 0) {
4035
- const firstPage = Array.from(pages.values())[0];
4036
- const firstPath = `${prefix}${modelNameToPath(firstPage.modelName)}`;
4037
- options.hono.get(prefix, async (ctx) => {
4038
- return ctx.redirect(firstPath);
4039
- });
4040
- }
4041
- }
4042
-
4043
- // src/plugin.tsx
4044
- init_cdn_cache();
4045
- var HtmxAdminPlugin = class {
4046
- name = "htmx-admin-plugin";
4047
- priority = imeanServiceEngine.PluginPriority.ROUTE;
4048
- engine;
4049
- hono;
4050
- options;
4051
- serviceName = "";
4052
- pages = /* @__PURE__ */ new Map();
4053
- constructor(options) {
4054
- this.options = {
4055
- title: options?.title || "\u7BA1\u7406\u540E\u53F0",
4056
- logo: options?.logo || "",
4057
- prefix: options?.prefix || "/admin",
4058
- homePath: options?.homePath || "",
4059
- navigation: options?.navigation ?? [],
4060
- authProvider: options?.authProvider
5146
+ if (context.refresh) {
5147
+ imeanServiceEngine.logger.info(
5148
+ `[ResponseRenderer] Refresh requested (isDialog: ${context.isDialog}, isHtmxRequest: ${context.isHtmxRequest})`
5149
+ );
5150
+ const headers = {
5151
+ "HX-Refresh": "true"
4061
5152
  };
4062
- }
4063
- /**
4064
- * 注册页面(统一接口,支持有模型和无模型)
4065
- */
4066
- registerPage(page) {
4067
- if (this.pages.has(page.modelName)) {
4068
- throw new Error(
4069
- `Page with name "${page.modelName}" is already registered`
5153
+ const notificationFragments = buildNotificationFragments(notifications);
5154
+ if (context.isDialog) {
5155
+ return ctx.html(
5156
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5157
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "dialog-container", "hx-swap-oob": "innerHTML" }),
5158
+ notificationFragments,
5159
+ /* @__PURE__ */ jsxRuntime.jsx("title", { children: metadata.title })
5160
+ ] }),
5161
+ 200,
5162
+ headers
4070
5163
  );
4071
5164
  }
4072
- this.pages.set(page.modelName, page);
4073
- imeanServiceEngine.logger.info(`[HtmxAdminPlugin] Registered page: ${page.modelName}`);
4074
- return this;
5165
+ return ctx.html(
5166
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5167
+ /* @__PURE__ */ jsxRuntime.jsx("div", {}),
5168
+ notificationFragments,
5169
+ /* @__PURE__ */ jsxRuntime.jsx("title", { children: metadata.title })
5170
+ ] }),
5171
+ 200,
5172
+ headers
5173
+ );
4075
5174
  }
4076
- /**
4077
- * 批量注册页面
4078
- */
4079
- registerPages(...pages) {
4080
- for (const page of pages) {
4081
- this.registerPage(page);
5175
+ if (context.redirectUrl && !context.refresh) {
5176
+ imeanServiceEngine.logger.info(
5177
+ `[ResponseRenderer] Redirect URL found: ${context.redirectUrl} (isHtmxRequest: ${context.isHtmxRequest}, isDialog: ${context.isDialog})`
5178
+ );
5179
+ if (context.isHtmxRequest) {
5180
+ return ctx.html(/* @__PURE__ */ jsxRuntime.jsx("div", {}), 200, {
5181
+ "HX-Redirect": context.redirectUrl
5182
+ });
5183
+ } else {
5184
+ return ctx.redirect(context.redirectUrl);
4082
5185
  }
4083
- return this;
4084
- }
4085
- /**
4086
- * 引擎初始化钩子
4087
- */
4088
- onInit(engine) {
4089
- this.engine = engine;
4090
- this.hono = engine.getHono();
4091
- this.serviceName = engine.options.name;
5186
+ } else if (context.redirectUrl && context.refresh) {
4092
5187
  imeanServiceEngine.logger.info(
4093
- `HtmxAdminPlugin initialized${this.serviceName ? ` (service: ${this.serviceName})` : ""}`
5188
+ `[ResponseRenderer] Both redirect URL and refresh are set, using refresh (isDialog: ${context.isDialog})`
5189
+ );
5190
+ } else if (!context.redirectUrl) {
5191
+ imeanServiceEngine.logger.info(
5192
+ `[ResponseRenderer] No redirect URL found (result: ${result === null ? "null" : typeof result}, isHtmxRequest: ${context.isHtmxRequest})`
4094
5193
  );
4095
- initializeCdnCache().catch((error) => {
4096
- imeanServiceEngine.logger.error("[HtmxAdminPlugin] CDN \u7F13\u5B58\u521D\u59CB\u5316\u5931\u8D25", error);
4097
- });
4098
5194
  }
4099
- /**
4100
- * 引擎启动后注册路由
4101
- */
4102
- onAfterStart(engine) {
4103
- const routeOptions = {
4104
- options: this.options,
4105
- hono: this.hono
5195
+ if (context.isHtmxRequest) {
5196
+ const notificationFragments = buildNotificationFragments(notifications);
5197
+ const titleFragment = /* @__PURE__ */ jsxRuntime.jsx("title", { children: metadata.title });
5198
+ const empty = isEmptyContent(result);
5199
+ if (empty && notifications.length > 0 && !context.redirectUrl) {
5200
+ return ctx.html(
5201
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5202
+ notificationFragments,
5203
+ titleFragment
5204
+ ] }),
5205
+ 200,
5206
+ {
5207
+ "HX-Reswap": "none"
5208
+ // 关键:阻止 HTMX 替换任何内容,即使 body 上有 hx-target
5209
+ }
5210
+ );
5211
+ }
5212
+ const target = context.isDialog ? "#dialog-container" : "#main-content";
5213
+ const swap = context.isDialog ? "innerHTML" : "outerHTML";
5214
+ const headers = {
5215
+ "HX-Retarget": target,
5216
+ "HX-Reswap": swap
4106
5217
  };
4107
- registerCdnCacheRoutes(routeOptions);
4108
- for (const [modelName, page] of this.pages) {
4109
- registerPageRoutes(page, routeOptions);
5218
+ if (!context.isDialog) {
5219
+ const hasError = notifications.some((n) => n.type === "error");
5220
+ if (hasError) {
5221
+ const referer = ctx.req.header("Referer");
5222
+ if (referer) {
5223
+ try {
5224
+ const refererUrl = new URL(referer);
5225
+ headers["HX-Push-Url"] = refererUrl.pathname + refererUrl.search;
5226
+ } catch {
5227
+ headers["HX-Push-Url"] = "false";
5228
+ }
5229
+ } else {
5230
+ headers["HX-Push-Url"] = "false";
5231
+ }
5232
+ } else {
5233
+ const url = new URL(ctx.req.url);
5234
+ headers["HX-Push-Url"] = url.pathname + url.search;
5235
+ }
5236
+ }
5237
+ const actions = await getActions(renderOptions.feature, context);
5238
+ if (context.isDialog) {
5239
+ const dialogSize = renderOptions.feature?.dialogSize || "lg";
5240
+ const closeOnBackdropClick = renderOptions.feature?.closeOnBackdropClick ?? true;
5241
+ const isFormFeature = renderOptions.feature?.type === "create" || renderOptions.feature?.type === "edit";
5242
+ const fixedContentHeight = isFormFeature;
5243
+ return ctx.html(
5244
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5245
+ /* @__PURE__ */ jsxRuntime.jsx(
5246
+ Dialog,
5247
+ {
5248
+ title: dynamicMetadata.title,
5249
+ size: dialogSize,
5250
+ closeOnBackdropClick,
5251
+ actions,
5252
+ fixedContentHeight,
5253
+ children: result
5254
+ }
5255
+ ),
5256
+ notificationFragments,
5257
+ titleFragment
5258
+ ] }),
5259
+ 200,
5260
+ headers
5261
+ );
5262
+ } else {
5263
+ const useAdminLayout = metadata.useAdminLayout !== false;
5264
+ if (useAdminLayout) {
5265
+ return ctx.html(
5266
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5267
+ /* @__PURE__ */ jsxRuntime.jsx(
5268
+ AdminLayout,
5269
+ {
5270
+ title: dynamicMetadata.title,
5271
+ description: dynamicMetadata.description,
5272
+ options,
5273
+ useAdminLayout,
5274
+ currentPath,
5275
+ userInfo: user,
5276
+ componentRegistry: renderOptions.componentRegistry,
5277
+ breadcrumbs,
5278
+ actions,
5279
+ children: result
5280
+ }
5281
+ ),
5282
+ notificationFragments,
5283
+ titleFragment
5284
+ ] }),
5285
+ 200,
5286
+ headers
5287
+ );
5288
+ } else {
5289
+ return ctx.html(
5290
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5291
+ /* @__PURE__ */ jsxRuntime.jsx(NoLayout, { children: result }),
5292
+ notificationFragments,
5293
+ titleFragment
5294
+ ] }),
5295
+ 200,
5296
+ headers
5297
+ );
5298
+ }
5299
+ }
5300
+ } else {
5301
+ `${options.prefix}/_cdn`;
5302
+ const useAdminLayout = metadata.useAdminLayout !== false;
5303
+ const actions = await getActions(renderOptions.feature, context);
5304
+ if (useAdminLayout) {
5305
+ return ctx.html(
5306
+ /* @__PURE__ */ jsxRuntime.jsx(
5307
+ BaseLayout,
5308
+ {
5309
+ prefix: options.prefix,
5310
+ title: dynamicMetadata.title,
5311
+ description: dynamicMetadata.description,
5312
+ componentRegistry: renderOptions.componentRegistry,
5313
+ children: /* @__PURE__ */ jsxRuntime.jsx(
5314
+ AdminLayout,
5315
+ {
5316
+ title: dynamicMetadata.title,
5317
+ description: dynamicMetadata.description,
5318
+ options,
5319
+ useAdminLayout,
5320
+ currentPath,
5321
+ userInfo: user,
5322
+ breadcrumbs,
5323
+ actions,
5324
+ componentRegistry: renderOptions.componentRegistry,
5325
+ children: result
5326
+ }
5327
+ )
5328
+ }
5329
+ )
5330
+ );
5331
+ } else {
5332
+ return ctx.html(
5333
+ /* @__PURE__ */ jsxRuntime.jsx(
5334
+ BaseLayout,
5335
+ {
5336
+ prefix: options.prefix,
5337
+ title: dynamicMetadata.title,
5338
+ description: dynamicMetadata.description,
5339
+ componentRegistry: renderOptions.componentRegistry,
5340
+ children: /* @__PURE__ */ jsxRuntime.jsx(NoLayout, { children: result })
5341
+ }
5342
+ )
5343
+ );
4110
5344
  }
4111
- registerHomeRedirect(this.pages, routeOptions);
4112
5345
  }
4113
- };
4114
- function ObjectEditor(props) {
4115
- const { value, fieldName, objectSchema } = props;
4116
- if (!objectSchema) {
4117
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 border border-yellow-300 rounded-lg bg-yellow-50 text-yellow-800 text-sm", children: "\u8BF7\u63D0\u4F9B objectSchema \u53C2\u6570\u4EE5\u4F7F\u7528\u5BF9\u8C61\u7F16\u8F91\u5668" });
5346
+ }
5347
+ function buildBreadcrumbs(currentPath, homePath, pageTitle) {
5348
+ const breadcrumbs = [];
5349
+ if (homePath) {
5350
+ if (currentPath === homePath) {
5351
+ breadcrumbs.push({ label: pageTitle, href: void 0 });
5352
+ } else {
5353
+ breadcrumbs.push({ label: "\u9996\u9875", href: homePath });
5354
+ breadcrumbs.push({ label: pageTitle, href: void 0 });
5355
+ }
5356
+ } else {
5357
+ breadcrumbs.push({ label: pageTitle, href: void 0 });
4118
5358
  }
4119
- const fields = parseSchemaToFields(objectSchema);
4120
- const initialObject = value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
4121
- fields.forEach((field) => {
4122
- if (!(field.name in initialObject)) {
4123
- if (field.type === "number") {
4124
- initialObject[field.name] = field.required ? 0 : void 0;
4125
- } else if (field.type === "checkbox") {
4126
- initialObject[field.name] = field.required ? false : void 0;
4127
- } else {
4128
- initialObject[field.name] = field.required ? "" : void 0;
5359
+ return breadcrumbs;
5360
+ }
5361
+ function getCurrentPath(ctx) {
5362
+ const referer = ctx.req.header("Referer");
5363
+ let currentPath = ctx.req.path;
5364
+ if (referer) {
5365
+ try {
5366
+ const refererUrl = new URL(referer);
5367
+ const method = ctx.req.method;
5368
+ if (["POST", "PUT", "DELETE"].includes(method)) {
5369
+ currentPath = refererUrl.pathname;
4129
5370
  }
5371
+ } catch (e) {
4130
5372
  }
4131
- });
4132
- const fieldNames = fields.map((f) => f.name);
4133
- const fieldNamesJson = JSON.stringify(fieldNames);
4134
- const initialValueJson = JSON.stringify(initialObject);
4135
- const xDataContent = `{
4136
- obj: {},
4137
- init() {
4138
- const dataAttr = this.$el.getAttribute('data-initial-value');
4139
- if (dataAttr) {
4140
- try {
4141
- this.obj = JSON.parse(dataAttr);
4142
- } catch (e) {
4143
- console.error('Failed to parse initial value:', e);
4144
- this.obj = {};
4145
- }
4146
- }
4147
- const fieldNames = ${fieldNamesJson};
4148
- fieldNames.forEach(fieldName => {
4149
- if (!(fieldName in this.obj)) {
4150
- this.obj[fieldName] = undefined;
5373
+ }
5374
+ return currentPath;
5375
+ }
5376
+ async function handleRequest(ctx, page, feature, handlerOptions) {
5377
+ try {
5378
+ imeanServiceEngine.logger.info(
5379
+ `[HtmxAdminPlugin] Handling request: ${ctx.req.method} ${ctx.req.path} (feature: ${feature.name}, page: ${page.modelName})`
5380
+ );
5381
+ const user = await getUserInfo(ctx, handlerOptions.options.authProvider);
5382
+ if (feature.permission !== null && handlerOptions.options.authProvider) {
5383
+ const permissionResult = await checkPermission(
5384
+ user,
5385
+ feature.permission,
5386
+ ctx,
5387
+ {
5388
+ authProvider: handlerOptions.options.authProvider,
5389
+ prefix: handlerOptions.options.prefix,
5390
+ title: handlerOptions.options.title
4151
5391
  }
4152
- });
4153
- this.updateHiddenField();
4154
- },
4155
- updateHiddenField() {
4156
- const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
4157
- if (hiddenInput) {
4158
- hiddenInput.value = JSON.stringify(this.obj);
4159
- }
4160
- },
4161
- updateField(fieldName, value, fieldType, required) {
4162
- let convertedValue = value;
4163
- if (fieldType === 'number') {
4164
- convertedValue = value === '' ? (required ? 0 : undefined) : Number(value);
4165
- if (isNaN(convertedValue)) convertedValue = required ? 0 : undefined;
4166
- } else if (fieldType === 'checkbox') {
4167
- convertedValue = value === 'true' || value === true || value === '1' || value === 1;
4168
- } else {
4169
- convertedValue = value || (required ? '' : undefined);
4170
- }
4171
- if (convertedValue === undefined && !required) {
4172
- delete this.obj[fieldName];
4173
- } else {
4174
- this.obj[fieldName] = convertedValue;
5392
+ );
5393
+ if (!permissionResult.allowed) {
5394
+ return await handlePermissionDenied(ctx, permissionResult, {
5395
+ authProvider: handlerOptions.options.authProvider,
5396
+ prefix: handlerOptions.options.prefix,
5397
+ title: handlerOptions.options.title
5398
+ });
4175
5399
  }
4176
- this.updateHiddenField();
4177
5400
  }
4178
- }`;
4179
- const generateField = (field) => {
4180
- const fieldId = `${fieldName}-${field.name}`;
4181
- const fieldNameVar = `obj.${field.name}`;
4182
- const requiredValue = field.required ? "true" : "false";
4183
- const fieldNameForJs = JSON.stringify(field.name);
4184
- let inputElement;
4185
- if (field.type === "text") {
4186
- inputElement = html.html`
4187
- <input
4188
- type="text"
4189
- id="${fieldId}"
4190
- x-bind:value="${fieldNameVar} || ''"
4191
- x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
4192
- class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
4193
- data-testid="${fieldName}-input-${field.name}"
4194
- ${field.required ? "required" : ""}
4195
- />
4196
- `;
4197
- } else if (field.type === "textarea") {
4198
- inputElement = html.html`
4199
- <textarea
4200
- id="${fieldId}"
4201
- x-bind:value="${fieldNameVar} || ''"
4202
- x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
4203
- rows="4"
4204
- class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-y"
4205
- data-testid="${fieldName}-input-${field.name}"
4206
- ${field.required ? "required" : ""}
4207
- ></textarea>
4208
- `;
4209
- } else if (field.type === "number") {
4210
- const step = field.step || (field.step === void 0 ? "1" : "any");
4211
- inputElement = html.html`
4212
- <input
4213
- type="number"
4214
- id="${fieldId}"
4215
- x-bind:value="${fieldNameVar} != null ? ${fieldNameVar} : ''"
4216
- x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'number', ${requiredValue})"
4217
- step="${step}"
4218
- class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
4219
- data-testid="${fieldName}-input-${field.name}"
4220
- ${field.required ? "required" : ""}
4221
- />
4222
- `;
4223
- } else if (field.type === "date") {
4224
- inputElement = html.html`
4225
- <input
4226
- type="date"
4227
- id="${fieldId}"
4228
- x-bind:value="${fieldNameVar} || ''"
4229
- x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'date', ${requiredValue})"
4230
- class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
4231
- data-testid="${fieldName}-input-${field.name}"
4232
- ${field.required ? "required" : ""}
4233
- />
4234
- `;
4235
- } else if (field.type === "email") {
4236
- inputElement = html.html`
4237
- <input
4238
- type="email"
4239
- id="${fieldId}"
4240
- x-bind:value="${fieldNameVar} || ''"
4241
- x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
4242
- class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
4243
- data-testid="${fieldName}-input-${field.name}"
4244
- ${field.required ? "required" : ""}
4245
- />
4246
- `;
4247
- } else if (field.type === "select" && field.options) {
4248
- inputElement = html.html`
4249
- <select
4250
- id="${fieldId}"
4251
- x-bind:value="${fieldNameVar} || ''"
4252
- x-on:change="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
4253
- class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
4254
- data-testid="${fieldName}-select-${field.name}"
4255
- ${field.required ? "required" : ""}
4256
- >
4257
- ${!field.required ? html.html`<option value="">请选择</option>` : ""}
4258
- ${field.options.map(
4259
- (option) => html.html`
4260
- <option value="${String(option.value)}">${option.label}</option>
4261
- `
4262
- )}
4263
- </select>
4264
- `;
4265
- } else if (field.type === "checkbox") {
4266
- inputElement = html.html`
4267
- <div class="flex items-center">
4268
- <input
4269
- type="checkbox"
4270
- id="${fieldId}"
4271
- x-bind:checked="${fieldNameVar} === true || ${fieldNameVar} === 'true' || ${fieldNameVar} === 1 || ${fieldNameVar} === '1'"
4272
- x-on:change="updateField(${fieldNameForJs}, $event.target.checked, 'checkbox', ${requiredValue})"
4273
- class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
4274
- data-testid="${fieldName}-checkbox-${field.name}"
4275
- />
4276
- <label for="${fieldId}" class="ml-2 text-sm text-gray-700">
4277
- ${field.label}
4278
- </label>
4279
- </div>
4280
- `;
5401
+ const context = await createFeatureContext(ctx, page, feature, user, {
5402
+ prefix: handlerOptions.options.prefix
5403
+ });
5404
+ const metadata = page.getMetadata();
5405
+ const currentPath = getCurrentPath(ctx);
5406
+ const breadcrumbs = buildBreadcrumbs(
5407
+ currentPath,
5408
+ handlerOptions.options.homePath,
5409
+ metadata.title
5410
+ );
5411
+ if (feature.handle) {
5412
+ const result = await feature.handle(context);
5413
+ if (result !== void 0) {
5414
+ return await renderResult(ctx, context, result, {
5415
+ options: handlerOptions.options,
5416
+ metadata,
5417
+ feature,
5418
+ currentPath,
5419
+ breadcrumbs,
5420
+ user
5421
+ });
5422
+ }
4281
5423
  }
4282
- return html.html`
4283
- <div class="space-y-2" data-testid="${fieldName}-field-${field.name}">
4284
- ${field.type !== "checkbox" ? html.html`
4285
- <label
4286
- for="${fieldId}"
4287
- class="block text-sm font-semibold text-gray-700"
4288
- data-testid="${fieldName}-label-${field.name}"
4289
- >
4290
- ${field.label}
4291
- ${field.required ? html.html`<span class="text-red-500 ml-1">*</span>` : ""}
4292
- </label>
4293
- ` : ""}
4294
- ${inputElement}
4295
- </div>
4296
- `;
4297
- };
4298
- return html.html`
4299
- <div
4300
- x-data="${xDataContent}"
4301
- data-initial-value="${initialValueJson}"
4302
- x-init="init()"
4303
- class="space-y-4"
4304
- >
4305
- <input
4306
- type="hidden"
4307
- name="${fieldName}"
4308
- value=""
4309
- data-testid="hidden-${fieldName}"
4310
- />
4311
- <div class="space-y-4">
4312
- ${fields.map((field) => generateField(field))}
4313
- </div>
4314
- </div>
4315
- `;
4316
- }
4317
- function StringArrayEditor(props) {
4318
- const {
4319
- value,
4320
- fieldName,
4321
- placeholder = "\u8BF7\u8F93\u5165\u5185\u5BB9",
4322
- allowEmpty = false
4323
- } = props;
4324
- const initialItems = value || [];
4325
- const initialValueJson = JSON.stringify(initialItems);
4326
- const xDataContent = `{
4327
- items: ${initialValueJson},
4328
- draggedIndex: null,
4329
- draggedOverIndex: null,
4330
- fieldName: ${JSON.stringify(fieldName)},
4331
- placeholder: ${JSON.stringify(placeholder)},
4332
- allowEmpty: ${allowEmpty},
4333
- init() {
4334
- const dataAttr = this.$el.getAttribute('data-initial-value');
4335
- if (dataAttr) {
4336
- try {
4337
- const parsed = JSON.parse(dataAttr);
4338
- if (Array.isArray(parsed)) {
4339
- this.items = parsed;
4340
- } else {
4341
- this.items = [];
4342
- }
4343
- } catch (e) {
4344
- console.error('Failed to parse initial value:', e);
4345
- this.items = [];
5424
+ const isHtmxRequest = ctx.req.header("HX-Request") === "true";
5425
+ if (isHtmxRequest) {
5426
+ return ctx.html(
5427
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5428
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "error-container", "hx-swap-oob": "beforeend", children: /* @__PURE__ */ jsxRuntime.jsx(
5429
+ ErrorAlert,
5430
+ {
5431
+ type: "error",
5432
+ title: "\u8BF7\u6C42\u5931\u8D25",
5433
+ message: "Feature has no handler or render method"
5434
+ }
5435
+ ) }),
5436
+ /* @__PURE__ */ jsxRuntime.jsx("title", { children: "\u9519\u8BEF" })
5437
+ ] }),
5438
+ 500,
5439
+ {
5440
+ "HX-Reswap": "none"
5441
+ // 关键:不替换任何内容
4346
5442
  }
4347
- }
4348
- this.updateHiddenField();
4349
- },
4350
- updateHiddenField() {
4351
- const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
4352
- if (hiddenInput) {
4353
- hiddenInput.value = JSON.stringify(this.items);
4354
- }
4355
- },
4356
- addItem() {
4357
- this.items.push('');
4358
- this.updateHiddenField();
4359
- this.$nextTick(() => {
4360
- const keyInputs = this.$el.querySelectorAll('input[data-testid*="-input-"]');
4361
- if (keyInputs.length > 0) {
4362
- const lastInput = keyInputs[keyInputs.length - 1];
4363
- if (lastInput && lastInput.focus) {
4364
- lastInput.focus();
4365
- }
5443
+ );
5444
+ }
5445
+ return ctx.json({ error: "Feature has no handler or render method" }, 500);
5446
+ } catch (error) {
5447
+ imeanServiceEngine.logger.error(`[HtmxAdminPlugin] Error handling request:`, error);
5448
+ const isHtmxRequest = ctx.req.header("HX-Request") === "true";
5449
+ if (isHtmxRequest) {
5450
+ const errorMessage = error instanceof Error ? error.message : "Internal server error";
5451
+ return ctx.html(
5452
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5453
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "error-container", "hx-swap-oob": "beforeend", children: /* @__PURE__ */ jsxRuntime.jsx(ErrorAlert, { type: "error", title: "\u8BF7\u6C42\u5931\u8D25", message: errorMessage }) }),
5454
+ /* @__PURE__ */ jsxRuntime.jsx("title", { children: "\u9519\u8BEF" })
5455
+ ] }),
5456
+ 500,
5457
+ {
5458
+ "HX-Reswap": "none"
5459
+ // 关键:不替换任何内容,只显示通知
4366
5460
  }
4367
- });
4368
- },
4369
- removeItem(index) {
4370
- this.items.splice(index, 1);
4371
- this.updateHiddenField();
4372
- },
4373
- updateItem(index, value) {
4374
- this.items[index] = value;
4375
- this.updateHiddenField();
4376
- },
4377
- handleDragStart(index, event) {
4378
- this.draggedIndex = index;
4379
- event.dataTransfer.effectAllowed = 'move';
4380
- event.dataTransfer.setData('text/plain', index.toString());
4381
- const target = event.currentTarget || event.target.closest('[draggable="true"]');
4382
- if (target) {
4383
- target.style.opacity = '0.5';
4384
- }
4385
- },
4386
- handleDragEnd(event) {
4387
- const target = event.currentTarget || event.target.closest('[draggable="true"]');
4388
- if (target) {
4389
- target.style.opacity = '';
4390
- }
4391
- this.draggedIndex = null;
4392
- this.draggedOverIndex = null;
4393
- },
4394
- handleDragOver(index, event) {
4395
- event.preventDefault();
4396
- event.dataTransfer.dropEffect = 'move';
4397
- this.draggedOverIndex = index;
4398
- },
4399
- handleDragLeave() {
4400
- this.draggedOverIndex = null;
4401
- },
4402
- handleDrop(index, event) {
4403
- event.preventDefault();
4404
- if (this.draggedIndex !== null && this.draggedIndex !== index) {
4405
- const draggedItem = this.items[this.draggedIndex];
4406
- this.items.splice(this.draggedIndex, 1);
4407
- this.items.splice(index, 0, draggedItem);
4408
- this.updateHiddenField();
4409
- }
4410
- this.draggedIndex = null;
4411
- this.draggedOverIndex = null;
5461
+ );
4412
5462
  }
4413
- }`;
4414
- return html.html`
4415
- <div
4416
- x-data="${xDataContent}"
4417
- data-initial-value="${initialValueJson}"
4418
- x-init="init()"
4419
- class="space-y-3"
4420
- >
4421
- <input
4422
- type="hidden"
4423
- name="${fieldName}"
4424
- value=""
4425
- data-testid="hidden-${fieldName}"
4426
- />
4427
- <div class="space-y-3">
4428
- <!-- 头部:显示数量和添加按钮 -->
4429
- <div class="flex items-center justify-between">
4430
- <span class="text-sm text-gray-600">
4431
- 共 <span x-text="items.length">0</span> 项
4432
- </span>
4433
- <button
4434
- type="button"
4435
- x-on:click="addItem()"
4436
- class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium flex items-center gap-2"
4437
- data-testid="${fieldName}-add-button"
4438
- >
4439
- <svg
4440
- class="w-4 h-4"
4441
- fill="none"
4442
- stroke="currentColor"
4443
- viewBox="0 0 24 24"
4444
- >
4445
- <path
4446
- stroke-linecap="round"
4447
- stroke-linejoin="round"
4448
- stroke-width="2"
4449
- d="M12 4v16m8-8H4"
4450
- />
4451
- </svg>
4452
- 添加项
4453
- </button>
4454
- </div>
4455
-
4456
- <!-- 列表项 -->
4457
- <div class="space-y-2" x-show="items.length > 0">
4458
- <template x-for="(item, index) in items" x-bind:key="index">
4459
- <div
4460
- class="flex items-center gap-2 group"
4461
- x-bind:class="{
4462
- 'opacity-50': draggedIndex === index,
4463
- 'border-blue-300 bg-blue-50': draggedOverIndex === index && draggedIndex !== null && draggedIndex !== index
4464
- }"
4465
- draggable="true"
4466
- x-on:dragstart="handleDragStart(index, $event)"
4467
- x-on:dragend="handleDragEnd($event)"
4468
- x-on:dragover="handleDragOver(index, $event)"
4469
- x-on:dragleave="handleDragLeave()"
4470
- x-on:drop="handleDrop(index, $event)"
4471
- x-bind:data-testid="fieldName + '-item-' + index"
4472
- >
4473
- <!-- 拖拽手柄 -->
4474
- <div
4475
- class="flex-shrink-0 cursor-move text-gray-400 hover:text-gray-600 transition-colors p-1"
4476
- x-bind:data-testid="fieldName + '-drag-handle-' + index"
4477
- title="拖拽排序"
4478
- >
4479
- <svg
4480
- class="w-5 h-5"
4481
- fill="none"
4482
- stroke="currentColor"
4483
- viewBox="0 0 24 24"
4484
- >
4485
- <path
4486
- stroke-linecap="round"
4487
- stroke-linejoin="round"
4488
- stroke-width="2"
4489
- d="M4 8h16M4 16h16"
4490
- />
4491
- </svg>
4492
- </div>
4493
-
4494
- <!-- 输入框 -->
4495
- <input
4496
- type="text"
4497
- x-bind:value="items[index] || ''"
4498
- x-on:input="updateItem(index, $event.target.value)"
4499
- x-bind:placeholder="placeholder + ' ' + (index + 1)"
4500
- class="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
4501
- x-bind:data-testid="fieldName + '-input-' + index"
4502
- x-bind:required="!allowEmpty"
4503
- />
4504
-
4505
- <!-- 删除按钮 -->
4506
- <button
4507
- type="button"
4508
- x-on:click="removeItem(index)"
4509
- class="flex-shrink-0 px-3 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors"
4510
- x-bind:data-testid="fieldName + '-remove-button-' + index"
4511
- title="删除此项"
4512
- >
4513
- <svg
4514
- class="w-5 h-5"
4515
- fill="none"
4516
- stroke="currentColor"
4517
- viewBox="0 0 24 24"
4518
- >
4519
- <path
4520
- stroke-linecap="round"
4521
- stroke-linejoin="round"
4522
- stroke-width="2"
4523
- d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
4524
- />
4525
- </svg>
4526
- </button>
4527
- </div>
4528
- </template>
4529
- </div>
5463
+ return ctx.json(
5464
+ {
5465
+ error: error instanceof Error ? error.message : "Internal server error"
5466
+ },
5467
+ 500
5468
+ );
5469
+ }
5470
+ }
4530
5471
 
4531
- <!-- 空状态提示 -->
4532
- <div
4533
- x-show="items.length === 0"
4534
- class="text-center py-8 text-gray-400 text-sm border border-dashed border-gray-300 rounded-lg"
4535
- >
4536
- 暂无项,点击"添加项"按钮添加
4537
- </div>
4538
- </div>
4539
- </div>
4540
- `;
5472
+ // src/internal/route-registrar.ts
5473
+ function registerCdnCacheRoutes(options) {
5474
+ const { prefix } = options.options;
5475
+ options.hono.get(`${prefix}/_cdn/:name`, async (ctx) => {
5476
+ const name = ctx.req.param("name");
5477
+ const { getCachedResource: getCachedResource2 } = await Promise.resolve().then(() => (init_cdn_cache(), cdn_cache_exports));
5478
+ const cached = getCachedResource2(name);
5479
+ if (cached) {
5480
+ return ctx.body(cached.content, 200, {
5481
+ "Content-Type": cached.mimeType,
5482
+ "Cache-Control": "public, max-age=31536000, immutable"
5483
+ // 缓存 1 年
5484
+ });
5485
+ }
5486
+ return ctx.text(`Resource "${name}" not cached`, 404);
5487
+ });
5488
+ imeanServiceEngine.logger.info(`[HtmxAdminPlugin] CDN \u7F13\u5B58\u4EE3\u7406\u8DEF\u7531\u5DF2\u6CE8\u518C: ${prefix}/_cdn/:name`);
4541
5489
  }
4542
- function TagsEditor(props) {
4543
- const {
4544
- value,
4545
- fieldName,
4546
- placeholder = "\u8F93\u5165\u6807\u7B7E\u540E\u6309\u56DE\u8F66\u6DFB\u52A0",
4547
- allowEmpty = false
4548
- } = props;
4549
- const initialItems = value || [];
4550
- const initialValueJson = JSON.stringify(initialItems);
4551
- const xDataContent = `{
4552
- items: ${initialValueJson},
4553
- draggedIndex: null,
4554
- draggedOverIndex: null,
4555
- newTag: '',
4556
- editingIndex: null,
4557
- editingValue: '',
4558
- fieldName: ${JSON.stringify(fieldName)},
4559
- placeholder: ${JSON.stringify(placeholder)},
4560
- allowEmpty: ${allowEmpty},
4561
- init() {
4562
- const dataAttr = this.$el.getAttribute('data-initial-value');
4563
- if (dataAttr) {
4564
- try {
4565
- const parsed = JSON.parse(dataAttr);
4566
- if (Array.isArray(parsed)) {
4567
- this.items = parsed;
4568
- } else {
4569
- this.items = [];
5490
+ function registerPageRoutes(page, options) {
5491
+ const basePath = `${options.options.prefix}${modelNameToPath(page.modelName)}`;
5492
+ const features = page.features.getAll();
5493
+ for (const feature of features) {
5494
+ const routes = feature.getRoutes();
5495
+ for (const route of routes) {
5496
+ const fullPath = `${basePath}${route.path}`;
5497
+ imeanServiceEngine.logger.info(
5498
+ `[HtmxAdminPlugin] Registering route: ${route.method.toUpperCase()} ${fullPath} (feature: ${feature.name}, permission: ${feature.permission || "none"})`
5499
+ );
5500
+ const handler = async (ctx) => {
5501
+ return handleRequest(ctx, page, feature, {
5502
+ options: options.options,
5503
+ plugin: options.plugin
5504
+ });
5505
+ };
5506
+ options.hono[route.method](fullPath, handler);
5507
+ if (route.method === "put" || route.method === "delete") {
5508
+ const postHandler = async (ctx) => {
5509
+ const methodOverride = ctx.req.header("X-HTTP-Method-Override");
5510
+ const expectedMethod = route.method.toUpperCase();
5511
+ if (methodOverride === expectedMethod) {
5512
+ imeanServiceEngine.logger.info(
5513
+ `[HtmxAdminPlugin] Method override detected: POST ${fullPath} -> ${expectedMethod} (feature: ${feature.name})`
5514
+ );
5515
+ return handleRequest(ctx, page, feature, {
5516
+ options: options.options,
5517
+ plugin: options.plugin
5518
+ });
4570
5519
  }
4571
- } catch (e) {
4572
- console.error('Failed to parse initial value:', e);
4573
- this.items = [];
4574
- }
4575
- }
4576
- this.updateHiddenField();
4577
- },
4578
- updateHiddenField() {
4579
- const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
4580
- if (hiddenInput) {
4581
- hiddenInput.value = JSON.stringify(this.items);
4582
- }
4583
- },
4584
- addTag() {
4585
- const trimmed = this.newTag.trim();
4586
- if (trimmed && !this.items.includes(trimmed)) {
4587
- this.items.push(trimmed);
4588
- this.newTag = '';
4589
- this.updateHiddenField();
4590
- }
4591
- },
4592
- handleKeyDown(event) {
4593
- if (event.key === 'Enter') {
4594
- event.preventDefault();
4595
- this.addTag();
4596
- }
4597
- },
4598
- removeTag(index) {
4599
- this.items.splice(index, 1);
4600
- this.updateHiddenField();
4601
- },
4602
- startEdit(index) {
4603
- this.editingIndex = index;
4604
- this.editingValue = this.items[index];
4605
- this.$nextTick(() => {
4606
- const input = this.$el.querySelector('input[data-testid="${fieldName}-tag-edit-input-' + index + '"]');
4607
- if (input && input.focus) {
4608
- input.focus();
4609
- input.select();
4610
- }
4611
- });
4612
- },
4613
- saveEdit(index) {
4614
- const trimmed = this.editingValue.trim();
4615
- if (trimmed) {
4616
- // \u68C0\u67E5\u662F\u5426\u4E0E\u5176\u4ED6\u6807\u7B7E\u91CD\u590D\uFF08\u6392\u9664\u5F53\u524D\u7F16\u8F91\u7684\u6807\u7B7E\uFF09
4617
- const isDuplicate = this.items.some((item, i) => i !== index && item === trimmed);
4618
- if (!isDuplicate) {
4619
- this.items[index] = trimmed;
4620
- this.updateHiddenField();
4621
- }
4622
- }
4623
- this.cancelEdit();
4624
- },
4625
- cancelEdit() {
4626
- this.editingIndex = null;
4627
- this.editingValue = '';
4628
- },
4629
- handleEditKeyDown(index, event) {
4630
- if (event.key === 'Enter') {
4631
- event.preventDefault();
4632
- this.saveEdit(index);
4633
- } else if (event.key === 'Escape') {
4634
- event.preventDefault();
4635
- this.cancelEdit();
4636
- }
4637
- },
4638
- handleDragStart(index, event) {
4639
- this.draggedIndex = index;
4640
- event.dataTransfer.effectAllowed = 'move';
4641
- event.dataTransfer.setData('text/plain', index.toString());
4642
- const target = event.currentTarget || event.target.closest('[draggable="true"]');
4643
- if (target) {
4644
- target.style.opacity = '0.5';
4645
- }
4646
- },
4647
- handleDragEnd(event) {
4648
- const target = event.currentTarget || event.target.closest('[draggable="true"]');
4649
- if (target) {
4650
- target.style.opacity = '';
4651
- }
4652
- this.draggedIndex = null;
4653
- this.draggedOverIndex = null;
4654
- },
4655
- handleDragOver(index, event) {
4656
- event.preventDefault();
4657
- event.dataTransfer.dropEffect = 'move';
4658
- this.draggedOverIndex = index;
4659
- },
4660
- handleDragLeave() {
4661
- this.draggedOverIndex = null;
4662
- },
4663
- handleDrop(index, event) {
4664
- event.preventDefault();
4665
- if (this.draggedIndex !== null && this.draggedIndex !== index) {
4666
- const draggedItem = this.items[this.draggedIndex];
4667
- this.items.splice(this.draggedIndex, 1);
4668
- this.items.splice(index, 0, draggedItem);
4669
- this.updateHiddenField();
5520
+ imeanServiceEngine.logger.warn(
5521
+ `[HtmxAdminPlugin] POST request to ${fullPath} without matching X-HTTP-Method-Override header (got: ${methodOverride || "none"}, expected: ${expectedMethod})`
5522
+ );
5523
+ return ctx.text("Method Not Allowed", 405);
5524
+ };
5525
+ imeanServiceEngine.logger.info(
5526
+ `[HtmxAdminPlugin] Registering POST route for method override: POST ${fullPath} (actual method: ${route.method.toUpperCase()}, feature: ${feature.name})`
5527
+ );
5528
+ options.hono.post(fullPath, postHandler);
4670
5529
  }
4671
- this.draggedIndex = null;
4672
- this.draggedOverIndex = null;
4673
5530
  }
4674
- }`;
4675
- return html.html`
4676
- <div
4677
- x-data="${xDataContent}"
4678
- data-initial-value="${initialValueJson}"
4679
- x-init="init()"
4680
- class="space-y-2"
4681
- >
4682
- <input
4683
- type="hidden"
4684
- name="${fieldName}"
4685
- x-bind:value="JSON.stringify(items)"
4686
- data-testid="hidden-${fieldName}"
4687
- />
4688
-
4689
- <!-- 输入框:用于添加新标签 -->
4690
- <div class="flex items-center gap-2">
4691
- <input
4692
- type="text"
4693
- x-model="newTag"
4694
- x-on:keydown="handleKeyDown($event)"
4695
- x-bind:placeholder="placeholder"
4696
- class="flex-1 px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
4697
- data-testid="${fieldName}-input"
4698
- />
4699
- <button
4700
- type="button"
4701
- x-on:click="addTag()"
4702
- class="px-3 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors text-sm font-medium flex items-center gap-1"
4703
- data-testid="${fieldName}-add-button"
4704
- >
4705
- <svg
4706
- class="w-4 h-4"
4707
- fill="none"
4708
- stroke="currentColor"
4709
- viewBox="0 0 24 24"
4710
- >
4711
- <path
4712
- stroke-linecap="round"
4713
- stroke-linejoin="round"
4714
- stroke-width="2"
4715
- d="M12 4v16m8-8H4"
4716
- />
4717
- </svg>
4718
- 添加
4719
- </button>
4720
- </div>
4721
-
4722
- <!-- 标签列表 -->
4723
- <div
4724
- class="flex flex-wrap gap-2"
4725
- x-show="items.length > 0"
4726
- data-testid="${fieldName}-tags-container"
4727
- >
4728
- <template x-for="(item, index) in items" x-bind:key="index">
4729
- <div
4730
- class="inline-flex items-center gap-1 px-2 py-1 bg-blue-50 border border-blue-200 rounded-md text-sm group"
4731
- x-bind:class="{
4732
- 'opacity-50': draggedIndex === index,
4733
- 'ring-2 ring-blue-400': draggedOverIndex === index && draggedIndex !== null && draggedIndex !== index
4734
- }"
4735
- draggable="true"
4736
- x-on:dragstart="handleDragStart(index, $event)"
4737
- x-on:dragend="handleDragEnd($event)"
4738
- x-on:dragover="handleDragOver(index, $event)"
4739
- x-on:dragleave="handleDragLeave()"
4740
- x-on:drop="handleDrop(index, $event)"
4741
- x-bind:data-testid="fieldName + '-tag-' + index"
4742
- >
4743
- <!-- 拖拽手柄 -->
4744
- <div
4745
- class="flex-shrink-0 cursor-move text-blue-400 hover:text-blue-600 transition-colors"
4746
- x-bind:data-testid="fieldName + '-drag-handle-' + index"
4747
- title="拖拽排序"
4748
- >
4749
- <svg
4750
- class="w-3 h-3"
4751
- fill="none"
4752
- stroke="currentColor"
4753
- viewBox="0 0 24 24"
4754
- >
4755
- <path
4756
- stroke-linecap="round"
4757
- stroke-linejoin="round"
4758
- stroke-width="2"
4759
- d="M4 8h16M4 16h16"
4760
- />
4761
- </svg>
4762
- </div>
4763
-
4764
- <!-- 标签内容:显示模式 -->
4765
- <span
4766
- x-show="editingIndex !== index"
4767
- class="flex-1 text-blue-900 cursor-pointer"
4768
- x-on:click="startEdit(index)"
4769
- x-text="item"
4770
- x-bind:data-testid="fieldName + '-tag-text-' + index"
4771
- ></span>
4772
- <!-- 标签内容:编辑模式 -->
4773
- <input
4774
- x-show="editingIndex === index"
4775
- type="text"
4776
- x-model="editingValue"
4777
- x-on:keydown="handleEditKeyDown(index, $event)"
4778
- x-on:blur="saveEdit(index)"
4779
- class="flex-1 px-1 py-0.5 border border-blue-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-500 min-w-[60px]"
4780
- x-bind:data-testid="fieldName + '-tag-edit-input-' + index"
4781
- />
4782
-
4783
- <!-- 删除按钮 -->
4784
- <button
4785
- type="button"
4786
- x-on:click="removeTag(index)"
4787
- class="flex-shrink-0 text-blue-600 hover:text-red-600 hover:bg-red-50 rounded transition-colors p-0.5"
4788
- x-bind:data-testid="fieldName + '-tag-remove-' + index"
4789
- title="删除标签"
4790
- >
4791
- <svg
4792
- class="w-3.5 h-3.5"
4793
- fill="none"
4794
- stroke="currentColor"
4795
- viewBox="0 0 24 24"
4796
- >
4797
- <path
4798
- stroke-linecap="round"
4799
- stroke-linejoin="round"
4800
- stroke-width="2"
4801
- d="M6 18L18 6M6 6l12 12"
4802
- />
4803
- </svg>
4804
- </button>
4805
- </div>
4806
- </template>
4807
- </div>
4808
-
4809
- <!-- 空状态提示 -->
4810
- <div
4811
- x-show="items.length === 0"
4812
- class="text-center py-4 text-gray-400 text-sm border border-dashed border-gray-300 rounded-md"
4813
- data-testid="${fieldName}-empty-state"
4814
- >
4815
- 暂无标签,在上方输入框中输入标签后按回车或点击"添加"按钮
4816
- </div>
4817
- </div>
4818
- `;
5531
+ }
5532
+ }
5533
+ function registerHomeRedirect(pages, options) {
5534
+ const { prefix, homePath } = options.options;
5535
+ if (homePath) {
5536
+ options.hono.get(prefix, async (ctx) => {
5537
+ return ctx.redirect(homePath);
5538
+ });
5539
+ } else if (pages.size > 0) {
5540
+ const firstPage = Array.from(pages.values())[0];
5541
+ const firstPath = `${prefix}${modelNameToPath(firstPage.modelName)}`;
5542
+ options.hono.get(prefix, async (ctx) => {
5543
+ return ctx.redirect(firstPath);
5544
+ });
5545
+ }
4819
5546
  }
4820
5547
 
5548
+ // src/plugin.tsx
5549
+ init_cdn_cache();
5550
+ var HtmxAdminPlugin = class {
5551
+ name = "htmx-admin-plugin";
5552
+ priority = imeanServiceEngine.PluginPriority.ROUTE;
5553
+ engine;
5554
+ hono;
5555
+ options;
5556
+ serviceName = "";
5557
+ pages = /* @__PURE__ */ new Map();
5558
+ componentHandler;
5559
+ constructor(options) {
5560
+ this.options = {
5561
+ title: options?.title || "\u7BA1\u7406\u540E\u53F0",
5562
+ logo: options?.logo || "",
5563
+ prefix: options?.prefix || "/admin",
5564
+ homePath: options?.homePath || "",
5565
+ navigation: options?.navigation ?? [],
5566
+ authProvider: options?.authProvider,
5567
+ pages: options?.pages ?? [],
5568
+ components: options?.components ?? []
5569
+ };
5570
+ this.initPages();
5571
+ }
5572
+ initPages() {
5573
+ for (const page of this.options.pages) {
5574
+ this.registerPage(page);
5575
+ }
5576
+ }
5577
+ /**
5578
+ * 注册页面
5579
+ */
5580
+ registerPage(page) {
5581
+ if (this.pages.has(page.modelName)) {
5582
+ throw new Error(
5583
+ `Page with name "${page.modelName}" is already registered`
5584
+ );
5585
+ }
5586
+ this.pages.set(page.modelName, page);
5587
+ imeanServiceEngine.logger.info(`[HtmxAdminPlugin] Registered page: ${page.modelName}`);
5588
+ return this;
5589
+ }
5590
+ /**
5591
+ * 引擎初始化钩子
5592
+ */
5593
+ onInit(engine) {
5594
+ this.engine = engine;
5595
+ this.hono = engine.getHono();
5596
+ this.serviceName = engine.options.name;
5597
+ imeanServiceEngine.logger.info(
5598
+ `HtmxAdminPlugin initialized${this.serviceName ? ` (service: ${this.serviceName})` : ""}`
5599
+ );
5600
+ this.componentHandler = new HtmxComponentHandler(
5601
+ this.hono,
5602
+ this.options.prefix,
5603
+ this.options.components
5604
+ );
5605
+ initializeCdnCache().catch((error) => {
5606
+ imeanServiceEngine.logger.error("[HtmxAdminPlugin] CDN \u7F13\u5B58\u521D\u59CB\u5316\u5931\u8D25", error);
5607
+ });
5608
+ }
5609
+ /**
5610
+ * 引擎启动后注册路由
5611
+ */
5612
+ onAfterStart(engine) {
5613
+ const routeOptions = {
5614
+ options: this.options,
5615
+ hono: this.hono,
5616
+ plugin: this
5617
+ };
5618
+ registerCdnCacheRoutes(routeOptions);
5619
+ for (const [_modelName, page] of this.pages) {
5620
+ registerPageRoutes(page, routeOptions);
5621
+ }
5622
+ registerHomeRedirect(this.pages, routeOptions);
5623
+ }
5624
+ };
5625
+
4821
5626
  exports.BaseFeature = BaseFeature;
5627
+ exports.ComponentContext = ComponentContext;
4822
5628
  exports.CustomFeature = CustomFeature;
4823
5629
  exports.DefaultCreateFeature = DefaultCreateFeature;
4824
5630
  exports.DefaultDeleteFeature = DefaultDeleteFeature;
@@ -4828,9 +5634,13 @@ exports.DefaultListFeature = DefaultListFeature;
4828
5634
  exports.Dialog = Dialog;
4829
5635
  exports.ErrorAlert = ErrorAlert;
4830
5636
  exports.HtmxAdminPlugin = HtmxAdminPlugin;
5637
+ exports.HtmxComponent = HtmxComponent;
4831
5638
  exports.LoadingBar = LoadingBar;
5639
+ exports.Method = Method;
4832
5640
  exports.ObjectEditor = ObjectEditor;
4833
5641
  exports.PageModel = PageModel;
5642
+ exports.RenderContext = RenderContext;
5643
+ exports.SortableList = SortableList;
4834
5644
  exports.StringArrayEditor = StringArrayEditor;
4835
5645
  exports.TagsEditor = TagsEditor;
4836
5646
  exports.checkUserPermission = checkUserPermission;