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