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