imean-service-engine-htmx-plugin 2.3.0 → 2.4.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 +211 -138
- package/dist/index.d.ts +211 -138
- package/dist/index.js +2320 -1140
- package/dist/index.mjs +2316 -1141
- package/docs/field-editor-best-practices.md +320 -0
- package/package.json +15 -13
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var jsxRuntime = require('hono/jsx/jsx-runtime');
|
|
4
4
|
var fs = require('fs');
|
|
5
|
+
var imeanServiceEngine = require('imean-service-engine');
|
|
5
6
|
var path = require('path');
|
|
6
|
-
var jsxRuntime = require('hono/jsx/jsx-runtime');
|
|
7
7
|
var cookie = require('hono/cookie');
|
|
8
8
|
var html = require('hono/html');
|
|
9
9
|
|
|
@@ -16,6 +16,170 @@ var __export = (target, all) => {
|
|
|
16
16
|
for (var name in all)
|
|
17
17
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
18
18
|
};
|
|
19
|
+
function Button(props) {
|
|
20
|
+
const {
|
|
21
|
+
children,
|
|
22
|
+
variant = "primary",
|
|
23
|
+
size = "md",
|
|
24
|
+
disabled = false,
|
|
25
|
+
className = "",
|
|
26
|
+
...rest
|
|
27
|
+
} = props;
|
|
28
|
+
const baseClasses = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2";
|
|
29
|
+
const variantClasses = {
|
|
30
|
+
primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
|
|
31
|
+
secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500",
|
|
32
|
+
danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
|
|
33
|
+
ghost: "bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-500"
|
|
34
|
+
};
|
|
35
|
+
const sizeClasses = {
|
|
36
|
+
sm: "px-3 py-1.5 text-sm",
|
|
37
|
+
md: "px-4 py-2 text-sm",
|
|
38
|
+
lg: "px-6 py-3 text-base"
|
|
39
|
+
};
|
|
40
|
+
const classes = [
|
|
41
|
+
baseClasses,
|
|
42
|
+
variantClasses[variant],
|
|
43
|
+
sizeClasses[size],
|
|
44
|
+
disabled ? "opacity-50 cursor-not-allowed" : "",
|
|
45
|
+
className
|
|
46
|
+
].filter(Boolean).join(" ");
|
|
47
|
+
const href = rest.href ?? "#";
|
|
48
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
49
|
+
"a",
|
|
50
|
+
{
|
|
51
|
+
className: classes,
|
|
52
|
+
disabled,
|
|
53
|
+
href,
|
|
54
|
+
...rest,
|
|
55
|
+
children
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
var init_button = __esm({
|
|
60
|
+
"src/components/button.tsx"() {
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// src/utils/action-button-renderer.tsx
|
|
65
|
+
var action_button_renderer_exports = {};
|
|
66
|
+
__export(action_button_renderer_exports, {
|
|
67
|
+
renderActionButton: () => renderActionButton,
|
|
68
|
+
renderActionButtons: () => renderActionButtons
|
|
69
|
+
});
|
|
70
|
+
function renderActionButton(action, index) {
|
|
71
|
+
const {
|
|
72
|
+
label,
|
|
73
|
+
href,
|
|
74
|
+
hxGet,
|
|
75
|
+
hxPost,
|
|
76
|
+
hxPut,
|
|
77
|
+
hxDelete,
|
|
78
|
+
variant = "primary",
|
|
79
|
+
confirm,
|
|
80
|
+
close,
|
|
81
|
+
submit,
|
|
82
|
+
formId,
|
|
83
|
+
onClick,
|
|
84
|
+
className = "",
|
|
85
|
+
target
|
|
86
|
+
} = action;
|
|
87
|
+
if (submit && formId) {
|
|
88
|
+
const variantStyles = {
|
|
89
|
+
primary: "bg-blue-600 text-white hover:bg-blue-700",
|
|
90
|
+
secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
|
|
91
|
+
danger: "bg-red-600 text-white hover:bg-red-700",
|
|
92
|
+
ghost: "bg-transparent text-gray-700 hover:bg-gray-100"
|
|
93
|
+
};
|
|
94
|
+
const buttonStyle = variantStyles[variant] || variantStyles.primary;
|
|
95
|
+
const testId2 = label === "\u521B\u5EFA" || label === "\u66F4\u65B0" ? "submit-button" : `action-${label}`;
|
|
96
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
97
|
+
"button",
|
|
98
|
+
{
|
|
99
|
+
type: "submit",
|
|
100
|
+
form: formId,
|
|
101
|
+
className: `px-4 py-2 rounded transition-colors font-medium ${buttonStyle} ${className}`,
|
|
102
|
+
"data-testid": testId2,
|
|
103
|
+
...confirm && { "data-confirm": confirm },
|
|
104
|
+
children: label
|
|
105
|
+
},
|
|
106
|
+
index
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const finalOnClick = close ? CLOSE_DIALOG_SCRIPT : onClick;
|
|
110
|
+
if (finalOnClick) {
|
|
111
|
+
const variantStyles = {
|
|
112
|
+
primary: "bg-blue-600 text-white hover:bg-blue-700",
|
|
113
|
+
secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
|
|
114
|
+
danger: "bg-red-600 text-white hover:bg-red-700",
|
|
115
|
+
ghost: "bg-transparent text-gray-700 hover:bg-gray-100"
|
|
116
|
+
};
|
|
117
|
+
const buttonStyle = variantStyles[variant] || variantStyles.secondary;
|
|
118
|
+
let testId2 = `action-${label}`;
|
|
119
|
+
if (label === "\u53D6\u6D88") {
|
|
120
|
+
testId2 = "cancel-button";
|
|
121
|
+
} else if (label === "\u5173\u95ED") {
|
|
122
|
+
testId2 = "close-button";
|
|
123
|
+
}
|
|
124
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
125
|
+
"button",
|
|
126
|
+
{
|
|
127
|
+
type: "button",
|
|
128
|
+
_: finalOnClick,
|
|
129
|
+
className: `px-4 py-2 rounded transition-colors font-medium ${buttonStyle} ${className}`,
|
|
130
|
+
"data-testid": testId2,
|
|
131
|
+
...confirm && { "data-confirm": confirm },
|
|
132
|
+
children: label
|
|
133
|
+
},
|
|
134
|
+
index
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
let testId = `action-${label}`;
|
|
138
|
+
if (label === "\u65B0\u5EFA" || label === "\u521B\u5EFA") {
|
|
139
|
+
testId = "create-button";
|
|
140
|
+
} else if (label === "\u53D6\u6D88") {
|
|
141
|
+
testId = "cancel-button";
|
|
142
|
+
} else if (label === "\u5173\u95ED") {
|
|
143
|
+
testId = "close-button";
|
|
144
|
+
}
|
|
145
|
+
const isNewWindow = target === "_blank";
|
|
146
|
+
const htmxAttrs = {};
|
|
147
|
+
if (!isNewWindow) {
|
|
148
|
+
if (hxGet) htmxAttrs["hx-get"] = hxGet;
|
|
149
|
+
if (hxPost) htmxAttrs["hx-post"] = hxPost;
|
|
150
|
+
if (hxPut) htmxAttrs["hx-put"] = hxPut;
|
|
151
|
+
if (hxDelete) htmxAttrs["hx-delete"] = hxDelete;
|
|
152
|
+
}
|
|
153
|
+
if (confirm) htmxAttrs["hx-confirm"] = confirm;
|
|
154
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
155
|
+
Button,
|
|
156
|
+
{
|
|
157
|
+
variant,
|
|
158
|
+
href,
|
|
159
|
+
className,
|
|
160
|
+
"data-testid": testId,
|
|
161
|
+
target,
|
|
162
|
+
rel: target === "_blank" ? "noopener noreferrer" : void 0,
|
|
163
|
+
...htmxAttrs,
|
|
164
|
+
children: label
|
|
165
|
+
},
|
|
166
|
+
index
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
function renderActionButtons(actions) {
|
|
170
|
+
return actions.map((action, index) => renderActionButton(action, index));
|
|
171
|
+
}
|
|
172
|
+
var CLOSE_DIALOG_SCRIPT;
|
|
173
|
+
var init_action_button_renderer = __esm({
|
|
174
|
+
"src/utils/action-button-renderer.tsx"() {
|
|
175
|
+
init_button();
|
|
176
|
+
CLOSE_DIALOG_SCRIPT = `on click
|
|
177
|
+
add .dialog-exit to .dialog-backdrop
|
|
178
|
+
add .dialog-content-exit to .dialog-content
|
|
179
|
+
wait 200ms
|
|
180
|
+
set #dialog-container's innerHTML to '' end`;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
19
183
|
|
|
20
184
|
// src/utils/cdn-cache.ts
|
|
21
185
|
var cdn_cache_exports = {};
|
|
@@ -62,7 +226,11 @@ async function saveToFileCache(name, content, mimeType) {
|
|
|
62
226
|
fs.promises.writeFile(contentPath, content, "utf-8"),
|
|
63
227
|
fs.promises.writeFile(
|
|
64
228
|
metadataPath,
|
|
65
|
-
JSON.stringify(
|
|
229
|
+
JSON.stringify(
|
|
230
|
+
{ mimeType, cachedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
231
|
+
null,
|
|
232
|
+
2
|
|
233
|
+
),
|
|
66
234
|
"utf-8"
|
|
67
235
|
)
|
|
68
236
|
]);
|
|
@@ -75,14 +243,18 @@ async function fetchAndCacheResource(resource) {
|
|
|
75
243
|
try {
|
|
76
244
|
const fileCache = await readFromFileCache(resource.name);
|
|
77
245
|
if (fileCache) {
|
|
78
|
-
imeanServiceEngine.logger.info(
|
|
246
|
+
imeanServiceEngine.logger.info(
|
|
247
|
+
`[CDNCache] \u4ECE\u6587\u4EF6\u7F13\u5B58\u52A0\u8F7D\u8D44\u6E90: ${resource.name} (${fileCache.content.length} \u5B57\u8282)`
|
|
248
|
+
);
|
|
79
249
|
cache.set(resource.name, fileCache);
|
|
80
250
|
return;
|
|
81
251
|
}
|
|
82
252
|
imeanServiceEngine.logger.info(`[CDNCache] \u6B63\u5728\u4E0B\u8F7D\u8D44\u6E90: ${resource.name} (${resource.url})`);
|
|
83
253
|
const response = await fetch(resource.url);
|
|
84
254
|
if (!response.ok) {
|
|
85
|
-
throw new Error(
|
|
255
|
+
throw new Error(
|
|
256
|
+
`Failed to fetch ${resource.url}: ${response.statusText}`
|
|
257
|
+
);
|
|
86
258
|
}
|
|
87
259
|
const content = await response.text();
|
|
88
260
|
const cached = {
|
|
@@ -90,10 +262,14 @@ async function fetchAndCacheResource(resource) {
|
|
|
90
262
|
mimeType: resource.mimeType
|
|
91
263
|
};
|
|
92
264
|
cache.set(resource.name, cached);
|
|
93
|
-
saveToFileCache(resource.name, content, resource.mimeType).catch(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
265
|
+
saveToFileCache(resource.name, content, resource.mimeType).catch(
|
|
266
|
+
(error) => {
|
|
267
|
+
imeanServiceEngine.logger.warn(`[CDNCache] \u4FDD\u5B58\u6587\u4EF6\u7F13\u5B58\u5931\u8D25: ${resource.name}`, error);
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
imeanServiceEngine.logger.info(
|
|
271
|
+
`[CDNCache] \u8D44\u6E90\u5DF2\u4E0B\u8F7D\u5E76\u7F13\u5B58: ${resource.name} (${content.length} \u5B57\u8282)`
|
|
272
|
+
);
|
|
97
273
|
} catch (error) {
|
|
98
274
|
imeanServiceEngine.logger.error(`[CDNCache] \u4E0B\u8F7D\u8D44\u6E90\u5931\u8D25: ${resource.name}`, error);
|
|
99
275
|
}
|
|
@@ -145,6 +321,16 @@ var init_cdn_cache = __esm({
|
|
|
145
321
|
name: "alpinejs",
|
|
146
322
|
url: "https://unpkg.com/alpinejs@latest/dist/cdn.min.js",
|
|
147
323
|
mimeType: "application/javascript"
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: "idiomorph",
|
|
327
|
+
url: "https://unpkg.com/idiomorph@0.7.4/dist/idiomorph-ext.min.js",
|
|
328
|
+
mimeType: "application/javascript"
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: "sortablejs",
|
|
332
|
+
url: "https://unpkg.com/sortablejs@latest/Sortable.min.js",
|
|
333
|
+
mimeType: "application/javascript"
|
|
148
334
|
}
|
|
149
335
|
];
|
|
150
336
|
cache = /* @__PURE__ */ new Map();
|
|
@@ -283,11 +469,17 @@ var BaseFeature = class {
|
|
|
283
469
|
return metadata.description;
|
|
284
470
|
}
|
|
285
471
|
/**
|
|
286
|
-
*
|
|
287
|
-
*
|
|
472
|
+
* 获取操作按钮(默认实现:返回 null)
|
|
473
|
+
* 子类可以覆盖此方法以提供操作按钮(返回 JSX)
|
|
288
474
|
*/
|
|
289
475
|
async getActions(context) {
|
|
290
|
-
return
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* 处理请求
|
|
480
|
+
*/
|
|
481
|
+
async handle(context) {
|
|
482
|
+
return await this.render?.(context) ?? null;
|
|
291
483
|
}
|
|
292
484
|
};
|
|
293
485
|
function getFieldValue(field, initialData) {
|
|
@@ -342,222 +534,497 @@ function renderFormField(field, initialData, formFieldRenderers) {
|
|
|
342
534
|
} else if (value) {
|
|
343
535
|
parsedValue = value;
|
|
344
536
|
}
|
|
345
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
346
|
-
|
|
347
|
-
"label",
|
|
348
|
-
{
|
|
349
|
-
htmlFor: field.name,
|
|
350
|
-
className: "block text-sm font-semibold text-gray-700",
|
|
351
|
-
"data-testid": `label-${field.name}`,
|
|
352
|
-
children: [
|
|
353
|
-
field.label,
|
|
354
|
-
field.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", children: "*" })
|
|
355
|
-
]
|
|
356
|
-
}
|
|
357
|
-
),
|
|
358
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { children: customRenderer({
|
|
359
|
-
field,
|
|
360
|
-
value: parsedValue,
|
|
361
|
-
initialData,
|
|
362
|
-
fieldName: field.name
|
|
363
|
-
}) }),
|
|
364
|
-
field.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
|
|
365
|
-
] }, field.name);
|
|
366
|
-
}
|
|
367
|
-
const shouldUseTextarea = field.type === "textarea" || value && isJsonString(value) && field.type !== "select";
|
|
368
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", "data-testid": `field-${field.name}`, children: [
|
|
369
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
370
|
-
"label",
|
|
371
|
-
{
|
|
372
|
-
htmlFor: field.name,
|
|
373
|
-
className: "block text-sm font-semibold text-gray-700",
|
|
374
|
-
"data-testid": `label-${field.name}`,
|
|
375
|
-
children: [
|
|
376
|
-
field.label,
|
|
377
|
-
field.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", children: "*" })
|
|
378
|
-
]
|
|
379
|
-
}
|
|
380
|
-
),
|
|
381
|
-
shouldUseTextarea ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
382
|
-
"textarea",
|
|
383
|
-
{
|
|
384
|
-
id: field.name,
|
|
385
|
-
name: field.name,
|
|
386
|
-
required: field.required,
|
|
387
|
-
placeholder: field.placeholder || (isJsonString(value) ? "JSON \u683C\u5F0F\u6570\u636E" : ""),
|
|
388
|
-
rows: isJsonString(value) ? 10 : 4,
|
|
389
|
-
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",
|
|
390
|
-
"data-testid": `input-${field.name}`,
|
|
391
|
-
children: isJsonString(value) ? formatJsonString(value) : value
|
|
392
|
-
},
|
|
393
|
-
`${field.name}-${value}`
|
|
394
|
-
) : field.type === "select" ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
395
|
-
"select",
|
|
537
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
538
|
+
"div",
|
|
396
539
|
{
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
required: field.required,
|
|
400
|
-
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 bg-white",
|
|
401
|
-
"data-testid": `select-${field.name}`,
|
|
540
|
+
className: "space-y-2",
|
|
541
|
+
"data-testid": `field-${field.name}`,
|
|
402
542
|
children: [
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
"
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
543
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
544
|
+
"label",
|
|
545
|
+
{
|
|
546
|
+
htmlFor: field.name,
|
|
547
|
+
className: "block text-sm font-semibold text-gray-700",
|
|
548
|
+
"data-testid": `label-${field.name}`,
|
|
549
|
+
children: [
|
|
550
|
+
field.label,
|
|
551
|
+
field.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", children: "*" })
|
|
552
|
+
]
|
|
553
|
+
}
|
|
554
|
+
),
|
|
555
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: customRenderer({
|
|
556
|
+
field,
|
|
557
|
+
value: parsedValue,
|
|
558
|
+
initialData,
|
|
559
|
+
fieldName: field.name
|
|
560
|
+
}) }),
|
|
561
|
+
field.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
|
|
417
562
|
]
|
|
418
563
|
},
|
|
419
|
-
|
|
420
|
-
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
421
|
-
"input",
|
|
422
|
-
{
|
|
423
|
-
type: field.type || "text",
|
|
424
|
-
id: field.name,
|
|
425
|
-
name: field.name,
|
|
426
|
-
required: field.required,
|
|
427
|
-
placeholder: field.placeholder,
|
|
428
|
-
step: field.type === "number" ? field.step ?? void 0 : void 0,
|
|
429
|
-
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",
|
|
430
|
-
value,
|
|
431
|
-
"data-testid": `input-${field.name}`
|
|
432
|
-
},
|
|
433
|
-
`${field.name}-${value}`
|
|
434
|
-
),
|
|
435
|
-
field.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
|
|
436
|
-
] }, field.name);
|
|
437
|
-
}
|
|
438
|
-
function FormPage(props) {
|
|
439
|
-
const { fields, groups, submitUrl, method = "post", initialData, formId, isDialog = false, formFieldRenderers } = props;
|
|
440
|
-
const finalFormId = formId || `form-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
441
|
-
if (process.env.NODE_ENV === "development" && initialData) {
|
|
442
|
-
const fieldNames = fields ? fields.map((f) => f.name).join(", ") : groups ? groups.map((g) => g.fields.map((f) => f.name).join(", ")).join(" | ") : "";
|
|
443
|
-
console.log(
|
|
444
|
-
`[FormPage] initialData: ${JSON.stringify(initialData)}, fields: ${fieldNames}`
|
|
564
|
+
field.name
|
|
445
565
|
);
|
|
446
566
|
}
|
|
447
|
-
|
|
448
|
-
|
|
567
|
+
if (field.type === "checkbox") {
|
|
568
|
+
const isChecked = value === "true" || value === "1" || value === "on" || String(value).toLowerCase() === "true";
|
|
569
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
449
570
|
"div",
|
|
450
571
|
{
|
|
451
|
-
|
|
452
|
-
|
|
572
|
+
className: "space-y-2",
|
|
573
|
+
"data-testid": `field-${field.name}`,
|
|
453
574
|
children: [
|
|
454
575
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
455
|
-
"
|
|
576
|
+
"label",
|
|
456
577
|
{
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
viewBox: "0 0 24 24",
|
|
578
|
+
htmlFor: field.name,
|
|
579
|
+
className: "flex items-center gap-3 cursor-pointer group py-2.5 px-3 rounded-lg hover:bg-gray-50 transition-colors border border-transparent hover:border-gray-200",
|
|
580
|
+
"data-testid": `label-${field.name}`,
|
|
461
581
|
children: [
|
|
462
582
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
463
|
-
"
|
|
583
|
+
"input",
|
|
464
584
|
{
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
585
|
+
type: "checkbox",
|
|
586
|
+
id: field.name,
|
|
587
|
+
name: field.name,
|
|
588
|
+
value: "true",
|
|
589
|
+
checked: isChecked,
|
|
590
|
+
required: field.required,
|
|
591
|
+
className: "w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 cursor-pointer transition-all flex-shrink-0",
|
|
592
|
+
"data-testid": `input-${field.name}`
|
|
471
593
|
}
|
|
472
594
|
),
|
|
473
|
-
/* @__PURE__ */ jsxRuntime.
|
|
474
|
-
|
|
475
|
-
{
|
|
476
|
-
|
|
477
|
-
fill: "currentColor",
|
|
478
|
-
d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
479
|
-
}
|
|
480
|
-
)
|
|
595
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm font-semibold text-gray-700 select-none flex-1", children: [
|
|
596
|
+
field.label,
|
|
597
|
+
field.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", children: "*" })
|
|
598
|
+
] })
|
|
481
599
|
]
|
|
482
600
|
}
|
|
483
601
|
),
|
|
484
|
-
/* @__PURE__ */ jsxRuntime.jsx("
|
|
602
|
+
field.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 ml-8", children: field.description })
|
|
485
603
|
]
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: `sticky top-0 z-10 bg-white border-b border-gray-200 shadow-sm ${isDialog ? "px-6" : "-mx-6 px-6 -mt-6"}`, children: /* @__PURE__ */ jsxRuntime.jsx("nav", { className: "flex -mb-px w-full", "aria-label": "Tabs", "data-testid": "form-tabs", children: groups.map((group, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
490
|
-
"button",
|
|
491
|
-
{
|
|
492
|
-
type: "button",
|
|
493
|
-
"x-on:click": `activeTab = ${index}`,
|
|
494
|
-
"x-bind:class": `activeTab === ${index} ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'`,
|
|
495
|
-
className: "px-6 py-4 text-sm font-medium border-b-2 transition-colors duration-150 whitespace-nowrap flex-1 text-center",
|
|
496
|
-
"data-testid": `form-tab-${index}`,
|
|
497
|
-
children: group.label
|
|
498
|
-
},
|
|
499
|
-
index
|
|
500
|
-
)) }) }),
|
|
501
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
502
|
-
"form",
|
|
503
|
-
{
|
|
504
|
-
id: finalFormId,
|
|
505
|
-
method: method === "put" ? "post" : method,
|
|
506
|
-
action: submitUrl,
|
|
507
|
-
"hx-boost": "true",
|
|
508
|
-
...method === "put" ? { "hx-put": submitUrl } : {},
|
|
509
|
-
"hx-indicator": "#form-loading-indicator",
|
|
510
|
-
"data-testid": "form",
|
|
511
|
-
className: isDialog ? "p-6" : "mt-6",
|
|
512
|
-
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6", children: groups.map((group, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
513
|
-
"div",
|
|
514
|
-
{
|
|
515
|
-
"x-show": `activeTab === ${index}`,
|
|
516
|
-
className: "space-y-6",
|
|
517
|
-
"data-testid": `form-tab-content-${index}`,
|
|
518
|
-
children: group.fields.map((field) => renderFormField(field, initialData, formFieldRenderers))
|
|
519
|
-
},
|
|
520
|
-
index
|
|
521
|
-
)) }) })
|
|
522
|
-
}
|
|
523
|
-
)
|
|
524
|
-
] }) : (
|
|
525
|
-
/* 平铺模式(向后兼容) */
|
|
526
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
527
|
-
"form",
|
|
528
|
-
{
|
|
529
|
-
id: finalFormId,
|
|
530
|
-
method: method === "put" ? "post" : method,
|
|
531
|
-
action: submitUrl,
|
|
532
|
-
"hx-boost": "true",
|
|
533
|
-
...method === "put" ? { "hx-put": submitUrl } : {},
|
|
534
|
-
"hx-indicator": "#form-loading-indicator",
|
|
535
|
-
className: "space-y-6",
|
|
536
|
-
"data-testid": "form",
|
|
537
|
-
children: fields && fields.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6 space-y-6", children: fields.map((field) => renderFormField(field, initialData, formFieldRenderers)) }) })
|
|
538
|
-
}
|
|
539
|
-
)
|
|
540
|
-
)
|
|
541
|
-
] });
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// src/utils/form-data-processor.ts
|
|
545
|
-
function preprocessFormData(data, zodSchema) {
|
|
546
|
-
if (!zodSchema) {
|
|
547
|
-
return data;
|
|
548
|
-
}
|
|
549
|
-
const processed = { ...data };
|
|
550
|
-
const def = zodSchema._def;
|
|
551
|
-
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
552
|
-
if (!shape || typeof shape !== "object") {
|
|
553
|
-
return processed;
|
|
604
|
+
},
|
|
605
|
+
field.name
|
|
606
|
+
);
|
|
554
607
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
608
|
+
const shouldUseTextarea = field.type === "textarea" || value && isJsonString(value) && field.type !== "select";
|
|
609
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
610
|
+
"div",
|
|
611
|
+
{
|
|
612
|
+
className: "space-y-2",
|
|
613
|
+
"data-testid": `field-${field.name}`,
|
|
614
|
+
children: [
|
|
615
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
616
|
+
"label",
|
|
617
|
+
{
|
|
618
|
+
htmlFor: field.name,
|
|
619
|
+
className: "block text-sm font-semibold text-gray-700",
|
|
620
|
+
"data-testid": `label-${field.name}`,
|
|
621
|
+
children: [
|
|
622
|
+
field.label,
|
|
623
|
+
field.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", children: "*" })
|
|
624
|
+
]
|
|
625
|
+
}
|
|
626
|
+
),
|
|
627
|
+
shouldUseTextarea ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
628
|
+
"textarea",
|
|
629
|
+
{
|
|
630
|
+
id: field.name,
|
|
631
|
+
name: field.name,
|
|
632
|
+
required: field.required,
|
|
633
|
+
placeholder: field.placeholder || (isJsonString(value) ? "JSON \u683C\u5F0F\u6570\u636E" : ""),
|
|
634
|
+
rows: isJsonString(value) ? 10 : 4,
|
|
635
|
+
className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 resize-y font-mono text-sm",
|
|
636
|
+
"data-testid": `input-${field.name}`,
|
|
637
|
+
children: isJsonString(value) ? formatJsonString(value) : value
|
|
638
|
+
},
|
|
639
|
+
`${field.name}-${value}`
|
|
640
|
+
) : field.type === "select" ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
641
|
+
"select",
|
|
642
|
+
{
|
|
643
|
+
id: field.name,
|
|
644
|
+
name: field.name,
|
|
645
|
+
required: field.required,
|
|
646
|
+
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 bg-white",
|
|
647
|
+
"data-testid": `select-${field.name}`,
|
|
648
|
+
children: [
|
|
649
|
+
!field.required && /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", selected: !value || value === "", children: "\u8BF7\u9009\u62E9" }),
|
|
650
|
+
field.options && field.options.length > 0 ? field.options.map((option) => {
|
|
651
|
+
const optionValue = String(option.value);
|
|
652
|
+
const isSelected = value === optionValue;
|
|
653
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
654
|
+
"option",
|
|
655
|
+
{
|
|
656
|
+
value: optionValue,
|
|
657
|
+
selected: isSelected,
|
|
658
|
+
children: option.label
|
|
659
|
+
},
|
|
660
|
+
optionValue
|
|
661
|
+
);
|
|
662
|
+
}) : /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", disabled: true, children: "\u6682\u65E0\u9009\u9879" })
|
|
663
|
+
]
|
|
664
|
+
},
|
|
665
|
+
`${field.name}-${value || ""}`
|
|
666
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
667
|
+
"input",
|
|
668
|
+
{
|
|
669
|
+
type: field.type || "text",
|
|
670
|
+
id: field.name,
|
|
671
|
+
name: field.name,
|
|
672
|
+
required: field.required,
|
|
673
|
+
placeholder: field.placeholder,
|
|
674
|
+
step: field.type === "number" ? field.step ?? void 0 : void 0,
|
|
675
|
+
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",
|
|
676
|
+
value,
|
|
677
|
+
"data-testid": `input-${field.name}`
|
|
678
|
+
},
|
|
679
|
+
`${field.name}-${value}`
|
|
680
|
+
),
|
|
681
|
+
field.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
|
|
682
|
+
]
|
|
683
|
+
},
|
|
684
|
+
field.name
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
function FormPage(props) {
|
|
688
|
+
const {
|
|
689
|
+
fields,
|
|
690
|
+
groups,
|
|
691
|
+
submitUrl,
|
|
692
|
+
method = "post",
|
|
693
|
+
initialData,
|
|
694
|
+
formId,
|
|
695
|
+
isDialog = false,
|
|
696
|
+
formFieldRenderers
|
|
697
|
+
} = props;
|
|
698
|
+
const finalFormId = formId || `form-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
699
|
+
if (process.env.NODE_ENV === "development" && initialData) {
|
|
700
|
+
const fieldNames = fields ? fields.map((f) => f.name).join(", ") : groups ? groups.map((g) => g.fields.map((f) => f.name).join(", ")).join(" | ") : "";
|
|
701
|
+
console.log(
|
|
702
|
+
`[FormPage] initialData: ${JSON.stringify(initialData)}, fields: ${fieldNames}`
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
706
|
+
"div",
|
|
707
|
+
{
|
|
708
|
+
className: "w-full",
|
|
709
|
+
"data-testid": "form-container",
|
|
710
|
+
"x-data": `{ activeTab: 0 }`,
|
|
711
|
+
children: [
|
|
712
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
713
|
+
"div",
|
|
714
|
+
{
|
|
715
|
+
id: "form-loading-indicator",
|
|
716
|
+
className: "htmx-indicator fixed top-4 right-4 bg-blue-600 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 z-50",
|
|
717
|
+
children: [
|
|
718
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
719
|
+
"svg",
|
|
720
|
+
{
|
|
721
|
+
className: "animate-spin h-4 w-4",
|
|
722
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
723
|
+
fill: "none",
|
|
724
|
+
viewBox: "0 0 24 24",
|
|
725
|
+
children: [
|
|
726
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
727
|
+
"circle",
|
|
728
|
+
{
|
|
729
|
+
className: "opacity-25",
|
|
730
|
+
cx: "12",
|
|
731
|
+
cy: "12",
|
|
732
|
+
r: "10",
|
|
733
|
+
stroke: "currentColor",
|
|
734
|
+
strokeWidth: "4"
|
|
735
|
+
}
|
|
736
|
+
),
|
|
737
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
738
|
+
"path",
|
|
739
|
+
{
|
|
740
|
+
className: "opacity-75",
|
|
741
|
+
fill: "currentColor",
|
|
742
|
+
d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
743
|
+
}
|
|
744
|
+
)
|
|
745
|
+
]
|
|
746
|
+
}
|
|
747
|
+
),
|
|
748
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: "\u63D0\u4EA4\u4E2D..." })
|
|
749
|
+
]
|
|
750
|
+
}
|
|
751
|
+
),
|
|
752
|
+
groups && groups.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
753
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
754
|
+
"div",
|
|
755
|
+
{
|
|
756
|
+
className: `sticky top-0 z-10 bg-white border-b border-gray-200 shadow-sm ${isDialog ? "px-6" : "-mx-6 px-6 -mt-6"}`,
|
|
757
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
758
|
+
"nav",
|
|
759
|
+
{
|
|
760
|
+
className: "flex -mb-px w-full",
|
|
761
|
+
"aria-label": "Tabs",
|
|
762
|
+
"data-testid": "form-tabs",
|
|
763
|
+
children: groups.map((group, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
764
|
+
"button",
|
|
765
|
+
{
|
|
766
|
+
type: "button",
|
|
767
|
+
"x-on:click": `activeTab = ${index}`,
|
|
768
|
+
"x-bind:class": `activeTab === ${index} ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'`,
|
|
769
|
+
className: "px-6 py-4 text-sm font-medium border-b-2 transition-colors duration-150 whitespace-nowrap flex-1 text-center",
|
|
770
|
+
"data-testid": `form-tab-${index}`,
|
|
771
|
+
children: group.label
|
|
772
|
+
},
|
|
773
|
+
index
|
|
774
|
+
))
|
|
775
|
+
}
|
|
776
|
+
)
|
|
777
|
+
}
|
|
778
|
+
),
|
|
779
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
780
|
+
"form",
|
|
781
|
+
{
|
|
782
|
+
id: finalFormId,
|
|
783
|
+
method: method === "put" ? "post" : method,
|
|
784
|
+
action: submitUrl,
|
|
785
|
+
...method === "put" ? { "hx-put": submitUrl } : {},
|
|
786
|
+
"hx-indicator": "#form-loading-indicator",
|
|
787
|
+
"data-testid": "form",
|
|
788
|
+
"hx-sync": "this:abort",
|
|
789
|
+
className: isDialog ? "p-6" : "mt-6",
|
|
790
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6", children: groups.map((group, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
791
|
+
"div",
|
|
792
|
+
{
|
|
793
|
+
"x-show": `activeTab === ${index}`,
|
|
794
|
+
className: "space-y-6",
|
|
795
|
+
"data-testid": `form-tab-content-${index}`,
|
|
796
|
+
children: group.fields.map(
|
|
797
|
+
(field) => renderFormField(field, initialData, formFieldRenderers)
|
|
798
|
+
)
|
|
799
|
+
},
|
|
800
|
+
index
|
|
801
|
+
)) }) })
|
|
802
|
+
}
|
|
803
|
+
)
|
|
804
|
+
] }) : (
|
|
805
|
+
/* 平铺模式(向后兼容) */
|
|
806
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
807
|
+
"form",
|
|
808
|
+
{
|
|
809
|
+
id: finalFormId,
|
|
810
|
+
method: method === "put" ? "post" : method,
|
|
811
|
+
action: submitUrl,
|
|
812
|
+
"hx-boost": "true",
|
|
813
|
+
"hx-trigger": "click from:#form-submit-button",
|
|
814
|
+
...method === "put" ? { "hx-put": submitUrl } : {},
|
|
815
|
+
"hx-indicator": "#form-loading-indicator",
|
|
816
|
+
className: "space-y-6",
|
|
817
|
+
"data-testid": "form",
|
|
818
|
+
children: fields && fields.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6 space-y-6", children: fields.map(
|
|
819
|
+
(field) => renderFormField(field, initialData, formFieldRenderers)
|
|
820
|
+
) }) })
|
|
821
|
+
}
|
|
822
|
+
)
|
|
823
|
+
)
|
|
824
|
+
]
|
|
825
|
+
}
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// src/utils/json-form-parser.ts
|
|
830
|
+
function parseNestedFormData(formData) {
|
|
831
|
+
const result = {};
|
|
832
|
+
const arrayIndicesMap = /* @__PURE__ */ new Map();
|
|
833
|
+
const arrayIndicesSet = /* @__PURE__ */ new Map();
|
|
834
|
+
for (const [key] of Object.entries(formData)) {
|
|
835
|
+
const parts = parseKey(key);
|
|
836
|
+
collectArrayIndices(parts, arrayIndicesSet);
|
|
837
|
+
}
|
|
838
|
+
for (const [pathKey, indices] of arrayIndicesSet.entries()) {
|
|
839
|
+
const sortedIndices = Array.from(indices).sort((a, b) => a - b);
|
|
840
|
+
const indexMap = /* @__PURE__ */ new Map();
|
|
841
|
+
for (let i = 0; i < sortedIndices.length; i++) {
|
|
842
|
+
indexMap.set(sortedIndices[i], i);
|
|
843
|
+
}
|
|
844
|
+
arrayIndicesMap.set(pathKey, indexMap);
|
|
845
|
+
}
|
|
846
|
+
for (const [key, value] of Object.entries(formData)) {
|
|
847
|
+
const parts = parseKey(key);
|
|
848
|
+
buildNestedObject(result, parts, value, arrayIndicesMap);
|
|
849
|
+
}
|
|
850
|
+
compressArrays(result);
|
|
851
|
+
return result;
|
|
852
|
+
}
|
|
853
|
+
function collectArrayIndices(parts, arrayIndicesSet) {
|
|
854
|
+
for (let i = 0; i < parts.length; i++) {
|
|
855
|
+
const part = parts[i];
|
|
856
|
+
if (typeof part === "number") {
|
|
857
|
+
const pathKey = getArrayPathKey(parts, i);
|
|
858
|
+
if (!arrayIndicesSet.has(pathKey)) {
|
|
859
|
+
arrayIndicesSet.set(pathKey, /* @__PURE__ */ new Set());
|
|
860
|
+
}
|
|
861
|
+
arrayIndicesSet.get(pathKey).add(part);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
function parseKey(key) {
|
|
866
|
+
const result = [];
|
|
867
|
+
let current = "";
|
|
868
|
+
let inBrackets = false;
|
|
869
|
+
let bracketContent = "";
|
|
870
|
+
for (let i = 0; i < key.length; i++) {
|
|
871
|
+
const char = key[i];
|
|
872
|
+
if (char === "[" && !inBrackets) {
|
|
873
|
+
if (current) {
|
|
874
|
+
result.push(current);
|
|
875
|
+
current = "";
|
|
876
|
+
}
|
|
877
|
+
inBrackets = true;
|
|
878
|
+
} else if (char === "]" && inBrackets) {
|
|
879
|
+
inBrackets = false;
|
|
880
|
+
const index = parseInt(bracketContent, 10);
|
|
881
|
+
if (!isNaN(index)) {
|
|
882
|
+
result.push(index);
|
|
883
|
+
}
|
|
884
|
+
bracketContent = "";
|
|
885
|
+
} else if (char === "." && !inBrackets) {
|
|
886
|
+
if (current) {
|
|
887
|
+
result.push(current);
|
|
888
|
+
current = "";
|
|
889
|
+
}
|
|
890
|
+
} else if (inBrackets) {
|
|
891
|
+
bracketContent += char;
|
|
892
|
+
} else {
|
|
893
|
+
current += char;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if (current) {
|
|
897
|
+
result.push(current);
|
|
898
|
+
}
|
|
899
|
+
return result;
|
|
900
|
+
}
|
|
901
|
+
function getArrayPathKey(parts, endIndex) {
|
|
902
|
+
const pathParts = [];
|
|
903
|
+
for (let i = 0; i < endIndex; i++) {
|
|
904
|
+
const part = parts[i];
|
|
905
|
+
if (typeof part === "string") {
|
|
906
|
+
pathParts.push(part);
|
|
907
|
+
} else {
|
|
908
|
+
pathParts.push(`[${part}]`);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return pathParts.join(".");
|
|
912
|
+
}
|
|
913
|
+
function getCompressedIndex(pathKey, originalIndex, arrayIndicesMap) {
|
|
914
|
+
if (!arrayIndicesMap.has(pathKey)) {
|
|
915
|
+
arrayIndicesMap.set(pathKey, /* @__PURE__ */ new Map());
|
|
916
|
+
}
|
|
917
|
+
const indexMap = arrayIndicesMap.get(pathKey);
|
|
918
|
+
let compressedIndex = indexMap.get(originalIndex);
|
|
919
|
+
if (compressedIndex === void 0) {
|
|
920
|
+
compressedIndex = indexMap.size;
|
|
921
|
+
indexMap.set(originalIndex, compressedIndex);
|
|
922
|
+
}
|
|
923
|
+
return compressedIndex;
|
|
924
|
+
}
|
|
925
|
+
function buildNestedObject(obj, parts, value, arrayIndicesMap) {
|
|
926
|
+
let current = obj;
|
|
927
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
928
|
+
const part = parts[i];
|
|
929
|
+
const nextPart = parts[i + 1];
|
|
930
|
+
if (typeof part === "number") {
|
|
931
|
+
if (!Array.isArray(current)) {
|
|
932
|
+
throw new Error(`Expected array at index ${i}, but got ${typeof current}`);
|
|
933
|
+
}
|
|
934
|
+
const pathKey = getArrayPathKey(parts, i);
|
|
935
|
+
const compressedIndex = getCompressedIndex(
|
|
936
|
+
pathKey,
|
|
937
|
+
part,
|
|
938
|
+
arrayIndicesMap
|
|
939
|
+
);
|
|
940
|
+
while (current.length <= compressedIndex) {
|
|
941
|
+
if (typeof nextPart === "number") {
|
|
942
|
+
current.push([]);
|
|
943
|
+
} else {
|
|
944
|
+
current.push({});
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
current = current[compressedIndex];
|
|
948
|
+
} else {
|
|
949
|
+
if (typeof nextPart === "number") {
|
|
950
|
+
if (!current.hasOwnProperty(part) || !Array.isArray(current[part])) {
|
|
951
|
+
current[part] = [];
|
|
952
|
+
}
|
|
953
|
+
} else {
|
|
954
|
+
if (!current.hasOwnProperty(part) || (typeof current[part] !== "object" || current[part] === null || Array.isArray(current[part]))) {
|
|
955
|
+
current[part] = {};
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
current = current[part];
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
const lastPart = parts[parts.length - 1];
|
|
962
|
+
if (typeof lastPart === "number") {
|
|
963
|
+
const pathKey = getArrayPathKey(parts, parts.length - 1);
|
|
964
|
+
const compressedIndex = getCompressedIndex(
|
|
965
|
+
pathKey,
|
|
966
|
+
lastPart,
|
|
967
|
+
arrayIndicesMap
|
|
968
|
+
);
|
|
969
|
+
if (!Array.isArray(current)) {
|
|
970
|
+
throw new Error(`Expected array for last part, but got ${typeof current}`);
|
|
971
|
+
}
|
|
972
|
+
while (current.length <= compressedIndex) {
|
|
973
|
+
current.push(void 0);
|
|
974
|
+
}
|
|
975
|
+
current[compressedIndex] = value;
|
|
976
|
+
} else {
|
|
977
|
+
if (Array.isArray(current)) {
|
|
978
|
+
throw new Error(`Cannot set property on array: ${lastPart}`);
|
|
979
|
+
}
|
|
980
|
+
if (typeof current !== "object" || current === null) {
|
|
981
|
+
current = {};
|
|
982
|
+
}
|
|
983
|
+
current[lastPart] = value;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
function compressArrays(obj, arrayIndicesMap) {
|
|
987
|
+
if (Array.isArray(obj)) {
|
|
988
|
+
const newArray = [];
|
|
989
|
+
for (let i = 0; i < obj.length; i++) {
|
|
990
|
+
if (obj[i] !== void 0) {
|
|
991
|
+
newArray.push(obj[i]);
|
|
992
|
+
if (typeof obj[i] === "object" && obj[i] !== null) {
|
|
993
|
+
compressArrays(obj[i]);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
obj.length = 0;
|
|
998
|
+
obj.push(...newArray);
|
|
999
|
+
} else if (typeof obj === "object" && obj !== null) {
|
|
1000
|
+
for (const key in obj) {
|
|
1001
|
+
if (obj.hasOwnProperty(key)) {
|
|
1002
|
+
compressArrays(obj[key]);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// src/utils/form-data-processor.ts
|
|
1009
|
+
function parseNestedFormData2(flatData) {
|
|
1010
|
+
return parseNestedFormData(flatData);
|
|
1011
|
+
}
|
|
1012
|
+
function preprocessFormData(data, zodSchema) {
|
|
1013
|
+
if (!zodSchema) {
|
|
1014
|
+
return data;
|
|
1015
|
+
}
|
|
1016
|
+
const processed = { ...data };
|
|
1017
|
+
const def = zodSchema._zod?.def || zodSchema._def;
|
|
1018
|
+
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
1019
|
+
if (!shape || typeof shape !== "object") {
|
|
1020
|
+
return processed;
|
|
1021
|
+
}
|
|
1022
|
+
for (const [fieldName, fieldSchema] of Object.entries(shape)) {
|
|
1023
|
+
const value = processed[fieldName];
|
|
1024
|
+
if (value === void 0) {
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
if (value === null) {
|
|
561
1028
|
processed[fieldName] = void 0;
|
|
562
1029
|
continue;
|
|
563
1030
|
}
|
|
@@ -565,16 +1032,18 @@ function preprocessFormData(data, zodSchema) {
|
|
|
565
1032
|
processed[fieldName] = void 0;
|
|
566
1033
|
continue;
|
|
567
1034
|
}
|
|
568
|
-
const fieldDef = fieldSchema._def;
|
|
569
|
-
let typeName = fieldDef?.type
|
|
570
|
-
|
|
571
|
-
|
|
1035
|
+
const fieldDef = fieldSchema._zod?.def || fieldSchema._def;
|
|
1036
|
+
let typeName = fieldDef?.type;
|
|
1037
|
+
let actualSchema = fieldSchema;
|
|
1038
|
+
if (typeName === "optional") {
|
|
1039
|
+
const innerType = fieldDef?.innerType;
|
|
572
1040
|
if (innerType) {
|
|
573
|
-
|
|
574
|
-
|
|
1041
|
+
actualSchema = innerType;
|
|
1042
|
+
const innerDef = innerType._zod?.def || innerType._def;
|
|
1043
|
+
typeName = innerDef?.type;
|
|
575
1044
|
}
|
|
576
1045
|
}
|
|
577
|
-
if (typeName === "number" || typeName === "
|
|
1046
|
+
if (typeName === "number" || typeName === "int") {
|
|
578
1047
|
if (typeof value === "string") {
|
|
579
1048
|
const trimmed = value.trim();
|
|
580
1049
|
if (trimmed !== "") {
|
|
@@ -587,7 +1056,7 @@ function preprocessFormData(data, zodSchema) {
|
|
|
587
1056
|
processed[fieldName] = value;
|
|
588
1057
|
}
|
|
589
1058
|
}
|
|
590
|
-
if (typeName === "boolean"
|
|
1059
|
+
if (typeName === "boolean") {
|
|
591
1060
|
if (typeof value === "string") {
|
|
592
1061
|
const trimmed = value.trim().toLowerCase();
|
|
593
1062
|
if (trimmed === "true" || trimmed === "1" || trimmed === "on") {
|
|
@@ -599,8 +1068,64 @@ function preprocessFormData(data, zodSchema) {
|
|
|
599
1068
|
processed[fieldName] = value;
|
|
600
1069
|
}
|
|
601
1070
|
}
|
|
602
|
-
if (typeName === "array"
|
|
603
|
-
if (
|
|
1071
|
+
if (typeName === "array") {
|
|
1072
|
+
if (Array.isArray(value)) {
|
|
1073
|
+
const arraySchema = actualSchema;
|
|
1074
|
+
const arrayDef = arraySchema._zod?.def || arraySchema._def;
|
|
1075
|
+
let elementType = arrayDef?.element;
|
|
1076
|
+
if (!elementType) {
|
|
1077
|
+
processed[fieldName] = value;
|
|
1078
|
+
} else {
|
|
1079
|
+
const elementDef = elementType._zod?.def || elementType._def;
|
|
1080
|
+
let elementTypeName = elementDef?.type;
|
|
1081
|
+
if (elementTypeName === "optional") {
|
|
1082
|
+
const innerElementType = elementDef?.innerType;
|
|
1083
|
+
if (innerElementType) {
|
|
1084
|
+
elementType = innerElementType;
|
|
1085
|
+
const innerElementDef = innerElementType._zod?.def || innerElementType._def;
|
|
1086
|
+
elementTypeName = innerElementDef?.type;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
processed[fieldName] = value.map((item) => {
|
|
1090
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
1091
|
+
if (elementTypeName === "object") {
|
|
1092
|
+
const processedItem = preprocessFormData(
|
|
1093
|
+
item,
|
|
1094
|
+
elementType
|
|
1095
|
+
);
|
|
1096
|
+
return { ...item, ...processedItem };
|
|
1097
|
+
}
|
|
1098
|
+
return item;
|
|
1099
|
+
}
|
|
1100
|
+
if (typeof item === "string") {
|
|
1101
|
+
return convertValueByType(item, elementType);
|
|
1102
|
+
}
|
|
1103
|
+
if (Array.isArray(item)) {
|
|
1104
|
+
const elementDef2 = elementType._zod?.def || elementType._def;
|
|
1105
|
+
const nestedInnerType = elementDef2?.element;
|
|
1106
|
+
if (nestedInnerType) {
|
|
1107
|
+
return item.map((subItem) => {
|
|
1108
|
+
if (typeof subItem === "string") {
|
|
1109
|
+
return convertValueByType(subItem, nestedInnerType);
|
|
1110
|
+
}
|
|
1111
|
+
if (typeof subItem === "object" && subItem !== null) {
|
|
1112
|
+
const subItemDef = nestedInnerType._zod?.def || nestedInnerType._def;
|
|
1113
|
+
if (subItemDef?.type === "object") {
|
|
1114
|
+
return preprocessFormData(
|
|
1115
|
+
subItem,
|
|
1116
|
+
nestedInnerType
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return subItem;
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
return item;
|
|
1124
|
+
}
|
|
1125
|
+
return item;
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
} else if (typeof value === "string") {
|
|
604
1129
|
const trimmed = value.trim();
|
|
605
1130
|
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
606
1131
|
try {
|
|
@@ -614,8 +1139,11 @@ function preprocessFormData(data, zodSchema) {
|
|
|
614
1139
|
}
|
|
615
1140
|
}
|
|
616
1141
|
}
|
|
617
|
-
if (typeName === "object"
|
|
618
|
-
if (typeof value === "
|
|
1142
|
+
if (typeName === "object") {
|
|
1143
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1144
|
+
const objectSchema = actualSchema;
|
|
1145
|
+
processed[fieldName] = preprocessFormData(value, objectSchema);
|
|
1146
|
+
} else if (typeof value === "string" && value.trim() !== "") {
|
|
619
1147
|
try {
|
|
620
1148
|
const trimmed = value.trim();
|
|
621
1149
|
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
@@ -626,7 +1154,7 @@ function preprocessFormData(data, zodSchema) {
|
|
|
626
1154
|
}
|
|
627
1155
|
}
|
|
628
1156
|
}
|
|
629
|
-
if (typeName === "any" || typeName === "
|
|
1157
|
+
if (typeName === "any" || typeName === "unknown") {
|
|
630
1158
|
if (typeof value === "string" && value.trim() !== "") {
|
|
631
1159
|
try {
|
|
632
1160
|
const trimmed = value.trim();
|
|
@@ -641,6 +1169,36 @@ function preprocessFormData(data, zodSchema) {
|
|
|
641
1169
|
}
|
|
642
1170
|
return processed;
|
|
643
1171
|
}
|
|
1172
|
+
function convertValueByType(value, schema) {
|
|
1173
|
+
const def = schema._zod?.def || schema._def;
|
|
1174
|
+
let typeName = def?.type;
|
|
1175
|
+
if (typeName === "optional") {
|
|
1176
|
+
const innerType = def?.innerType;
|
|
1177
|
+
if (innerType) {
|
|
1178
|
+
const innerDef = innerType._zod?.def || innerType._def;
|
|
1179
|
+
typeName = innerDef?.type;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
if (typeName === "number" || typeName === "int") {
|
|
1183
|
+
const trimmed = value.trim();
|
|
1184
|
+
if (trimmed !== "") {
|
|
1185
|
+
const numValue = Number(trimmed);
|
|
1186
|
+
if (!isNaN(numValue)) {
|
|
1187
|
+
return numValue;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
return value;
|
|
1191
|
+
} else if (typeName === "boolean") {
|
|
1192
|
+
const trimmed = value.trim().toLowerCase();
|
|
1193
|
+
if (trimmed === "true" || trimmed === "1" || trimmed === "on") {
|
|
1194
|
+
return true;
|
|
1195
|
+
} else if (trimmed === "false" || trimmed === "0" || trimmed === "off" || trimmed === "") {
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
return value;
|
|
1199
|
+
}
|
|
1200
|
+
return value;
|
|
1201
|
+
}
|
|
644
1202
|
|
|
645
1203
|
// src/utils/schema-utils.ts
|
|
646
1204
|
function parseSchemaToFields(schema) {
|
|
@@ -931,22 +1489,9 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
931
1489
|
if (ctx.req.method === "GET") {
|
|
932
1490
|
return this.render(context);
|
|
933
1491
|
} else if (ctx.req.method === "POST" || ctx.req.method === "PUT" || ctx.req.method === "PATCH") {
|
|
934
|
-
const methodOverride = ctx.req.header("X-HTTP-Method-Override");
|
|
935
|
-
const actualMethod = methodOverride || ctx.req.method;
|
|
936
|
-
const expectedMethod = this.getFormAction() === "edit" ? "PUT" : "POST";
|
|
937
|
-
if (actualMethod.toUpperCase() !== expectedMethod) {
|
|
938
|
-
imeanServiceEngine.logger.warn(
|
|
939
|
-
`[BaseFormFeature] Method mismatch: expected ${expectedMethod}, got ${actualMethod} (request method: ${ctx.req.method}, X-HTTP-Method-Override: ${methodOverride || "none"})`
|
|
940
|
-
);
|
|
941
|
-
}
|
|
942
1492
|
const originalData = { ...context.body };
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
);
|
|
946
|
-
let data = this.preprocessFormData(context.body);
|
|
947
|
-
imeanServiceEngine.logger.info(
|
|
948
|
-
`[BaseFormFeature] Preprocessed data: ${JSON.stringify(data)}`
|
|
949
|
-
);
|
|
1493
|
+
const nestedData = parseNestedFormData2(context.body);
|
|
1494
|
+
let data = this.preprocessFormData(nestedData);
|
|
950
1495
|
if (!this.schema) {
|
|
951
1496
|
throw new Error("Schema is required for form validation");
|
|
952
1497
|
}
|
|
@@ -958,9 +1503,6 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
958
1503
|
const errorMessage = firstError.message;
|
|
959
1504
|
const errorText = fieldName ? `${fieldName}: ${errorMessage}` : errorMessage;
|
|
960
1505
|
context.sendError("\u9A8C\u8BC1\u5931\u8D25", errorText);
|
|
961
|
-
imeanServiceEngine.logger.info(
|
|
962
|
-
`[BaseFormFeature] Validation failed, returning form with originalData: ${JSON.stringify(originalData)}`
|
|
963
|
-
);
|
|
964
1506
|
return this.render(context, originalData);
|
|
965
1507
|
}
|
|
966
1508
|
const item = await this.handleSubmit(
|
|
@@ -980,9 +1522,6 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
980
1522
|
`${context.model.getMetadata().title}\u5DF2\u6210\u529F${actionText}`
|
|
981
1523
|
);
|
|
982
1524
|
if (context.isDialog) {
|
|
983
|
-
imeanServiceEngine.logger.info(
|
|
984
|
-
`[BaseFormFeature] Dialog mode: setting refresh to close dialog and refresh list`
|
|
985
|
-
);
|
|
986
1525
|
context.setRefresh(true);
|
|
987
1526
|
if (context.redirectUrl) {
|
|
988
1527
|
context.redirectUrl = void 0;
|
|
@@ -990,9 +1529,6 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
990
1529
|
return null;
|
|
991
1530
|
} else {
|
|
992
1531
|
const redirectUrl = this.getSuccessRedirectUrl(context, item);
|
|
993
|
-
imeanServiceEngine.logger.info(
|
|
994
|
-
`[BaseFormFeature] Page mode: Setting redirect URL: ${redirectUrl} (isHtmxRequest: ${context.isHtmxRequest})`
|
|
995
|
-
);
|
|
996
1532
|
context.redirect(redirectUrl);
|
|
997
1533
|
return null;
|
|
998
1534
|
}
|
|
@@ -1014,9 +1550,6 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
1014
1550
|
} else {
|
|
1015
1551
|
formData = initialData;
|
|
1016
1552
|
}
|
|
1017
|
-
imeanServiceEngine.logger.info(
|
|
1018
|
-
`[BaseFormFeature] render: initialData=${JSON.stringify(initialData)}, formData=${JSON.stringify(formData)}`
|
|
1019
|
-
);
|
|
1020
1553
|
let submitUrl = this.getSubmitUrl(context);
|
|
1021
1554
|
if (context.isDialog) {
|
|
1022
1555
|
const url = new URL(submitUrl, "http://localhost");
|
|
@@ -1083,7 +1616,7 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
1083
1616
|
);
|
|
1084
1617
|
}
|
|
1085
1618
|
/**
|
|
1086
|
-
*
|
|
1619
|
+
* 获取操作按钮(返回 JSX)
|
|
1087
1620
|
*/
|
|
1088
1621
|
async getActions(context) {
|
|
1089
1622
|
const actions = [];
|
|
@@ -1111,7 +1644,8 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
1111
1644
|
});
|
|
1112
1645
|
}
|
|
1113
1646
|
}
|
|
1114
|
-
|
|
1647
|
+
const { renderActionButtons: renderActionButtons2 } = await Promise.resolve().then(() => (init_action_button_renderer(), action_button_renderer_exports));
|
|
1648
|
+
return renderActionButtons2(actions);
|
|
1115
1649
|
}
|
|
1116
1650
|
};
|
|
1117
1651
|
|
|
@@ -1160,7 +1694,7 @@ var CustomFeature = class extends BaseFeature {
|
|
|
1160
1694
|
if (this.handlerFn) {
|
|
1161
1695
|
return await this.handlerFn(context);
|
|
1162
1696
|
}
|
|
1163
|
-
return
|
|
1697
|
+
return await this.render(context);
|
|
1164
1698
|
}
|
|
1165
1699
|
async render(context) {
|
|
1166
1700
|
if (this.renderFn) {
|
|
@@ -1303,7 +1837,13 @@ function renderDefaultValue(value) {
|
|
|
1303
1837
|
return /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "bg-gray-50 border border-gray-200 rounded p-3 text-xs overflow-x-auto", children: JSON.stringify(value, null, 2) });
|
|
1304
1838
|
}
|
|
1305
1839
|
if (typeof value === "boolean") {
|
|
1306
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1840
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1841
|
+
"span",
|
|
1842
|
+
{
|
|
1843
|
+
className: `px-2 py-1 rounded text-sm font-medium ${value ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-600"}`,
|
|
1844
|
+
children: value ? "\u662F" : "\u5426"
|
|
1845
|
+
}
|
|
1846
|
+
);
|
|
1307
1847
|
}
|
|
1308
1848
|
if (typeof value === "number") {
|
|
1309
1849
|
return /* @__PURE__ */ jsxRuntime.jsx("span", { children: value.toLocaleString() });
|
|
@@ -1329,8 +1869,20 @@ function renderField(field, value, item) {
|
|
|
1329
1869
|
{
|
|
1330
1870
|
className: "\r\n px-4 py-4 sm:px-6 sm:py-5\r\n flex flex-col\r\n hover:bg-gray-50/50 transition-colors duration-150\r\n group\r\n gap-1.5\r\n ",
|
|
1331
1871
|
children: [
|
|
1332
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1333
|
-
|
|
1872
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1873
|
+
"dt",
|
|
1874
|
+
{
|
|
1875
|
+
className: "\r\n text-xs sm:text-sm font-semibold text-gray-600 sm:text-gray-700\r\n flex items-start\r\n leading-tight sm:leading-5\r\n tracking-wide\r\n ",
|
|
1876
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 uppercase sm:normal-case", children: field.label })
|
|
1877
|
+
}
|
|
1878
|
+
),
|
|
1879
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1880
|
+
"dd",
|
|
1881
|
+
{
|
|
1882
|
+
className: "\r\n text-sm sm:text-base text-gray-900\r\n break-words\r\n leading-relaxed\r\n min-w-0\r\n ",
|
|
1883
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0", children: content })
|
|
1884
|
+
}
|
|
1885
|
+
)
|
|
1334
1886
|
]
|
|
1335
1887
|
},
|
|
1336
1888
|
field.key
|
|
@@ -1364,7 +1916,6 @@ function DetailPage(props) {
|
|
|
1364
1916
|
}
|
|
1365
1917
|
var DefaultDetailFeature = class extends BaseFeature {
|
|
1366
1918
|
getItem;
|
|
1367
|
-
deleteItem;
|
|
1368
1919
|
titleGetter;
|
|
1369
1920
|
descriptionGetter;
|
|
1370
1921
|
detailFieldNames;
|
|
@@ -1381,7 +1932,6 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
1381
1932
|
this.schema = options.schema;
|
|
1382
1933
|
this.fields = parseSchemaToFields(options.schema);
|
|
1383
1934
|
this.getItem = options.getItem;
|
|
1384
|
-
this.deleteItem = options.deleteItem;
|
|
1385
1935
|
this.titleGetter = options.getTitle;
|
|
1386
1936
|
this.descriptionGetter = options.getDescription;
|
|
1387
1937
|
this.detailFieldNames = options.detailFieldNames;
|
|
@@ -1423,32 +1973,40 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
1423
1973
|
}
|
|
1424
1974
|
const schema = this.schema;
|
|
1425
1975
|
const groupSchemas = this.groups.map((group) => {
|
|
1426
|
-
const pickObject = group.fields.reduce(
|
|
1427
|
-
acc
|
|
1428
|
-
|
|
1429
|
-
|
|
1976
|
+
const pickObject = group.fields.reduce(
|
|
1977
|
+
(acc, fieldName) => {
|
|
1978
|
+
acc[fieldName] = true;
|
|
1979
|
+
return acc;
|
|
1980
|
+
},
|
|
1981
|
+
{}
|
|
1982
|
+
);
|
|
1430
1983
|
return {
|
|
1431
1984
|
label: group.label,
|
|
1432
1985
|
schema: schema.pick(pickObject),
|
|
1433
1986
|
fields: group.fields
|
|
1434
1987
|
};
|
|
1435
1988
|
});
|
|
1436
|
-
const groupFields = groupSchemas.map(
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1989
|
+
const groupFields = groupSchemas.map(
|
|
1990
|
+
({ label, schema: schema2, fields: fieldNames }) => {
|
|
1991
|
+
const groupFields2 = parseSchemaToFields(schema2);
|
|
1992
|
+
const detailFields2 = groupFields2.map((field) => ({
|
|
1993
|
+
key: field.name,
|
|
1994
|
+
label: field.label,
|
|
1995
|
+
render: this.fieldRenderers?.[field.name]
|
|
1996
|
+
}));
|
|
1997
|
+
return {
|
|
1998
|
+
label,
|
|
1999
|
+
fields: detailFields2,
|
|
2000
|
+
values: fieldNames.reduce(
|
|
2001
|
+
(acc, fieldName) => {
|
|
2002
|
+
acc[fieldName] = item[fieldName];
|
|
2003
|
+
return acc;
|
|
2004
|
+
},
|
|
2005
|
+
{}
|
|
2006
|
+
)
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
);
|
|
1452
2010
|
return /* @__PURE__ */ jsxRuntime.jsx(DetailPage, { item, groups: groupFields });
|
|
1453
2011
|
}
|
|
1454
2012
|
const detailFields = this.detailFieldNames ? filterFieldsByNames(this.fields || [], this.detailFieldNames) : this.fields || [];
|
|
@@ -1479,7 +2037,7 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
1479
2037
|
const id = context.params.id;
|
|
1480
2038
|
const item = await this.getItem(id);
|
|
1481
2039
|
if (!item) {
|
|
1482
|
-
return
|
|
2040
|
+
return null;
|
|
1483
2041
|
}
|
|
1484
2042
|
const model = context.model;
|
|
1485
2043
|
const prefix = context.prefix || "";
|
|
@@ -1516,7 +2074,8 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
1516
2074
|
});
|
|
1517
2075
|
}
|
|
1518
2076
|
}
|
|
1519
|
-
|
|
2077
|
+
const { renderActionButtons: renderActionButtons2 } = await Promise.resolve().then(() => (init_action_button_renderer(), action_button_renderer_exports));
|
|
2078
|
+
return renderActionButtons2(actions);
|
|
1520
2079
|
}
|
|
1521
2080
|
};
|
|
1522
2081
|
|
|
@@ -1722,85 +2281,24 @@ function FilterForm(props) {
|
|
|
1722
2281
|
}
|
|
1723
2282
|
) });
|
|
1724
2283
|
}
|
|
1725
|
-
|
|
2284
|
+
|
|
2285
|
+
// src/components/table.tsx
|
|
2286
|
+
init_button();
|
|
2287
|
+
function EmptyState(props) {
|
|
2288
|
+
const { message = "\u6682\u65E0\u6570\u636E", children } = props;
|
|
2289
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-12", children: children || /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-500 text-sm", children: message }) });
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
// src/components/pagination.tsx
|
|
2293
|
+
init_button();
|
|
2294
|
+
function Pagination(props) {
|
|
1726
2295
|
const {
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
hxPost,
|
|
1734
|
-
hxPut,
|
|
1735
|
-
hxDelete,
|
|
1736
|
-
hxTarget,
|
|
1737
|
-
hxSwap,
|
|
1738
|
-
hxPushUrl,
|
|
1739
|
-
hxIndicator,
|
|
1740
|
-
hxConfirm,
|
|
1741
|
-
hxHeaders,
|
|
1742
|
-
...rest
|
|
1743
|
-
} = props;
|
|
1744
|
-
const baseClasses = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2";
|
|
1745
|
-
const variantClasses = {
|
|
1746
|
-
primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
|
|
1747
|
-
secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500",
|
|
1748
|
-
danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
|
|
1749
|
-
ghost: "bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-500"
|
|
1750
|
-
};
|
|
1751
|
-
const sizeClasses = {
|
|
1752
|
-
sm: "px-3 py-1.5 text-sm",
|
|
1753
|
-
md: "px-4 py-2 text-sm",
|
|
1754
|
-
lg: "px-6 py-3 text-base"
|
|
1755
|
-
};
|
|
1756
|
-
const classes = [
|
|
1757
|
-
baseClasses,
|
|
1758
|
-
variantClasses[variant],
|
|
1759
|
-
sizeClasses[size],
|
|
1760
|
-
disabled ? "opacity-50 cursor-not-allowed" : "",
|
|
1761
|
-
className
|
|
1762
|
-
].filter(Boolean).join(" ");
|
|
1763
|
-
const isNewWindow = rest.target === "_blank";
|
|
1764
|
-
const htmxAttrs = {};
|
|
1765
|
-
if (!isNewWindow) {
|
|
1766
|
-
if (hxGet) htmxAttrs["hx-get"] = hxGet;
|
|
1767
|
-
if (hxPost) htmxAttrs["hx-post"] = hxPost;
|
|
1768
|
-
if (hxPut) htmxAttrs["hx-put"] = hxPut;
|
|
1769
|
-
if (hxDelete) htmxAttrs["hx-delete"] = hxDelete;
|
|
1770
|
-
if (hxTarget) htmxAttrs["hx-target"] = hxTarget;
|
|
1771
|
-
if (hxSwap) htmxAttrs["hx-swap"] = hxSwap;
|
|
1772
|
-
if (hxPushUrl !== void 0)
|
|
1773
|
-
htmxAttrs["hx-push-url"] = hxPushUrl === true ? "true" : hxPushUrl;
|
|
1774
|
-
if (hxIndicator) htmxAttrs["hx-indicator"] = hxIndicator;
|
|
1775
|
-
if (hxConfirm) htmxAttrs["hx-confirm"] = hxConfirm;
|
|
1776
|
-
if (hxHeaders) htmxAttrs["hx-headers"] = hxHeaders;
|
|
1777
|
-
}
|
|
1778
|
-
const href = rest.href ?? hxGet ?? "#";
|
|
1779
|
-
const { className: _, ...otherRest } = rest;
|
|
1780
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1781
|
-
"a",
|
|
1782
|
-
{
|
|
1783
|
-
className: classes,
|
|
1784
|
-
disabled,
|
|
1785
|
-
href,
|
|
1786
|
-
...htmxAttrs,
|
|
1787
|
-
...otherRest,
|
|
1788
|
-
children
|
|
1789
|
-
}
|
|
1790
|
-
);
|
|
1791
|
-
}
|
|
1792
|
-
function EmptyState(props) {
|
|
1793
|
-
const { message = "\u6682\u65E0\u6570\u636E", children } = props;
|
|
1794
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-12", children: children || /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-500 text-sm", children: message }) });
|
|
1795
|
-
}
|
|
1796
|
-
function Pagination(props) {
|
|
1797
|
-
const {
|
|
1798
|
-
page,
|
|
1799
|
-
pageSize,
|
|
1800
|
-
total,
|
|
1801
|
-
totalPages,
|
|
1802
|
-
baseUrl,
|
|
1803
|
-
currentParams = {}
|
|
2296
|
+
page,
|
|
2297
|
+
pageSize,
|
|
2298
|
+
total,
|
|
2299
|
+
totalPages,
|
|
2300
|
+
baseUrl,
|
|
2301
|
+
currentParams = {}
|
|
1804
2302
|
} = props;
|
|
1805
2303
|
if (totalPages <= 1) {
|
|
1806
2304
|
return null;
|
|
@@ -1835,8 +2333,8 @@ function Pagination(props) {
|
|
|
1835
2333
|
{
|
|
1836
2334
|
variant: "secondary",
|
|
1837
2335
|
size: "sm",
|
|
1838
|
-
hxGet: buildUrl(page - 1),
|
|
1839
2336
|
href: buildUrl(page - 1),
|
|
2337
|
+
"hx-get": buildUrl(page - 1),
|
|
1840
2338
|
"data-testid": "pagination-prev",
|
|
1841
2339
|
"aria-label": "\u4E0A\u4E00\u9875",
|
|
1842
2340
|
children: "\u4E0A\u4E00\u9875"
|
|
@@ -1847,8 +2345,8 @@ function Pagination(props) {
|
|
|
1847
2345
|
{
|
|
1848
2346
|
variant: "secondary",
|
|
1849
2347
|
size: "sm",
|
|
1850
|
-
hxGet: buildUrl(page + 1),
|
|
1851
2348
|
href: buildUrl(page + 1),
|
|
2349
|
+
"hx-get": buildUrl(page + 1),
|
|
1852
2350
|
"data-testid": "pagination-next",
|
|
1853
2351
|
"aria-label": "\u4E0B\u4E00\u9875",
|
|
1854
2352
|
children: "\u4E0B\u4E00\u9875"
|
|
@@ -1906,10 +2404,10 @@ function TableHeader(props) {
|
|
|
1906
2404
|
{
|
|
1907
2405
|
variant: action.variant || "secondary",
|
|
1908
2406
|
href: action.href,
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
2407
|
+
"hx-get": action.hxGet,
|
|
2408
|
+
"hx-post": action.hxPost,
|
|
2409
|
+
"hx-delete": action.hxDelete,
|
|
2410
|
+
"hx-confirm": action.hxConfirm,
|
|
1913
2411
|
"data-testid": testId,
|
|
1914
2412
|
children: action.label
|
|
1915
2413
|
},
|
|
@@ -2306,7 +2804,288 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
2306
2804
|
});
|
|
2307
2805
|
}
|
|
2308
2806
|
}
|
|
2309
|
-
|
|
2807
|
+
const { renderActionButtons: renderActionButtons2 } = await Promise.resolve().then(() => (init_action_button_renderer(), action_button_renderer_exports));
|
|
2808
|
+
return renderActionButtons2(actions);
|
|
2809
|
+
}
|
|
2810
|
+
};
|
|
2811
|
+
|
|
2812
|
+
// src/component-system/store.ts
|
|
2813
|
+
var STATE_EXPIRATION_TIME = 864e5;
|
|
2814
|
+
var StateStore = class _StateStore {
|
|
2815
|
+
static instance;
|
|
2816
|
+
state;
|
|
2817
|
+
constructor() {
|
|
2818
|
+
this.state = /* @__PURE__ */ new Map();
|
|
2819
|
+
setInterval(() => {
|
|
2820
|
+
this.state.forEach((state) => {
|
|
2821
|
+
if (state.lastUpdated < Date.now() - STATE_EXPIRATION_TIME) {
|
|
2822
|
+
this.state.delete(state.instanceId);
|
|
2823
|
+
}
|
|
2824
|
+
});
|
|
2825
|
+
}, 3e4);
|
|
2826
|
+
}
|
|
2827
|
+
static get() {
|
|
2828
|
+
if (!_StateStore.instance) {
|
|
2829
|
+
_StateStore.instance = new _StateStore();
|
|
2830
|
+
}
|
|
2831
|
+
return _StateStore.instance;
|
|
2832
|
+
}
|
|
2833
|
+
/** 获取实例状态 */
|
|
2834
|
+
getState(instanceId) {
|
|
2835
|
+
const state = this.state.get(instanceId);
|
|
2836
|
+
if (state) {
|
|
2837
|
+
return state;
|
|
2838
|
+
}
|
|
2839
|
+
const newState = {
|
|
2840
|
+
instanceId,
|
|
2841
|
+
data: {
|
|
2842
|
+
props: {},
|
|
2843
|
+
state: {}
|
|
2844
|
+
},
|
|
2845
|
+
lastUpdated: Date.now()
|
|
2846
|
+
};
|
|
2847
|
+
this.state.set(instanceId, newState);
|
|
2848
|
+
return newState;
|
|
2849
|
+
}
|
|
2850
|
+
/** 设置实例状态 */
|
|
2851
|
+
setState(instanceId, state) {
|
|
2852
|
+
const instanceState = this.getState(instanceId);
|
|
2853
|
+
instanceState.data = Object.assign(instanceState.data.state, state);
|
|
2854
|
+
instanceState.lastUpdated = Date.now();
|
|
2855
|
+
}
|
|
2856
|
+
};
|
|
2857
|
+
|
|
2858
|
+
// src/component-system/utils.ts
|
|
2859
|
+
var globalIdCounter = 0;
|
|
2860
|
+
function generateUniqueId() {
|
|
2861
|
+
return `htmx-cid-${globalIdCounter++}`;
|
|
2862
|
+
}
|
|
2863
|
+
var HTMX_COMPONENT_PREFIX = "/_htmx_components";
|
|
2864
|
+
|
|
2865
|
+
// src/component-system/context.tsx
|
|
2866
|
+
var RenderContext = class {
|
|
2867
|
+
constructor(prefix, instanceId, componentName) {
|
|
2868
|
+
this.prefix = prefix;
|
|
2869
|
+
this.instanceId = instanceId;
|
|
2870
|
+
this.componentName = componentName;
|
|
2871
|
+
}
|
|
2872
|
+
// 生成唯一 ID(使用全局共享计数器,避免冲突)
|
|
2873
|
+
$id() {
|
|
2874
|
+
return generateUniqueId();
|
|
2875
|
+
}
|
|
2876
|
+
setState(state) {
|
|
2877
|
+
StateStore.get().setState(this.instanceId, state);
|
|
2878
|
+
}
|
|
2879
|
+
get state() {
|
|
2880
|
+
return StateStore.get().getState(this.instanceId).data.state;
|
|
2881
|
+
}
|
|
2882
|
+
get props() {
|
|
2883
|
+
return StateStore.get().getState(this.instanceId).data.props;
|
|
2884
|
+
}
|
|
2885
|
+
// 生成方法 URL(使用当前 instanceId)
|
|
2886
|
+
url(methodName, params) {
|
|
2887
|
+
const baseUrl = `${this.prefix}/${HTMX_COMPONENT_PREFIX}/${this.componentName}/${this.instanceId}/${methodName}`;
|
|
2888
|
+
if (params && Object.keys(params).length > 0) {
|
|
2889
|
+
const queryString = new URLSearchParams(
|
|
2890
|
+
Object.entries(params).reduce(
|
|
2891
|
+
(acc, [key, value]) => {
|
|
2892
|
+
acc[key] = String(value);
|
|
2893
|
+
return acc;
|
|
2894
|
+
},
|
|
2895
|
+
{}
|
|
2896
|
+
)
|
|
2897
|
+
).toString();
|
|
2898
|
+
return `${baseUrl}?${queryString}`;
|
|
2899
|
+
}
|
|
2900
|
+
return baseUrl;
|
|
2901
|
+
}
|
|
2902
|
+
callMethod(methodName, params) {
|
|
2903
|
+
const selectors = Object.entries(params).map(([name, expression]) => `${name}:${expression}`).join(",");
|
|
2904
|
+
return {
|
|
2905
|
+
"hx-post": this.url(methodName),
|
|
2906
|
+
"hx-vals": `js:{_params_:{${selectors}}}`,
|
|
2907
|
+
"hx-params": "_state_,_params_,_this_value_"
|
|
2908
|
+
};
|
|
2909
|
+
}
|
|
2910
|
+
};
|
|
2911
|
+
var ComponentContext = class extends RenderContext {
|
|
2912
|
+
constructor(prefix, ctx, componentName) {
|
|
2913
|
+
const routeParams = ctx.req.param();
|
|
2914
|
+
const instanceId = String(routeParams.instanceId || "");
|
|
2915
|
+
super(prefix, instanceId, componentName);
|
|
2916
|
+
this.ctx = ctx;
|
|
2917
|
+
}
|
|
2918
|
+
// 获取所有参数(统一接口:聚合 query string 和 body)
|
|
2919
|
+
async params() {
|
|
2920
|
+
const params = {};
|
|
2921
|
+
const routeParams = this.ctx.req.param();
|
|
2922
|
+
Object.assign(params, routeParams);
|
|
2923
|
+
const url = new URL(this.ctx.req.url);
|
|
2924
|
+
for (const [key, value] of url.searchParams.entries()) {
|
|
2925
|
+
params[key] = value;
|
|
2926
|
+
}
|
|
2927
|
+
const contentType = this.ctx.req.header("Content-Type") || "";
|
|
2928
|
+
if (contentType.includes("application/json")) {
|
|
2929
|
+
try {
|
|
2930
|
+
const body = await this.ctx.req.json();
|
|
2931
|
+
Object.assign(params, body);
|
|
2932
|
+
} catch (e) {
|
|
2933
|
+
console.warn("[ComponentContext] Failed to parse JSON body:", e);
|
|
2934
|
+
}
|
|
2935
|
+
} else if (this.ctx.req.method === "POST" || this.ctx.req.method === "PUT" || this.ctx.req.method === "PATCH" || this.ctx.req.method === "DELETE") {
|
|
2936
|
+
try {
|
|
2937
|
+
const formData = await this.ctx.req.formData();
|
|
2938
|
+
for (const [key, value] of formData.entries()) {
|
|
2939
|
+
params[key] = value instanceof File ? value : value.toString();
|
|
2940
|
+
}
|
|
2941
|
+
} catch (e) {
|
|
2942
|
+
console.warn("[ComponentContext] Failed to parse form data:", e);
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
return params;
|
|
2946
|
+
}
|
|
2947
|
+
// 获取查询参数
|
|
2948
|
+
query() {
|
|
2949
|
+
const url = new URL(this.ctx.req.url);
|
|
2950
|
+
return Object.fromEntries(url.searchParams.entries());
|
|
2951
|
+
}
|
|
2952
|
+
// 获取请求体
|
|
2953
|
+
async body() {
|
|
2954
|
+
const contentType = this.ctx.req.header("Content-Type") || "";
|
|
2955
|
+
if (contentType.includes("application/json")) {
|
|
2956
|
+
try {
|
|
2957
|
+
return await this.ctx.req.json();
|
|
2958
|
+
} catch (e) {
|
|
2959
|
+
console.warn("[ComponentContext] Failed to parse JSON body:", e);
|
|
2960
|
+
return {};
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
try {
|
|
2964
|
+
const formData = await this.ctx.req.formData();
|
|
2965
|
+
const body = {};
|
|
2966
|
+
for (const [key, value] of formData.entries()) {
|
|
2967
|
+
body[key] = value instanceof File ? value : value.toString();
|
|
2968
|
+
}
|
|
2969
|
+
return body;
|
|
2970
|
+
} catch (e) {
|
|
2971
|
+
console.warn("[ComponentContext] Failed to parse form data:", e);
|
|
2972
|
+
return {};
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
};
|
|
2976
|
+
|
|
2977
|
+
// src/component-system/component.tsx
|
|
2978
|
+
var METHOD_METADATA_KEY = /* @__PURE__ */ Symbol("htmx:method");
|
|
2979
|
+
function Method(config) {
|
|
2980
|
+
return function(target, propertyKey, _descriptor) {
|
|
2981
|
+
if (!target[METHOD_METADATA_KEY]) {
|
|
2982
|
+
target[METHOD_METADATA_KEY] = /* @__PURE__ */ new Map();
|
|
2983
|
+
}
|
|
2984
|
+
target[METHOD_METADATA_KEY].set(propertyKey, {
|
|
2985
|
+
method: config?.method || "get",
|
|
2986
|
+
path: config?.path
|
|
2987
|
+
});
|
|
2988
|
+
};
|
|
2989
|
+
}
|
|
2990
|
+
var HtmxComponent = class {
|
|
2991
|
+
constructor(name) {
|
|
2992
|
+
this.name = name;
|
|
2993
|
+
}
|
|
2994
|
+
prefix;
|
|
2995
|
+
// 组件函数
|
|
2996
|
+
Component = (props) => {
|
|
2997
|
+
const instanceId = generateUniqueId();
|
|
2998
|
+
const state = StateStore.get().getState(instanceId).data;
|
|
2999
|
+
state.props = props;
|
|
3000
|
+
state.state = {};
|
|
3001
|
+
const renderCtx = new RenderContext(
|
|
3002
|
+
this.prefix,
|
|
3003
|
+
instanceId,
|
|
3004
|
+
this.name
|
|
3005
|
+
);
|
|
3006
|
+
return this.render(renderCtx, props);
|
|
3007
|
+
};
|
|
3008
|
+
// 返回 JSX script 元素
|
|
3009
|
+
// 获取所有标记为 @Method() 的方法
|
|
3010
|
+
// 注意:handler 不再绑定 this,因为方法会接收 ComponentContext 作为第一个参数
|
|
3011
|
+
static getMethods(component) {
|
|
3012
|
+
const methods = /* @__PURE__ */ new Map();
|
|
3013
|
+
const metadata = component[METHOD_METADATA_KEY];
|
|
3014
|
+
if (!metadata) return methods;
|
|
3015
|
+
for (const [methodName, config] of metadata.entries()) {
|
|
3016
|
+
const handler = component[methodName];
|
|
3017
|
+
methods.set(methodName, {
|
|
3018
|
+
method: config.method,
|
|
3019
|
+
path: config.path,
|
|
3020
|
+
handler
|
|
3021
|
+
});
|
|
3022
|
+
}
|
|
3023
|
+
return methods;
|
|
3024
|
+
}
|
|
3025
|
+
};
|
|
3026
|
+
var HtmxComponentHandler = class {
|
|
3027
|
+
constructor(hono, prefix, components) {
|
|
3028
|
+
this.hono = hono;
|
|
3029
|
+
this.prefix = prefix;
|
|
3030
|
+
this.components = components;
|
|
3031
|
+
for (const component of this.components) {
|
|
3032
|
+
component.prefix = this.prefix;
|
|
3033
|
+
this.registerHandler(component);
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
registerHandler(component) {
|
|
3037
|
+
const methods = HtmxComponent.getMethods(component);
|
|
3038
|
+
for (const [methodName, methodConfig] of methods) {
|
|
3039
|
+
const routePath = `${this.prefix}/${HTMX_COMPONENT_PREFIX}/${component.name}/:instanceId/${methodName}`;
|
|
3040
|
+
imeanServiceEngine.logger.info(
|
|
3041
|
+
`[HtmxComponent] Registering handler ${methodConfig.method} ${routePath}`
|
|
3042
|
+
);
|
|
3043
|
+
this.hono[methodConfig.method](routePath, async (ctx) => {
|
|
3044
|
+
return this.handleComponentMethod(
|
|
3045
|
+
ctx,
|
|
3046
|
+
component,
|
|
3047
|
+
methodConfig.handler.bind(component)
|
|
3048
|
+
);
|
|
3049
|
+
});
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
async handleComponentMethod(ctx, component, handler) {
|
|
3053
|
+
const componentContext = new ComponentContext(
|
|
3054
|
+
this.prefix,
|
|
3055
|
+
ctx,
|
|
3056
|
+
component.name
|
|
3057
|
+
);
|
|
3058
|
+
const result = await handler(componentContext);
|
|
3059
|
+
if (result instanceof Object && ("target" in result || "swap" in result || "body" in result || "oobs" in result)) {
|
|
3060
|
+
const { target, swap, body, oobs, trigger } = result;
|
|
3061
|
+
const headers = {};
|
|
3062
|
+
let bodyContent = body;
|
|
3063
|
+
if (target) headers["HX-Retarget"] = target;
|
|
3064
|
+
if (swap) headers["HX-Reswap"] = swap;
|
|
3065
|
+
if (trigger) headers["HX-Trigger"] = trigger;
|
|
3066
|
+
if (oobs) {
|
|
3067
|
+
oobs.forEach((oob) => {
|
|
3068
|
+
oob.props["hx-swap-oob"] = "true";
|
|
3069
|
+
});
|
|
3070
|
+
if (!body) {
|
|
3071
|
+
headers["HX-Reswap"] = "delete";
|
|
3072
|
+
}
|
|
3073
|
+
bodyContent = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3074
|
+
body,
|
|
3075
|
+
oobs
|
|
3076
|
+
] });
|
|
3077
|
+
}
|
|
3078
|
+
return ctx.html(bodyContent, 200, headers);
|
|
3079
|
+
}
|
|
3080
|
+
if (result === null) {
|
|
3081
|
+
return ctx.html(/* @__PURE__ */ jsxRuntime.jsx("div", {}), 200, {
|
|
3082
|
+
"HX-Reswap": "none"
|
|
3083
|
+
});
|
|
3084
|
+
}
|
|
3085
|
+
if (result instanceof Response) {
|
|
3086
|
+
return result;
|
|
3087
|
+
}
|
|
3088
|
+
return ctx.html(result, 200);
|
|
2310
3089
|
}
|
|
2311
3090
|
};
|
|
2312
3091
|
|
|
@@ -2606,102 +3385,6 @@ async function createFeatureContext(ctx, page, feature, user, options) {
|
|
|
2606
3385
|
};
|
|
2607
3386
|
return featureContext;
|
|
2608
3387
|
}
|
|
2609
|
-
var CLOSE_DIALOG_SCRIPT = `on click
|
|
2610
|
-
add .dialog-exit to .dialog-backdrop
|
|
2611
|
-
add .dialog-content-exit to .dialog-content
|
|
2612
|
-
wait 200ms
|
|
2613
|
-
set #dialog-container's innerHTML to '' end`;
|
|
2614
|
-
function renderActionButton(action, index) {
|
|
2615
|
-
const {
|
|
2616
|
-
label,
|
|
2617
|
-
href,
|
|
2618
|
-
hxGet,
|
|
2619
|
-
hxPost,
|
|
2620
|
-
hxPut,
|
|
2621
|
-
hxDelete,
|
|
2622
|
-
variant = "primary",
|
|
2623
|
-
confirm,
|
|
2624
|
-
close,
|
|
2625
|
-
submit,
|
|
2626
|
-
formId,
|
|
2627
|
-
onClick,
|
|
2628
|
-
className = ""
|
|
2629
|
-
} = action;
|
|
2630
|
-
if (submit && formId) {
|
|
2631
|
-
const variantStyles = {
|
|
2632
|
-
primary: "bg-blue-600 text-white hover:bg-blue-700",
|
|
2633
|
-
secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
|
|
2634
|
-
danger: "bg-red-600 text-white hover:bg-red-700",
|
|
2635
|
-
ghost: "bg-transparent text-gray-700 hover:bg-gray-100"
|
|
2636
|
-
};
|
|
2637
|
-
const buttonStyle = variantStyles[variant] || variantStyles.primary;
|
|
2638
|
-
const testId2 = label === "\u521B\u5EFA" || label === "\u66F4\u65B0" ? "submit-button" : `action-${label}`;
|
|
2639
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2640
|
-
"button",
|
|
2641
|
-
{
|
|
2642
|
-
type: "submit",
|
|
2643
|
-
form: formId,
|
|
2644
|
-
className: `px-4 py-2 rounded transition-colors font-medium ${buttonStyle} ${className}`,
|
|
2645
|
-
"data-testid": testId2,
|
|
2646
|
-
...confirm && { "data-confirm": confirm },
|
|
2647
|
-
children: label
|
|
2648
|
-
},
|
|
2649
|
-
index
|
|
2650
|
-
);
|
|
2651
|
-
}
|
|
2652
|
-
const finalOnClick = close ? CLOSE_DIALOG_SCRIPT : onClick;
|
|
2653
|
-
if (finalOnClick) {
|
|
2654
|
-
const variantStyles = {
|
|
2655
|
-
primary: "bg-blue-600 text-white hover:bg-blue-700",
|
|
2656
|
-
secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
|
|
2657
|
-
danger: "bg-red-600 text-white hover:bg-red-700",
|
|
2658
|
-
ghost: "bg-transparent text-gray-700 hover:bg-gray-100"
|
|
2659
|
-
};
|
|
2660
|
-
const buttonStyle = variantStyles[variant] || variantStyles.secondary;
|
|
2661
|
-
let testId2 = `action-${label}`;
|
|
2662
|
-
if (label === "\u53D6\u6D88") {
|
|
2663
|
-
testId2 = "cancel-button";
|
|
2664
|
-
} else if (label === "\u5173\u95ED") {
|
|
2665
|
-
testId2 = "close-button";
|
|
2666
|
-
}
|
|
2667
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2668
|
-
"button",
|
|
2669
|
-
{
|
|
2670
|
-
type: "button",
|
|
2671
|
-
_: finalOnClick,
|
|
2672
|
-
className: `px-4 py-2 rounded transition-colors font-medium ${buttonStyle} ${className}`,
|
|
2673
|
-
"data-testid": testId2,
|
|
2674
|
-
...confirm && { "data-confirm": confirm },
|
|
2675
|
-
children: label
|
|
2676
|
-
},
|
|
2677
|
-
index
|
|
2678
|
-
);
|
|
2679
|
-
}
|
|
2680
|
-
let testId = `action-${label}`;
|
|
2681
|
-
if (label === "\u65B0\u5EFA" || label === "\u521B\u5EFA") {
|
|
2682
|
-
testId = "create-button";
|
|
2683
|
-
} else if (label === "\u53D6\u6D88") {
|
|
2684
|
-
testId = "cancel-button";
|
|
2685
|
-
} else if (label === "\u5173\u95ED") {
|
|
2686
|
-
testId = "close-button";
|
|
2687
|
-
}
|
|
2688
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2689
|
-
Button,
|
|
2690
|
-
{
|
|
2691
|
-
variant,
|
|
2692
|
-
href,
|
|
2693
|
-
hxGet,
|
|
2694
|
-
hxPost,
|
|
2695
|
-
hxPut,
|
|
2696
|
-
hxDelete,
|
|
2697
|
-
hxConfirm: confirm,
|
|
2698
|
-
className,
|
|
2699
|
-
"data-testid": testId,
|
|
2700
|
-
children: label
|
|
2701
|
-
},
|
|
2702
|
-
index
|
|
2703
|
-
);
|
|
2704
|
-
}
|
|
2705
3388
|
function Dialog(props) {
|
|
2706
3389
|
const {
|
|
2707
3390
|
title,
|
|
@@ -2777,13 +3460,16 @@ function Dialog(props) {
|
|
|
2777
3460
|
children
|
|
2778
3461
|
}
|
|
2779
3462
|
),
|
|
2780
|
-
actions
|
|
3463
|
+
actions && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 border-t border-gray-200 bg-white flex justify-end gap-2", children: actions })
|
|
2781
3464
|
]
|
|
2782
3465
|
}
|
|
2783
3466
|
)
|
|
2784
3467
|
}
|
|
2785
3468
|
);
|
|
2786
3469
|
}
|
|
3470
|
+
|
|
3471
|
+
// src/components/permission-denied.tsx
|
|
3472
|
+
init_button();
|
|
2787
3473
|
function PermissionDeniedContent(props) {
|
|
2788
3474
|
const {
|
|
2789
3475
|
operationId,
|
|
@@ -2861,6 +3547,178 @@ function PermissionDeniedPage(props) {
|
|
|
2861
3547
|
}
|
|
2862
3548
|
) }) });
|
|
2863
3549
|
}
|
|
3550
|
+
var getResourceUrl = (prefix, name) => {
|
|
3551
|
+
return `${prefix}/_cdn/${name}`;
|
|
3552
|
+
};
|
|
3553
|
+
function globalScripts(prefix) {
|
|
3554
|
+
return html.html`
|
|
3555
|
+
<script src=${getResourceUrl(prefix, "htmx")}></script>
|
|
3556
|
+
<script src=${getResourceUrl(prefix, "htmx-ext-form-json")}></script>
|
|
3557
|
+
<script src=${getResourceUrl(prefix, "hyperscript")}></script>
|
|
3558
|
+
<script src=${getResourceUrl(prefix, "tailwindcss")}></script>
|
|
3559
|
+
<script src=${getResourceUrl(prefix, "alpinejs")} defer></script>
|
|
3560
|
+
<script src=${getResourceUrl(prefix, "sortablejs")}></script>
|
|
3561
|
+
<script src=${getResourceUrl(prefix, "idiomorph")}></script>
|
|
3562
|
+
<script type="module" src=${getResourceUrl(prefix, "datastar")}></script>
|
|
3563
|
+
`;
|
|
3564
|
+
}
|
|
3565
|
+
function globalStyles() {
|
|
3566
|
+
return html.html`<style>
|
|
3567
|
+
@keyframes fadeIn {
|
|
3568
|
+
from {
|
|
3569
|
+
opacity: 0;
|
|
3570
|
+
}
|
|
3571
|
+
to {
|
|
3572
|
+
opacity: 1;
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
@keyframes slideIn {
|
|
3577
|
+
from {
|
|
3578
|
+
opacity: 0;
|
|
3579
|
+
transform: scale(0.95) translateY(-10px);
|
|
3580
|
+
}
|
|
3581
|
+
to {
|
|
3582
|
+
opacity: 1;
|
|
3583
|
+
transform: scale(1) translateY(0);
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
@keyframes slideInRight {
|
|
3588
|
+
from {
|
|
3589
|
+
opacity: 0;
|
|
3590
|
+
transform: translateX(100%);
|
|
3591
|
+
}
|
|
3592
|
+
to {
|
|
3593
|
+
opacity: 1;
|
|
3594
|
+
transform: translateX(0);
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
@keyframes slideOutRight {
|
|
3599
|
+
from {
|
|
3600
|
+
opacity: 1;
|
|
3601
|
+
transform: translateX(0);
|
|
3602
|
+
}
|
|
3603
|
+
to {
|
|
3604
|
+
opacity: 0;
|
|
3605
|
+
transform: translateX(100%);
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
@keyframes fadeOut {
|
|
3610
|
+
from {
|
|
3611
|
+
opacity: 1;
|
|
3612
|
+
}
|
|
3613
|
+
to {
|
|
3614
|
+
opacity: 0;
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
@keyframes scaleOut {
|
|
3619
|
+
from {
|
|
3620
|
+
opacity: 1;
|
|
3621
|
+
transform: scale(1) translateY(0);
|
|
3622
|
+
}
|
|
3623
|
+
to {
|
|
3624
|
+
opacity: 0;
|
|
3625
|
+
transform: scale(0.95) translateY(-10px);
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3629
|
+
/* Dialog 退出动画 */
|
|
3630
|
+
.dialog-exit {
|
|
3631
|
+
animation: fadeOut 0.2s ease-in forwards !important;
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3634
|
+
.dialog-content-exit {
|
|
3635
|
+
animation: scaleOut 0.2s ease-in forwards !important;
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
.error-alert-exit {
|
|
3639
|
+
animation:
|
|
3640
|
+
slideOutRight 0.3s ease-in forwards,
|
|
3641
|
+
fadeOut 0.3s ease-in forwards;
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
/* SortableList 拖拽样式 */
|
|
3645
|
+
/* 使用伪元素创建覆盖层,不影响原始布局 */
|
|
3646
|
+
.sortable-dragging {
|
|
3647
|
+
position: relative !important;
|
|
3648
|
+
opacity: 0.6 !important;
|
|
3649
|
+
transform: scale(0.98) !important;
|
|
3650
|
+
transition: all 0.2s ease-in-out !important;
|
|
3651
|
+
outline: 2px solid rgb(96, 165, 250) !important; /* blue-400 */
|
|
3652
|
+
outline-offset: 2px !important;
|
|
3653
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important;
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
.sortable-dragging::before {
|
|
3657
|
+
content: '' !important;
|
|
3658
|
+
position: absolute !important;
|
|
3659
|
+
top: 0 !important;
|
|
3660
|
+
left: 0 !important;
|
|
3661
|
+
right: 0 !important;
|
|
3662
|
+
bottom: 0 !important;
|
|
3663
|
+
background-color: rgba(59, 130, 246, 0.1) !important; /* blue-500 with opacity */
|
|
3664
|
+
border-radius: 0.375rem !important;
|
|
3665
|
+
z-index: 1 !important;
|
|
3666
|
+
pointer-events: none !important;
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
/* 拖拽悬停样式 */
|
|
3670
|
+
.sortable-drag-over {
|
|
3671
|
+
position: relative !important;
|
|
3672
|
+
transition: all 0.2s ease-in-out !important;
|
|
3673
|
+
outline: 2px solid rgb(59, 130, 246) !important; /* blue-500 */
|
|
3674
|
+
outline-offset: 2px !important;
|
|
3675
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
.sortable-drag-over::before {
|
|
3679
|
+
content: '' !important;
|
|
3680
|
+
position: absolute !important;
|
|
3681
|
+
top: 0 !important;
|
|
3682
|
+
left: 0 !important;
|
|
3683
|
+
right: 0 !important;
|
|
3684
|
+
bottom: 0 !important;
|
|
3685
|
+
background-color: rgba(59, 130, 246, 0.15) !important; /* blue-500 with more opacity */
|
|
3686
|
+
border-radius: 0.375rem !important;
|
|
3687
|
+
z-index: 1 !important;
|
|
3688
|
+
pointer-events: none !important;
|
|
3689
|
+
}
|
|
3690
|
+
</style>`;
|
|
3691
|
+
}
|
|
3692
|
+
function sortableScript() {
|
|
3693
|
+
html.html`<script>
|
|
3694
|
+
if (typeof htmx !== "undefined" && typeof Sortable !== "undefined") {
|
|
3695
|
+
htmx.onLoad(function (content) {
|
|
3696
|
+
var sortables = content.querySelectorAll(".sortable");
|
|
3697
|
+
for (var i = 0; i < sortables.length; i++) {
|
|
3698
|
+
var sortable = sortables[i];
|
|
3699
|
+
// 检查是否已经初始化
|
|
3700
|
+
if (sortable.sortableInstance) {
|
|
3701
|
+
continue;
|
|
3702
|
+
}
|
|
3703
|
+
var sortableInstance = new Sortable(sortable, {
|
|
3704
|
+
animation: 150,
|
|
3705
|
+
ghostClass: "sortable-ghost",
|
|
3706
|
+
filter: ".htmx-indicator",
|
|
3707
|
+
onMove: function (evt) {
|
|
3708
|
+
return evt.related.className.indexOf("htmx-indicator") === -1;
|
|
3709
|
+
},
|
|
3710
|
+
});
|
|
3711
|
+
sortable.sortableInstance = sortableInstance;
|
|
3712
|
+
sortable.addEventListener("htmx:afterSwap", function () {
|
|
3713
|
+
if (sortable.sortableInstance) {
|
|
3714
|
+
sortable.sortableInstance.option("disabled", false);
|
|
3715
|
+
}
|
|
3716
|
+
});
|
|
3717
|
+
}
|
|
3718
|
+
});
|
|
3719
|
+
}
|
|
3720
|
+
</script>`;
|
|
3721
|
+
}
|
|
2864
3722
|
function Breadcrumb(props) {
|
|
2865
3723
|
const { items } = props;
|
|
2866
3724
|
if (items.length === 0) {
|
|
@@ -2988,247 +3846,972 @@ function LoadingBar() {
|
|
|
2988
3846
|
` })
|
|
2989
3847
|
] });
|
|
2990
3848
|
}
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3849
|
+
|
|
3850
|
+
// src/components/index.ts
|
|
3851
|
+
init_button();
|
|
3852
|
+
function SortableList(props) {
|
|
3853
|
+
const {
|
|
3854
|
+
children,
|
|
3855
|
+
className = "",
|
|
3856
|
+
handle,
|
|
3857
|
+
draggingClass = "sortable-dragging",
|
|
3858
|
+
dragOverClass = "sortable-drag-over",
|
|
3859
|
+
onSortableChange,
|
|
3860
|
+
...rest
|
|
3861
|
+
} = props;
|
|
3862
|
+
const xDataContent = `{
|
|
3863
|
+
draggedIndex: null,
|
|
3864
|
+
dragOverIndex: null,
|
|
3865
|
+
handleSelector: ${JSON.stringify(handle || null)},
|
|
3866
|
+
draggingClass: ${JSON.stringify(draggingClass)},
|
|
3867
|
+
dragOverClass: ${JSON.stringify(dragOverClass)},
|
|
3868
|
+
handleDragStart(event) {
|
|
3869
|
+
// \u4E8B\u4EF6\u59D4\u6258\uFF1A\u5904\u7406\u6240\u6709\u5B50\u5143\u7D20\u7684\u62D6\u62FD\u5F00\u59CB\u4E8B\u4EF6
|
|
3870
|
+
var target = event.target;
|
|
3871
|
+
// \u5982\u679C\u70B9\u51FB\u7684\u662F\u62D6\u62FD\u624B\u67C4\uFF0C\u9700\u8981\u627E\u5230\u7236\u5143\u7D20
|
|
3872
|
+
var item = target.closest('[data-sortable-index]');
|
|
3873
|
+
// \u5982\u679C\u6CA1\u627E\u5230\uFF0C\u53EF\u80FD\u662F\u70B9\u51FB\u4E86\u624B\u67C4\uFF0C\u9700\u8981\u5411\u4E0A\u67E5\u627E
|
|
3874
|
+
if (!item && target.hasAttribute('data-sortable-handle')) {
|
|
3875
|
+
item = target.closest('[data-sortable-index]') || target.parentElement;
|
|
3876
|
+
}
|
|
3877
|
+
if (item && item.hasAttribute('data-sortable-index')) {
|
|
3878
|
+
var index = parseInt(item.getAttribute('data-sortable-index'));
|
|
3879
|
+
this.draggedIndex = index;
|
|
3880
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
3881
|
+
event.dataTransfer.setData('text/html', '');
|
|
3882
|
+
// \u6DFB\u52A0\u62D6\u62FD\u6837\u5F0F\u7C7B\uFF08\u652F\u6301\u591A\u4E2A\u7C7B\u540D\uFF09
|
|
3883
|
+
if (this.draggingClass) {
|
|
3884
|
+
var classes = this.draggingClass.split(' ').filter(function(c) { return c.trim(); });
|
|
3885
|
+
classes.forEach(function(cls) {
|
|
3886
|
+
if (cls) {
|
|
3887
|
+
item.classList.add(cls);
|
|
3888
|
+
console.log('Added dragging class:', cls, 'to element:', item);
|
|
3889
|
+
}
|
|
3890
|
+
});
|
|
3018
3891
|
}
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3892
|
+
}
|
|
3893
|
+
},
|
|
3894
|
+
handleDragEnd(event) {
|
|
3895
|
+
// \u4E8B\u4EF6\u59D4\u6258\uFF1A\u5904\u7406\u6240\u6709\u5B50\u5143\u7D20\u7684\u62D6\u62FD\u7ED3\u675F\u4E8B\u4EF6
|
|
3896
|
+
var target = event.target;
|
|
3897
|
+
var item = target.closest('[data-sortable-index]');
|
|
3898
|
+
if (item) {
|
|
3899
|
+
// \u79FB\u9664\u62D6\u62FD\u6837\u5F0F\u7C7B\uFF08\u652F\u6301\u591A\u4E2A\u7C7B\u540D\uFF09
|
|
3900
|
+
if (this.draggingClass) {
|
|
3901
|
+
var draggingClasses = this.draggingClass.split(' ').filter(function(c) { return c.trim(); });
|
|
3902
|
+
draggingClasses.forEach(function(cls) {
|
|
3903
|
+
item.classList.remove(cls);
|
|
3904
|
+
});
|
|
3025
3905
|
}
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
{
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3906
|
+
this.draggedIndex = null;
|
|
3907
|
+
this.dragOverIndex = null;
|
|
3908
|
+
// \u6E05\u9664\u6240\u6709\u62D6\u62FD\u60AC\u505C\u6837\u5F0F
|
|
3909
|
+
if (this.dragOverClass) {
|
|
3910
|
+
var dragOverClasses = this.dragOverClass.split(' ').filter(function(c) { return c.trim(); });
|
|
3911
|
+
Array.from($el.children).forEach(function(c) {
|
|
3912
|
+
dragOverClasses.forEach(function(cls) {
|
|
3913
|
+
c.classList.remove(cls);
|
|
3914
|
+
});
|
|
3915
|
+
});
|
|
3033
3916
|
}
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3917
|
+
}
|
|
3918
|
+
},
|
|
3919
|
+
handleDragOver(event) {
|
|
3920
|
+
// \u4E8B\u4EF6\u59D4\u6258\uFF1A\u5904\u7406\u6240\u6709\u5B50\u5143\u7D20\u7684\u62D6\u62FD\u60AC\u505C\u4E8B\u4EF6
|
|
3921
|
+
var target = event.target;
|
|
3922
|
+
var item = target.closest('[data-sortable-index]');
|
|
3923
|
+
if (item && this.draggedIndex !== null) {
|
|
3924
|
+
var index = parseInt(item.getAttribute('data-sortable-index'));
|
|
3925
|
+
if (this.draggedIndex !== index) {
|
|
3926
|
+
event.preventDefault();
|
|
3927
|
+
event.dataTransfer.dropEffect = 'move';
|
|
3928
|
+
// \u6E05\u9664\u4E4B\u524D\u60AC\u505C\u5143\u7D20\u7684\u6837\u5F0F
|
|
3929
|
+
if (this.dragOverIndex !== null && this.dragOverIndex !== index && this.dragOverClass) {
|
|
3930
|
+
var self = this;
|
|
3931
|
+
var prevItem = Array.from($el.children).find(function(c) {
|
|
3932
|
+
return c.getAttribute('data-sortable-index') === String(self.dragOverIndex);
|
|
3933
|
+
});
|
|
3934
|
+
if (prevItem) {
|
|
3935
|
+
var dragOverClasses = this.dragOverClass.split(' ').filter(function(c) { return c.trim(); });
|
|
3936
|
+
dragOverClasses.forEach(function(cls) {
|
|
3937
|
+
prevItem.classList.remove(cls);
|
|
3938
|
+
});
|
|
3044
3939
|
}
|
|
3045
3940
|
}
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3941
|
+
this.dragOverIndex = index;
|
|
3942
|
+
// \u6DFB\u52A0\u62D6\u62FD\u60AC\u505C\u6837\u5F0F\u7C7B\uFF08\u652F\u6301\u591A\u4E2A\u7C7B\u540D\uFF09
|
|
3943
|
+
if (this.dragOverClass) {
|
|
3944
|
+
var dragOverClasses = this.dragOverClass.split(' ').filter(function(c) { return c.trim(); });
|
|
3945
|
+
dragOverClasses.forEach(function(cls) {
|
|
3946
|
+
item.classList.add(cls);
|
|
3947
|
+
});
|
|
3050
3948
|
}
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
},
|
|
3952
|
+
handleDragLeave(event) {
|
|
3953
|
+
// \u4E8B\u4EF6\u59D4\u6258\uFF1A\u5904\u7406\u6240\u6709\u5B50\u5143\u7D20\u7684\u62D6\u62FD\u79BB\u5F00\u4E8B\u4EF6
|
|
3954
|
+
// \u53EA\u6709\u5F53\u79BB\u5F00\u7684\u5143\u7D20\u662F\u5F53\u524D\u60AC\u505C\u7684\u5143\u7D20\u65F6\u624D\u79FB\u9664\u6837\u5F0F
|
|
3955
|
+
var target = event.target;
|
|
3956
|
+
var item = target.closest('[data-sortable-index]');
|
|
3957
|
+
if (item && this.dragOverIndex !== null) {
|
|
3958
|
+
var index = parseInt(item.getAttribute('data-sortable-index'));
|
|
3959
|
+
if (index === this.dragOverIndex) {
|
|
3960
|
+
// \u68C0\u67E5\u662F\u5426\u771F\u7684\u79BB\u5F00\u4E86\u5143\u7D20\uFF08\u800C\u4E0D\u662F\u8FDB\u5165\u5B50\u5143\u7D20\uFF09
|
|
3961
|
+
var relatedTarget = event.relatedTarget;
|
|
3962
|
+
if (!item.contains(relatedTarget)) {
|
|
3963
|
+
if (this.dragOverClass) {
|
|
3964
|
+
var dragOverClasses = this.dragOverClass.split(' ').filter(function(c) { return c.trim(); });
|
|
3965
|
+
dragOverClasses.forEach(function(cls) {
|
|
3966
|
+
item.classList.remove(cls);
|
|
3967
|
+
});
|
|
3968
|
+
}
|
|
3969
|
+
this.dragOverIndex = null;
|
|
3055
3970
|
}
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
},
|
|
3974
|
+
handleDrop(event) {
|
|
3975
|
+
// \u4E8B\u4EF6\u59D4\u6258\uFF1A\u5904\u7406\u6240\u6709\u5B50\u5143\u7D20\u7684\u653E\u7F6E\u4E8B\u4EF6
|
|
3976
|
+
var target = event.target;
|
|
3977
|
+
var item = target.closest('[data-sortable-index]');
|
|
3978
|
+
if (item && this.draggedIndex !== null) {
|
|
3979
|
+
event.preventDefault();
|
|
3980
|
+
var index = parseInt(item.getAttribute('data-sortable-index'));
|
|
3981
|
+
if (this.draggedIndex !== index) {
|
|
3982
|
+
// \u4E0D\u79FB\u52A8 DOM\uFF0C\u53EA\u53D1\u51FA\u4EA4\u6362\u4E8B\u4EF6\uFF0C\u8BA9\u7236\u7EA7\uFF08Alpine.js\uFF09\u5904\u7406\u6570\u7EC4\u4EA4\u6362
|
|
3983
|
+
// \u89E6\u53D1\u81EA\u5B9A\u4E49\u4E8B\u4EF6\uFF0C\u901A\u77E5\u7236\u7EC4\u4EF6\u9700\u8981\u4EA4\u6362\u4F4D\u7F6E
|
|
3984
|
+
var changeEvent = new CustomEvent('sortable:change', {
|
|
3985
|
+
bubbles: true,
|
|
3986
|
+
cancelable: true,
|
|
3987
|
+
detail: {
|
|
3988
|
+
oldIndex: this.draggedIndex,
|
|
3989
|
+
newIndex: index,
|
|
3990
|
+
}
|
|
3991
|
+
});
|
|
3992
|
+
$el.dispatchEvent(changeEvent);
|
|
3993
|
+
}
|
|
3994
|
+
|
|
3995
|
+
if (this.dragOverClass) {
|
|
3996
|
+
var dragOverClasses = this.dragOverClass.split(' ').filter(function(c) { return c.trim(); });
|
|
3997
|
+
dragOverClasses.forEach(function(cls) {
|
|
3998
|
+
item.classList.remove(cls);
|
|
3999
|
+
});
|
|
4000
|
+
}
|
|
4001
|
+
this.draggedIndex = null;
|
|
4002
|
+
this.dragOverIndex = null;
|
|
4003
|
+
}
|
|
4004
|
+
},
|
|
4005
|
+
init() {
|
|
4006
|
+
// \u4E3A\u6240\u6709\u76F4\u63A5\u5B50\u5143\u7D20\u6DFB\u52A0\u62D6\u62FD\u652F\u6301
|
|
4007
|
+
var container = $el;
|
|
4008
|
+
var self = this;
|
|
4009
|
+
|
|
4010
|
+
var initSortable = function() {
|
|
4011
|
+
// \u53EA\u5904\u7406\u5B9E\u9645\u6E32\u67D3\u7684\u5143\u7D20\uFF0C\u6392\u9664 template \u5143\u7D20
|
|
4012
|
+
// Alpine.js \u7684 x-for \u4F1A\u5728 template \u7684\u4F4D\u7F6E\u63D2\u5165\u5B9E\u9645\u6E32\u67D3\u7684\u5143\u7D20
|
|
4013
|
+
var children = Array.from(container.children).filter(function(c) {
|
|
4014
|
+
return c.tagName !== 'TEMPLATE';
|
|
4015
|
+
});
|
|
4016
|
+
children.forEach(function(child, index) {
|
|
4017
|
+
child.setAttribute('data-sortable-index', index);
|
|
4018
|
+
// \u6DFB\u52A0\u8FC7\u6E21\u52A8\u753B\uFF0C\u8BA9\u62D6\u653E\u6548\u679C\u66F4\u5E73\u6ED1
|
|
4019
|
+
if (!child.style.transition) {
|
|
4020
|
+
child.style.transition = 'all 0.2s ease-in-out';
|
|
3060
4021
|
}
|
|
3061
4022
|
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
4023
|
+
// \u5982\u679C\u6307\u5B9A\u4E86\u62D6\u62FD\u624B\u67C4\uFF0C\u53EA\u6709\u624B\u67C4\u53EF\u62D6\u62FD
|
|
4024
|
+
if (self.handleSelector) {
|
|
4025
|
+
var handleElement = child.querySelector(self.handleSelector);
|
|
4026
|
+
if (handleElement) {
|
|
4027
|
+
child.setAttribute('draggable', 'false');
|
|
4028
|
+
handleElement.setAttribute('draggable', 'true');
|
|
4029
|
+
handleElement.setAttribute('data-sortable-handle', 'true');
|
|
4030
|
+
handleElement.style.cursor = 'move';
|
|
4031
|
+
}
|
|
4032
|
+
} else {
|
|
4033
|
+
// \u6574\u4E2A\u5143\u7D20\u53EF\u62D6\u62FD
|
|
4034
|
+
child.setAttribute('draggable', 'true');
|
|
4035
|
+
child.style.cursor = 'move';
|
|
3065
4036
|
}
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
4037
|
+
});
|
|
4038
|
+
};
|
|
4039
|
+
|
|
4040
|
+
// \u521D\u59CB\u8BBE\u7F6E
|
|
4041
|
+
initSortable();
|
|
4042
|
+
|
|
4043
|
+
// \u4F7F\u7528 MutationObserver \u76D1\u542C\u5B50\u5143\u7D20\u53D8\u5316
|
|
4044
|
+
var observer = new MutationObserver(function() {
|
|
4045
|
+
initSortable();
|
|
4046
|
+
});
|
|
4047
|
+
|
|
4048
|
+
observer.observe(container, {
|
|
4049
|
+
childList: true,
|
|
4050
|
+
subtree: false,
|
|
4051
|
+
});
|
|
4052
|
+
}
|
|
4053
|
+
}`;
|
|
4054
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4055
|
+
"div",
|
|
4056
|
+
{
|
|
4057
|
+
className,
|
|
4058
|
+
...{
|
|
4059
|
+
"x-data": xDataContent
|
|
4060
|
+
},
|
|
4061
|
+
...{
|
|
4062
|
+
"x-init": "init()",
|
|
4063
|
+
"x-on:dragstart": "handleDragStart($event)",
|
|
4064
|
+
"x-on:dragend": "handleDragEnd($event)",
|
|
4065
|
+
"x-on:dragover": "handleDragOver($event)",
|
|
4066
|
+
"x-on:dragleave": "handleDragLeave($event)",
|
|
4067
|
+
"x-on:drop": "handleDrop($event)"
|
|
4068
|
+
},
|
|
4069
|
+
...rest,
|
|
4070
|
+
children
|
|
4071
|
+
}
|
|
4072
|
+
);
|
|
4073
|
+
}
|
|
4074
|
+
function StringArrayEditor(props) {
|
|
4075
|
+
const {
|
|
4076
|
+
value,
|
|
4077
|
+
fieldName,
|
|
4078
|
+
placeholder = "\u8BF7\u8F93\u5165\u5185\u5BB9",
|
|
4079
|
+
allowEmpty = false,
|
|
4080
|
+
rows = 1
|
|
4081
|
+
} = props;
|
|
4082
|
+
const initialItems = value || [];
|
|
4083
|
+
const initialDataJson = JSON.stringify({
|
|
4084
|
+
items: initialItems.map((item) => item || ""),
|
|
4085
|
+
fieldName,
|
|
4086
|
+
placeholder,
|
|
4087
|
+
allowEmpty,
|
|
4088
|
+
rows
|
|
4089
|
+
});
|
|
4090
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4091
|
+
"div",
|
|
4092
|
+
{
|
|
4093
|
+
className: "space-y-3",
|
|
4094
|
+
"x-data": initialDataJson,
|
|
4095
|
+
children: [
|
|
4096
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-end", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4097
|
+
"button",
|
|
4098
|
+
{
|
|
4099
|
+
type: "button",
|
|
4100
|
+
...{
|
|
4101
|
+
"x-on:click": `
|
|
4102
|
+
items.push('');
|
|
4103
|
+
`
|
|
4104
|
+
},
|
|
4105
|
+
className: "px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium flex items-center gap-2",
|
|
4106
|
+
"data-testid": `${fieldName}-add-button`,
|
|
4107
|
+
children: [
|
|
4108
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4109
|
+
"svg",
|
|
4110
|
+
{
|
|
4111
|
+
className: "w-4 h-4",
|
|
4112
|
+
fill: "none",
|
|
4113
|
+
stroke: "currentColor",
|
|
4114
|
+
viewBox: "0 0 24 24",
|
|
4115
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4116
|
+
"path",
|
|
4117
|
+
{
|
|
4118
|
+
strokeLinecap: "round",
|
|
4119
|
+
strokeLinejoin: "round",
|
|
4120
|
+
strokeWidth: "2",
|
|
4121
|
+
d: "M12 4v16m8-8H4"
|
|
4122
|
+
}
|
|
4123
|
+
)
|
|
4124
|
+
}
|
|
4125
|
+
),
|
|
4126
|
+
"\u6DFB\u52A0\u9879"
|
|
4127
|
+
]
|
|
3070
4128
|
}
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
4129
|
+
) }),
|
|
4130
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4131
|
+
"div",
|
|
4132
|
+
{
|
|
4133
|
+
"x-show": "items.length > 0",
|
|
4134
|
+
"data-testid": `${fieldName}-list-container`,
|
|
4135
|
+
...{
|
|
4136
|
+
"@sortable:change.stop": `
|
|
4137
|
+
(function() {
|
|
4138
|
+
const { oldIndex, newIndex } = $event.detail;
|
|
4139
|
+
[items[oldIndex], items[newIndex]] = [items[newIndex], items[oldIndex]];
|
|
4140
|
+
})();
|
|
4141
|
+
`
|
|
4142
|
+
},
|
|
4143
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(SortableList, { className: "space-y-2", handle: "[data-drag-handle]", children: /* @__PURE__ */ jsxRuntime.jsx("template", { "x-for": "(item, index) in items", "x-bind:key": "index", children: /* @__PURE__ */ jsxRuntime.jsx(ArrayItem, { fieldName, rows }) }) })
|
|
3075
4144
|
}
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
4145
|
+
),
|
|
4146
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4147
|
+
"div",
|
|
4148
|
+
{
|
|
4149
|
+
className: "empty-state text-center py-8 text-gray-400 text-sm border border-dashed border-gray-300 rounded-lg",
|
|
4150
|
+
"x-show": "items.length === 0",
|
|
4151
|
+
"data-testid": `${fieldName}-empty-state`,
|
|
4152
|
+
children: '\u6682\u65E0\u9879\uFF0C\u70B9\u51FB"\u6DFB\u52A0\u9879"\u6309\u94AE\u6DFB\u52A0'
|
|
3080
4153
|
}
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
4154
|
+
)
|
|
4155
|
+
]
|
|
4156
|
+
}
|
|
4157
|
+
);
|
|
4158
|
+
}
|
|
4159
|
+
function ArrayItem({ fieldName, rows = 1 }) {
|
|
4160
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4161
|
+
"div",
|
|
4162
|
+
{
|
|
4163
|
+
"data-array-item": true,
|
|
4164
|
+
className: "flex items-center gap-2 group",
|
|
4165
|
+
children: [
|
|
4166
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4167
|
+
"div",
|
|
4168
|
+
{
|
|
4169
|
+
className: "flex-shrink-0 cursor-move text-gray-400 hover:text-gray-600 transition-colors p-1",
|
|
4170
|
+
"data-drag-handle": true,
|
|
4171
|
+
"data-testid": `${fieldName}-drag-handle`,
|
|
4172
|
+
title: "\u62D6\u62FD\u6392\u5E8F",
|
|
4173
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4174
|
+
"svg",
|
|
4175
|
+
{
|
|
4176
|
+
className: "w-5 h-5",
|
|
4177
|
+
fill: "none",
|
|
4178
|
+
stroke: "currentColor",
|
|
4179
|
+
viewBox: "0 0 24 24",
|
|
4180
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4181
|
+
"path",
|
|
4182
|
+
{
|
|
4183
|
+
strokeLinecap: "round",
|
|
4184
|
+
strokeLinejoin: "round",
|
|
4185
|
+
strokeWidth: "2",
|
|
4186
|
+
d: "M4 8h16M4 16h16"
|
|
4187
|
+
}
|
|
4188
|
+
)
|
|
4189
|
+
}
|
|
4190
|
+
)
|
|
3084
4191
|
}
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
4192
|
+
),
|
|
4193
|
+
rows === 1 ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
4194
|
+
"input",
|
|
4195
|
+
{
|
|
4196
|
+
type: "text",
|
|
4197
|
+
"x-model": "items[index]",
|
|
4198
|
+
"x-bind:placeholder": "placeholder + ' ' + (index + 1)",
|
|
4199
|
+
className: "flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent",
|
|
4200
|
+
"data-testid": `${fieldName}-input`,
|
|
4201
|
+
"x-bind:required": "!allowEmpty"
|
|
4202
|
+
}
|
|
4203
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
4204
|
+
"textarea",
|
|
4205
|
+
{
|
|
4206
|
+
"x-model": "items[index]",
|
|
4207
|
+
"x-bind:placeholder": "placeholder + ' ' + (index + 1)",
|
|
4208
|
+
rows,
|
|
4209
|
+
className: "flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-y",
|
|
4210
|
+
"data-testid": `${fieldName}-input`,
|
|
4211
|
+
"x-bind:required": "!allowEmpty"
|
|
4212
|
+
}
|
|
4213
|
+
),
|
|
4214
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4215
|
+
"input",
|
|
4216
|
+
{
|
|
4217
|
+
type: "hidden",
|
|
4218
|
+
"x-bind:name": "fieldName + '[' + index + ']'",
|
|
4219
|
+
"x-bind:value": "item"
|
|
3089
4220
|
}
|
|
3090
|
-
|
|
4221
|
+
),
|
|
4222
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4223
|
+
"button",
|
|
4224
|
+
{
|
|
4225
|
+
type: "button",
|
|
4226
|
+
...{
|
|
4227
|
+
"x-on:click": `
|
|
4228
|
+
items.splice(index, 1);
|
|
4229
|
+
`
|
|
4230
|
+
},
|
|
4231
|
+
className: "flex-shrink-0 px-3 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors",
|
|
4232
|
+
"data-testid": `${fieldName}-remove-button`,
|
|
4233
|
+
title: "\u5220\u9664\u6B64\u9879",
|
|
4234
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4235
|
+
"svg",
|
|
4236
|
+
{
|
|
4237
|
+
className: "w-5 h-5",
|
|
4238
|
+
fill: "none",
|
|
4239
|
+
stroke: "currentColor",
|
|
4240
|
+
viewBox: "0 0 24 24",
|
|
4241
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4242
|
+
"path",
|
|
4243
|
+
{
|
|
4244
|
+
strokeLinecap: "round",
|
|
4245
|
+
strokeLinejoin: "round",
|
|
4246
|
+
strokeWidth: "2",
|
|
4247
|
+
d: "M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
4248
|
+
}
|
|
4249
|
+
)
|
|
4250
|
+
}
|
|
4251
|
+
)
|
|
3091
4252
|
}
|
|
4253
|
+
)
|
|
4254
|
+
]
|
|
4255
|
+
}
|
|
4256
|
+
);
|
|
4257
|
+
}
|
|
4258
|
+
function TagsEditor(props) {
|
|
4259
|
+
const { value, fieldName, placeholder = "\u8F93\u5165\u6807\u7B7E\u540E\u6309\u56DE\u8F66\u6DFB\u52A0" } = props;
|
|
4260
|
+
const initialTags = value || [];
|
|
4261
|
+
const initialDataJson = JSON.stringify({
|
|
4262
|
+
tags: initialTags.map((tag) => tag || ""),
|
|
4263
|
+
fieldName,
|
|
4264
|
+
newTag: "",
|
|
4265
|
+
editingIndex: null,
|
|
4266
|
+
editingValue: "",
|
|
4267
|
+
error: ""
|
|
4268
|
+
});
|
|
4269
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", "x-data": initialDataJson, children: [
|
|
4270
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4271
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4272
|
+
"input",
|
|
4273
|
+
{
|
|
4274
|
+
type: "text",
|
|
4275
|
+
"x-model": "newTag",
|
|
4276
|
+
...{
|
|
4277
|
+
"x-on:keydown.enter.prevent": `
|
|
4278
|
+
if (newTag.trim()) {
|
|
4279
|
+
tags.push(newTag.trim());
|
|
4280
|
+
newTag = '';
|
|
4281
|
+
error = '';
|
|
4282
|
+
} else {
|
|
4283
|
+
error = '\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A';
|
|
4284
|
+
}
|
|
4285
|
+
`
|
|
4286
|
+
},
|
|
4287
|
+
placeholder,
|
|
4288
|
+
autocomplete: "off",
|
|
4289
|
+
className: "w-full px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent",
|
|
4290
|
+
"data-testid": `${fieldName}-input`
|
|
3092
4291
|
}
|
|
3093
|
-
)
|
|
3094
|
-
] }),
|
|
3095
|
-
/* @__PURE__ */ jsxRuntime.jsxs("body", { className: "bg-gray-50", "hx-indicator": "#loading-bar", children: [
|
|
3096
|
-
/* @__PURE__ */ jsxRuntime.jsx(LoadingBar, {}),
|
|
3097
|
-
props.children,
|
|
4292
|
+
),
|
|
3098
4293
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3099
4294
|
"div",
|
|
3100
4295
|
{
|
|
3101
|
-
|
|
3102
|
-
|
|
4296
|
+
"x-show": "error && editingIndex === null",
|
|
4297
|
+
"x-text": "error",
|
|
4298
|
+
className: "text-red-600 text-sm p-2",
|
|
4299
|
+
id: `${fieldName}-error`
|
|
3103
4300
|
}
|
|
3104
|
-
)
|
|
3105
|
-
|
|
3106
|
-
|
|
4301
|
+
)
|
|
4302
|
+
] }),
|
|
4303
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4304
|
+
"div",
|
|
4305
|
+
{
|
|
4306
|
+
"x-show": "tags.length > 0",
|
|
4307
|
+
"data-testid": `${fieldName}-tags-container`,
|
|
4308
|
+
...{
|
|
4309
|
+
"@sortable:change.stop": `
|
|
4310
|
+
(function() {
|
|
4311
|
+
const { oldIndex, newIndex } = $event.detail;
|
|
4312
|
+
[tags[oldIndex], tags[newIndex]] = [tags[newIndex], tags[oldIndex]];
|
|
4313
|
+
})();
|
|
4314
|
+
`
|
|
4315
|
+
},
|
|
4316
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4317
|
+
SortableList,
|
|
4318
|
+
{
|
|
4319
|
+
className: "flex flex-wrap gap-2",
|
|
4320
|
+
handle: "[data-drag-handle]",
|
|
4321
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("template", { "x-for": "(tag, index) in tags", "x-bind:key": "index", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "inline-flex", children: [
|
|
4322
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4323
|
+
"input",
|
|
4324
|
+
{
|
|
4325
|
+
type: "hidden",
|
|
4326
|
+
"x-bind:name": `fieldName + '[' + index + ']'`,
|
|
4327
|
+
"x-bind:value": "editingIndex === index ? editingValue : tag"
|
|
4328
|
+
}
|
|
4329
|
+
),
|
|
4330
|
+
/* @__PURE__ */ jsxRuntime.jsx(TagItem, { fieldName }),
|
|
4331
|
+
/* @__PURE__ */ jsxRuntime.jsx(TagItemEdit, { fieldName })
|
|
4332
|
+
] }) })
|
|
4333
|
+
}
|
|
4334
|
+
)
|
|
4335
|
+
}
|
|
4336
|
+
),
|
|
4337
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4338
|
+
"div",
|
|
4339
|
+
{
|
|
4340
|
+
className: "empty-state text-center py-4 text-gray-400 text-sm border border-dashed border-gray-300 rounded-md",
|
|
4341
|
+
"x-show": "tags.length === 0",
|
|
4342
|
+
"data-testid": `${fieldName}-empty-state`,
|
|
4343
|
+
children: "\u6682\u65E0\u6807\u7B7E\uFF0C\u5728\u4E0A\u65B9\u8F93\u5165\u6846\u4E2D\u8F93\u5165\u6807\u7B7E\u540E\u6309\u56DE\u8F66\u6DFB\u52A0"
|
|
4344
|
+
}
|
|
4345
|
+
)
|
|
3107
4346
|
] });
|
|
3108
4347
|
}
|
|
3109
|
-
function
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
4348
|
+
function TagItem({ fieldName }) {
|
|
4349
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4350
|
+
"div",
|
|
4351
|
+
{
|
|
4352
|
+
className: "inline-flex items-center gap-1 px-2 py-1 bg-blue-50 border border-blue-200 rounded-md text-sm group",
|
|
4353
|
+
"x-show": "editingIndex !== index",
|
|
4354
|
+
children: [
|
|
4355
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4356
|
+
"div",
|
|
4357
|
+
{
|
|
4358
|
+
className: "flex-shrink-0 cursor-move text-blue-400 hover:text-blue-600 transition-colors",
|
|
4359
|
+
"data-drag-handle": true,
|
|
4360
|
+
"data-testid": `${fieldName}-drag-handle`,
|
|
4361
|
+
title: "\u62D6\u62FD\u6392\u5E8F",
|
|
4362
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4363
|
+
"svg",
|
|
4364
|
+
{
|
|
4365
|
+
className: "w-3 h-3",
|
|
4366
|
+
fill: "none",
|
|
4367
|
+
stroke: "currentColor",
|
|
4368
|
+
viewBox: "0 0 24 24",
|
|
4369
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4370
|
+
"path",
|
|
4371
|
+
{
|
|
4372
|
+
strokeLinecap: "round",
|
|
4373
|
+
strokeLinejoin: "round",
|
|
4374
|
+
strokeWidth: "2",
|
|
4375
|
+
d: "M4 8h16M4 16h16"
|
|
4376
|
+
}
|
|
4377
|
+
)
|
|
4378
|
+
}
|
|
4379
|
+
)
|
|
4380
|
+
}
|
|
4381
|
+
),
|
|
4382
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4383
|
+
"span",
|
|
4384
|
+
{
|
|
4385
|
+
className: "flex-1 text-blue-900 cursor-pointer",
|
|
4386
|
+
"data-testid": `${fieldName}-tag-text`,
|
|
4387
|
+
...{
|
|
4388
|
+
"x-on:click.stop": `
|
|
4389
|
+
editingIndex = index;
|
|
4390
|
+
editingValue = tag;
|
|
4391
|
+
`
|
|
4392
|
+
},
|
|
4393
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("span", { "x-text": "tag" })
|
|
4394
|
+
}
|
|
4395
|
+
),
|
|
4396
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4397
|
+
"button",
|
|
4398
|
+
{
|
|
4399
|
+
type: "button",
|
|
4400
|
+
...{ "x-on:click.stop": "tags.splice(index, 1)" },
|
|
4401
|
+
className: "flex-shrink-0 text-blue-600 hover:text-red-600 hover:bg-red-50 rounded transition-colors p-0.5 delete-tag-button",
|
|
4402
|
+
"data-testid": `${fieldName}-tag-remove`,
|
|
4403
|
+
title: "\u5220\u9664\u6807\u7B7E",
|
|
4404
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4405
|
+
"svg",
|
|
4406
|
+
{
|
|
4407
|
+
className: "w-3.5 h-3.5",
|
|
4408
|
+
fill: "none",
|
|
4409
|
+
stroke: "currentColor",
|
|
4410
|
+
viewBox: "0 0 24 24",
|
|
4411
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4412
|
+
"path",
|
|
4413
|
+
{
|
|
4414
|
+
strokeLinecap: "round",
|
|
4415
|
+
strokeLinejoin: "round",
|
|
4416
|
+
strokeWidth: "2",
|
|
4417
|
+
d: "M6 18L18 6M6 6l12 12"
|
|
4418
|
+
}
|
|
4419
|
+
)
|
|
4420
|
+
}
|
|
4421
|
+
)
|
|
4422
|
+
}
|
|
4423
|
+
)
|
|
4424
|
+
]
|
|
4425
|
+
}
|
|
3113
4426
|
);
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
4427
|
+
}
|
|
4428
|
+
function TagItemEdit({ fieldName }) {
|
|
4429
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4430
|
+
"div",
|
|
4431
|
+
{
|
|
4432
|
+
className: "inline-flex items-center gap-1 px-2 py-1 bg-blue-50 border border-blue-300 rounded-md text-sm",
|
|
4433
|
+
"x-show": "editingIndex === index",
|
|
4434
|
+
children: [
|
|
4435
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4436
|
+
"div",
|
|
4437
|
+
{
|
|
4438
|
+
className: "flex-shrink-0 cursor-move text-blue-400 hover:text-blue-600 transition-colors",
|
|
4439
|
+
"data-drag-handle": true,
|
|
4440
|
+
"data-testid": `${fieldName}-drag-handle`,
|
|
4441
|
+
title: "\u62D6\u62FD\u6392\u5E8F",
|
|
4442
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4443
|
+
"svg",
|
|
4444
|
+
{
|
|
4445
|
+
className: "w-3 h-3",
|
|
4446
|
+
fill: "none",
|
|
4447
|
+
stroke: "currentColor",
|
|
4448
|
+
viewBox: "0 0 24 24",
|
|
4449
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4450
|
+
"path",
|
|
4451
|
+
{
|
|
4452
|
+
strokeLinecap: "round",
|
|
4453
|
+
strokeLinejoin: "round",
|
|
4454
|
+
strokeWidth: "2",
|
|
4455
|
+
d: "M4 8h16M4 16h16"
|
|
4456
|
+
}
|
|
4457
|
+
)
|
|
4458
|
+
}
|
|
4459
|
+
)
|
|
4460
|
+
}
|
|
4461
|
+
),
|
|
4462
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4463
|
+
"input",
|
|
4464
|
+
{
|
|
4465
|
+
type: "text",
|
|
4466
|
+
"x-model": "editingValue",
|
|
4467
|
+
...{
|
|
4468
|
+
"x-on:keydown.enter.prevent": `
|
|
4469
|
+
if (editingValue.trim()) {
|
|
4470
|
+
tags[index] = editingValue.trim();
|
|
4471
|
+
editingIndex = null;
|
|
4472
|
+
editingValue = '';
|
|
4473
|
+
error = '';
|
|
4474
|
+
} else {
|
|
4475
|
+
error = '\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A';
|
|
4476
|
+
}
|
|
4477
|
+
`,
|
|
4478
|
+
"x-on:blur": `
|
|
4479
|
+
if (editingValue.trim()) {
|
|
4480
|
+
tags[index] = editingValue.trim();
|
|
4481
|
+
editingIndex = null;
|
|
4482
|
+
editingValue = '';
|
|
4483
|
+
error = '';
|
|
4484
|
+
} else {
|
|
4485
|
+
error = '\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A';
|
|
4486
|
+
}
|
|
4487
|
+
`
|
|
4488
|
+
},
|
|
4489
|
+
"x-bind:required": "editingIndex === index",
|
|
4490
|
+
className: "flex-1 px-1 py-0.5 border border-blue-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-500 min-w-[60px]",
|
|
4491
|
+
"data-testid": `${fieldName}-tag-edit-input`
|
|
4492
|
+
}
|
|
4493
|
+
),
|
|
4494
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4495
|
+
"button",
|
|
4496
|
+
{
|
|
4497
|
+
type: "button",
|
|
4498
|
+
...{
|
|
4499
|
+
"x-on:click.stop": `
|
|
4500
|
+
editingIndex = null;
|
|
4501
|
+
editingValue = '';
|
|
4502
|
+
error = '';
|
|
4503
|
+
`
|
|
4504
|
+
},
|
|
4505
|
+
className: "px-1.5 py-0.5 text-xs bg-gray-300 text-gray-700 rounded hover:bg-gray-400 transition-colors",
|
|
4506
|
+
"data-testid": `${fieldName}-tag-cancel`,
|
|
4507
|
+
children: "\u53D6\u6D88"
|
|
4508
|
+
}
|
|
4509
|
+
),
|
|
4510
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4511
|
+
"div",
|
|
4512
|
+
{
|
|
4513
|
+
"x-show": "error && editingIndex === index",
|
|
4514
|
+
"x-text": "error",
|
|
4515
|
+
className: "text-red-600 text-xs mt-1"
|
|
4516
|
+
}
|
|
4517
|
+
)
|
|
4518
|
+
]
|
|
4519
|
+
}
|
|
4520
|
+
);
|
|
4521
|
+
}
|
|
4522
|
+
function ObjectEditor(props) {
|
|
4523
|
+
const { value, fieldName, objectSchema } = props;
|
|
4524
|
+
if (!objectSchema) {
|
|
4525
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 border border-yellow-300 rounded-lg bg-yellow-50 text-yellow-800 text-sm", children: "\u8BF7\u63D0\u4F9B objectSchema \u53C2\u6570\u4EE5\u4F7F\u7528\u5BF9\u8C61\u7F16\u8F91\u5668" });
|
|
4526
|
+
}
|
|
4527
|
+
const fields = parseSchemaToFields(objectSchema);
|
|
4528
|
+
const initialObject = value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
|
|
4529
|
+
fields.forEach((field) => {
|
|
4530
|
+
if (!(field.name in initialObject)) {
|
|
4531
|
+
if (field.type === "number") {
|
|
4532
|
+
initialObject[field.name] = field.required ? 0 : void 0;
|
|
4533
|
+
} else if (field.type === "checkbox") {
|
|
4534
|
+
initialObject[field.name] = field.required ? false : void 0;
|
|
4535
|
+
} else {
|
|
4536
|
+
initialObject[field.name] = field.required ? "" : void 0;
|
|
3128
4537
|
}
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
4538
|
+
}
|
|
4539
|
+
});
|
|
4540
|
+
const initialValueJson = JSON.stringify(initialObject);
|
|
4541
|
+
JSON.stringify(fields.map((f) => f.name));
|
|
4542
|
+
const generateField = (field) => {
|
|
4543
|
+
const fieldId = `${fieldName}-${field.name}`;
|
|
4544
|
+
const fieldValue = initialObject[field.name];
|
|
4545
|
+
const fieldValueStr = fieldValue === void 0 || fieldValue === null ? "" : typeof fieldValue === "object" ? JSON.stringify(fieldValue) : String(fieldValue);
|
|
4546
|
+
const requiredAttr = field.required ? "required" : "";
|
|
4547
|
+
let inputElement;
|
|
4548
|
+
if (field.type === "text") {
|
|
4549
|
+
inputElement = html.html`
|
|
4550
|
+
<input
|
|
4551
|
+
type="text"
|
|
4552
|
+
id="${fieldId}"
|
|
4553
|
+
name="${fieldName}.${field.name}"
|
|
4554
|
+
value="${fieldValueStr}"
|
|
4555
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
4556
|
+
data-testid="${fieldName}-input-${field.name}"
|
|
4557
|
+
oninput="updateObjectField('${fieldName}', '${field.name}', this.value, 'text', ${field.required})"
|
|
4558
|
+
${requiredAttr}
|
|
4559
|
+
/>
|
|
4560
|
+
`;
|
|
4561
|
+
} else if (field.type === "textarea") {
|
|
4562
|
+
inputElement = html.html`
|
|
4563
|
+
<textarea
|
|
4564
|
+
id="${fieldId}"
|
|
4565
|
+
name="${fieldName}.${field.name}"
|
|
4566
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-y"
|
|
4567
|
+
rows="4"
|
|
4568
|
+
data-testid="${fieldName}-input-${field.name}"
|
|
4569
|
+
oninput="updateObjectField('${fieldName}', '${field.name}', this.value, 'text', ${field.required})"
|
|
4570
|
+
${requiredAttr}
|
|
4571
|
+
>${fieldValueStr}</textarea>
|
|
4572
|
+
`;
|
|
4573
|
+
} else if (field.type === "number") {
|
|
4574
|
+
const step = field.step || (field.step === void 0 ? "1" : "any");
|
|
4575
|
+
inputElement = html.html`
|
|
4576
|
+
<input
|
|
4577
|
+
type="number"
|
|
4578
|
+
id="${fieldId}"
|
|
4579
|
+
name="${fieldName}.${field.name}"
|
|
4580
|
+
value="${fieldValueStr}"
|
|
4581
|
+
step="${step}"
|
|
4582
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
4583
|
+
data-testid="${fieldName}-input-${field.name}"
|
|
4584
|
+
oninput="updateObjectField('${fieldName}', '${field.name}', this.value, 'number', ${field.required})"
|
|
4585
|
+
${requiredAttr}
|
|
4586
|
+
/>
|
|
4587
|
+
`;
|
|
4588
|
+
} else if (field.type === "date") {
|
|
4589
|
+
inputElement = html.html`
|
|
4590
|
+
<input
|
|
4591
|
+
type="date"
|
|
4592
|
+
id="${fieldId}"
|
|
4593
|
+
name="${fieldName}.${field.name}"
|
|
4594
|
+
value="${fieldValueStr}"
|
|
4595
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
4596
|
+
data-testid="${fieldName}-input-${field.name}"
|
|
4597
|
+
oninput="updateObjectField('${fieldName}', '${field.name}', this.value, 'date', ${field.required})"
|
|
4598
|
+
${requiredAttr}
|
|
4599
|
+
/>
|
|
4600
|
+
`;
|
|
4601
|
+
} else if (field.type === "email") {
|
|
4602
|
+
inputElement = html.html`
|
|
4603
|
+
<input
|
|
4604
|
+
type="email"
|
|
4605
|
+
id="${fieldId}"
|
|
4606
|
+
name="${fieldName}.${field.name}"
|
|
4607
|
+
value="${fieldValueStr}"
|
|
4608
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
4609
|
+
data-testid="${fieldName}-input-${field.name}"
|
|
4610
|
+
oninput="updateObjectField('${fieldName}', '${field.name}', this.value, 'text', ${field.required})"
|
|
4611
|
+
${requiredAttr}
|
|
4612
|
+
/>
|
|
4613
|
+
`;
|
|
4614
|
+
} else if (field.type === "select" && field.options) {
|
|
4615
|
+
inputElement = html.html`
|
|
4616
|
+
<select
|
|
4617
|
+
id="${fieldId}"
|
|
4618
|
+
name="${fieldName}.${field.name}"
|
|
4619
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
|
|
4620
|
+
data-testid="${fieldName}-select-${field.name}"
|
|
4621
|
+
onchange="updateObjectField('${fieldName}', '${field.name}', this.value, 'text', ${field.required})"
|
|
4622
|
+
${requiredAttr}
|
|
4623
|
+
>
|
|
4624
|
+
${!field.required ? html.html`<option value="">请选择</option>` : ""}
|
|
4625
|
+
${field.options.map(
|
|
4626
|
+
(option) => html.html`
|
|
4627
|
+
<option
|
|
4628
|
+
value="${String(option.value)}"
|
|
4629
|
+
${fieldValueStr === String(option.value) ? "selected" : ""}
|
|
4630
|
+
>
|
|
4631
|
+
${option.label}
|
|
4632
|
+
</option>
|
|
4633
|
+
`
|
|
4634
|
+
)}
|
|
4635
|
+
</select>
|
|
4636
|
+
`;
|
|
4637
|
+
} else if (field.type === "checkbox") {
|
|
4638
|
+
const checked = fieldValue === true || fieldValue === "true" || fieldValue === 1 || fieldValue === "1";
|
|
4639
|
+
inputElement = html.html`
|
|
4640
|
+
<div class="flex items-center">
|
|
4641
|
+
<input
|
|
4642
|
+
type="checkbox"
|
|
4643
|
+
id="${fieldId}"
|
|
4644
|
+
name="${fieldName}.${field.name}"
|
|
4645
|
+
${checked ? "checked" : ""}
|
|
4646
|
+
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
|
4647
|
+
data-testid="${fieldName}-checkbox-${field.name}"
|
|
4648
|
+
onchange="updateObjectField('${fieldName}', '${field.name}', this.checked, 'checkbox', ${field.required}')"
|
|
4649
|
+
/>
|
|
4650
|
+
<label for="${fieldId}" class="ml-2 text-sm text-gray-700">
|
|
4651
|
+
${field.label}
|
|
4652
|
+
</label>
|
|
4653
|
+
</div>
|
|
4654
|
+
`;
|
|
4655
|
+
}
|
|
4656
|
+
return html.html`
|
|
4657
|
+
<div class="space-y-2" data-testid="${fieldName}-field-${field.name}">
|
|
4658
|
+
${field.type !== "checkbox" ? html.html`
|
|
4659
|
+
<label
|
|
4660
|
+
for="${fieldId}"
|
|
4661
|
+
class="block text-sm font-semibold text-gray-700"
|
|
4662
|
+
data-testid="${fieldName}-label-${field.name}"
|
|
4663
|
+
>
|
|
4664
|
+
${field.label}
|
|
4665
|
+
${field.required ? html.html`<span class="text-red-500 ml-1">*</span>` : ""}
|
|
4666
|
+
</label>
|
|
4667
|
+
` : ""}
|
|
4668
|
+
${inputElement}
|
|
4669
|
+
</div>
|
|
4670
|
+
`;
|
|
4671
|
+
};
|
|
4672
|
+
return html.html`
|
|
4673
|
+
<div
|
|
4674
|
+
id="object-editor-${fieldName}"
|
|
4675
|
+
class="space-y-4"
|
|
4676
|
+
data-initial-value="${initialValueJson}"
|
|
4677
|
+
>
|
|
4678
|
+
<input
|
|
4679
|
+
type="hidden"
|
|
4680
|
+
name="${fieldName}"
|
|
4681
|
+
value="${initialValueJson}"
|
|
4682
|
+
data-testid="hidden-${fieldName}"
|
|
4683
|
+
/>
|
|
4684
|
+
<div class="space-y-4">
|
|
4685
|
+
${fields.map((field) => generateField(field))}
|
|
4686
|
+
</div>
|
|
4687
|
+
</div>
|
|
4688
|
+
<script>
|
|
4689
|
+
(function() {
|
|
4690
|
+
// 更新对象字段
|
|
4691
|
+
function updateObjectField(fieldName, subFieldName, value, fieldType, required) {
|
|
4692
|
+
const container = document.getElementById('object-editor-' + fieldName);
|
|
4693
|
+
if (!container) return;
|
|
4694
|
+
|
|
4695
|
+
const hiddenInput = container.querySelector('input[name="' + fieldName + '"][type="hidden"]');
|
|
4696
|
+
if (!hiddenInput) return;
|
|
4697
|
+
|
|
4698
|
+
try {
|
|
4699
|
+
const obj = JSON.parse(hiddenInput.value || '{}');
|
|
4700
|
+
|
|
4701
|
+
// 类型转换
|
|
4702
|
+
let convertedValue = value;
|
|
4703
|
+
if (fieldType === 'number') {
|
|
4704
|
+
convertedValue = value === '' ? (required ? 0 : undefined) : Number(value);
|
|
4705
|
+
if (isNaN(convertedValue)) convertedValue = required ? 0 : undefined;
|
|
4706
|
+
} else if (fieldType === 'checkbox') {
|
|
4707
|
+
convertedValue = value === 'true' || value === true || value === '1' || value === 1;
|
|
4708
|
+
} else {
|
|
4709
|
+
convertedValue = value || (required ? '' : undefined);
|
|
4710
|
+
}
|
|
4711
|
+
|
|
4712
|
+
// 更新对象
|
|
4713
|
+
if (convertedValue === undefined && !required) {
|
|
4714
|
+
delete obj[subFieldName];
|
|
4715
|
+
} else {
|
|
4716
|
+
obj[subFieldName] = convertedValue;
|
|
4717
|
+
}
|
|
4718
|
+
|
|
4719
|
+
// 更新隐藏字段
|
|
4720
|
+
hiddenInput.value = JSON.stringify(obj);
|
|
4721
|
+
} catch (e) {
|
|
4722
|
+
console.error('Failed to update object field:', e);
|
|
4723
|
+
}
|
|
3145
4724
|
}
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
4725
|
+
|
|
4726
|
+
// 将函数暴露到全局作用域
|
|
4727
|
+
if (typeof window !== 'undefined') {
|
|
4728
|
+
window.updateObjectField = updateObjectField;
|
|
4729
|
+
}
|
|
4730
|
+
})();
|
|
4731
|
+
</script>
|
|
4732
|
+
`;
|
|
3149
4733
|
}
|
|
3150
|
-
function
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
form: formId,
|
|
3180
|
-
className: `px-4 py-2 rounded transition-colors font-medium ${buttonStyle} ${className}`,
|
|
3181
|
-
"data-testid": testId2,
|
|
3182
|
-
...confirm && { "data-confirm": confirm },
|
|
3183
|
-
children: label
|
|
3184
|
-
},
|
|
3185
|
-
index
|
|
3186
|
-
);
|
|
3187
|
-
}
|
|
3188
|
-
if (onClick) {
|
|
3189
|
-
const variantStyles = {
|
|
3190
|
-
primary: "bg-blue-600 text-white hover:bg-blue-700",
|
|
3191
|
-
secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
|
|
3192
|
-
danger: "bg-red-600 text-white hover:bg-red-700",
|
|
3193
|
-
ghost: "bg-transparent text-gray-700 hover:bg-gray-100"
|
|
3194
|
-
};
|
|
3195
|
-
const buttonStyle = variantStyles[variant] || variantStyles.secondary;
|
|
3196
|
-
const testId2 = label === "\u53D6\u6D88" ? "cancel-button" : `action-${label}`;
|
|
3197
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3198
|
-
"button",
|
|
3199
|
-
{
|
|
3200
|
-
type: "button",
|
|
3201
|
-
_: onClick,
|
|
3202
|
-
className: `px-4 py-2 rounded transition-colors font-medium ${buttonStyle} ${className}`,
|
|
3203
|
-
"data-testid": testId2,
|
|
3204
|
-
...confirm && { "data-confirm": confirm },
|
|
3205
|
-
children: label
|
|
3206
|
-
},
|
|
3207
|
-
index
|
|
3208
|
-
);
|
|
3209
|
-
}
|
|
3210
|
-
let testId = `action-${label}`;
|
|
3211
|
-
if (label === "\u65B0\u5EFA" || label === "\u521B\u5EFA") {
|
|
3212
|
-
testId = "create-button";
|
|
3213
|
-
} else if (label === "\u53D6\u6D88") {
|
|
3214
|
-
testId = "cancel-button";
|
|
3215
|
-
}
|
|
3216
|
-
const isNewWindow = target === "_blank";
|
|
3217
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3218
|
-
Button,
|
|
4734
|
+
function BaseLayout(props) {
|
|
4735
|
+
return html.html`
|
|
4736
|
+
<html>
|
|
4737
|
+
<head>
|
|
4738
|
+
<meta charset="UTF-8" />
|
|
4739
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
4740
|
+
<title>${props.title}</title>
|
|
4741
|
+
<meta name="description" content="${props.description || ""}" />
|
|
4742
|
+
${globalScripts(props.prefix)} ${globalStyles()}
|
|
4743
|
+
</head>
|
|
4744
|
+
<body hx-ext="morph" className="bg-gray-50" hx-indicator="#loading-bar">
|
|
4745
|
+
${LoadingBar()} ${props.children}
|
|
4746
|
+
<div
|
|
4747
|
+
id="error-container"
|
|
4748
|
+
className="fixed top-4 right-4 z-[200] w-full max-w-2xl px-4"
|
|
4749
|
+
></div>
|
|
4750
|
+
<div id="dialog-container"></div>
|
|
4751
|
+
${sortableScript()}
|
|
4752
|
+
</body>
|
|
4753
|
+
</html>
|
|
4754
|
+
`;
|
|
4755
|
+
}
|
|
4756
|
+
function renderNavItem(item, currentPath, index) {
|
|
4757
|
+
const isActive = currentPath === item.href || currentPath && currentPath.startsWith(item.href + "/");
|
|
4758
|
+
const hasActiveChild = item.children?.some(
|
|
4759
|
+
(child) => currentPath === child.href || currentPath && currentPath.startsWith(child.href + "/")
|
|
4760
|
+
);
|
|
4761
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4762
|
+
"li",
|
|
3219
4763
|
{
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
4764
|
+
className: "relative group",
|
|
4765
|
+
"data-testid": `nav-item-${index}`,
|
|
4766
|
+
children: [
|
|
4767
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4768
|
+
"a",
|
|
4769
|
+
{
|
|
4770
|
+
href: item.href,
|
|
4771
|
+
"hx-get": item.href,
|
|
4772
|
+
className: `flex items-center px-4 py-2.5 rounded-lg transition-all duration-200 ${isActive || hasActiveChild ? "bg-blue-600 text-white shadow-md font-medium" : "text-gray-300 hover:bg-gray-700 hover:text-white"}`,
|
|
4773
|
+
"data-testid": `nav-link-${item.label}`,
|
|
4774
|
+
"aria-current": isActive ? "page" : void 0,
|
|
4775
|
+
"aria-label": `\u5BFC\u822A\u5230 ${item.label}`,
|
|
4776
|
+
children: [
|
|
4777
|
+
item.icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mr-2.5 text-lg", "aria-hidden": "true", children: item.icon }),
|
|
4778
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "whitespace-nowrap overflow-hidden text-ellipsis", children: item.label })
|
|
4779
|
+
]
|
|
4780
|
+
}
|
|
4781
|
+
),
|
|
4782
|
+
item.children && item.children.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4783
|
+
"ul",
|
|
4784
|
+
{
|
|
4785
|
+
className: "ml-4 mt-1 space-y-1",
|
|
4786
|
+
"data-testid": `nav-submenu-${index}`,
|
|
4787
|
+
children: item.children.map((child, childIndex) => {
|
|
4788
|
+
const isChildActive = currentPath === child.href || currentPath && currentPath.startsWith(child.href + "/");
|
|
4789
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4790
|
+
"li",
|
|
4791
|
+
{
|
|
4792
|
+
"data-testid": `nav-subitem-${index}-${childIndex}`,
|
|
4793
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4794
|
+
"a",
|
|
4795
|
+
{
|
|
4796
|
+
href: child.href,
|
|
4797
|
+
"hx-get": child.href,
|
|
4798
|
+
className: `flex items-center px-4 py-2 rounded-lg text-sm transition-all duration-200 ${isChildActive ? "bg-blue-500 text-white font-medium" : "text-gray-400 hover:bg-gray-700 hover:text-white"}`,
|
|
4799
|
+
"data-testid": `nav-sublink-${child.label}`,
|
|
4800
|
+
"aria-current": isChildActive ? "page" : void 0,
|
|
4801
|
+
"aria-label": `\u5BFC\u822A\u5230 ${child.label}`,
|
|
4802
|
+
children: [
|
|
4803
|
+
child.icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mr-2", "aria-hidden": "true", children: child.icon }),
|
|
4804
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "whitespace-nowrap overflow-hidden text-ellipsis", children: child.label })
|
|
4805
|
+
]
|
|
4806
|
+
}
|
|
4807
|
+
)
|
|
4808
|
+
},
|
|
4809
|
+
childIndex
|
|
4810
|
+
);
|
|
4811
|
+
})
|
|
4812
|
+
}
|
|
4813
|
+
)
|
|
4814
|
+
]
|
|
3232
4815
|
},
|
|
3233
4816
|
index
|
|
3234
4817
|
);
|
|
@@ -3242,16 +4825,31 @@ function AdminLayout(props) {
|
|
|
3242
4825
|
currentPath,
|
|
3243
4826
|
userInfo,
|
|
3244
4827
|
breadcrumbs,
|
|
3245
|
-
actions
|
|
4828
|
+
actions
|
|
3246
4829
|
} = props;
|
|
3247
4830
|
const navItems = options.navigation || [];
|
|
3248
4831
|
const logoutUrl = options.authProvider?.logoutUrl;
|
|
3249
4832
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-screen", id: "main-content", children: [
|
|
3250
4833
|
/* @__PURE__ */ jsxRuntime.jsx("aside", { className: "w-64 bg-gradient-to-b from-gray-900 to-gray-800 text-white shadow-xl", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-6 h-full flex flex-col", children: [
|
|
3251
4834
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-8", children: options.logo ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: options.logo, alt: "Logo", className: "h-10 mb-2" }) : /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-xl font-bold text-white whitespace-nowrap overflow-hidden text-ellipsis", children: options.title || "\u7BA1\u7406\u540E\u53F0" }) }),
|
|
3252
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3253
|
-
|
|
3254
|
-
|
|
4835
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4836
|
+
"nav",
|
|
4837
|
+
{
|
|
4838
|
+
className: "flex-1 overflow-y-auto",
|
|
4839
|
+
"data-testid": "main-navigation",
|
|
4840
|
+
"aria-label": "\u4E3B\u5BFC\u822A",
|
|
4841
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "space-y-1", "data-testid": "nav-list", children: navItems.length > 0 ? navItems.map(
|
|
4842
|
+
(item, index) => renderNavItem(item, currentPath, index)
|
|
4843
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
4844
|
+
"li",
|
|
4845
|
+
{
|
|
4846
|
+
className: "px-4 py-2 text-gray-400 text-sm",
|
|
4847
|
+
"data-testid": "nav-empty",
|
|
4848
|
+
children: "\u6682\u65E0\u5BFC\u822A\u9879"
|
|
4849
|
+
}
|
|
4850
|
+
) })
|
|
4851
|
+
}
|
|
4852
|
+
)
|
|
3255
4853
|
] }) }),
|
|
3256
4854
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col overflow-hidden", children: [
|
|
3257
4855
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -3263,14 +4861,19 @@ function AdminLayout(props) {
|
|
|
3263
4861
|
logoutUrl
|
|
3264
4862
|
}
|
|
3265
4863
|
),
|
|
3266
|
-
(title || description || actions
|
|
4864
|
+
(title || description || actions) && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white border-b border-gray-200 px-6 py-3", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between items-center gap-4", children: [
|
|
3267
4865
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
3268
4866
|
title && /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-semibold text-gray-900 mb-0.5 leading-tight", children: title }),
|
|
3269
4867
|
description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600 text-xs leading-snug", children: description })
|
|
3270
4868
|
] }),
|
|
3271
|
-
actions
|
|
3272
|
-
|
|
3273
|
-
|
|
4869
|
+
actions && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4870
|
+
"div",
|
|
4871
|
+
{
|
|
4872
|
+
className: "flex gap-2 flex-shrink-0",
|
|
4873
|
+
"data-testid": "page-actions",
|
|
4874
|
+
children: actions
|
|
4875
|
+
}
|
|
4876
|
+
)
|
|
3274
4877
|
] }) }),
|
|
3275
4878
|
/* @__PURE__ */ jsxRuntime.jsx("main", { className: "flex-1 overflow-auto bg-gray-50", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6", children }) })
|
|
3276
4879
|
] })
|
|
@@ -3416,14 +5019,13 @@ async function handlePermissionDenied(ctx, result, options) {
|
|
|
3416
5019
|
headers
|
|
3417
5020
|
);
|
|
3418
5021
|
}
|
|
3419
|
-
const cdnProxyPrefix = `${options.prefix}/_cdn`;
|
|
3420
5022
|
return ctx.html(
|
|
3421
5023
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3422
5024
|
BaseLayout,
|
|
3423
5025
|
{
|
|
5026
|
+
prefix: options.prefix,
|
|
3424
5027
|
title: `\u6743\u9650\u4E0D\u8DB3 - ${options.title}`,
|
|
3425
5028
|
description: "\u60A8\u6CA1\u6709\u6743\u9650\u8BBF\u95EE\u6B64\u8D44\u6E90",
|
|
3426
|
-
cdnProxyPrefix,
|
|
3427
5029
|
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3428
5030
|
PermissionDeniedPage,
|
|
3429
5031
|
{
|
|
@@ -3491,7 +5093,7 @@ async function getActions(feature, context) {
|
|
|
3491
5093
|
if (feature?.getActions) {
|
|
3492
5094
|
return await feature.getActions(context);
|
|
3493
5095
|
}
|
|
3494
|
-
return
|
|
5096
|
+
return null;
|
|
3495
5097
|
}
|
|
3496
5098
|
async function renderResult(ctx, context, result, renderOptions) {
|
|
3497
5099
|
const { options, metadata, currentPath, breadcrumbs, user, feature } = renderOptions;
|
|
@@ -3671,6 +5273,7 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3671
5273
|
useAdminLayout,
|
|
3672
5274
|
currentPath,
|
|
3673
5275
|
userInfo: user,
|
|
5276
|
+
componentRegistry: renderOptions.componentRegistry,
|
|
3674
5277
|
breadcrumbs,
|
|
3675
5278
|
actions,
|
|
3676
5279
|
children: result
|
|
@@ -3695,7 +5298,7 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3695
5298
|
}
|
|
3696
5299
|
}
|
|
3697
5300
|
} else {
|
|
3698
|
-
|
|
5301
|
+
`${options.prefix}/_cdn`;
|
|
3699
5302
|
const useAdminLayout = metadata.useAdminLayout !== false;
|
|
3700
5303
|
const actions = await getActions(renderOptions.feature, context);
|
|
3701
5304
|
if (useAdminLayout) {
|
|
@@ -3703,9 +5306,10 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3703
5306
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3704
5307
|
BaseLayout,
|
|
3705
5308
|
{
|
|
5309
|
+
prefix: options.prefix,
|
|
3706
5310
|
title: dynamicMetadata.title,
|
|
3707
5311
|
description: dynamicMetadata.description,
|
|
3708
|
-
|
|
5312
|
+
componentRegistry: renderOptions.componentRegistry,
|
|
3709
5313
|
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3710
5314
|
AdminLayout,
|
|
3711
5315
|
{
|
|
@@ -3717,6 +5321,7 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3717
5321
|
userInfo: user,
|
|
3718
5322
|
breadcrumbs,
|
|
3719
5323
|
actions,
|
|
5324
|
+
componentRegistry: renderOptions.componentRegistry,
|
|
3720
5325
|
children: result
|
|
3721
5326
|
}
|
|
3722
5327
|
)
|
|
@@ -3728,9 +5333,10 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3728
5333
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3729
5334
|
BaseLayout,
|
|
3730
5335
|
{
|
|
5336
|
+
prefix: options.prefix,
|
|
3731
5337
|
title: dynamicMetadata.title,
|
|
3732
5338
|
description: dynamicMetadata.description,
|
|
3733
|
-
|
|
5339
|
+
componentRegistry: renderOptions.componentRegistry,
|
|
3734
5340
|
children: /* @__PURE__ */ jsxRuntime.jsx(NoLayout, { children: result })
|
|
3735
5341
|
}
|
|
3736
5342
|
)
|
|
@@ -3815,17 +5421,6 @@ async function handleRequest(ctx, page, feature, handlerOptions) {
|
|
|
3815
5421
|
});
|
|
3816
5422
|
}
|
|
3817
5423
|
}
|
|
3818
|
-
if (feature.render) {
|
|
3819
|
-
const result = await feature.render(context);
|
|
3820
|
-
return await renderResult(ctx, context, result, {
|
|
3821
|
-
options: handlerOptions.options,
|
|
3822
|
-
metadata,
|
|
3823
|
-
feature,
|
|
3824
|
-
currentPath,
|
|
3825
|
-
breadcrumbs,
|
|
3826
|
-
user
|
|
3827
|
-
});
|
|
3828
|
-
}
|
|
3829
5424
|
const isHtmxRequest = ctx.req.header("HX-Request") === "true";
|
|
3830
5425
|
if (isHtmxRequest) {
|
|
3831
5426
|
return ctx.html(
|
|
@@ -3904,7 +5499,8 @@ function registerPageRoutes(page, options) {
|
|
|
3904
5499
|
);
|
|
3905
5500
|
const handler = async (ctx) => {
|
|
3906
5501
|
return handleRequest(ctx, page, feature, {
|
|
3907
|
-
options: options.options
|
|
5502
|
+
options: options.options,
|
|
5503
|
+
plugin: options.plugin
|
|
3908
5504
|
});
|
|
3909
5505
|
};
|
|
3910
5506
|
options.hono[route.method](fullPath, handler);
|
|
@@ -3917,7 +5513,8 @@ function registerPageRoutes(page, options) {
|
|
|
3917
5513
|
`[HtmxAdminPlugin] Method override detected: POST ${fullPath} -> ${expectedMethod} (feature: ${feature.name})`
|
|
3918
5514
|
);
|
|
3919
5515
|
return handleRequest(ctx, page, feature, {
|
|
3920
|
-
options: options.options
|
|
5516
|
+
options: options.options,
|
|
5517
|
+
plugin: options.plugin
|
|
3921
5518
|
});
|
|
3922
5519
|
}
|
|
3923
5520
|
imeanServiceEngine.logger.warn(
|
|
@@ -3958,6 +5555,7 @@ var HtmxAdminPlugin = class {
|
|
|
3958
5555
|
options;
|
|
3959
5556
|
serviceName = "";
|
|
3960
5557
|
pages = /* @__PURE__ */ new Map();
|
|
5558
|
+
componentHandler;
|
|
3961
5559
|
constructor(options) {
|
|
3962
5560
|
this.options = {
|
|
3963
5561
|
title: options?.title || "\u7BA1\u7406\u540E\u53F0",
|
|
@@ -3965,11 +5563,19 @@ var HtmxAdminPlugin = class {
|
|
|
3965
5563
|
prefix: options?.prefix || "/admin",
|
|
3966
5564
|
homePath: options?.homePath || "",
|
|
3967
5565
|
navigation: options?.navigation ?? [],
|
|
3968
|
-
authProvider: options?.authProvider
|
|
5566
|
+
authProvider: options?.authProvider,
|
|
5567
|
+
pages: options?.pages ?? [],
|
|
5568
|
+
components: options?.components ?? []
|
|
3969
5569
|
};
|
|
5570
|
+
this.initPages();
|
|
5571
|
+
}
|
|
5572
|
+
initPages() {
|
|
5573
|
+
for (const page of this.options.pages) {
|
|
5574
|
+
this.registerPage(page);
|
|
5575
|
+
}
|
|
3970
5576
|
}
|
|
3971
5577
|
/**
|
|
3972
|
-
*
|
|
5578
|
+
* 注册页面
|
|
3973
5579
|
*/
|
|
3974
5580
|
registerPage(page) {
|
|
3975
5581
|
if (this.pages.has(page.modelName)) {
|
|
@@ -3981,15 +5587,6 @@ var HtmxAdminPlugin = class {
|
|
|
3981
5587
|
imeanServiceEngine.logger.info(`[HtmxAdminPlugin] Registered page: ${page.modelName}`);
|
|
3982
5588
|
return this;
|
|
3983
5589
|
}
|
|
3984
|
-
/**
|
|
3985
|
-
* 批量注册页面
|
|
3986
|
-
*/
|
|
3987
|
-
registerPages(...pages) {
|
|
3988
|
-
for (const page of pages) {
|
|
3989
|
-
this.registerPage(page);
|
|
3990
|
-
}
|
|
3991
|
-
return this;
|
|
3992
|
-
}
|
|
3993
5590
|
/**
|
|
3994
5591
|
* 引擎初始化钩子
|
|
3995
5592
|
*/
|
|
@@ -4000,6 +5597,11 @@ var HtmxAdminPlugin = class {
|
|
|
4000
5597
|
imeanServiceEngine.logger.info(
|
|
4001
5598
|
`HtmxAdminPlugin initialized${this.serviceName ? ` (service: ${this.serviceName})` : ""}`
|
|
4002
5599
|
);
|
|
5600
|
+
this.componentHandler = new HtmxComponentHandler(
|
|
5601
|
+
this.hono,
|
|
5602
|
+
this.options.prefix,
|
|
5603
|
+
this.options.components
|
|
5604
|
+
);
|
|
4003
5605
|
initializeCdnCache().catch((error) => {
|
|
4004
5606
|
imeanServiceEngine.logger.error("[HtmxAdminPlugin] CDN \u7F13\u5B58\u521D\u59CB\u5316\u5931\u8D25", error);
|
|
4005
5607
|
});
|
|
@@ -4010,445 +5612,19 @@ var HtmxAdminPlugin = class {
|
|
|
4010
5612
|
onAfterStart(engine) {
|
|
4011
5613
|
const routeOptions = {
|
|
4012
5614
|
options: this.options,
|
|
4013
|
-
hono: this.hono
|
|
5615
|
+
hono: this.hono,
|
|
5616
|
+
plugin: this
|
|
4014
5617
|
};
|
|
4015
5618
|
registerCdnCacheRoutes(routeOptions);
|
|
4016
|
-
for (const [
|
|
5619
|
+
for (const [_modelName, page] of this.pages) {
|
|
4017
5620
|
registerPageRoutes(page, routeOptions);
|
|
4018
5621
|
}
|
|
4019
5622
|
registerHomeRedirect(this.pages, routeOptions);
|
|
4020
5623
|
}
|
|
4021
5624
|
};
|
|
4022
|
-
function ObjectEditor(props) {
|
|
4023
|
-
const { value, fieldName, objectSchema } = props;
|
|
4024
|
-
if (!objectSchema) {
|
|
4025
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 border border-yellow-300 rounded-lg bg-yellow-50 text-yellow-800 text-sm", children: "\u8BF7\u63D0\u4F9B objectSchema \u53C2\u6570\u4EE5\u4F7F\u7528\u5BF9\u8C61\u7F16\u8F91\u5668" });
|
|
4026
|
-
}
|
|
4027
|
-
const fields = parseSchemaToFields(objectSchema);
|
|
4028
|
-
const initialObject = value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
|
|
4029
|
-
fields.forEach((field) => {
|
|
4030
|
-
if (!(field.name in initialObject)) {
|
|
4031
|
-
if (field.type === "number") {
|
|
4032
|
-
initialObject[field.name] = field.required ? 0 : void 0;
|
|
4033
|
-
} else if (field.type === "checkbox") {
|
|
4034
|
-
initialObject[field.name] = field.required ? false : void 0;
|
|
4035
|
-
} else {
|
|
4036
|
-
initialObject[field.name] = field.required ? "" : void 0;
|
|
4037
|
-
}
|
|
4038
|
-
}
|
|
4039
|
-
});
|
|
4040
|
-
const fieldNames = fields.map((f) => f.name);
|
|
4041
|
-
const fieldNamesJson = JSON.stringify(fieldNames);
|
|
4042
|
-
const initialValueJson = JSON.stringify(initialObject);
|
|
4043
|
-
const xDataContent = `{
|
|
4044
|
-
obj: {},
|
|
4045
|
-
init() {
|
|
4046
|
-
const dataAttr = this.$el.getAttribute('data-initial-value');
|
|
4047
|
-
if (dataAttr) {
|
|
4048
|
-
try {
|
|
4049
|
-
this.obj = JSON.parse(dataAttr);
|
|
4050
|
-
} catch (e) {
|
|
4051
|
-
console.error('Failed to parse initial value:', e);
|
|
4052
|
-
this.obj = {};
|
|
4053
|
-
}
|
|
4054
|
-
}
|
|
4055
|
-
const fieldNames = ${fieldNamesJson};
|
|
4056
|
-
fieldNames.forEach(fieldName => {
|
|
4057
|
-
if (!(fieldName in this.obj)) {
|
|
4058
|
-
this.obj[fieldName] = undefined;
|
|
4059
|
-
}
|
|
4060
|
-
});
|
|
4061
|
-
this.updateHiddenField();
|
|
4062
|
-
},
|
|
4063
|
-
updateHiddenField() {
|
|
4064
|
-
const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
|
|
4065
|
-
if (hiddenInput) {
|
|
4066
|
-
hiddenInput.value = JSON.stringify(this.obj);
|
|
4067
|
-
}
|
|
4068
|
-
},
|
|
4069
|
-
updateField(fieldName, value, fieldType, required) {
|
|
4070
|
-
let convertedValue = value;
|
|
4071
|
-
if (fieldType === 'number') {
|
|
4072
|
-
convertedValue = value === '' ? (required ? 0 : undefined) : Number(value);
|
|
4073
|
-
if (isNaN(convertedValue)) convertedValue = required ? 0 : undefined;
|
|
4074
|
-
} else if (fieldType === 'checkbox') {
|
|
4075
|
-
convertedValue = value === 'true' || value === true || value === '1' || value === 1;
|
|
4076
|
-
} else {
|
|
4077
|
-
convertedValue = value || (required ? '' : undefined);
|
|
4078
|
-
}
|
|
4079
|
-
if (convertedValue === undefined && !required) {
|
|
4080
|
-
delete this.obj[fieldName];
|
|
4081
|
-
} else {
|
|
4082
|
-
this.obj[fieldName] = convertedValue;
|
|
4083
|
-
}
|
|
4084
|
-
this.updateHiddenField();
|
|
4085
|
-
}
|
|
4086
|
-
}`;
|
|
4087
|
-
const generateField = (field) => {
|
|
4088
|
-
const fieldId = `${fieldName}-${field.name}`;
|
|
4089
|
-
const fieldNameVar = `obj.${field.name}`;
|
|
4090
|
-
const requiredValue = field.required ? "true" : "false";
|
|
4091
|
-
const fieldNameForJs = JSON.stringify(field.name);
|
|
4092
|
-
let inputElement;
|
|
4093
|
-
if (field.type === "text") {
|
|
4094
|
-
inputElement = html.html`
|
|
4095
|
-
<input
|
|
4096
|
-
type="text"
|
|
4097
|
-
id="${fieldId}"
|
|
4098
|
-
x-bind:value="${fieldNameVar} || ''"
|
|
4099
|
-
x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
|
|
4100
|
-
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"
|
|
4101
|
-
data-testid="${fieldName}-input-${field.name}"
|
|
4102
|
-
${field.required ? "required" : ""}
|
|
4103
|
-
/>
|
|
4104
|
-
`;
|
|
4105
|
-
} else if (field.type === "textarea") {
|
|
4106
|
-
inputElement = html.html`
|
|
4107
|
-
<textarea
|
|
4108
|
-
id="${fieldId}"
|
|
4109
|
-
x-bind:value="${fieldNameVar} || ''"
|
|
4110
|
-
x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
|
|
4111
|
-
rows="4"
|
|
4112
|
-
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"
|
|
4113
|
-
data-testid="${fieldName}-input-${field.name}"
|
|
4114
|
-
${field.required ? "required" : ""}
|
|
4115
|
-
></textarea>
|
|
4116
|
-
`;
|
|
4117
|
-
} else if (field.type === "number") {
|
|
4118
|
-
const step = field.step || (field.step === void 0 ? "1" : "any");
|
|
4119
|
-
inputElement = html.html`
|
|
4120
|
-
<input
|
|
4121
|
-
type="number"
|
|
4122
|
-
id="${fieldId}"
|
|
4123
|
-
x-bind:value="${fieldNameVar} != null ? ${fieldNameVar} : ''"
|
|
4124
|
-
x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'number', ${requiredValue})"
|
|
4125
|
-
step="${step}"
|
|
4126
|
-
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"
|
|
4127
|
-
data-testid="${fieldName}-input-${field.name}"
|
|
4128
|
-
${field.required ? "required" : ""}
|
|
4129
|
-
/>
|
|
4130
|
-
`;
|
|
4131
|
-
} else if (field.type === "date") {
|
|
4132
|
-
inputElement = html.html`
|
|
4133
|
-
<input
|
|
4134
|
-
type="date"
|
|
4135
|
-
id="${fieldId}"
|
|
4136
|
-
x-bind:value="${fieldNameVar} || ''"
|
|
4137
|
-
x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'date', ${requiredValue})"
|
|
4138
|
-
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"
|
|
4139
|
-
data-testid="${fieldName}-input-${field.name}"
|
|
4140
|
-
${field.required ? "required" : ""}
|
|
4141
|
-
/>
|
|
4142
|
-
`;
|
|
4143
|
-
} else if (field.type === "email") {
|
|
4144
|
-
inputElement = html.html`
|
|
4145
|
-
<input
|
|
4146
|
-
type="email"
|
|
4147
|
-
id="${fieldId}"
|
|
4148
|
-
x-bind:value="${fieldNameVar} || ''"
|
|
4149
|
-
x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
|
|
4150
|
-
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"
|
|
4151
|
-
data-testid="${fieldName}-input-${field.name}"
|
|
4152
|
-
${field.required ? "required" : ""}
|
|
4153
|
-
/>
|
|
4154
|
-
`;
|
|
4155
|
-
} else if (field.type === "select" && field.options) {
|
|
4156
|
-
inputElement = html.html`
|
|
4157
|
-
<select
|
|
4158
|
-
id="${fieldId}"
|
|
4159
|
-
x-bind:value="${fieldNameVar} || ''"
|
|
4160
|
-
x-on:change="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
|
|
4161
|
-
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"
|
|
4162
|
-
data-testid="${fieldName}-select-${field.name}"
|
|
4163
|
-
${field.required ? "required" : ""}
|
|
4164
|
-
>
|
|
4165
|
-
${!field.required ? html.html`<option value="">请选择</option>` : ""}
|
|
4166
|
-
${field.options.map(
|
|
4167
|
-
(option) => html.html`
|
|
4168
|
-
<option value="${String(option.value)}">${option.label}</option>
|
|
4169
|
-
`
|
|
4170
|
-
)}
|
|
4171
|
-
</select>
|
|
4172
|
-
`;
|
|
4173
|
-
} else if (field.type === "checkbox") {
|
|
4174
|
-
inputElement = html.html`
|
|
4175
|
-
<div class="flex items-center">
|
|
4176
|
-
<input
|
|
4177
|
-
type="checkbox"
|
|
4178
|
-
id="${fieldId}"
|
|
4179
|
-
x-bind:checked="${fieldNameVar} === true || ${fieldNameVar} === 'true' || ${fieldNameVar} === 1 || ${fieldNameVar} === '1'"
|
|
4180
|
-
x-on:change="updateField(${fieldNameForJs}, $event.target.checked, 'checkbox', ${requiredValue})"
|
|
4181
|
-
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
|
4182
|
-
data-testid="${fieldName}-checkbox-${field.name}"
|
|
4183
|
-
/>
|
|
4184
|
-
<label for="${fieldId}" class="ml-2 text-sm text-gray-700">
|
|
4185
|
-
${field.label}
|
|
4186
|
-
</label>
|
|
4187
|
-
</div>
|
|
4188
|
-
`;
|
|
4189
|
-
}
|
|
4190
|
-
return html.html`
|
|
4191
|
-
<div class="space-y-2" data-testid="${fieldName}-field-${field.name}">
|
|
4192
|
-
${field.type !== "checkbox" ? html.html`
|
|
4193
|
-
<label
|
|
4194
|
-
for="${fieldId}"
|
|
4195
|
-
class="block text-sm font-semibold text-gray-700"
|
|
4196
|
-
data-testid="${fieldName}-label-${field.name}"
|
|
4197
|
-
>
|
|
4198
|
-
${field.label}
|
|
4199
|
-
${field.required ? html.html`<span class="text-red-500 ml-1">*</span>` : ""}
|
|
4200
|
-
</label>
|
|
4201
|
-
` : ""}
|
|
4202
|
-
${inputElement}
|
|
4203
|
-
</div>
|
|
4204
|
-
`;
|
|
4205
|
-
};
|
|
4206
|
-
return html.html`
|
|
4207
|
-
<div
|
|
4208
|
-
x-data="${xDataContent}"
|
|
4209
|
-
data-initial-value="${initialValueJson}"
|
|
4210
|
-
x-init="init()"
|
|
4211
|
-
class="space-y-4"
|
|
4212
|
-
>
|
|
4213
|
-
<input
|
|
4214
|
-
type="hidden"
|
|
4215
|
-
name="${fieldName}"
|
|
4216
|
-
value=""
|
|
4217
|
-
data-testid="hidden-${fieldName}"
|
|
4218
|
-
/>
|
|
4219
|
-
<div class="space-y-4">
|
|
4220
|
-
${fields.map((field) => generateField(field))}
|
|
4221
|
-
</div>
|
|
4222
|
-
</div>
|
|
4223
|
-
`;
|
|
4224
|
-
}
|
|
4225
|
-
function StringArrayEditor(props) {
|
|
4226
|
-
const {
|
|
4227
|
-
value,
|
|
4228
|
-
fieldName,
|
|
4229
|
-
placeholder = "\u8BF7\u8F93\u5165\u5185\u5BB9",
|
|
4230
|
-
allowEmpty = false
|
|
4231
|
-
} = props;
|
|
4232
|
-
const initialItems = value || [];
|
|
4233
|
-
const initialValueJson = JSON.stringify(initialItems);
|
|
4234
|
-
const xDataContent = `{
|
|
4235
|
-
items: ${initialValueJson},
|
|
4236
|
-
draggedIndex: null,
|
|
4237
|
-
draggedOverIndex: null,
|
|
4238
|
-
fieldName: ${JSON.stringify(fieldName)},
|
|
4239
|
-
placeholder: ${JSON.stringify(placeholder)},
|
|
4240
|
-
allowEmpty: ${allowEmpty},
|
|
4241
|
-
init() {
|
|
4242
|
-
const dataAttr = this.$el.getAttribute('data-initial-value');
|
|
4243
|
-
if (dataAttr) {
|
|
4244
|
-
try {
|
|
4245
|
-
const parsed = JSON.parse(dataAttr);
|
|
4246
|
-
if (Array.isArray(parsed)) {
|
|
4247
|
-
this.items = parsed;
|
|
4248
|
-
} else {
|
|
4249
|
-
this.items = [];
|
|
4250
|
-
}
|
|
4251
|
-
} catch (e) {
|
|
4252
|
-
console.error('Failed to parse initial value:', e);
|
|
4253
|
-
this.items = [];
|
|
4254
|
-
}
|
|
4255
|
-
}
|
|
4256
|
-
this.updateHiddenField();
|
|
4257
|
-
},
|
|
4258
|
-
updateHiddenField() {
|
|
4259
|
-
const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
|
|
4260
|
-
if (hiddenInput) {
|
|
4261
|
-
hiddenInput.value = JSON.stringify(this.items);
|
|
4262
|
-
}
|
|
4263
|
-
},
|
|
4264
|
-
addItem() {
|
|
4265
|
-
this.items.push('');
|
|
4266
|
-
this.updateHiddenField();
|
|
4267
|
-
this.$nextTick(() => {
|
|
4268
|
-
const keyInputs = this.$el.querySelectorAll('input[data-testid*="-input-"]');
|
|
4269
|
-
if (keyInputs.length > 0) {
|
|
4270
|
-
const lastInput = keyInputs[keyInputs.length - 1];
|
|
4271
|
-
if (lastInput && lastInput.focus) {
|
|
4272
|
-
lastInput.focus();
|
|
4273
|
-
}
|
|
4274
|
-
}
|
|
4275
|
-
});
|
|
4276
|
-
},
|
|
4277
|
-
removeItem(index) {
|
|
4278
|
-
this.items.splice(index, 1);
|
|
4279
|
-
this.updateHiddenField();
|
|
4280
|
-
},
|
|
4281
|
-
updateItem(index, value) {
|
|
4282
|
-
this.items[index] = value;
|
|
4283
|
-
this.updateHiddenField();
|
|
4284
|
-
},
|
|
4285
|
-
handleDragStart(index, event) {
|
|
4286
|
-
this.draggedIndex = index;
|
|
4287
|
-
event.dataTransfer.effectAllowed = 'move';
|
|
4288
|
-
event.dataTransfer.setData('text/plain', index.toString());
|
|
4289
|
-
const target = event.currentTarget || event.target.closest('[draggable="true"]');
|
|
4290
|
-
if (target) {
|
|
4291
|
-
target.style.opacity = '0.5';
|
|
4292
|
-
}
|
|
4293
|
-
},
|
|
4294
|
-
handleDragEnd(event) {
|
|
4295
|
-
const target = event.currentTarget || event.target.closest('[draggable="true"]');
|
|
4296
|
-
if (target) {
|
|
4297
|
-
target.style.opacity = '';
|
|
4298
|
-
}
|
|
4299
|
-
this.draggedIndex = null;
|
|
4300
|
-
this.draggedOverIndex = null;
|
|
4301
|
-
},
|
|
4302
|
-
handleDragOver(index, event) {
|
|
4303
|
-
event.preventDefault();
|
|
4304
|
-
event.dataTransfer.dropEffect = 'move';
|
|
4305
|
-
this.draggedOverIndex = index;
|
|
4306
|
-
},
|
|
4307
|
-
handleDragLeave() {
|
|
4308
|
-
this.draggedOverIndex = null;
|
|
4309
|
-
},
|
|
4310
|
-
handleDrop(index, event) {
|
|
4311
|
-
event.preventDefault();
|
|
4312
|
-
if (this.draggedIndex !== null && this.draggedIndex !== index) {
|
|
4313
|
-
const draggedItem = this.items[this.draggedIndex];
|
|
4314
|
-
this.items.splice(this.draggedIndex, 1);
|
|
4315
|
-
this.items.splice(index, 0, draggedItem);
|
|
4316
|
-
this.updateHiddenField();
|
|
4317
|
-
}
|
|
4318
|
-
this.draggedIndex = null;
|
|
4319
|
-
this.draggedOverIndex = null;
|
|
4320
|
-
}
|
|
4321
|
-
}`;
|
|
4322
|
-
return html.html`
|
|
4323
|
-
<div
|
|
4324
|
-
x-data="${xDataContent}"
|
|
4325
|
-
data-initial-value="${initialValueJson}"
|
|
4326
|
-
x-init="init()"
|
|
4327
|
-
class="space-y-3"
|
|
4328
|
-
>
|
|
4329
|
-
<input
|
|
4330
|
-
type="hidden"
|
|
4331
|
-
name="${fieldName}"
|
|
4332
|
-
value=""
|
|
4333
|
-
data-testid="hidden-${fieldName}"
|
|
4334
|
-
/>
|
|
4335
|
-
<div class="space-y-3">
|
|
4336
|
-
<!-- 头部:显示数量和添加按钮 -->
|
|
4337
|
-
<div class="flex items-center justify-between">
|
|
4338
|
-
<span class="text-sm text-gray-600">
|
|
4339
|
-
共 <span x-text="items.length">0</span> 项
|
|
4340
|
-
</span>
|
|
4341
|
-
<button
|
|
4342
|
-
type="button"
|
|
4343
|
-
x-on:click="addItem()"
|
|
4344
|
-
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"
|
|
4345
|
-
data-testid="${fieldName}-add-button"
|
|
4346
|
-
>
|
|
4347
|
-
<svg
|
|
4348
|
-
class="w-4 h-4"
|
|
4349
|
-
fill="none"
|
|
4350
|
-
stroke="currentColor"
|
|
4351
|
-
viewBox="0 0 24 24"
|
|
4352
|
-
>
|
|
4353
|
-
<path
|
|
4354
|
-
stroke-linecap="round"
|
|
4355
|
-
stroke-linejoin="round"
|
|
4356
|
-
stroke-width="2"
|
|
4357
|
-
d="M12 4v16m8-8H4"
|
|
4358
|
-
/>
|
|
4359
|
-
</svg>
|
|
4360
|
-
添加项
|
|
4361
|
-
</button>
|
|
4362
|
-
</div>
|
|
4363
|
-
|
|
4364
|
-
<!-- 列表项 -->
|
|
4365
|
-
<div class="space-y-2" x-show="items.length > 0">
|
|
4366
|
-
<template x-for="(item, index) in items" x-bind:key="index">
|
|
4367
|
-
<div
|
|
4368
|
-
class="flex items-center gap-2 group"
|
|
4369
|
-
x-bind:class="{
|
|
4370
|
-
'opacity-50': draggedIndex === index,
|
|
4371
|
-
'border-blue-300 bg-blue-50': draggedOverIndex === index && draggedIndex !== null && draggedIndex !== index
|
|
4372
|
-
}"
|
|
4373
|
-
draggable="true"
|
|
4374
|
-
x-on:dragstart="handleDragStart(index, $event)"
|
|
4375
|
-
x-on:dragend="handleDragEnd($event)"
|
|
4376
|
-
x-on:dragover="handleDragOver(index, $event)"
|
|
4377
|
-
x-on:dragleave="handleDragLeave()"
|
|
4378
|
-
x-on:drop="handleDrop(index, $event)"
|
|
4379
|
-
x-bind:data-testid="fieldName + '-item-' + index"
|
|
4380
|
-
>
|
|
4381
|
-
<!-- 拖拽手柄 -->
|
|
4382
|
-
<div
|
|
4383
|
-
class="flex-shrink-0 cursor-move text-gray-400 hover:text-gray-600 transition-colors p-1"
|
|
4384
|
-
x-bind:data-testid="fieldName + '-drag-handle-' + index"
|
|
4385
|
-
title="拖拽排序"
|
|
4386
|
-
>
|
|
4387
|
-
<svg
|
|
4388
|
-
class="w-5 h-5"
|
|
4389
|
-
fill="none"
|
|
4390
|
-
stroke="currentColor"
|
|
4391
|
-
viewBox="0 0 24 24"
|
|
4392
|
-
>
|
|
4393
|
-
<path
|
|
4394
|
-
stroke-linecap="round"
|
|
4395
|
-
stroke-linejoin="round"
|
|
4396
|
-
stroke-width="2"
|
|
4397
|
-
d="M4 8h16M4 16h16"
|
|
4398
|
-
/>
|
|
4399
|
-
</svg>
|
|
4400
|
-
</div>
|
|
4401
|
-
|
|
4402
|
-
<!-- 输入框 -->
|
|
4403
|
-
<input
|
|
4404
|
-
type="text"
|
|
4405
|
-
x-bind:value="items[index] || ''"
|
|
4406
|
-
x-on:input="updateItem(index, $event.target.value)"
|
|
4407
|
-
x-bind:placeholder="placeholder + ' ' + (index + 1)"
|
|
4408
|
-
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"
|
|
4409
|
-
x-bind:data-testid="fieldName + '-input-' + index"
|
|
4410
|
-
x-bind:required="!allowEmpty"
|
|
4411
|
-
/>
|
|
4412
|
-
|
|
4413
|
-
<!-- 删除按钮 -->
|
|
4414
|
-
<button
|
|
4415
|
-
type="button"
|
|
4416
|
-
x-on:click="removeItem(index)"
|
|
4417
|
-
class="flex-shrink-0 px-3 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
|
4418
|
-
x-bind:data-testid="fieldName + '-remove-button-' + index"
|
|
4419
|
-
title="删除此项"
|
|
4420
|
-
>
|
|
4421
|
-
<svg
|
|
4422
|
-
class="w-5 h-5"
|
|
4423
|
-
fill="none"
|
|
4424
|
-
stroke="currentColor"
|
|
4425
|
-
viewBox="0 0 24 24"
|
|
4426
|
-
>
|
|
4427
|
-
<path
|
|
4428
|
-
stroke-linecap="round"
|
|
4429
|
-
stroke-linejoin="round"
|
|
4430
|
-
stroke-width="2"
|
|
4431
|
-
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"
|
|
4432
|
-
/>
|
|
4433
|
-
</svg>
|
|
4434
|
-
</button>
|
|
4435
|
-
</div>
|
|
4436
|
-
</template>
|
|
4437
|
-
</div>
|
|
4438
|
-
|
|
4439
|
-
<!-- 空状态提示 -->
|
|
4440
|
-
<div
|
|
4441
|
-
x-show="items.length === 0"
|
|
4442
|
-
class="text-center py-8 text-gray-400 text-sm border border-dashed border-gray-300 rounded-lg"
|
|
4443
|
-
>
|
|
4444
|
-
暂无项,点击"添加项"按钮添加
|
|
4445
|
-
</div>
|
|
4446
|
-
</div>
|
|
4447
|
-
</div>
|
|
4448
|
-
`;
|
|
4449
|
-
}
|
|
4450
5625
|
|
|
4451
5626
|
exports.BaseFeature = BaseFeature;
|
|
5627
|
+
exports.ComponentContext = ComponentContext;
|
|
4452
5628
|
exports.CustomFeature = CustomFeature;
|
|
4453
5629
|
exports.DefaultCreateFeature = DefaultCreateFeature;
|
|
4454
5630
|
exports.DefaultDeleteFeature = DefaultDeleteFeature;
|
|
@@ -4458,10 +5634,14 @@ exports.DefaultListFeature = DefaultListFeature;
|
|
|
4458
5634
|
exports.Dialog = Dialog;
|
|
4459
5635
|
exports.ErrorAlert = ErrorAlert;
|
|
4460
5636
|
exports.HtmxAdminPlugin = HtmxAdminPlugin;
|
|
5637
|
+
exports.HtmxComponent = HtmxComponent;
|
|
4461
5638
|
exports.LoadingBar = LoadingBar;
|
|
5639
|
+
exports.Method = Method;
|
|
4462
5640
|
exports.ObjectEditor = ObjectEditor;
|
|
4463
5641
|
exports.PageModel = PageModel;
|
|
5642
|
+
exports.RenderContext = RenderContext;
|
|
4464
5643
|
exports.StringArrayEditor = StringArrayEditor;
|
|
5644
|
+
exports.TagsEditor = TagsEditor;
|
|
4465
5645
|
exports.checkUserPermission = checkUserPermission;
|
|
4466
5646
|
exports.getUserInfo = getUserInfo;
|
|
4467
5647
|
exports.modelNameToPath = modelNameToPath;
|