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