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.d.mts +231 -143
- package/dist/index.d.ts +231 -143
- package/dist/index.js +2686 -1876
- package/dist/index.mjs +2682 -1877
- package/docs/field-editor-best-practices.md +320 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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 :
|
|
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
|
|
662
|
-
|
|
663
|
-
|
|
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
|
-
|
|
666
|
-
|
|
1041
|
+
actualSchema = innerType;
|
|
1042
|
+
const innerDef = innerType._zod?.def || innerType._def;
|
|
1043
|
+
typeName = innerDef?.type;
|
|
667
1044
|
}
|
|
668
1045
|
}
|
|
669
|
-
if (typeName === "number" || typeName === "
|
|
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"
|
|
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"
|
|
695
|
-
if (
|
|
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"
|
|
710
|
-
if (typeof value === "
|
|
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 === "
|
|
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
|
-
|
|
1036
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
1425
|
-
|
|
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(
|
|
1519
|
-
acc
|
|
1520
|
-
|
|
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(
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
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
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
"
|
|
2733
|
-
{
|
|
2734
|
-
|
|
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
|
|
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
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
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
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
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
|
-
|
|
3120
|
-
|
|
3121
|
-
{
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
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
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
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
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
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
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
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
|
-
|
|
3175
|
-
|
|
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
|
-
|
|
3179
|
-
.
|
|
3180
|
-
|
|
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
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
}
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
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
|
|
4074
|
+
function StringArrayEditor(props) {
|
|
3243
4075
|
const {
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
}
|
|
3258
|
-
|
|
3259
|
-
|
|
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
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
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
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
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
|
-
|
|
4272
|
+
"input",
|
|
3351
4273
|
{
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
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
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
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
|
|
3372
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
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
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
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 (
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
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
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
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-
|
|
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.
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
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
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
"
|
|
3672
|
-
|
|
3673
|
-
|
|
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
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
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
|
|
3834
|
-
|
|
3835
|
-
if (
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
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
|
|
5077
|
+
return false;
|
|
3846
5078
|
}
|
|
3847
|
-
function
|
|
3848
|
-
const
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
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
|
|
5090
|
+
return defaultMetadata;
|
|
3861
5091
|
}
|
|
3862
|
-
async function
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
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 (
|
|
3911
|
-
const
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
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
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
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
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
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
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
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
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
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
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
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
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
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
|
|
4115
|
-
const
|
|
4116
|
-
if (
|
|
4117
|
-
|
|
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
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
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
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
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
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
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
|
-
|
|
4180
|
-
|
|
4181
|
-
const
|
|
4182
|
-
const
|
|
4183
|
-
const
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
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
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
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
|
-
|
|
4349
|
-
},
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
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
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
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
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
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
|
|
4543
|
-
const {
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
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
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
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
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
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;
|