imean-service-engine-htmx-plugin 2.3.0 → 2.3.1
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 +23 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +545 -174
- package/dist/index.mjs +545 -175
- package/package.json +14 -13
package/dist/index.mjs
CHANGED
|
@@ -340,203 +340,295 @@ function renderFormField(field, initialData, formFieldRenderers) {
|
|
|
340
340
|
} else if (value) {
|
|
341
341
|
parsedValue = value;
|
|
342
342
|
}
|
|
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",
|
|
343
|
+
return /* @__PURE__ */ jsxs(
|
|
344
|
+
"div",
|
|
394
345
|
{
|
|
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}`,
|
|
346
|
+
className: "space-y-2",
|
|
347
|
+
"data-testid": `field-${field.name}`,
|
|
400
348
|
children: [
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
"
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
349
|
+
/* @__PURE__ */ jsxs(
|
|
350
|
+
"label",
|
|
351
|
+
{
|
|
352
|
+
htmlFor: field.name,
|
|
353
|
+
className: "block text-sm font-semibold text-gray-700",
|
|
354
|
+
"data-testid": `label-${field.name}`,
|
|
355
|
+
children: [
|
|
356
|
+
field.label,
|
|
357
|
+
field.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", children: "*" })
|
|
358
|
+
]
|
|
359
|
+
}
|
|
360
|
+
),
|
|
361
|
+
/* @__PURE__ */ jsx("div", { children: customRenderer({
|
|
362
|
+
field,
|
|
363
|
+
value: parsedValue,
|
|
364
|
+
initialData,
|
|
365
|
+
fieldName: field.name
|
|
366
|
+
}) }),
|
|
367
|
+
field.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
|
|
415
368
|
]
|
|
416
369
|
},
|
|
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}`
|
|
370
|
+
field.name
|
|
443
371
|
);
|
|
444
372
|
}
|
|
445
|
-
|
|
446
|
-
|
|
373
|
+
if (field.type === "checkbox") {
|
|
374
|
+
const isChecked = value === "true" || value === "1" || value === "on" || String(value).toLowerCase() === "true";
|
|
375
|
+
return /* @__PURE__ */ jsxs(
|
|
447
376
|
"div",
|
|
448
377
|
{
|
|
449
|
-
|
|
450
|
-
|
|
378
|
+
className: "space-y-2",
|
|
379
|
+
"data-testid": `field-${field.name}`,
|
|
451
380
|
children: [
|
|
452
381
|
/* @__PURE__ */ jsxs(
|
|
453
|
-
"
|
|
382
|
+
"label",
|
|
454
383
|
{
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
viewBox: "0 0 24 24",
|
|
384
|
+
htmlFor: field.name,
|
|
385
|
+
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",
|
|
386
|
+
"data-testid": `label-${field.name}`,
|
|
459
387
|
children: [
|
|
460
388
|
/* @__PURE__ */ jsx(
|
|
461
|
-
"
|
|
389
|
+
"input",
|
|
462
390
|
{
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
391
|
+
type: "checkbox",
|
|
392
|
+
id: field.name,
|
|
393
|
+
name: field.name,
|
|
394
|
+
value: "true",
|
|
395
|
+
checked: isChecked,
|
|
396
|
+
required: field.required,
|
|
397
|
+
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",
|
|
398
|
+
"data-testid": `input-${field.name}`
|
|
469
399
|
}
|
|
470
400
|
),
|
|
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
|
-
)
|
|
401
|
+
/* @__PURE__ */ jsxs("span", { className: "text-sm font-semibold text-gray-700 select-none flex-1", children: [
|
|
402
|
+
field.label,
|
|
403
|
+
field.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", children: "*" })
|
|
404
|
+
] })
|
|
479
405
|
]
|
|
480
406
|
}
|
|
481
407
|
),
|
|
482
|
-
/* @__PURE__ */ jsx("
|
|
408
|
+
field.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 ml-8", children: field.description })
|
|
483
409
|
]
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
"
|
|
509
|
-
|
|
510
|
-
|
|
410
|
+
},
|
|
411
|
+
field.name
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
const shouldUseTextarea = field.type === "textarea" || value && isJsonString(value) && field.type !== "select";
|
|
415
|
+
return /* @__PURE__ */ jsxs(
|
|
416
|
+
"div",
|
|
417
|
+
{
|
|
418
|
+
className: "space-y-2",
|
|
419
|
+
"data-testid": `field-${field.name}`,
|
|
420
|
+
children: [
|
|
421
|
+
/* @__PURE__ */ jsxs(
|
|
422
|
+
"label",
|
|
423
|
+
{
|
|
424
|
+
htmlFor: field.name,
|
|
425
|
+
className: "block text-sm font-semibold text-gray-700",
|
|
426
|
+
"data-testid": `label-${field.name}`,
|
|
427
|
+
children: [
|
|
428
|
+
field.label,
|
|
429
|
+
field.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", children: "*" })
|
|
430
|
+
]
|
|
431
|
+
}
|
|
432
|
+
),
|
|
433
|
+
shouldUseTextarea ? /* @__PURE__ */ jsx(
|
|
434
|
+
"textarea",
|
|
435
|
+
{
|
|
436
|
+
id: field.name,
|
|
437
|
+
name: field.name,
|
|
438
|
+
required: field.required,
|
|
439
|
+
placeholder: field.placeholder || (isJsonString(value) ? "JSON \u683C\u5F0F\u6570\u636E" : ""),
|
|
440
|
+
rows: isJsonString(value) ? 10 : 4,
|
|
441
|
+
className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 resize-y font-mono text-sm",
|
|
442
|
+
"data-testid": `input-${field.name}`,
|
|
443
|
+
children: isJsonString(value) ? formatJsonString(value) : value
|
|
444
|
+
},
|
|
445
|
+
`${field.name}-${value}`
|
|
446
|
+
) : field.type === "select" ? /* @__PURE__ */ jsxs(
|
|
447
|
+
"select",
|
|
448
|
+
{
|
|
449
|
+
id: field.name,
|
|
450
|
+
name: field.name,
|
|
451
|
+
required: field.required,
|
|
452
|
+
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",
|
|
453
|
+
"data-testid": `select-${field.name}`,
|
|
454
|
+
children: [
|
|
455
|
+
!field.required && /* @__PURE__ */ jsx("option", { value: "", selected: !value || value === "", children: "\u8BF7\u9009\u62E9" }),
|
|
456
|
+
field.options && field.options.length > 0 ? field.options.map((option) => {
|
|
457
|
+
const optionValue = String(option.value);
|
|
458
|
+
const isSelected = value === optionValue;
|
|
459
|
+
return /* @__PURE__ */ jsx(
|
|
460
|
+
"option",
|
|
461
|
+
{
|
|
462
|
+
value: optionValue,
|
|
463
|
+
selected: isSelected,
|
|
464
|
+
children: option.label
|
|
465
|
+
},
|
|
466
|
+
optionValue
|
|
467
|
+
);
|
|
468
|
+
}) : /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "\u6682\u65E0\u9009\u9879" })
|
|
469
|
+
]
|
|
470
|
+
},
|
|
471
|
+
`${field.name}-${value || ""}`
|
|
472
|
+
) : /* @__PURE__ */ jsx(
|
|
473
|
+
"input",
|
|
474
|
+
{
|
|
475
|
+
type: field.type || "text",
|
|
476
|
+
id: field.name,
|
|
477
|
+
name: field.name,
|
|
478
|
+
required: field.required,
|
|
479
|
+
placeholder: field.placeholder,
|
|
480
|
+
step: field.type === "number" ? field.step ?? void 0 : void 0,
|
|
481
|
+
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",
|
|
482
|
+
value,
|
|
483
|
+
"data-testid": `input-${field.name}`
|
|
484
|
+
},
|
|
485
|
+
`${field.name}-${value}`
|
|
486
|
+
),
|
|
487
|
+
field.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
|
|
488
|
+
]
|
|
489
|
+
},
|
|
490
|
+
field.name
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
function FormPage(props) {
|
|
494
|
+
const {
|
|
495
|
+
fields,
|
|
496
|
+
groups,
|
|
497
|
+
submitUrl,
|
|
498
|
+
method = "post",
|
|
499
|
+
initialData,
|
|
500
|
+
formId,
|
|
501
|
+
isDialog = false,
|
|
502
|
+
formFieldRenderers
|
|
503
|
+
} = props;
|
|
504
|
+
const finalFormId = formId || `form-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
505
|
+
if (process.env.NODE_ENV === "development" && initialData) {
|
|
506
|
+
const fieldNames = fields ? fields.map((f) => f.name).join(", ") : groups ? groups.map((g) => g.fields.map((f) => f.name).join(", ")).join(" | ") : "";
|
|
507
|
+
console.log(
|
|
508
|
+
`[FormPage] initialData: ${JSON.stringify(initialData)}, fields: ${fieldNames}`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
return /* @__PURE__ */ jsxs(
|
|
512
|
+
"div",
|
|
513
|
+
{
|
|
514
|
+
className: "w-full",
|
|
515
|
+
"data-testid": "form-container",
|
|
516
|
+
"x-data": `{ activeTab: 0 }`,
|
|
517
|
+
children: [
|
|
518
|
+
/* @__PURE__ */ jsxs(
|
|
519
|
+
"div",
|
|
520
|
+
{
|
|
521
|
+
id: "form-loading-indicator",
|
|
522
|
+
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",
|
|
523
|
+
children: [
|
|
524
|
+
/* @__PURE__ */ jsxs(
|
|
525
|
+
"svg",
|
|
526
|
+
{
|
|
527
|
+
className: "animate-spin h-4 w-4",
|
|
528
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
529
|
+
fill: "none",
|
|
530
|
+
viewBox: "0 0 24 24",
|
|
531
|
+
children: [
|
|
532
|
+
/* @__PURE__ */ jsx(
|
|
533
|
+
"circle",
|
|
534
|
+
{
|
|
535
|
+
className: "opacity-25",
|
|
536
|
+
cx: "12",
|
|
537
|
+
cy: "12",
|
|
538
|
+
r: "10",
|
|
539
|
+
stroke: "currentColor",
|
|
540
|
+
strokeWidth: "4"
|
|
541
|
+
}
|
|
542
|
+
),
|
|
543
|
+
/* @__PURE__ */ jsx(
|
|
544
|
+
"path",
|
|
545
|
+
{
|
|
546
|
+
className: "opacity-75",
|
|
547
|
+
fill: "currentColor",
|
|
548
|
+
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"
|
|
549
|
+
}
|
|
550
|
+
)
|
|
551
|
+
]
|
|
552
|
+
}
|
|
553
|
+
),
|
|
554
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "\u63D0\u4EA4\u4E2D..." })
|
|
555
|
+
]
|
|
556
|
+
}
|
|
557
|
+
),
|
|
558
|
+
groups && groups.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
559
|
+
/* @__PURE__ */ jsx(
|
|
511
560
|
"div",
|
|
512
561
|
{
|
|
513
|
-
"
|
|
562
|
+
className: `sticky top-0 z-10 bg-white border-b border-gray-200 shadow-sm ${isDialog ? "px-6" : "-mx-6 px-6 -mt-6"}`,
|
|
563
|
+
children: /* @__PURE__ */ jsx(
|
|
564
|
+
"nav",
|
|
565
|
+
{
|
|
566
|
+
className: "flex -mb-px w-full",
|
|
567
|
+
"aria-label": "Tabs",
|
|
568
|
+
"data-testid": "form-tabs",
|
|
569
|
+
children: groups.map((group, index) => /* @__PURE__ */ jsx(
|
|
570
|
+
"button",
|
|
571
|
+
{
|
|
572
|
+
type: "button",
|
|
573
|
+
"x-on:click": `activeTab = ${index}`,
|
|
574
|
+
"x-bind:class": `activeTab === ${index} ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'`,
|
|
575
|
+
className: "px-6 py-4 text-sm font-medium border-b-2 transition-colors duration-150 whitespace-nowrap flex-1 text-center",
|
|
576
|
+
"data-testid": `form-tab-${index}`,
|
|
577
|
+
children: group.label
|
|
578
|
+
},
|
|
579
|
+
index
|
|
580
|
+
))
|
|
581
|
+
}
|
|
582
|
+
)
|
|
583
|
+
}
|
|
584
|
+
),
|
|
585
|
+
/* @__PURE__ */ jsx(
|
|
586
|
+
"form",
|
|
587
|
+
{
|
|
588
|
+
id: finalFormId,
|
|
589
|
+
method: method === "put" ? "post" : method,
|
|
590
|
+
action: submitUrl,
|
|
591
|
+
"hx-boost": "true",
|
|
592
|
+
...method === "put" ? { "hx-put": submitUrl } : {},
|
|
593
|
+
"hx-indicator": "#form-loading-indicator",
|
|
594
|
+
"data-testid": "form",
|
|
595
|
+
className: isDialog ? "p-6" : "mt-6",
|
|
596
|
+
children: /* @__PURE__ */ jsx("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm", children: /* @__PURE__ */ jsx("div", { className: "p-6", children: groups.map((group, index) => /* @__PURE__ */ jsx(
|
|
597
|
+
"div",
|
|
598
|
+
{
|
|
599
|
+
"x-show": `activeTab === ${index}`,
|
|
600
|
+
className: "space-y-6",
|
|
601
|
+
"data-testid": `form-tab-content-${index}`,
|
|
602
|
+
children: group.fields.map(
|
|
603
|
+
(field) => renderFormField(field, initialData, formFieldRenderers)
|
|
604
|
+
)
|
|
605
|
+
},
|
|
606
|
+
index
|
|
607
|
+
)) }) })
|
|
608
|
+
}
|
|
609
|
+
)
|
|
610
|
+
] }) : (
|
|
611
|
+
/* 平铺模式(向后兼容) */
|
|
612
|
+
/* @__PURE__ */ jsx(
|
|
613
|
+
"form",
|
|
614
|
+
{
|
|
615
|
+
id: finalFormId,
|
|
616
|
+
method: method === "put" ? "post" : method,
|
|
617
|
+
action: submitUrl,
|
|
618
|
+
"hx-boost": "true",
|
|
619
|
+
...method === "put" ? { "hx-put": submitUrl } : {},
|
|
620
|
+
"hx-indicator": "#form-loading-indicator",
|
|
514
621
|
className: "space-y-6",
|
|
515
|
-
"data-testid":
|
|
516
|
-
children:
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
] });
|
|
622
|
+
"data-testid": "form",
|
|
623
|
+
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(
|
|
624
|
+
(field) => renderFormField(field, initialData, formFieldRenderers)
|
|
625
|
+
) }) })
|
|
626
|
+
}
|
|
627
|
+
)
|
|
628
|
+
)
|
|
629
|
+
]
|
|
630
|
+
}
|
|
631
|
+
);
|
|
540
632
|
}
|
|
541
633
|
|
|
542
634
|
// src/utils/form-data-processor.ts
|
|
@@ -4445,5 +4537,283 @@ function StringArrayEditor(props) {
|
|
|
4445
4537
|
</div>
|
|
4446
4538
|
`;
|
|
4447
4539
|
}
|
|
4540
|
+
function TagsEditor(props) {
|
|
4541
|
+
const {
|
|
4542
|
+
value,
|
|
4543
|
+
fieldName,
|
|
4544
|
+
placeholder = "\u8F93\u5165\u6807\u7B7E\u540E\u6309\u56DE\u8F66\u6DFB\u52A0",
|
|
4545
|
+
allowEmpty = false
|
|
4546
|
+
} = props;
|
|
4547
|
+
const initialItems = value || [];
|
|
4548
|
+
const initialValueJson = JSON.stringify(initialItems);
|
|
4549
|
+
const xDataContent = `{
|
|
4550
|
+
items: ${initialValueJson},
|
|
4551
|
+
draggedIndex: null,
|
|
4552
|
+
draggedOverIndex: null,
|
|
4553
|
+
newTag: '',
|
|
4554
|
+
editingIndex: null,
|
|
4555
|
+
editingValue: '',
|
|
4556
|
+
fieldName: ${JSON.stringify(fieldName)},
|
|
4557
|
+
placeholder: ${JSON.stringify(placeholder)},
|
|
4558
|
+
allowEmpty: ${allowEmpty},
|
|
4559
|
+
init() {
|
|
4560
|
+
const dataAttr = this.$el.getAttribute('data-initial-value');
|
|
4561
|
+
if (dataAttr) {
|
|
4562
|
+
try {
|
|
4563
|
+
const parsed = JSON.parse(dataAttr);
|
|
4564
|
+
if (Array.isArray(parsed)) {
|
|
4565
|
+
this.items = parsed;
|
|
4566
|
+
} else {
|
|
4567
|
+
this.items = [];
|
|
4568
|
+
}
|
|
4569
|
+
} catch (e) {
|
|
4570
|
+
console.error('Failed to parse initial value:', e);
|
|
4571
|
+
this.items = [];
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4574
|
+
this.updateHiddenField();
|
|
4575
|
+
},
|
|
4576
|
+
updateHiddenField() {
|
|
4577
|
+
const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
|
|
4578
|
+
if (hiddenInput) {
|
|
4579
|
+
hiddenInput.value = JSON.stringify(this.items);
|
|
4580
|
+
}
|
|
4581
|
+
},
|
|
4582
|
+
addTag() {
|
|
4583
|
+
const trimmed = this.newTag.trim();
|
|
4584
|
+
if (trimmed && !this.items.includes(trimmed)) {
|
|
4585
|
+
this.items.push(trimmed);
|
|
4586
|
+
this.newTag = '';
|
|
4587
|
+
this.updateHiddenField();
|
|
4588
|
+
}
|
|
4589
|
+
},
|
|
4590
|
+
handleKeyDown(event) {
|
|
4591
|
+
if (event.key === 'Enter') {
|
|
4592
|
+
event.preventDefault();
|
|
4593
|
+
this.addTag();
|
|
4594
|
+
}
|
|
4595
|
+
},
|
|
4596
|
+
removeTag(index) {
|
|
4597
|
+
this.items.splice(index, 1);
|
|
4598
|
+
this.updateHiddenField();
|
|
4599
|
+
},
|
|
4600
|
+
startEdit(index) {
|
|
4601
|
+
this.editingIndex = index;
|
|
4602
|
+
this.editingValue = this.items[index];
|
|
4603
|
+
this.$nextTick(() => {
|
|
4604
|
+
const input = this.$el.querySelector('input[data-testid="${fieldName}-tag-edit-input-' + index + '"]');
|
|
4605
|
+
if (input && input.focus) {
|
|
4606
|
+
input.focus();
|
|
4607
|
+
input.select();
|
|
4608
|
+
}
|
|
4609
|
+
});
|
|
4610
|
+
},
|
|
4611
|
+
saveEdit(index) {
|
|
4612
|
+
const trimmed = this.editingValue.trim();
|
|
4613
|
+
if (trimmed) {
|
|
4614
|
+
// \u68C0\u67E5\u662F\u5426\u4E0E\u5176\u4ED6\u6807\u7B7E\u91CD\u590D\uFF08\u6392\u9664\u5F53\u524D\u7F16\u8F91\u7684\u6807\u7B7E\uFF09
|
|
4615
|
+
const isDuplicate = this.items.some((item, i) => i !== index && item === trimmed);
|
|
4616
|
+
if (!isDuplicate) {
|
|
4617
|
+
this.items[index] = trimmed;
|
|
4618
|
+
this.updateHiddenField();
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
this.cancelEdit();
|
|
4622
|
+
},
|
|
4623
|
+
cancelEdit() {
|
|
4624
|
+
this.editingIndex = null;
|
|
4625
|
+
this.editingValue = '';
|
|
4626
|
+
},
|
|
4627
|
+
handleEditKeyDown(index, event) {
|
|
4628
|
+
if (event.key === 'Enter') {
|
|
4629
|
+
event.preventDefault();
|
|
4630
|
+
this.saveEdit(index);
|
|
4631
|
+
} else if (event.key === 'Escape') {
|
|
4632
|
+
event.preventDefault();
|
|
4633
|
+
this.cancelEdit();
|
|
4634
|
+
}
|
|
4635
|
+
},
|
|
4636
|
+
handleDragStart(index, event) {
|
|
4637
|
+
this.draggedIndex = index;
|
|
4638
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
4639
|
+
event.dataTransfer.setData('text/plain', index.toString());
|
|
4640
|
+
const target = event.currentTarget || event.target.closest('[draggable="true"]');
|
|
4641
|
+
if (target) {
|
|
4642
|
+
target.style.opacity = '0.5';
|
|
4643
|
+
}
|
|
4644
|
+
},
|
|
4645
|
+
handleDragEnd(event) {
|
|
4646
|
+
const target = event.currentTarget || event.target.closest('[draggable="true"]');
|
|
4647
|
+
if (target) {
|
|
4648
|
+
target.style.opacity = '';
|
|
4649
|
+
}
|
|
4650
|
+
this.draggedIndex = null;
|
|
4651
|
+
this.draggedOverIndex = null;
|
|
4652
|
+
},
|
|
4653
|
+
handleDragOver(index, event) {
|
|
4654
|
+
event.preventDefault();
|
|
4655
|
+
event.dataTransfer.dropEffect = 'move';
|
|
4656
|
+
this.draggedOverIndex = index;
|
|
4657
|
+
},
|
|
4658
|
+
handleDragLeave() {
|
|
4659
|
+
this.draggedOverIndex = null;
|
|
4660
|
+
},
|
|
4661
|
+
handleDrop(index, event) {
|
|
4662
|
+
event.preventDefault();
|
|
4663
|
+
if (this.draggedIndex !== null && this.draggedIndex !== index) {
|
|
4664
|
+
const draggedItem = this.items[this.draggedIndex];
|
|
4665
|
+
this.items.splice(this.draggedIndex, 1);
|
|
4666
|
+
this.items.splice(index, 0, draggedItem);
|
|
4667
|
+
this.updateHiddenField();
|
|
4668
|
+
}
|
|
4669
|
+
this.draggedIndex = null;
|
|
4670
|
+
this.draggedOverIndex = null;
|
|
4671
|
+
}
|
|
4672
|
+
}`;
|
|
4673
|
+
return html`
|
|
4674
|
+
<div
|
|
4675
|
+
x-data="${xDataContent}"
|
|
4676
|
+
data-initial-value="${initialValueJson}"
|
|
4677
|
+
x-init="init()"
|
|
4678
|
+
class="space-y-2"
|
|
4679
|
+
>
|
|
4680
|
+
<input
|
|
4681
|
+
type="hidden"
|
|
4682
|
+
name="${fieldName}"
|
|
4683
|
+
x-bind:value="JSON.stringify(items)"
|
|
4684
|
+
data-testid="hidden-${fieldName}"
|
|
4685
|
+
/>
|
|
4686
|
+
|
|
4687
|
+
<!-- 输入框:用于添加新标签 -->
|
|
4688
|
+
<div class="flex items-center gap-2">
|
|
4689
|
+
<input
|
|
4690
|
+
type="text"
|
|
4691
|
+
x-model="newTag"
|
|
4692
|
+
x-on:keydown="handleKeyDown($event)"
|
|
4693
|
+
x-bind:placeholder="placeholder"
|
|
4694
|
+
class="flex-1 px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
4695
|
+
data-testid="${fieldName}-input"
|
|
4696
|
+
/>
|
|
4697
|
+
<button
|
|
4698
|
+
type="button"
|
|
4699
|
+
x-on:click="addTag()"
|
|
4700
|
+
class="px-3 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors text-sm font-medium flex items-center gap-1"
|
|
4701
|
+
data-testid="${fieldName}-add-button"
|
|
4702
|
+
>
|
|
4703
|
+
<svg
|
|
4704
|
+
class="w-4 h-4"
|
|
4705
|
+
fill="none"
|
|
4706
|
+
stroke="currentColor"
|
|
4707
|
+
viewBox="0 0 24 24"
|
|
4708
|
+
>
|
|
4709
|
+
<path
|
|
4710
|
+
stroke-linecap="round"
|
|
4711
|
+
stroke-linejoin="round"
|
|
4712
|
+
stroke-width="2"
|
|
4713
|
+
d="M12 4v16m8-8H4"
|
|
4714
|
+
/>
|
|
4715
|
+
</svg>
|
|
4716
|
+
添加
|
|
4717
|
+
</button>
|
|
4718
|
+
</div>
|
|
4719
|
+
|
|
4720
|
+
<!-- 标签列表 -->
|
|
4721
|
+
<div
|
|
4722
|
+
class="flex flex-wrap gap-2"
|
|
4723
|
+
x-show="items.length > 0"
|
|
4724
|
+
data-testid="${fieldName}-tags-container"
|
|
4725
|
+
>
|
|
4726
|
+
<template x-for="(item, index) in items" x-bind:key="index">
|
|
4727
|
+
<div
|
|
4728
|
+
class="inline-flex items-center gap-1 px-2 py-1 bg-blue-50 border border-blue-200 rounded-md text-sm group"
|
|
4729
|
+
x-bind:class="{
|
|
4730
|
+
'opacity-50': draggedIndex === index,
|
|
4731
|
+
'ring-2 ring-blue-400': draggedOverIndex === index && draggedIndex !== null && draggedIndex !== index
|
|
4732
|
+
}"
|
|
4733
|
+
draggable="true"
|
|
4734
|
+
x-on:dragstart="handleDragStart(index, $event)"
|
|
4735
|
+
x-on:dragend="handleDragEnd($event)"
|
|
4736
|
+
x-on:dragover="handleDragOver(index, $event)"
|
|
4737
|
+
x-on:dragleave="handleDragLeave()"
|
|
4738
|
+
x-on:drop="handleDrop(index, $event)"
|
|
4739
|
+
x-bind:data-testid="fieldName + '-tag-' + index"
|
|
4740
|
+
>
|
|
4741
|
+
<!-- 拖拽手柄 -->
|
|
4742
|
+
<div
|
|
4743
|
+
class="flex-shrink-0 cursor-move text-blue-400 hover:text-blue-600 transition-colors"
|
|
4744
|
+
x-bind:data-testid="fieldName + '-drag-handle-' + index"
|
|
4745
|
+
title="拖拽排序"
|
|
4746
|
+
>
|
|
4747
|
+
<svg
|
|
4748
|
+
class="w-3 h-3"
|
|
4749
|
+
fill="none"
|
|
4750
|
+
stroke="currentColor"
|
|
4751
|
+
viewBox="0 0 24 24"
|
|
4752
|
+
>
|
|
4753
|
+
<path
|
|
4754
|
+
stroke-linecap="round"
|
|
4755
|
+
stroke-linejoin="round"
|
|
4756
|
+
stroke-width="2"
|
|
4757
|
+
d="M4 8h16M4 16h16"
|
|
4758
|
+
/>
|
|
4759
|
+
</svg>
|
|
4760
|
+
</div>
|
|
4761
|
+
|
|
4762
|
+
<!-- 标签内容:显示模式 -->
|
|
4763
|
+
<span
|
|
4764
|
+
x-show="editingIndex !== index"
|
|
4765
|
+
class="flex-1 text-blue-900 cursor-pointer"
|
|
4766
|
+
x-on:click="startEdit(index)"
|
|
4767
|
+
x-text="item"
|
|
4768
|
+
x-bind:data-testid="fieldName + '-tag-text-' + index"
|
|
4769
|
+
></span>
|
|
4770
|
+
<!-- 标签内容:编辑模式 -->
|
|
4771
|
+
<input
|
|
4772
|
+
x-show="editingIndex === index"
|
|
4773
|
+
type="text"
|
|
4774
|
+
x-model="editingValue"
|
|
4775
|
+
x-on:keydown="handleEditKeyDown(index, $event)"
|
|
4776
|
+
x-on:blur="saveEdit(index)"
|
|
4777
|
+
class="flex-1 px-1 py-0.5 border border-blue-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-500 min-w-[60px]"
|
|
4778
|
+
x-bind:data-testid="fieldName + '-tag-edit-input-' + index"
|
|
4779
|
+
/>
|
|
4780
|
+
|
|
4781
|
+
<!-- 删除按钮 -->
|
|
4782
|
+
<button
|
|
4783
|
+
type="button"
|
|
4784
|
+
x-on:click="removeTag(index)"
|
|
4785
|
+
class="flex-shrink-0 text-blue-600 hover:text-red-600 hover:bg-red-50 rounded transition-colors p-0.5"
|
|
4786
|
+
x-bind:data-testid="fieldName + '-tag-remove-' + index"
|
|
4787
|
+
title="删除标签"
|
|
4788
|
+
>
|
|
4789
|
+
<svg
|
|
4790
|
+
class="w-3.5 h-3.5"
|
|
4791
|
+
fill="none"
|
|
4792
|
+
stroke="currentColor"
|
|
4793
|
+
viewBox="0 0 24 24"
|
|
4794
|
+
>
|
|
4795
|
+
<path
|
|
4796
|
+
stroke-linecap="round"
|
|
4797
|
+
stroke-linejoin="round"
|
|
4798
|
+
stroke-width="2"
|
|
4799
|
+
d="M6 18L18 6M6 6l12 12"
|
|
4800
|
+
/>
|
|
4801
|
+
</svg>
|
|
4802
|
+
</button>
|
|
4803
|
+
</div>
|
|
4804
|
+
</template>
|
|
4805
|
+
</div>
|
|
4806
|
+
|
|
4807
|
+
<!-- 空状态提示 -->
|
|
4808
|
+
<div
|
|
4809
|
+
x-show="items.length === 0"
|
|
4810
|
+
class="text-center py-4 text-gray-400 text-sm border border-dashed border-gray-300 rounded-md"
|
|
4811
|
+
data-testid="${fieldName}-empty-state"
|
|
4812
|
+
>
|
|
4813
|
+
暂无标签,在上方输入框中输入标签后按回车或点击"添加"按钮
|
|
4814
|
+
</div>
|
|
4815
|
+
</div>
|
|
4816
|
+
`;
|
|
4817
|
+
}
|
|
4448
4818
|
|
|
4449
|
-
export { BaseFeature, CustomFeature, DefaultCreateFeature, DefaultDeleteFeature, DefaultDetailFeature, DefaultEditFeature, DefaultListFeature, Dialog, ErrorAlert, HtmxAdminPlugin, LoadingBar, ObjectEditor, PageModel, StringArrayEditor, checkUserPermission, getUserInfo, modelNameToPath, parseListParams };
|
|
4819
|
+
export { BaseFeature, CustomFeature, DefaultCreateFeature, DefaultDeleteFeature, DefaultDetailFeature, DefaultEditFeature, DefaultListFeature, Dialog, ErrorAlert, HtmxAdminPlugin, LoadingBar, ObjectEditor, PageModel, StringArrayEditor, TagsEditor, checkUserPermission, getUserInfo, modelNameToPath, parseListParams };
|