imean-service-engine-htmx-plugin 2.1.1 → 2.3.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 +99 -14
- package/dist/index.d.ts +99 -14
- package/dist/index.js +1147 -267
- package/dist/index.mjs +1146 -268
- package/docs/README.md +39 -0
- package/docs/alpinejs-interactive-components.md +653 -0
- package/docs/custom-form-field-renderers.md +541 -0
- package/docs/hono-html-best-practices.md +509 -0
- package/package.json +13 -14
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ var fs = require('fs');
|
|
|
5
5
|
var path = require('path');
|
|
6
6
|
var jsxRuntime = require('hono/jsx/jsx-runtime');
|
|
7
7
|
var cookie = require('hono/cookie');
|
|
8
|
+
var html = require('hono/html');
|
|
8
9
|
|
|
9
10
|
var __defProp = Object.defineProperty;
|
|
10
11
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -295,144 +296,350 @@ function getFieldValue(field, initialData) {
|
|
|
295
296
|
if (value === null || value === void 0) {
|
|
296
297
|
return "";
|
|
297
298
|
}
|
|
299
|
+
if (typeof value === "object") {
|
|
300
|
+
if (value instanceof Date) {
|
|
301
|
+
return value.toISOString();
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
return JSON.stringify(value);
|
|
305
|
+
} catch (e) {
|
|
306
|
+
return "";
|
|
307
|
+
}
|
|
308
|
+
}
|
|
298
309
|
return String(value);
|
|
299
310
|
}
|
|
300
311
|
return "";
|
|
301
312
|
}
|
|
313
|
+
function isJsonString(value) {
|
|
314
|
+
if (!value || typeof value !== "string") {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
const trimmed = value.trim();
|
|
318
|
+
return trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]");
|
|
319
|
+
}
|
|
320
|
+
function formatJsonString(value) {
|
|
321
|
+
if (!value || typeof value !== "string") {
|
|
322
|
+
return value;
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
const parsed = JSON.parse(value);
|
|
326
|
+
return JSON.stringify(parsed, null, 2);
|
|
327
|
+
} catch (e) {
|
|
328
|
+
return value;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function renderFormField(field, initialData, formFieldRenderers) {
|
|
332
|
+
const value = getFieldValue(field, initialData);
|
|
333
|
+
const customRenderer = formFieldRenderers?.[field.name];
|
|
334
|
+
if (customRenderer) {
|
|
335
|
+
let parsedValue = null;
|
|
336
|
+
if (value && isJsonString(value)) {
|
|
337
|
+
try {
|
|
338
|
+
parsedValue = JSON.parse(value);
|
|
339
|
+
} catch (e) {
|
|
340
|
+
parsedValue = null;
|
|
341
|
+
}
|
|
342
|
+
} else if (value) {
|
|
343
|
+
parsedValue = value;
|
|
344
|
+
}
|
|
345
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", "data-testid": `field-${field.name}`, children: [
|
|
346
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
347
|
+
"label",
|
|
348
|
+
{
|
|
349
|
+
htmlFor: field.name,
|
|
350
|
+
className: "block text-sm font-semibold text-gray-700",
|
|
351
|
+
"data-testid": `label-${field.name}`,
|
|
352
|
+
children: [
|
|
353
|
+
field.label,
|
|
354
|
+
field.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", children: "*" })
|
|
355
|
+
]
|
|
356
|
+
}
|
|
357
|
+
),
|
|
358
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: customRenderer({
|
|
359
|
+
field,
|
|
360
|
+
value: parsedValue,
|
|
361
|
+
initialData,
|
|
362
|
+
fieldName: field.name
|
|
363
|
+
}) }),
|
|
364
|
+
field.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
|
|
365
|
+
] }, field.name);
|
|
366
|
+
}
|
|
367
|
+
const shouldUseTextarea = field.type === "textarea" || value && isJsonString(value) && field.type !== "select";
|
|
368
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", "data-testid": `field-${field.name}`, children: [
|
|
369
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
370
|
+
"label",
|
|
371
|
+
{
|
|
372
|
+
htmlFor: field.name,
|
|
373
|
+
className: "block text-sm font-semibold text-gray-700",
|
|
374
|
+
"data-testid": `label-${field.name}`,
|
|
375
|
+
children: [
|
|
376
|
+
field.label,
|
|
377
|
+
field.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", children: "*" })
|
|
378
|
+
]
|
|
379
|
+
}
|
|
380
|
+
),
|
|
381
|
+
shouldUseTextarea ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
382
|
+
"textarea",
|
|
383
|
+
{
|
|
384
|
+
id: field.name,
|
|
385
|
+
name: field.name,
|
|
386
|
+
required: field.required,
|
|
387
|
+
placeholder: field.placeholder || (isJsonString(value) ? "JSON \u683C\u5F0F\u6570\u636E" : ""),
|
|
388
|
+
rows: isJsonString(value) ? 10 : 4,
|
|
389
|
+
className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 resize-y font-mono text-sm",
|
|
390
|
+
"data-testid": `input-${field.name}`,
|
|
391
|
+
children: isJsonString(value) ? formatJsonString(value) : value
|
|
392
|
+
},
|
|
393
|
+
`${field.name}-${value}`
|
|
394
|
+
) : field.type === "select" ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
395
|
+
"select",
|
|
396
|
+
{
|
|
397
|
+
id: field.name,
|
|
398
|
+
name: field.name,
|
|
399
|
+
required: field.required,
|
|
400
|
+
className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 bg-white",
|
|
401
|
+
"data-testid": `select-${field.name}`,
|
|
402
|
+
children: [
|
|
403
|
+
!field.required && /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", selected: !value || value === "", children: "\u8BF7\u9009\u62E9" }),
|
|
404
|
+
field.options && field.options.length > 0 ? field.options.map((option) => {
|
|
405
|
+
const optionValue = String(option.value);
|
|
406
|
+
const isSelected = value === optionValue;
|
|
407
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
408
|
+
"option",
|
|
409
|
+
{
|
|
410
|
+
value: optionValue,
|
|
411
|
+
selected: isSelected,
|
|
412
|
+
children: option.label
|
|
413
|
+
},
|
|
414
|
+
optionValue
|
|
415
|
+
);
|
|
416
|
+
}) : /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", disabled: true, children: "\u6682\u65E0\u9009\u9879" })
|
|
417
|
+
]
|
|
418
|
+
},
|
|
419
|
+
`${field.name}-${value || ""}`
|
|
420
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
421
|
+
"input",
|
|
422
|
+
{
|
|
423
|
+
type: field.type || "text",
|
|
424
|
+
id: field.name,
|
|
425
|
+
name: field.name,
|
|
426
|
+
required: field.required,
|
|
427
|
+
placeholder: field.placeholder,
|
|
428
|
+
step: field.type === "number" ? field.step ?? void 0 : void 0,
|
|
429
|
+
className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200",
|
|
430
|
+
value,
|
|
431
|
+
"data-testid": `input-${field.name}`
|
|
432
|
+
},
|
|
433
|
+
`${field.name}-${value}`
|
|
434
|
+
),
|
|
435
|
+
field.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
|
|
436
|
+
] }, field.name);
|
|
437
|
+
}
|
|
302
438
|
function FormPage(props) {
|
|
303
|
-
const { fields, submitUrl, method = "post", initialData, formId } = props;
|
|
439
|
+
const { fields, groups, submitUrl, method = "post", initialData, formId, isDialog = false, formFieldRenderers } = props;
|
|
304
440
|
const finalFormId = formId || `form-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
305
441
|
if (process.env.NODE_ENV === "development" && initialData) {
|
|
442
|
+
const fieldNames = fields ? fields.map((f) => f.name).join(", ") : groups ? groups.map((g) => g.fields.map((f) => f.name).join(", ")).join(" | ") : "";
|
|
306
443
|
console.log(
|
|
307
|
-
`[FormPage] initialData: ${JSON.stringify(initialData)}, fields: ${
|
|
444
|
+
`[FormPage] initialData: ${JSON.stringify(initialData)}, fields: ${fieldNames}`
|
|
308
445
|
);
|
|
309
446
|
}
|
|
310
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
447
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full", "data-testid": "form-container", "x-data": `{ activeTab: 0 }`, children: [
|
|
448
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
449
|
+
"div",
|
|
450
|
+
{
|
|
451
|
+
id: "form-loading-indicator",
|
|
452
|
+
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",
|
|
453
|
+
children: [
|
|
454
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
455
|
+
"svg",
|
|
456
|
+
{
|
|
457
|
+
className: "animate-spin h-4 w-4",
|
|
458
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
459
|
+
fill: "none",
|
|
460
|
+
viewBox: "0 0 24 24",
|
|
461
|
+
children: [
|
|
462
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
463
|
+
"circle",
|
|
464
|
+
{
|
|
465
|
+
className: "opacity-25",
|
|
466
|
+
cx: "12",
|
|
467
|
+
cy: "12",
|
|
468
|
+
r: "10",
|
|
469
|
+
stroke: "currentColor",
|
|
470
|
+
strokeWidth: "4"
|
|
471
|
+
}
|
|
472
|
+
),
|
|
473
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
474
|
+
"path",
|
|
475
|
+
{
|
|
476
|
+
className: "opacity-75",
|
|
477
|
+
fill: "currentColor",
|
|
478
|
+
d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
479
|
+
}
|
|
480
|
+
)
|
|
481
|
+
]
|
|
482
|
+
}
|
|
483
|
+
),
|
|
484
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: "\u63D0\u4EA4\u4E2D..." })
|
|
485
|
+
]
|
|
486
|
+
}
|
|
487
|
+
),
|
|
488
|
+
groups && groups.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
489
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: `sticky top-0 z-10 bg-white border-b border-gray-200 shadow-sm ${isDialog ? "px-6" : "-mx-6 px-6 -mt-6"}`, children: /* @__PURE__ */ jsxRuntime.jsx("nav", { className: "flex -mb-px w-full", "aria-label": "Tabs", "data-testid": "form-tabs", children: groups.map((group, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
490
|
+
"button",
|
|
491
|
+
{
|
|
492
|
+
type: "button",
|
|
493
|
+
"x-on:click": `activeTab = ${index}`,
|
|
494
|
+
"x-bind:class": `activeTab === ${index} ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'`,
|
|
495
|
+
className: "px-6 py-4 text-sm font-medium border-b-2 transition-colors duration-150 whitespace-nowrap flex-1 text-center",
|
|
496
|
+
"data-testid": `form-tab-${index}`,
|
|
497
|
+
children: group.label
|
|
498
|
+
},
|
|
499
|
+
index
|
|
500
|
+
)) }) }),
|
|
501
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
502
|
+
"form",
|
|
503
|
+
{
|
|
504
|
+
id: finalFormId,
|
|
505
|
+
method: method === "put" ? "post" : method,
|
|
506
|
+
action: submitUrl,
|
|
507
|
+
"hx-boost": "true",
|
|
508
|
+
...method === "put" ? { "hx-put": submitUrl } : {},
|
|
509
|
+
"hx-indicator": "#form-loading-indicator",
|
|
510
|
+
"data-testid": "form",
|
|
511
|
+
className: isDialog ? "p-6" : "mt-6",
|
|
512
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6", children: groups.map((group, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
513
|
+
"div",
|
|
514
|
+
{
|
|
515
|
+
"x-show": `activeTab === ${index}`,
|
|
516
|
+
className: "space-y-6",
|
|
517
|
+
"data-testid": `form-tab-content-${index}`,
|
|
518
|
+
children: group.fields.map((field) => renderFormField(field, initialData, formFieldRenderers))
|
|
519
|
+
},
|
|
520
|
+
index
|
|
521
|
+
)) }) })
|
|
522
|
+
}
|
|
523
|
+
)
|
|
524
|
+
] }) : (
|
|
525
|
+
/* 平铺模式(向后兼容) */
|
|
526
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
527
|
+
"form",
|
|
528
|
+
{
|
|
529
|
+
id: finalFormId,
|
|
530
|
+
method: method === "put" ? "post" : method,
|
|
531
|
+
action: submitUrl,
|
|
532
|
+
"hx-boost": "true",
|
|
533
|
+
...method === "put" ? { "hx-put": submitUrl } : {},
|
|
534
|
+
"hx-indicator": "#form-loading-indicator",
|
|
535
|
+
className: "space-y-6",
|
|
536
|
+
"data-testid": "form",
|
|
537
|
+
children: fields && fields.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6 space-y-6", children: fields.map((field) => renderFormField(field, initialData, formFieldRenderers)) }) })
|
|
538
|
+
}
|
|
539
|
+
)
|
|
540
|
+
)
|
|
541
|
+
] });
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/utils/form-data-processor.ts
|
|
545
|
+
function preprocessFormData(data, zodSchema) {
|
|
546
|
+
if (!zodSchema) {
|
|
547
|
+
return data;
|
|
548
|
+
}
|
|
549
|
+
const processed = { ...data };
|
|
550
|
+
const def = zodSchema._def;
|
|
551
|
+
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
552
|
+
if (!shape || typeof shape !== "object") {
|
|
553
|
+
return processed;
|
|
554
|
+
}
|
|
555
|
+
for (const [fieldName, fieldSchema] of Object.entries(shape)) {
|
|
556
|
+
const value = processed[fieldName];
|
|
557
|
+
if (value === void 0) {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (value === null) {
|
|
561
|
+
processed[fieldName] = void 0;
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
if (value === "") {
|
|
565
|
+
processed[fieldName] = void 0;
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
const fieldDef = fieldSchema._def;
|
|
569
|
+
let typeName = fieldDef?.type || fieldDef?.typeName;
|
|
570
|
+
if (typeName === "optional" || typeName === "ZodOptional") {
|
|
571
|
+
const innerType = fieldDef.innerType;
|
|
572
|
+
if (innerType) {
|
|
573
|
+
const innerDef = innerType._def;
|
|
574
|
+
typeName = innerDef?.type || innerDef?.typeName;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (typeName === "number" || typeName === "ZodNumber") {
|
|
578
|
+
if (typeof value === "string") {
|
|
579
|
+
const trimmed = value.trim();
|
|
580
|
+
if (trimmed !== "") {
|
|
581
|
+
const numValue = Number(trimmed);
|
|
582
|
+
if (!isNaN(numValue)) {
|
|
583
|
+
processed[fieldName] = numValue;
|
|
360
584
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
366
|
-
"label",
|
|
367
|
-
{
|
|
368
|
-
htmlFor: field.name,
|
|
369
|
-
className: "block text-sm font-semibold text-gray-700",
|
|
370
|
-
"data-testid": `label-${field.name}`,
|
|
371
|
-
children: [
|
|
372
|
-
field.label,
|
|
373
|
-
field.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", children: "*" })
|
|
374
|
-
]
|
|
375
|
-
}
|
|
376
|
-
),
|
|
377
|
-
field.type === "textarea" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
378
|
-
"textarea",
|
|
379
|
-
{
|
|
380
|
-
id: field.name,
|
|
381
|
-
name: field.name,
|
|
382
|
-
required: field.required,
|
|
383
|
-
placeholder: field.placeholder,
|
|
384
|
-
rows: 4,
|
|
385
|
-
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",
|
|
386
|
-
value,
|
|
387
|
-
"data-testid": `input-${field.name}`
|
|
388
|
-
},
|
|
389
|
-
`${field.name}-${value}`
|
|
390
|
-
) : field.type === "select" ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
391
|
-
"select",
|
|
392
|
-
{
|
|
393
|
-
id: field.name,
|
|
394
|
-
name: field.name,
|
|
395
|
-
required: field.required,
|
|
396
|
-
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",
|
|
397
|
-
"data-testid": `select-${field.name}`,
|
|
398
|
-
children: [
|
|
399
|
-
!field.required && /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", selected: !value || value === "", children: "\u8BF7\u9009\u62E9" }),
|
|
400
|
-
field.options && field.options.length > 0 ? field.options.map((option) => {
|
|
401
|
-
const optionValue = String(option.value);
|
|
402
|
-
const isSelected = value === optionValue;
|
|
403
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
404
|
-
"option",
|
|
405
|
-
{
|
|
406
|
-
value: optionValue,
|
|
407
|
-
selected: isSelected,
|
|
408
|
-
children: option.label
|
|
409
|
-
},
|
|
410
|
-
optionValue
|
|
411
|
-
);
|
|
412
|
-
}) : /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", disabled: true, children: "\u6682\u65E0\u9009\u9879" })
|
|
413
|
-
]
|
|
414
|
-
},
|
|
415
|
-
`${field.name}-${value || ""}`
|
|
416
|
-
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
417
|
-
"input",
|
|
418
|
-
{
|
|
419
|
-
type: field.type || "text",
|
|
420
|
-
id: field.name,
|
|
421
|
-
name: field.name,
|
|
422
|
-
required: field.required,
|
|
423
|
-
placeholder: field.placeholder,
|
|
424
|
-
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",
|
|
425
|
-
value,
|
|
426
|
-
"data-testid": `input-${field.name}`
|
|
427
|
-
},
|
|
428
|
-
`${field.name}-${value}`
|
|
429
|
-
),
|
|
430
|
-
field.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
|
|
431
|
-
] }, field.name);
|
|
432
|
-
}) }) })
|
|
433
|
-
]
|
|
585
|
+
}
|
|
586
|
+
} else if (typeof value === "number") {
|
|
587
|
+
processed[fieldName] = value;
|
|
588
|
+
}
|
|
434
589
|
}
|
|
435
|
-
|
|
590
|
+
if (typeName === "boolean" || typeName === "ZodBoolean") {
|
|
591
|
+
if (typeof value === "string") {
|
|
592
|
+
const trimmed = value.trim().toLowerCase();
|
|
593
|
+
if (trimmed === "true" || trimmed === "1" || trimmed === "on") {
|
|
594
|
+
processed[fieldName] = true;
|
|
595
|
+
} else if (trimmed === "false" || trimmed === "0" || trimmed === "off" || trimmed === "") {
|
|
596
|
+
processed[fieldName] = false;
|
|
597
|
+
}
|
|
598
|
+
} else if (typeof value === "boolean") {
|
|
599
|
+
processed[fieldName] = value;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (typeName === "array" || typeName === "ZodArray") {
|
|
603
|
+
if (typeof value === "string") {
|
|
604
|
+
const trimmed = value.trim();
|
|
605
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
606
|
+
try {
|
|
607
|
+
const parsed = JSON.parse(trimmed);
|
|
608
|
+
processed[fieldName] = parsed;
|
|
609
|
+
} catch (e) {
|
|
610
|
+
}
|
|
611
|
+
} else {
|
|
612
|
+
const parts = trimmed.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
613
|
+
processed[fieldName] = parts;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (typeName === "object" || typeName === "ZodObject") {
|
|
618
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
619
|
+
try {
|
|
620
|
+
const trimmed = value.trim();
|
|
621
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
622
|
+
const parsed = JSON.parse(trimmed);
|
|
623
|
+
processed[fieldName] = parsed;
|
|
624
|
+
}
|
|
625
|
+
} catch (e) {
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (typeName === "any" || typeName === "ZodAny") {
|
|
630
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
631
|
+
try {
|
|
632
|
+
const trimmed = value.trim();
|
|
633
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
634
|
+
const parsed = JSON.parse(trimmed);
|
|
635
|
+
processed[fieldName] = parsed;
|
|
636
|
+
}
|
|
637
|
+
} catch (e) {
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return processed;
|
|
436
643
|
}
|
|
437
644
|
|
|
438
645
|
// src/utils/schema-utils.ts
|
|
@@ -459,13 +666,14 @@ function parseFieldSchema(fieldName, fieldSchema) {
|
|
|
459
666
|
return null;
|
|
460
667
|
}
|
|
461
668
|
const label = getFieldDescription(fieldSchema) || fieldName;
|
|
462
|
-
const { type, required, options, innerSchema } = analyzeFieldType(fieldSchema);
|
|
669
|
+
const { type, required, options, innerSchema, step } = analyzeFieldType(fieldSchema);
|
|
463
670
|
return {
|
|
464
671
|
name: fieldName,
|
|
465
672
|
label,
|
|
466
673
|
type,
|
|
467
674
|
required,
|
|
468
675
|
options,
|
|
676
|
+
step,
|
|
469
677
|
schema: innerSchema || fieldSchema
|
|
470
678
|
};
|
|
471
679
|
}
|
|
@@ -522,10 +730,20 @@ function analyzeFieldType(schema) {
|
|
|
522
730
|
let fieldType = "text";
|
|
523
731
|
if (def?.checks) {
|
|
524
732
|
const hasEmailCheck = def.checks.some(
|
|
525
|
-
(check) => check.
|
|
733
|
+
(check) => check.format === "email" || check.constructor?.name === "ZodEmail" || check._zod?.def?.format === "email"
|
|
526
734
|
);
|
|
527
735
|
if (hasEmailCheck) {
|
|
528
736
|
fieldType = "email";
|
|
737
|
+
} else {
|
|
738
|
+
const maxLengthCheck = def.checks.find(
|
|
739
|
+
(check) => check.constructor?.name === "$ZodCheckMaxLength" || check._zod?.def?.check === "max_length" || check._zod?.def?.maximum !== void 0
|
|
740
|
+
);
|
|
741
|
+
if (maxLengthCheck) {
|
|
742
|
+
const maxLength = maxLengthCheck._zod?.def?.maximum ?? maxLengthCheck.value ?? maxLengthCheck.maximum;
|
|
743
|
+
if (maxLength !== void 0 && maxLength > 50) {
|
|
744
|
+
fieldType = "textarea";
|
|
745
|
+
}
|
|
746
|
+
}
|
|
529
747
|
}
|
|
530
748
|
}
|
|
531
749
|
return {
|
|
@@ -535,10 +753,22 @@ function analyzeFieldType(schema) {
|
|
|
535
753
|
};
|
|
536
754
|
}
|
|
537
755
|
if (typeName === "number" || typeName === "ZodNumber") {
|
|
756
|
+
let step = void 0;
|
|
757
|
+
if (def?.checks) {
|
|
758
|
+
const hasIntCheck = def.checks.some(
|
|
759
|
+
(check) => check.isInt === true || check.format === "safeint" || check.def?.format === "safeint" || check.kind === "int" || check.constructor?.name === "ZodInt" || check._zod?.def?.check === "int" || check._zod?.def?.kind === "int"
|
|
760
|
+
);
|
|
761
|
+
if (!hasIntCheck) {
|
|
762
|
+
step = "any";
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
step = "any";
|
|
766
|
+
}
|
|
538
767
|
return {
|
|
539
768
|
type: "number",
|
|
540
769
|
required: true,
|
|
541
|
-
innerSchema: schema
|
|
770
|
+
innerSchema: schema,
|
|
771
|
+
step
|
|
542
772
|
};
|
|
543
773
|
}
|
|
544
774
|
if (typeName === "date" || typeName === "ZodDate") {
|
|
@@ -603,7 +833,8 @@ function modelFieldsToFormFields(fields) {
|
|
|
603
833
|
type: field.type,
|
|
604
834
|
label: field.label,
|
|
605
835
|
required: field.required,
|
|
606
|
-
options: field.options
|
|
836
|
+
options: field.options,
|
|
837
|
+
step: field.step
|
|
607
838
|
}));
|
|
608
839
|
}
|
|
609
840
|
function getFieldNamesFromFields(fields) {
|
|
@@ -621,6 +852,8 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
621
852
|
descriptionGetter;
|
|
622
853
|
/** 当前请求的表单 ID(用于在 render 和 getActions 之间共享) */
|
|
623
854
|
currentFormId;
|
|
855
|
+
/** 自定义表单字段渲染器 */
|
|
856
|
+
formFieldRenderers;
|
|
624
857
|
/**
|
|
625
858
|
* 获取或生成表单 ID(确保在同一个请求中保持一致)
|
|
626
859
|
*/
|
|
@@ -687,59 +920,7 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
687
920
|
if (!this.schema) {
|
|
688
921
|
return data;
|
|
689
922
|
}
|
|
690
|
-
|
|
691
|
-
for (const key of Object.keys(processed)) {
|
|
692
|
-
if (processed[key] === "") {
|
|
693
|
-
processed[key] = void 0;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
const def = this.schema._def;
|
|
697
|
-
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
698
|
-
if (!shape || typeof shape !== "object") {
|
|
699
|
-
return data;
|
|
700
|
-
}
|
|
701
|
-
for (const [fieldName, fieldSchema] of Object.entries(shape)) {
|
|
702
|
-
const value = processed[fieldName];
|
|
703
|
-
if (value === void 0 || value === null) {
|
|
704
|
-
continue;
|
|
705
|
-
}
|
|
706
|
-
const fieldDef = fieldSchema._def;
|
|
707
|
-
let typeName = fieldDef?.typeName;
|
|
708
|
-
let isOptional = false;
|
|
709
|
-
if (typeName === "ZodOptional") {
|
|
710
|
-
isOptional = true;
|
|
711
|
-
const innerType = fieldDef.innerType?._def;
|
|
712
|
-
typeName = innerType?.typeName;
|
|
713
|
-
}
|
|
714
|
-
if (isOptional && typeof value === "string" && value.trim() === "") {
|
|
715
|
-
if (fieldName === "authorId") {
|
|
716
|
-
imeanServiceEngine.logger.info(`[BaseFormFeature] authorId empty optional -> undefined`);
|
|
717
|
-
}
|
|
718
|
-
processed[fieldName] = void 0;
|
|
719
|
-
continue;
|
|
720
|
-
}
|
|
721
|
-
if (typeName === "ZodNumber") {
|
|
722
|
-
if (typeof value === "string") {
|
|
723
|
-
const numValue = Number(value);
|
|
724
|
-
if (fieldName === "authorId") {
|
|
725
|
-
imeanServiceEngine.logger.info(
|
|
726
|
-
`[BaseFormFeature] authorId raw value="${value}", numValue=${numValue}, isOptional=${isOptional}`
|
|
727
|
-
);
|
|
728
|
-
}
|
|
729
|
-
if (!isNaN(numValue)) {
|
|
730
|
-
processed[fieldName] = numValue;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
if (typeName === "ZodArray") {
|
|
735
|
-
if (typeof value === "string") {
|
|
736
|
-
const parts = value.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
737
|
-
processed[fieldName] = parts;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
imeanServiceEngine.logger.info(`[BaseFormFeature] processed form data: ${JSON.stringify(processed)}`);
|
|
742
|
-
return processed;
|
|
923
|
+
return preprocessFormData(data, this.schema);
|
|
743
924
|
}
|
|
744
925
|
/**
|
|
745
926
|
* 处理请求
|
|
@@ -749,12 +930,23 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
749
930
|
this.currentFormId = void 0;
|
|
750
931
|
if (ctx.req.method === "GET") {
|
|
751
932
|
return this.render(context);
|
|
752
|
-
} else if (ctx.req.method === "POST" || ctx.req.method === "PUT") {
|
|
933
|
+
} else if (ctx.req.method === "POST" || ctx.req.method === "PUT" || ctx.req.method === "PATCH") {
|
|
934
|
+
const methodOverride = ctx.req.header("X-HTTP-Method-Override");
|
|
935
|
+
const actualMethod = methodOverride || ctx.req.method;
|
|
936
|
+
const expectedMethod = this.getFormAction() === "edit" ? "PUT" : "POST";
|
|
937
|
+
if (actualMethod.toUpperCase() !== expectedMethod) {
|
|
938
|
+
imeanServiceEngine.logger.warn(
|
|
939
|
+
`[BaseFormFeature] Method mismatch: expected ${expectedMethod}, got ${actualMethod} (request method: ${ctx.req.method}, X-HTTP-Method-Override: ${methodOverride || "none"})`
|
|
940
|
+
);
|
|
941
|
+
}
|
|
753
942
|
const originalData = { ...context.body };
|
|
754
943
|
imeanServiceEngine.logger.info(
|
|
755
944
|
`[BaseFormFeature] Original body data: ${JSON.stringify(originalData)}`
|
|
756
945
|
);
|
|
757
946
|
let data = this.preprocessFormData(context.body);
|
|
947
|
+
imeanServiceEngine.logger.info(
|
|
948
|
+
`[BaseFormFeature] Preprocessed data: ${JSON.stringify(data)}`
|
|
949
|
+
);
|
|
758
950
|
if (!this.schema) {
|
|
759
951
|
throw new Error("Schema is required for form validation");
|
|
760
952
|
}
|
|
@@ -771,7 +963,10 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
771
963
|
);
|
|
772
964
|
return this.render(context, originalData);
|
|
773
965
|
}
|
|
774
|
-
const item = await this.handleSubmit(
|
|
966
|
+
const item = await this.handleSubmit(
|
|
967
|
+
context,
|
|
968
|
+
parseResult.data
|
|
969
|
+
);
|
|
775
970
|
if (!item) {
|
|
776
971
|
context.sendError(
|
|
777
972
|
this.getFormAction() === "create" ? "\u521B\u5EFA\u5931\u8D25" : "\u66F4\u65B0\u5931\u8D25",
|
|
@@ -789,6 +984,9 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
789
984
|
`[BaseFormFeature] Dialog mode: setting refresh to close dialog and refresh list`
|
|
790
985
|
);
|
|
791
986
|
context.setRefresh(true);
|
|
987
|
+
if (context.redirectUrl) {
|
|
988
|
+
context.redirectUrl = void 0;
|
|
989
|
+
}
|
|
792
990
|
return null;
|
|
793
991
|
} else {
|
|
794
992
|
const redirectUrl = this.getSuccessRedirectUrl(context, item);
|
|
@@ -801,12 +999,11 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
801
999
|
}
|
|
802
1000
|
}
|
|
803
1001
|
formFieldNames;
|
|
1002
|
+
groups;
|
|
804
1003
|
/**
|
|
805
1004
|
* 渲染表单页面
|
|
806
1005
|
*/
|
|
807
1006
|
async render(context, initialData) {
|
|
808
|
-
const filteredFields = this.formFieldNames ? filterFieldsByNames(this.fields || [], this.formFieldNames) : this.fields || [];
|
|
809
|
-
const fields = modelFieldsToFormFields(filteredFields);
|
|
810
1007
|
let formData;
|
|
811
1008
|
if (this.getFormAction() === "edit") {
|
|
812
1009
|
if (initialData) {
|
|
@@ -828,6 +1025,50 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
828
1025
|
}
|
|
829
1026
|
const method = this.getFormAction() === "create" ? "post" : "put";
|
|
830
1027
|
const formId = this.getFormId(context);
|
|
1028
|
+
if (this.groups && this.groups.length > 0) {
|
|
1029
|
+
if (!this.schema) {
|
|
1030
|
+
throw new Error("Schema is required when using groups");
|
|
1031
|
+
}
|
|
1032
|
+
const schema = this.schema;
|
|
1033
|
+
const groupSchemas = this.groups.map((group) => {
|
|
1034
|
+
const pickObject = group.fields.reduce(
|
|
1035
|
+
(acc, fieldName) => {
|
|
1036
|
+
acc[fieldName] = true;
|
|
1037
|
+
return acc;
|
|
1038
|
+
},
|
|
1039
|
+
{}
|
|
1040
|
+
);
|
|
1041
|
+
return {
|
|
1042
|
+
label: group.label,
|
|
1043
|
+
schema: schema.pick(pickObject),
|
|
1044
|
+
fields: group.fields
|
|
1045
|
+
};
|
|
1046
|
+
});
|
|
1047
|
+
const groupFields = groupSchemas.map(
|
|
1048
|
+
({ label, schema: schema2, fields: fieldNames }) => {
|
|
1049
|
+
const groupFields2 = parseSchemaToFields(schema2);
|
|
1050
|
+
const formFields = modelFieldsToFormFields(groupFields2);
|
|
1051
|
+
return {
|
|
1052
|
+
label,
|
|
1053
|
+
fields: formFields
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
);
|
|
1057
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1058
|
+
FormPage,
|
|
1059
|
+
{
|
|
1060
|
+
groups: groupFields,
|
|
1061
|
+
submitUrl,
|
|
1062
|
+
method,
|
|
1063
|
+
initialData: formData,
|
|
1064
|
+
formId,
|
|
1065
|
+
isDialog: context.isDialog,
|
|
1066
|
+
formFieldRenderers: this.formFieldRenderers
|
|
1067
|
+
}
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
const filteredFields = this.formFieldNames ? filterFieldsByNames(this.fields || [], this.formFieldNames) : this.fields || [];
|
|
1071
|
+
const fields = modelFieldsToFormFields(filteredFields);
|
|
831
1072
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
832
1073
|
FormPage,
|
|
833
1074
|
{
|
|
@@ -835,7 +1076,9 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
835
1076
|
submitUrl,
|
|
836
1077
|
method,
|
|
837
1078
|
initialData: formData,
|
|
838
|
-
formId
|
|
1079
|
+
formId,
|
|
1080
|
+
isDialog: context.isDialog,
|
|
1081
|
+
formFieldRenderers: this.formFieldRenderers
|
|
839
1082
|
}
|
|
840
1083
|
);
|
|
841
1084
|
}
|
|
@@ -944,6 +1187,8 @@ var DefaultCreateFeature = class extends BaseFormFeature {
|
|
|
944
1187
|
this.fields = parseSchemaToFields(options.schema);
|
|
945
1188
|
this.createItem = options.createItem;
|
|
946
1189
|
this.formFieldNames = options.formFieldNames;
|
|
1190
|
+
this.groups = options.groups;
|
|
1191
|
+
this.formFieldRenderers = options.formFieldRenderers;
|
|
947
1192
|
}
|
|
948
1193
|
getFormAction() {
|
|
949
1194
|
return "create";
|
|
@@ -1003,12 +1248,119 @@ var DefaultDeleteFeature = class extends BaseFeature {
|
|
|
1003
1248
|
}
|
|
1004
1249
|
}
|
|
1005
1250
|
};
|
|
1251
|
+
function Card(props) {
|
|
1252
|
+
const {
|
|
1253
|
+
children,
|
|
1254
|
+
title,
|
|
1255
|
+
className = "",
|
|
1256
|
+
shadow = true,
|
|
1257
|
+
bordered = false,
|
|
1258
|
+
noPadding = false
|
|
1259
|
+
} = props;
|
|
1260
|
+
const baseClasses = "bg-white rounded-lg";
|
|
1261
|
+
const shadowClass = shadow ? "shadow-sm hover:shadow-md transition-shadow" : "";
|
|
1262
|
+
const borderClass = bordered ? "border border-gray-200" : "";
|
|
1263
|
+
const paddingClass = noPadding ? "" : "p-6";
|
|
1264
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1265
|
+
"div",
|
|
1266
|
+
{
|
|
1267
|
+
className: `${baseClasses} ${shadowClass} ${borderClass} ${className}`,
|
|
1268
|
+
children: [
|
|
1269
|
+
title && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 border-b border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title }) }),
|
|
1270
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: noPadding ? "" : paddingClass, children })
|
|
1271
|
+
]
|
|
1272
|
+
}
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
function renderDefaultValue(value) {
|
|
1276
|
+
if (value === null || value === void 0) {
|
|
1277
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400", children: "-" });
|
|
1278
|
+
}
|
|
1279
|
+
if (Array.isArray(value)) {
|
|
1280
|
+
if (value.length === 0) {
|
|
1281
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400", children: "\u6682\u65E0\u6570\u636E" });
|
|
1282
|
+
}
|
|
1283
|
+
if (value.length > 0 && typeof value[0] === "object" && value[0] !== null) {
|
|
1284
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-gray-600", children: [
|
|
1285
|
+
"\u5305\u542B ",
|
|
1286
|
+
value.length,
|
|
1287
|
+
" \u9879\uFF08\u5BF9\u8C61\u6570\u7EC4\uFF0C\u5EFA\u8BAE\u4F7F\u7528\u81EA\u5B9A\u4E49\u6E32\u67D3\uFF09"
|
|
1288
|
+
] });
|
|
1289
|
+
}
|
|
1290
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: value.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1291
|
+
"span",
|
|
1292
|
+
{
|
|
1293
|
+
className: "px-2 py-1 bg-gray-100 text-gray-700 rounded text-sm",
|
|
1294
|
+
children: String(item)
|
|
1295
|
+
},
|
|
1296
|
+
index
|
|
1297
|
+
)) });
|
|
1298
|
+
}
|
|
1299
|
+
if (typeof value === "object") {
|
|
1300
|
+
if (value instanceof Date) {
|
|
1301
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { children: value.toLocaleString() });
|
|
1302
|
+
}
|
|
1303
|
+
return /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "bg-gray-50 border border-gray-200 rounded p-3 text-xs overflow-x-auto", children: JSON.stringify(value, null, 2) });
|
|
1304
|
+
}
|
|
1305
|
+
if (typeof value === "boolean") {
|
|
1306
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `px-2 py-1 rounded text-sm font-medium ${value ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-600"}`, children: value ? "\u662F" : "\u5426" });
|
|
1307
|
+
}
|
|
1308
|
+
if (typeof value === "number") {
|
|
1309
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { children: value.toLocaleString() });
|
|
1310
|
+
}
|
|
1311
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { children: String(value) });
|
|
1312
|
+
}
|
|
1313
|
+
function renderField(field, value, item) {
|
|
1314
|
+
let content;
|
|
1315
|
+
if (field.render) {
|
|
1316
|
+
const rendered = field.render(value, item);
|
|
1317
|
+
if (rendered === null || rendered === void 0) {
|
|
1318
|
+
content = /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400", children: "-" });
|
|
1319
|
+
} else if (typeof rendered === "string" || typeof rendered === "number" || typeof rendered === "boolean") {
|
|
1320
|
+
content = /* @__PURE__ */ jsxRuntime.jsx("span", { children: String(rendered) });
|
|
1321
|
+
} else {
|
|
1322
|
+
content = rendered;
|
|
1323
|
+
}
|
|
1324
|
+
} else {
|
|
1325
|
+
content = renderDefaultValue(value);
|
|
1326
|
+
}
|
|
1327
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1328
|
+
"div",
|
|
1329
|
+
{
|
|
1330
|
+
className: "\r\n px-4 py-4 sm:px-6 sm:py-5\r\n flex flex-col\r\n hover:bg-gray-50/50 transition-colors duration-150\r\n group\r\n gap-1.5\r\n ",
|
|
1331
|
+
children: [
|
|
1332
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "\r\n text-xs sm:text-sm font-semibold text-gray-600 sm:text-gray-700\r\n flex items-start\r\n leading-tight sm:leading-5\r\n tracking-wide\r\n ", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 uppercase sm:normal-case", children: field.label }) }),
|
|
1333
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { className: "\r\n text-sm sm:text-base text-gray-900\r\n break-words\r\n leading-relaxed\r\n min-w-0\r\n ", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0", children: content }) })
|
|
1334
|
+
]
|
|
1335
|
+
},
|
|
1336
|
+
field.key
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1006
1339
|
function DetailPage(props) {
|
|
1007
|
-
const { item, fields } = props;
|
|
1008
|
-
|
|
1009
|
-
/* @__PURE__ */ jsxRuntime.jsx("
|
|
1010
|
-
|
|
1011
|
-
|
|
1340
|
+
const { item, fields, groups } = props;
|
|
1341
|
+
if (groups && groups.length > 0) {
|
|
1342
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-6", children: groups.map((group, groupIndex) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1343
|
+
Card,
|
|
1344
|
+
{
|
|
1345
|
+
title: group.label,
|
|
1346
|
+
shadow: true,
|
|
1347
|
+
bordered: true,
|
|
1348
|
+
noPadding: true,
|
|
1349
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("dl", { className: "divide-y divide-gray-100", children: group.fields.map((field) => {
|
|
1350
|
+
const value = group.values[field.key];
|
|
1351
|
+
return renderField(field, value, item);
|
|
1352
|
+
}) })
|
|
1353
|
+
},
|
|
1354
|
+
groupIndex
|
|
1355
|
+
)) });
|
|
1356
|
+
}
|
|
1357
|
+
if (fields && fields.length > 0) {
|
|
1358
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx("dl", { className: "divide-y divide-gray-100", children: fields.map((field) => {
|
|
1359
|
+
const value = item[field.key];
|
|
1360
|
+
return renderField(field, value, item);
|
|
1361
|
+
}) }) });
|
|
1362
|
+
}
|
|
1363
|
+
return null;
|
|
1012
1364
|
}
|
|
1013
1365
|
var DefaultDetailFeature = class extends BaseFeature {
|
|
1014
1366
|
getItem;
|
|
@@ -1016,6 +1368,8 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
1016
1368
|
titleGetter;
|
|
1017
1369
|
descriptionGetter;
|
|
1018
1370
|
detailFieldNames;
|
|
1371
|
+
fieldRenderers;
|
|
1372
|
+
groups;
|
|
1019
1373
|
constructor(options) {
|
|
1020
1374
|
super({
|
|
1021
1375
|
name: "detail",
|
|
@@ -1031,6 +1385,8 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
1031
1385
|
this.titleGetter = options.getTitle;
|
|
1032
1386
|
this.descriptionGetter = options.getDescription;
|
|
1033
1387
|
this.detailFieldNames = options.detailFieldNames;
|
|
1388
|
+
this.fieldRenderers = options.fieldRenderers;
|
|
1389
|
+
this.groups = options.groups;
|
|
1034
1390
|
}
|
|
1035
1391
|
async getTitle(context) {
|
|
1036
1392
|
if (this.titleGetter) {
|
|
@@ -1061,6 +1417,40 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
1061
1417
|
if (!item) {
|
|
1062
1418
|
return context.ctx.json({ error: "Not found" }, 404);
|
|
1063
1419
|
}
|
|
1420
|
+
if (this.groups && this.groups.length > 0) {
|
|
1421
|
+
if (!this.schema) {
|
|
1422
|
+
throw new Error("Schema is required when using groups");
|
|
1423
|
+
}
|
|
1424
|
+
const schema = this.schema;
|
|
1425
|
+
const groupSchemas = this.groups.map((group) => {
|
|
1426
|
+
const pickObject = group.fields.reduce((acc, fieldName) => {
|
|
1427
|
+
acc[fieldName] = true;
|
|
1428
|
+
return acc;
|
|
1429
|
+
}, {});
|
|
1430
|
+
return {
|
|
1431
|
+
label: group.label,
|
|
1432
|
+
schema: schema.pick(pickObject),
|
|
1433
|
+
fields: group.fields
|
|
1434
|
+
};
|
|
1435
|
+
});
|
|
1436
|
+
const groupFields = groupSchemas.map(({ label, schema: schema2, fields: fieldNames }) => {
|
|
1437
|
+
const groupFields2 = parseSchemaToFields(schema2);
|
|
1438
|
+
const detailFields2 = groupFields2.map((field) => ({
|
|
1439
|
+
key: field.name,
|
|
1440
|
+
label: field.label,
|
|
1441
|
+
render: this.fieldRenderers?.[field.name]
|
|
1442
|
+
}));
|
|
1443
|
+
return {
|
|
1444
|
+
label,
|
|
1445
|
+
fields: detailFields2,
|
|
1446
|
+
values: fieldNames.reduce((acc, fieldName) => {
|
|
1447
|
+
acc[fieldName] = item[fieldName];
|
|
1448
|
+
return acc;
|
|
1449
|
+
}, {})
|
|
1450
|
+
};
|
|
1451
|
+
});
|
|
1452
|
+
return /* @__PURE__ */ jsxRuntime.jsx(DetailPage, { item, groups: groupFields });
|
|
1453
|
+
}
|
|
1064
1454
|
const detailFields = this.detailFieldNames ? filterFieldsByNames(this.fields || [], this.detailFieldNames) : this.fields || [];
|
|
1065
1455
|
if (this.detailFieldNames) {
|
|
1066
1456
|
const systemFields = ["id", "createdAt", "updatedAt"];
|
|
@@ -1079,7 +1469,9 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
1079
1469
|
const detailFieldNames = getFieldNamesFromFields(detailFields);
|
|
1080
1470
|
const fields = detailFieldNames.map((fieldName) => ({
|
|
1081
1471
|
key: fieldName,
|
|
1082
|
-
label: getFieldLabelFromFields(this.fields || [], fieldName) || fieldName
|
|
1472
|
+
label: getFieldLabelFromFields(this.fields || [], fieldName) || fieldName,
|
|
1473
|
+
render: this.fieldRenderers?.[fieldName]
|
|
1474
|
+
// 如果有自定义渲染函数则使用
|
|
1083
1475
|
}));
|
|
1084
1476
|
return /* @__PURE__ */ jsxRuntime.jsx(DetailPage, { item, fields });
|
|
1085
1477
|
}
|
|
@@ -1157,6 +1549,8 @@ var DefaultEditFeature = class extends BaseFormFeature {
|
|
|
1157
1549
|
this.getItem = options.getItem;
|
|
1158
1550
|
this.updateItem = options.updateItem;
|
|
1159
1551
|
this.formFieldNames = options.formFieldNames;
|
|
1552
|
+
this.groups = options.groups;
|
|
1553
|
+
this.formFieldRenderers = options.formFieldRenderers;
|
|
1160
1554
|
}
|
|
1161
1555
|
getFormAction() {
|
|
1162
1556
|
return "edit";
|
|
@@ -1366,18 +1760,21 @@ function Button(props) {
|
|
|
1366
1760
|
disabled ? "opacity-50 cursor-not-allowed" : "",
|
|
1367
1761
|
className
|
|
1368
1762
|
].filter(Boolean).join(" ");
|
|
1763
|
+
const isNewWindow = rest.target === "_blank";
|
|
1369
1764
|
const htmxAttrs = {};
|
|
1370
|
-
if (
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1765
|
+
if (!isNewWindow) {
|
|
1766
|
+
if (hxGet) htmxAttrs["hx-get"] = hxGet;
|
|
1767
|
+
if (hxPost) htmxAttrs["hx-post"] = hxPost;
|
|
1768
|
+
if (hxPut) htmxAttrs["hx-put"] = hxPut;
|
|
1769
|
+
if (hxDelete) htmxAttrs["hx-delete"] = hxDelete;
|
|
1770
|
+
if (hxTarget) htmxAttrs["hx-target"] = hxTarget;
|
|
1771
|
+
if (hxSwap) htmxAttrs["hx-swap"] = hxSwap;
|
|
1772
|
+
if (hxPushUrl !== void 0)
|
|
1773
|
+
htmxAttrs["hx-push-url"] = hxPushUrl === true ? "true" : hxPushUrl;
|
|
1774
|
+
if (hxIndicator) htmxAttrs["hx-indicator"] = hxIndicator;
|
|
1775
|
+
if (hxConfirm) htmxAttrs["hx-confirm"] = hxConfirm;
|
|
1776
|
+
if (hxHeaders) htmxAttrs["hx-headers"] = hxHeaders;
|
|
1777
|
+
}
|
|
1381
1778
|
const href = rest.href ?? hxGet ?? "#";
|
|
1382
1779
|
const { className: _, ...otherRest } = rest;
|
|
1383
1780
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -1392,30 +1789,6 @@ function Button(props) {
|
|
|
1392
1789
|
}
|
|
1393
1790
|
);
|
|
1394
1791
|
}
|
|
1395
|
-
function Card(props) {
|
|
1396
|
-
const {
|
|
1397
|
-
children,
|
|
1398
|
-
title,
|
|
1399
|
-
className = "",
|
|
1400
|
-
shadow = true,
|
|
1401
|
-
bordered = false,
|
|
1402
|
-
noPadding = false
|
|
1403
|
-
} = props;
|
|
1404
|
-
const baseClasses = "bg-white rounded-lg";
|
|
1405
|
-
const shadowClass = shadow ? "shadow-sm hover:shadow-md transition-shadow" : "";
|
|
1406
|
-
const borderClass = bordered ? "border border-gray-200" : "";
|
|
1407
|
-
const paddingClass = noPadding ? "" : "p-6";
|
|
1408
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1409
|
-
"div",
|
|
1410
|
-
{
|
|
1411
|
-
className: `${baseClasses} ${shadowClass} ${borderClass} ${className}`,
|
|
1412
|
-
children: [
|
|
1413
|
-
title && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 border-b border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title }) }),
|
|
1414
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: noPadding ? "" : paddingClass, children })
|
|
1415
|
-
]
|
|
1416
|
-
}
|
|
1417
|
-
);
|
|
1418
|
-
}
|
|
1419
1792
|
function EmptyState(props) {
|
|
1420
1793
|
const { message = "\u6682\u65E0\u6570\u636E", children } = props;
|
|
1421
1794
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-12", children: children || /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-500 text-sm", children: message }) });
|
|
@@ -1599,20 +1972,24 @@ function ActionLink(props) {
|
|
|
1599
1972
|
const { action, item } = props;
|
|
1600
1973
|
const hrefValue = action.href(item);
|
|
1601
1974
|
const isDelete = action.method === "delete";
|
|
1975
|
+
const isNewWindow = action.target === "_blank";
|
|
1602
1976
|
const className = `${STYLES.actionLink.base} ${isDelete ? STYLES.actionLink.delete : STYLES.actionLink.default}`;
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1977
|
+
const linkProps = {
|
|
1978
|
+
href: hrefValue,
|
|
1979
|
+
className,
|
|
1980
|
+
"data-testid": `table-action-${action.label}`,
|
|
1981
|
+
"aria-label": `${action.label}\uFF1A${item.id || item}`
|
|
1982
|
+
};
|
|
1983
|
+
if (isNewWindow) {
|
|
1984
|
+
linkProps.target = "_blank";
|
|
1985
|
+
linkProps.rel = "noopener noreferrer";
|
|
1986
|
+
} else if (isDelete) {
|
|
1987
|
+
linkProps["hx-delete"] = hrefValue;
|
|
1988
|
+
linkProps["hx-confirm"] = "\u786E\u5B9A\u5220\u9664\u5417\uFF1F";
|
|
1989
|
+
} else {
|
|
1990
|
+
linkProps["hx-get"] = hrefValue;
|
|
1991
|
+
}
|
|
1992
|
+
return /* @__PURE__ */ jsxRuntime.jsx("a", { ...linkProps, children: action.label });
|
|
1616
1993
|
}
|
|
1617
1994
|
function ActionCell(props) {
|
|
1618
1995
|
const { actions, item, actionStyle } = props;
|
|
@@ -1633,15 +2010,18 @@ function ActionCell(props) {
|
|
|
1633
2010
|
) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: STYLES.actionButton, "data-testid": "table-actions", children: actions.map((action, idx) => {
|
|
1634
2011
|
const hrefValue = action.href(item);
|
|
1635
2012
|
const isDelete = action.method === "delete";
|
|
2013
|
+
const isNewWindow = action.target === "_blank";
|
|
1636
2014
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1637
2015
|
Button,
|
|
1638
2016
|
{
|
|
1639
2017
|
variant: isDelete ? "danger" : "secondary",
|
|
1640
2018
|
size: "sm",
|
|
1641
2019
|
href: hrefValue,
|
|
1642
|
-
"hx-get": !isDelete ? hrefValue : void 0,
|
|
2020
|
+
"hx-get": !isDelete && !isNewWindow ? hrefValue : void 0,
|
|
1643
2021
|
"hx-delete": isDelete ? hrefValue : void 0,
|
|
1644
2022
|
"hx-confirm": isDelete ? "\u786E\u5B9A\u5220\u9664\u5417\uFF1F" : void 0,
|
|
2023
|
+
target: isNewWindow ? "_blank" : void 0,
|
|
2024
|
+
rel: isNewWindow ? "noopener noreferrer" : void 0,
|
|
1645
2025
|
className: action.class,
|
|
1646
2026
|
"data-testid": `table-action-${action.label}`,
|
|
1647
2027
|
"aria-label": `${action.label}\uFF1A${item.id || item}`,
|
|
@@ -1738,19 +2118,24 @@ function ListPage(props) {
|
|
|
1738
2118
|
editPath,
|
|
1739
2119
|
deletePath,
|
|
1740
2120
|
listPath,
|
|
1741
|
-
filterFields
|
|
2121
|
+
filterFields,
|
|
2122
|
+
openMode
|
|
1742
2123
|
} = props;
|
|
1743
2124
|
const actions = [];
|
|
1744
2125
|
if (detailPath) {
|
|
2126
|
+
const mode = openMode?.detail || "dialog";
|
|
1745
2127
|
actions.push({
|
|
1746
2128
|
label: "\u67E5\u770B",
|
|
1747
|
-
href: detailPath
|
|
2129
|
+
href: detailPath,
|
|
2130
|
+
target: mode === "newWindow" ? "_blank" : void 0
|
|
1748
2131
|
});
|
|
1749
2132
|
}
|
|
1750
2133
|
if (editPath) {
|
|
2134
|
+
const mode = openMode?.edit || "dialog";
|
|
1751
2135
|
actions.push({
|
|
1752
2136
|
label: "\u7F16\u8F91",
|
|
1753
|
-
href: editPath
|
|
2137
|
+
href: editPath,
|
|
2138
|
+
target: mode === "newWindow" ? "_blank" : void 0
|
|
1754
2139
|
});
|
|
1755
2140
|
}
|
|
1756
2141
|
if (deletePath) {
|
|
@@ -1795,7 +2180,8 @@ function ListPage(props) {
|
|
|
1795
2180
|
actions: actions.length > 0 ? actions.map((action) => ({
|
|
1796
2181
|
label: action.label,
|
|
1797
2182
|
href: (item) => action.href(item),
|
|
1798
|
-
method: action.method
|
|
2183
|
+
method: action.method,
|
|
2184
|
+
target: action.target
|
|
1799
2185
|
})) : void 0,
|
|
1800
2186
|
pagination: {
|
|
1801
2187
|
page: result.page,
|
|
@@ -1836,6 +2222,7 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
1836
2222
|
listFieldNames;
|
|
1837
2223
|
filterSchema;
|
|
1838
2224
|
columnRenderers;
|
|
2225
|
+
openMode;
|
|
1839
2226
|
constructor(options) {
|
|
1840
2227
|
super({
|
|
1841
2228
|
name: "list",
|
|
@@ -1851,6 +2238,7 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
1851
2238
|
this.listFieldNames = options.listFieldNames;
|
|
1852
2239
|
this.filterSchema = options.filterSchema;
|
|
1853
2240
|
this.columnRenderers = options.columnRenderers;
|
|
2241
|
+
this.openMode = options.openMode;
|
|
1854
2242
|
}
|
|
1855
2243
|
getRoutes() {
|
|
1856
2244
|
return [{ method: "get", path: "/list" }];
|
|
@@ -1871,9 +2259,12 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
1871
2259
|
const prefix = context.prefix || "";
|
|
1872
2260
|
const basePath = `${prefix}/${model.modelName}`;
|
|
1873
2261
|
const listPath = `${basePath}/list`;
|
|
1874
|
-
const
|
|
1875
|
-
const
|
|
1876
|
-
const
|
|
2262
|
+
const createMode = this.openMode?.create || "dialog";
|
|
2263
|
+
const detailMode = this.openMode?.detail || "dialog";
|
|
2264
|
+
const editMode = this.openMode?.edit || "dialog";
|
|
2265
|
+
const createPath = model.features.get("create") ? createMode === "dialog" ? `${basePath}/new?dialog=true` : `${basePath}/new` : void 0;
|
|
2266
|
+
const detailPath = model.features.get("detail") ? (item) => detailMode === "dialog" ? `${basePath}/detail/${item.id}?dialog=true` : `${basePath}/detail/${item.id}` : void 0;
|
|
2267
|
+
const editPath = model.features.get("edit") ? (item) => editMode === "dialog" ? `${basePath}/edit/${item.id}?dialog=true` : `${basePath}/edit/${item.id}` : void 0;
|
|
1877
2268
|
const deletePath = this.deleteItem && model.features.get("delete") ? (item) => `${basePath}/${item.id}` : void 0;
|
|
1878
2269
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1879
2270
|
ListPage,
|
|
@@ -1886,7 +2277,8 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
1886
2277
|
editPath,
|
|
1887
2278
|
deletePath,
|
|
1888
2279
|
listPath,
|
|
1889
|
-
filterFields
|
|
2280
|
+
filterFields,
|
|
2281
|
+
openMode: this.openMode
|
|
1890
2282
|
}
|
|
1891
2283
|
);
|
|
1892
2284
|
}
|
|
@@ -1897,11 +2289,22 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
1897
2289
|
const hasCreate = model.features.get("create") !== void 0;
|
|
1898
2290
|
const actions = [];
|
|
1899
2291
|
if (hasCreate) {
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2292
|
+
const createMode = this.openMode?.create || "dialog";
|
|
2293
|
+
const createUrl = createMode === "dialog" ? `${basePath}/new?dialog=true` : `${basePath}/new`;
|
|
2294
|
+
if (createMode === "newWindow") {
|
|
2295
|
+
actions.push({
|
|
2296
|
+
label: "\u65B0\u5EFA",
|
|
2297
|
+
href: createUrl,
|
|
2298
|
+
variant: "primary",
|
|
2299
|
+
target: "_blank"
|
|
2300
|
+
});
|
|
2301
|
+
} else {
|
|
2302
|
+
actions.push({
|
|
2303
|
+
label: "\u65B0\u5EFA",
|
|
2304
|
+
hxGet: createUrl,
|
|
2305
|
+
variant: "primary"
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
1905
2308
|
}
|
|
1906
2309
|
return actions;
|
|
1907
2310
|
}
|
|
@@ -2307,7 +2710,8 @@ function Dialog(props) {
|
|
|
2307
2710
|
className = "",
|
|
2308
2711
|
size = "lg",
|
|
2309
2712
|
closeOnBackdropClick = true,
|
|
2310
|
-
actions = []
|
|
2713
|
+
actions = [],
|
|
2714
|
+
fixedContentHeight = false
|
|
2311
2715
|
} = props;
|
|
2312
2716
|
const sizeClasses = {
|
|
2313
2717
|
sm: "max-w-md",
|
|
@@ -2366,7 +2770,13 @@ function Dialog(props) {
|
|
|
2366
2770
|
}
|
|
2367
2771
|
)
|
|
2368
2772
|
] }),
|
|
2369
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2773
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2774
|
+
"div",
|
|
2775
|
+
{
|
|
2776
|
+
className: `${fixedContentHeight ? "h-[70vh]" : "flex-1"} overflow-y-auto ${fixedContentHeight ? "p-0" : "p-6"} bg-gray-50`,
|
|
2777
|
+
children
|
|
2778
|
+
}
|
|
2779
|
+
),
|
|
2370
2780
|
actions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 border-t border-gray-200 bg-white flex justify-end gap-2", children: actions.map((action, index) => renderActionButton(action, index)) })
|
|
2371
2781
|
]
|
|
2372
2782
|
}
|
|
@@ -2627,6 +3037,13 @@ function BaseLayout(props) {
|
|
|
2627
3037
|
{
|
|
2628
3038
|
dangerouslySetInnerHTML: {
|
|
2629
3039
|
__html: `
|
|
3040
|
+
/* \u5BB9\u5668\u67E5\u8BE2\u652F\u6301 - \u5982\u679C Tailwind CDN \u4E0D\u652F\u6301\uFF0C\u4F7F\u7528\u539F\u751F CSS \u5BB9\u5668\u67E5\u8BE2 */
|
|
3041
|
+
@supports (container-type: inline-size) {
|
|
3042
|
+
.\\@container {
|
|
3043
|
+
container-type: inline-size;
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
|
|
2630
3047
|
@keyframes fadeIn {
|
|
2631
3048
|
from { opacity: 0;}
|
|
2632
3049
|
to { opacity: 1;}
|
|
@@ -2743,7 +3160,8 @@ function renderActionButton2(action, index) {
|
|
|
2743
3160
|
submit,
|
|
2744
3161
|
formId,
|
|
2745
3162
|
onClick,
|
|
2746
|
-
className = ""
|
|
3163
|
+
className = "",
|
|
3164
|
+
target
|
|
2747
3165
|
} = action;
|
|
2748
3166
|
if (submit && formId) {
|
|
2749
3167
|
const variantStyles = {
|
|
@@ -2795,18 +3213,21 @@ function renderActionButton2(action, index) {
|
|
|
2795
3213
|
} else if (label === "\u53D6\u6D88") {
|
|
2796
3214
|
testId = "cancel-button";
|
|
2797
3215
|
}
|
|
3216
|
+
const isNewWindow = target === "_blank";
|
|
2798
3217
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2799
3218
|
Button,
|
|
2800
3219
|
{
|
|
2801
3220
|
variant,
|
|
2802
3221
|
href,
|
|
2803
|
-
hxGet,
|
|
2804
|
-
hxPost,
|
|
2805
|
-
hxPut,
|
|
2806
|
-
hxDelete,
|
|
2807
|
-
hxConfirm: confirm,
|
|
3222
|
+
hxGet: !isNewWindow ? hxGet : void 0,
|
|
3223
|
+
hxPost: !isNewWindow ? hxPost : void 0,
|
|
3224
|
+
hxPut: !isNewWindow ? hxPut : void 0,
|
|
3225
|
+
hxDelete: !isNewWindow ? hxDelete : void 0,
|
|
3226
|
+
hxConfirm: !isNewWindow ? confirm : void 0,
|
|
2808
3227
|
className,
|
|
2809
3228
|
"data-testid": testId,
|
|
3229
|
+
target,
|
|
3230
|
+
rel: target === "_blank" ? "noopener noreferrer" : void 0,
|
|
2810
3231
|
children: label
|
|
2811
3232
|
},
|
|
2812
3233
|
index
|
|
@@ -3149,9 +3570,9 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3149
3570
|
headers
|
|
3150
3571
|
);
|
|
3151
3572
|
}
|
|
3152
|
-
if (context.redirectUrl) {
|
|
3573
|
+
if (context.redirectUrl && !context.refresh) {
|
|
3153
3574
|
imeanServiceEngine.logger.info(
|
|
3154
|
-
`[ResponseRenderer] Redirect URL found: ${context.redirectUrl} (isHtmxRequest: ${context.isHtmxRequest})`
|
|
3575
|
+
`[ResponseRenderer] Redirect URL found: ${context.redirectUrl} (isHtmxRequest: ${context.isHtmxRequest}, isDialog: ${context.isDialog})`
|
|
3155
3576
|
);
|
|
3156
3577
|
if (context.isHtmxRequest) {
|
|
3157
3578
|
return ctx.html(/* @__PURE__ */ jsxRuntime.jsx("div", {}), 200, {
|
|
@@ -3160,7 +3581,11 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3160
3581
|
} else {
|
|
3161
3582
|
return ctx.redirect(context.redirectUrl);
|
|
3162
3583
|
}
|
|
3163
|
-
} else {
|
|
3584
|
+
} else if (context.redirectUrl && context.refresh) {
|
|
3585
|
+
imeanServiceEngine.logger.info(
|
|
3586
|
+
`[ResponseRenderer] Both redirect URL and refresh are set, using refresh (isDialog: ${context.isDialog})`
|
|
3587
|
+
);
|
|
3588
|
+
} else if (!context.redirectUrl) {
|
|
3164
3589
|
imeanServiceEngine.logger.info(
|
|
3165
3590
|
`[ResponseRenderer] No redirect URL found (result: ${result === null ? "null" : typeof result}, isHtmxRequest: ${context.isHtmxRequest})`
|
|
3166
3591
|
);
|
|
@@ -3211,6 +3636,8 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3211
3636
|
if (context.isDialog) {
|
|
3212
3637
|
const dialogSize = renderOptions.feature?.dialogSize || "lg";
|
|
3213
3638
|
const closeOnBackdropClick = renderOptions.feature?.closeOnBackdropClick ?? true;
|
|
3639
|
+
const isFormFeature = renderOptions.feature?.type === "create" || renderOptions.feature?.type === "edit";
|
|
3640
|
+
const fixedContentHeight = isFormFeature;
|
|
3214
3641
|
return ctx.html(
|
|
3215
3642
|
/* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3216
3643
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -3220,6 +3647,7 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3220
3647
|
size: dialogSize,
|
|
3221
3648
|
closeOnBackdropClick,
|
|
3222
3649
|
actions,
|
|
3650
|
+
fixedContentHeight,
|
|
3223
3651
|
children: result
|
|
3224
3652
|
}
|
|
3225
3653
|
),
|
|
@@ -3480,6 +3908,28 @@ function registerPageRoutes(page, options) {
|
|
|
3480
3908
|
});
|
|
3481
3909
|
};
|
|
3482
3910
|
options.hono[route.method](fullPath, handler);
|
|
3911
|
+
if (route.method === "put" || route.method === "delete") {
|
|
3912
|
+
const postHandler = async (ctx) => {
|
|
3913
|
+
const methodOverride = ctx.req.header("X-HTTP-Method-Override");
|
|
3914
|
+
const expectedMethod = route.method.toUpperCase();
|
|
3915
|
+
if (methodOverride === expectedMethod) {
|
|
3916
|
+
imeanServiceEngine.logger.info(
|
|
3917
|
+
`[HtmxAdminPlugin] Method override detected: POST ${fullPath} -> ${expectedMethod} (feature: ${feature.name})`
|
|
3918
|
+
);
|
|
3919
|
+
return handleRequest(ctx, page, feature, {
|
|
3920
|
+
options: options.options
|
|
3921
|
+
});
|
|
3922
|
+
}
|
|
3923
|
+
imeanServiceEngine.logger.warn(
|
|
3924
|
+
`[HtmxAdminPlugin] POST request to ${fullPath} without matching X-HTTP-Method-Override header (got: ${methodOverride || "none"}, expected: ${expectedMethod})`
|
|
3925
|
+
);
|
|
3926
|
+
return ctx.text("Method Not Allowed", 405);
|
|
3927
|
+
};
|
|
3928
|
+
imeanServiceEngine.logger.info(
|
|
3929
|
+
`[HtmxAdminPlugin] Registering POST route for method override: POST ${fullPath} (actual method: ${route.method.toUpperCase()}, feature: ${feature.name})`
|
|
3930
|
+
);
|
|
3931
|
+
options.hono.post(fullPath, postHandler);
|
|
3932
|
+
}
|
|
3483
3933
|
}
|
|
3484
3934
|
}
|
|
3485
3935
|
}
|
|
@@ -3569,6 +4019,434 @@ var HtmxAdminPlugin = class {
|
|
|
3569
4019
|
registerHomeRedirect(this.pages, routeOptions);
|
|
3570
4020
|
}
|
|
3571
4021
|
};
|
|
4022
|
+
function ObjectEditor(props) {
|
|
4023
|
+
const { value, fieldName, objectSchema } = props;
|
|
4024
|
+
if (!objectSchema) {
|
|
4025
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 border border-yellow-300 rounded-lg bg-yellow-50 text-yellow-800 text-sm", children: "\u8BF7\u63D0\u4F9B objectSchema \u53C2\u6570\u4EE5\u4F7F\u7528\u5BF9\u8C61\u7F16\u8F91\u5668" });
|
|
4026
|
+
}
|
|
4027
|
+
const fields = parseSchemaToFields(objectSchema);
|
|
4028
|
+
const initialObject = value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
|
|
4029
|
+
fields.forEach((field) => {
|
|
4030
|
+
if (!(field.name in initialObject)) {
|
|
4031
|
+
if (field.type === "number") {
|
|
4032
|
+
initialObject[field.name] = field.required ? 0 : void 0;
|
|
4033
|
+
} else if (field.type === "checkbox") {
|
|
4034
|
+
initialObject[field.name] = field.required ? false : void 0;
|
|
4035
|
+
} else {
|
|
4036
|
+
initialObject[field.name] = field.required ? "" : void 0;
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
});
|
|
4040
|
+
const fieldNames = fields.map((f) => f.name);
|
|
4041
|
+
const fieldNamesJson = JSON.stringify(fieldNames);
|
|
4042
|
+
const initialValueJson = JSON.stringify(initialObject);
|
|
4043
|
+
const xDataContent = `{
|
|
4044
|
+
obj: {},
|
|
4045
|
+
init() {
|
|
4046
|
+
const dataAttr = this.$el.getAttribute('data-initial-value');
|
|
4047
|
+
if (dataAttr) {
|
|
4048
|
+
try {
|
|
4049
|
+
this.obj = JSON.parse(dataAttr);
|
|
4050
|
+
} catch (e) {
|
|
4051
|
+
console.error('Failed to parse initial value:', e);
|
|
4052
|
+
this.obj = {};
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
const fieldNames = ${fieldNamesJson};
|
|
4056
|
+
fieldNames.forEach(fieldName => {
|
|
4057
|
+
if (!(fieldName in this.obj)) {
|
|
4058
|
+
this.obj[fieldName] = undefined;
|
|
4059
|
+
}
|
|
4060
|
+
});
|
|
4061
|
+
this.updateHiddenField();
|
|
4062
|
+
},
|
|
4063
|
+
updateHiddenField() {
|
|
4064
|
+
const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
|
|
4065
|
+
if (hiddenInput) {
|
|
4066
|
+
hiddenInput.value = JSON.stringify(this.obj);
|
|
4067
|
+
}
|
|
4068
|
+
},
|
|
4069
|
+
updateField(fieldName, value, fieldType, required) {
|
|
4070
|
+
let convertedValue = value;
|
|
4071
|
+
if (fieldType === 'number') {
|
|
4072
|
+
convertedValue = value === '' ? (required ? 0 : undefined) : Number(value);
|
|
4073
|
+
if (isNaN(convertedValue)) convertedValue = required ? 0 : undefined;
|
|
4074
|
+
} else if (fieldType === 'checkbox') {
|
|
4075
|
+
convertedValue = value === 'true' || value === true || value === '1' || value === 1;
|
|
4076
|
+
} else {
|
|
4077
|
+
convertedValue = value || (required ? '' : undefined);
|
|
4078
|
+
}
|
|
4079
|
+
if (convertedValue === undefined && !required) {
|
|
4080
|
+
delete this.obj[fieldName];
|
|
4081
|
+
} else {
|
|
4082
|
+
this.obj[fieldName] = convertedValue;
|
|
4083
|
+
}
|
|
4084
|
+
this.updateHiddenField();
|
|
4085
|
+
}
|
|
4086
|
+
}`;
|
|
4087
|
+
const generateField = (field) => {
|
|
4088
|
+
const fieldId = `${fieldName}-${field.name}`;
|
|
4089
|
+
const fieldNameVar = `obj.${field.name}`;
|
|
4090
|
+
const requiredValue = field.required ? "true" : "false";
|
|
4091
|
+
const fieldNameForJs = JSON.stringify(field.name);
|
|
4092
|
+
let inputElement;
|
|
4093
|
+
if (field.type === "text") {
|
|
4094
|
+
inputElement = html.html`
|
|
4095
|
+
<input
|
|
4096
|
+
type="text"
|
|
4097
|
+
id="${fieldId}"
|
|
4098
|
+
x-bind:value="${fieldNameVar} || ''"
|
|
4099
|
+
x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
|
|
4100
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
4101
|
+
data-testid="${fieldName}-input-${field.name}"
|
|
4102
|
+
${field.required ? "required" : ""}
|
|
4103
|
+
/>
|
|
4104
|
+
`;
|
|
4105
|
+
} else if (field.type === "textarea") {
|
|
4106
|
+
inputElement = html.html`
|
|
4107
|
+
<textarea
|
|
4108
|
+
id="${fieldId}"
|
|
4109
|
+
x-bind:value="${fieldNameVar} || ''"
|
|
4110
|
+
x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
|
|
4111
|
+
rows="4"
|
|
4112
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-y"
|
|
4113
|
+
data-testid="${fieldName}-input-${field.name}"
|
|
4114
|
+
${field.required ? "required" : ""}
|
|
4115
|
+
></textarea>
|
|
4116
|
+
`;
|
|
4117
|
+
} else if (field.type === "number") {
|
|
4118
|
+
const step = field.step || (field.step === void 0 ? "1" : "any");
|
|
4119
|
+
inputElement = html.html`
|
|
4120
|
+
<input
|
|
4121
|
+
type="number"
|
|
4122
|
+
id="${fieldId}"
|
|
4123
|
+
x-bind:value="${fieldNameVar} != null ? ${fieldNameVar} : ''"
|
|
4124
|
+
x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'number', ${requiredValue})"
|
|
4125
|
+
step="${step}"
|
|
4126
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
4127
|
+
data-testid="${fieldName}-input-${field.name}"
|
|
4128
|
+
${field.required ? "required" : ""}
|
|
4129
|
+
/>
|
|
4130
|
+
`;
|
|
4131
|
+
} else if (field.type === "date") {
|
|
4132
|
+
inputElement = html.html`
|
|
4133
|
+
<input
|
|
4134
|
+
type="date"
|
|
4135
|
+
id="${fieldId}"
|
|
4136
|
+
x-bind:value="${fieldNameVar} || ''"
|
|
4137
|
+
x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'date', ${requiredValue})"
|
|
4138
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
4139
|
+
data-testid="${fieldName}-input-${field.name}"
|
|
4140
|
+
${field.required ? "required" : ""}
|
|
4141
|
+
/>
|
|
4142
|
+
`;
|
|
4143
|
+
} else if (field.type === "email") {
|
|
4144
|
+
inputElement = html.html`
|
|
4145
|
+
<input
|
|
4146
|
+
type="email"
|
|
4147
|
+
id="${fieldId}"
|
|
4148
|
+
x-bind:value="${fieldNameVar} || ''"
|
|
4149
|
+
x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
|
|
4150
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
4151
|
+
data-testid="${fieldName}-input-${field.name}"
|
|
4152
|
+
${field.required ? "required" : ""}
|
|
4153
|
+
/>
|
|
4154
|
+
`;
|
|
4155
|
+
} else if (field.type === "select" && field.options) {
|
|
4156
|
+
inputElement = html.html`
|
|
4157
|
+
<select
|
|
4158
|
+
id="${fieldId}"
|
|
4159
|
+
x-bind:value="${fieldNameVar} || ''"
|
|
4160
|
+
x-on:change="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
|
|
4161
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
|
|
4162
|
+
data-testid="${fieldName}-select-${field.name}"
|
|
4163
|
+
${field.required ? "required" : ""}
|
|
4164
|
+
>
|
|
4165
|
+
${!field.required ? html.html`<option value="">请选择</option>` : ""}
|
|
4166
|
+
${field.options.map(
|
|
4167
|
+
(option) => html.html`
|
|
4168
|
+
<option value="${String(option.value)}">${option.label}</option>
|
|
4169
|
+
`
|
|
4170
|
+
)}
|
|
4171
|
+
</select>
|
|
4172
|
+
`;
|
|
4173
|
+
} else if (field.type === "checkbox") {
|
|
4174
|
+
inputElement = html.html`
|
|
4175
|
+
<div class="flex items-center">
|
|
4176
|
+
<input
|
|
4177
|
+
type="checkbox"
|
|
4178
|
+
id="${fieldId}"
|
|
4179
|
+
x-bind:checked="${fieldNameVar} === true || ${fieldNameVar} === 'true' || ${fieldNameVar} === 1 || ${fieldNameVar} === '1'"
|
|
4180
|
+
x-on:change="updateField(${fieldNameForJs}, $event.target.checked, 'checkbox', ${requiredValue})"
|
|
4181
|
+
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
|
4182
|
+
data-testid="${fieldName}-checkbox-${field.name}"
|
|
4183
|
+
/>
|
|
4184
|
+
<label for="${fieldId}" class="ml-2 text-sm text-gray-700">
|
|
4185
|
+
${field.label}
|
|
4186
|
+
</label>
|
|
4187
|
+
</div>
|
|
4188
|
+
`;
|
|
4189
|
+
}
|
|
4190
|
+
return html.html`
|
|
4191
|
+
<div class="space-y-2" data-testid="${fieldName}-field-${field.name}">
|
|
4192
|
+
${field.type !== "checkbox" ? html.html`
|
|
4193
|
+
<label
|
|
4194
|
+
for="${fieldId}"
|
|
4195
|
+
class="block text-sm font-semibold text-gray-700"
|
|
4196
|
+
data-testid="${fieldName}-label-${field.name}"
|
|
4197
|
+
>
|
|
4198
|
+
${field.label}
|
|
4199
|
+
${field.required ? html.html`<span class="text-red-500 ml-1">*</span>` : ""}
|
|
4200
|
+
</label>
|
|
4201
|
+
` : ""}
|
|
4202
|
+
${inputElement}
|
|
4203
|
+
</div>
|
|
4204
|
+
`;
|
|
4205
|
+
};
|
|
4206
|
+
return html.html`
|
|
4207
|
+
<div
|
|
4208
|
+
x-data="${xDataContent}"
|
|
4209
|
+
data-initial-value="${initialValueJson}"
|
|
4210
|
+
x-init="init()"
|
|
4211
|
+
class="space-y-4"
|
|
4212
|
+
>
|
|
4213
|
+
<input
|
|
4214
|
+
type="hidden"
|
|
4215
|
+
name="${fieldName}"
|
|
4216
|
+
value=""
|
|
4217
|
+
data-testid="hidden-${fieldName}"
|
|
4218
|
+
/>
|
|
4219
|
+
<div class="space-y-4">
|
|
4220
|
+
${fields.map((field) => generateField(field))}
|
|
4221
|
+
</div>
|
|
4222
|
+
</div>
|
|
4223
|
+
`;
|
|
4224
|
+
}
|
|
4225
|
+
function StringArrayEditor(props) {
|
|
4226
|
+
const {
|
|
4227
|
+
value,
|
|
4228
|
+
fieldName,
|
|
4229
|
+
placeholder = "\u8BF7\u8F93\u5165\u5185\u5BB9",
|
|
4230
|
+
allowEmpty = false
|
|
4231
|
+
} = props;
|
|
4232
|
+
const initialItems = value || [];
|
|
4233
|
+
const initialValueJson = JSON.stringify(initialItems);
|
|
4234
|
+
const xDataContent = `{
|
|
4235
|
+
items: ${initialValueJson},
|
|
4236
|
+
draggedIndex: null,
|
|
4237
|
+
draggedOverIndex: null,
|
|
4238
|
+
fieldName: ${JSON.stringify(fieldName)},
|
|
4239
|
+
placeholder: ${JSON.stringify(placeholder)},
|
|
4240
|
+
allowEmpty: ${allowEmpty},
|
|
4241
|
+
init() {
|
|
4242
|
+
const dataAttr = this.$el.getAttribute('data-initial-value');
|
|
4243
|
+
if (dataAttr) {
|
|
4244
|
+
try {
|
|
4245
|
+
const parsed = JSON.parse(dataAttr);
|
|
4246
|
+
if (Array.isArray(parsed)) {
|
|
4247
|
+
this.items = parsed;
|
|
4248
|
+
} else {
|
|
4249
|
+
this.items = [];
|
|
4250
|
+
}
|
|
4251
|
+
} catch (e) {
|
|
4252
|
+
console.error('Failed to parse initial value:', e);
|
|
4253
|
+
this.items = [];
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
this.updateHiddenField();
|
|
4257
|
+
},
|
|
4258
|
+
updateHiddenField() {
|
|
4259
|
+
const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
|
|
4260
|
+
if (hiddenInput) {
|
|
4261
|
+
hiddenInput.value = JSON.stringify(this.items);
|
|
4262
|
+
}
|
|
4263
|
+
},
|
|
4264
|
+
addItem() {
|
|
4265
|
+
this.items.push('');
|
|
4266
|
+
this.updateHiddenField();
|
|
4267
|
+
this.$nextTick(() => {
|
|
4268
|
+
const keyInputs = this.$el.querySelectorAll('input[data-testid*="-input-"]');
|
|
4269
|
+
if (keyInputs.length > 0) {
|
|
4270
|
+
const lastInput = keyInputs[keyInputs.length - 1];
|
|
4271
|
+
if (lastInput && lastInput.focus) {
|
|
4272
|
+
lastInput.focus();
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
});
|
|
4276
|
+
},
|
|
4277
|
+
removeItem(index) {
|
|
4278
|
+
this.items.splice(index, 1);
|
|
4279
|
+
this.updateHiddenField();
|
|
4280
|
+
},
|
|
4281
|
+
updateItem(index, value) {
|
|
4282
|
+
this.items[index] = value;
|
|
4283
|
+
this.updateHiddenField();
|
|
4284
|
+
},
|
|
4285
|
+
handleDragStart(index, event) {
|
|
4286
|
+
this.draggedIndex = index;
|
|
4287
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
4288
|
+
event.dataTransfer.setData('text/plain', index.toString());
|
|
4289
|
+
const target = event.currentTarget || event.target.closest('[draggable="true"]');
|
|
4290
|
+
if (target) {
|
|
4291
|
+
target.style.opacity = '0.5';
|
|
4292
|
+
}
|
|
4293
|
+
},
|
|
4294
|
+
handleDragEnd(event) {
|
|
4295
|
+
const target = event.currentTarget || event.target.closest('[draggable="true"]');
|
|
4296
|
+
if (target) {
|
|
4297
|
+
target.style.opacity = '';
|
|
4298
|
+
}
|
|
4299
|
+
this.draggedIndex = null;
|
|
4300
|
+
this.draggedOverIndex = null;
|
|
4301
|
+
},
|
|
4302
|
+
handleDragOver(index, event) {
|
|
4303
|
+
event.preventDefault();
|
|
4304
|
+
event.dataTransfer.dropEffect = 'move';
|
|
4305
|
+
this.draggedOverIndex = index;
|
|
4306
|
+
},
|
|
4307
|
+
handleDragLeave() {
|
|
4308
|
+
this.draggedOverIndex = null;
|
|
4309
|
+
},
|
|
4310
|
+
handleDrop(index, event) {
|
|
4311
|
+
event.preventDefault();
|
|
4312
|
+
if (this.draggedIndex !== null && this.draggedIndex !== index) {
|
|
4313
|
+
const draggedItem = this.items[this.draggedIndex];
|
|
4314
|
+
this.items.splice(this.draggedIndex, 1);
|
|
4315
|
+
this.items.splice(index, 0, draggedItem);
|
|
4316
|
+
this.updateHiddenField();
|
|
4317
|
+
}
|
|
4318
|
+
this.draggedIndex = null;
|
|
4319
|
+
this.draggedOverIndex = null;
|
|
4320
|
+
}
|
|
4321
|
+
}`;
|
|
4322
|
+
return html.html`
|
|
4323
|
+
<div
|
|
4324
|
+
x-data="${xDataContent}"
|
|
4325
|
+
data-initial-value="${initialValueJson}"
|
|
4326
|
+
x-init="init()"
|
|
4327
|
+
class="space-y-3"
|
|
4328
|
+
>
|
|
4329
|
+
<input
|
|
4330
|
+
type="hidden"
|
|
4331
|
+
name="${fieldName}"
|
|
4332
|
+
value=""
|
|
4333
|
+
data-testid="hidden-${fieldName}"
|
|
4334
|
+
/>
|
|
4335
|
+
<div class="space-y-3">
|
|
4336
|
+
<!-- 头部:显示数量和添加按钮 -->
|
|
4337
|
+
<div class="flex items-center justify-between">
|
|
4338
|
+
<span class="text-sm text-gray-600">
|
|
4339
|
+
共 <span x-text="items.length">0</span> 项
|
|
4340
|
+
</span>
|
|
4341
|
+
<button
|
|
4342
|
+
type="button"
|
|
4343
|
+
x-on:click="addItem()"
|
|
4344
|
+
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium flex items-center gap-2"
|
|
4345
|
+
data-testid="${fieldName}-add-button"
|
|
4346
|
+
>
|
|
4347
|
+
<svg
|
|
4348
|
+
class="w-4 h-4"
|
|
4349
|
+
fill="none"
|
|
4350
|
+
stroke="currentColor"
|
|
4351
|
+
viewBox="0 0 24 24"
|
|
4352
|
+
>
|
|
4353
|
+
<path
|
|
4354
|
+
stroke-linecap="round"
|
|
4355
|
+
stroke-linejoin="round"
|
|
4356
|
+
stroke-width="2"
|
|
4357
|
+
d="M12 4v16m8-8H4"
|
|
4358
|
+
/>
|
|
4359
|
+
</svg>
|
|
4360
|
+
添加项
|
|
4361
|
+
</button>
|
|
4362
|
+
</div>
|
|
4363
|
+
|
|
4364
|
+
<!-- 列表项 -->
|
|
4365
|
+
<div class="space-y-2" x-show="items.length > 0">
|
|
4366
|
+
<template x-for="(item, index) in items" x-bind:key="index">
|
|
4367
|
+
<div
|
|
4368
|
+
class="flex items-center gap-2 group"
|
|
4369
|
+
x-bind:class="{
|
|
4370
|
+
'opacity-50': draggedIndex === index,
|
|
4371
|
+
'border-blue-300 bg-blue-50': draggedOverIndex === index && draggedIndex !== null && draggedIndex !== index
|
|
4372
|
+
}"
|
|
4373
|
+
draggable="true"
|
|
4374
|
+
x-on:dragstart="handleDragStart(index, $event)"
|
|
4375
|
+
x-on:dragend="handleDragEnd($event)"
|
|
4376
|
+
x-on:dragover="handleDragOver(index, $event)"
|
|
4377
|
+
x-on:dragleave="handleDragLeave()"
|
|
4378
|
+
x-on:drop="handleDrop(index, $event)"
|
|
4379
|
+
x-bind:data-testid="fieldName + '-item-' + index"
|
|
4380
|
+
>
|
|
4381
|
+
<!-- 拖拽手柄 -->
|
|
4382
|
+
<div
|
|
4383
|
+
class="flex-shrink-0 cursor-move text-gray-400 hover:text-gray-600 transition-colors p-1"
|
|
4384
|
+
x-bind:data-testid="fieldName + '-drag-handle-' + index"
|
|
4385
|
+
title="拖拽排序"
|
|
4386
|
+
>
|
|
4387
|
+
<svg
|
|
4388
|
+
class="w-5 h-5"
|
|
4389
|
+
fill="none"
|
|
4390
|
+
stroke="currentColor"
|
|
4391
|
+
viewBox="0 0 24 24"
|
|
4392
|
+
>
|
|
4393
|
+
<path
|
|
4394
|
+
stroke-linecap="round"
|
|
4395
|
+
stroke-linejoin="round"
|
|
4396
|
+
stroke-width="2"
|
|
4397
|
+
d="M4 8h16M4 16h16"
|
|
4398
|
+
/>
|
|
4399
|
+
</svg>
|
|
4400
|
+
</div>
|
|
4401
|
+
|
|
4402
|
+
<!-- 输入框 -->
|
|
4403
|
+
<input
|
|
4404
|
+
type="text"
|
|
4405
|
+
x-bind:value="items[index] || ''"
|
|
4406
|
+
x-on:input="updateItem(index, $event.target.value)"
|
|
4407
|
+
x-bind:placeholder="placeholder + ' ' + (index + 1)"
|
|
4408
|
+
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
4409
|
+
x-bind:data-testid="fieldName + '-input-' + index"
|
|
4410
|
+
x-bind:required="!allowEmpty"
|
|
4411
|
+
/>
|
|
4412
|
+
|
|
4413
|
+
<!-- 删除按钮 -->
|
|
4414
|
+
<button
|
|
4415
|
+
type="button"
|
|
4416
|
+
x-on:click="removeItem(index)"
|
|
4417
|
+
class="flex-shrink-0 px-3 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
|
4418
|
+
x-bind:data-testid="fieldName + '-remove-button-' + index"
|
|
4419
|
+
title="删除此项"
|
|
4420
|
+
>
|
|
4421
|
+
<svg
|
|
4422
|
+
class="w-5 h-5"
|
|
4423
|
+
fill="none"
|
|
4424
|
+
stroke="currentColor"
|
|
4425
|
+
viewBox="0 0 24 24"
|
|
4426
|
+
>
|
|
4427
|
+
<path
|
|
4428
|
+
stroke-linecap="round"
|
|
4429
|
+
stroke-linejoin="round"
|
|
4430
|
+
stroke-width="2"
|
|
4431
|
+
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
4432
|
+
/>
|
|
4433
|
+
</svg>
|
|
4434
|
+
</button>
|
|
4435
|
+
</div>
|
|
4436
|
+
</template>
|
|
4437
|
+
</div>
|
|
4438
|
+
|
|
4439
|
+
<!-- 空状态提示 -->
|
|
4440
|
+
<div
|
|
4441
|
+
x-show="items.length === 0"
|
|
4442
|
+
class="text-center py-8 text-gray-400 text-sm border border-dashed border-gray-300 rounded-lg"
|
|
4443
|
+
>
|
|
4444
|
+
暂无项,点击"添加项"按钮添加
|
|
4445
|
+
</div>
|
|
4446
|
+
</div>
|
|
4447
|
+
</div>
|
|
4448
|
+
`;
|
|
4449
|
+
}
|
|
3572
4450
|
|
|
3573
4451
|
exports.BaseFeature = BaseFeature;
|
|
3574
4452
|
exports.CustomFeature = CustomFeature;
|
|
@@ -3581,7 +4459,9 @@ exports.Dialog = Dialog;
|
|
|
3581
4459
|
exports.ErrorAlert = ErrorAlert;
|
|
3582
4460
|
exports.HtmxAdminPlugin = HtmxAdminPlugin;
|
|
3583
4461
|
exports.LoadingBar = LoadingBar;
|
|
4462
|
+
exports.ObjectEditor = ObjectEditor;
|
|
3584
4463
|
exports.PageModel = PageModel;
|
|
4464
|
+
exports.StringArrayEditor = StringArrayEditor;
|
|
3585
4465
|
exports.checkUserPermission = checkUserPermission;
|
|
3586
4466
|
exports.getUserInfo = getUserInfo;
|
|
3587
4467
|
exports.modelNameToPath = modelNameToPath;
|