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.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("div", { className: "space-y-2", "data-testid": `field-${field.name}`, children: [
344
- /* @__PURE__ */ jsxs(
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
- id: field.name,
396
- name: field.name,
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
- !field.required && /* @__PURE__ */ jsx("option", { value: "", selected: !value || value === "", children: "\u8BF7\u9009\u62E9" }),
402
- field.options && field.options.length > 0 ? field.options.map((option) => {
403
- const optionValue = String(option.value);
404
- const isSelected = value === optionValue;
405
- return /* @__PURE__ */ jsx(
406
- "option",
407
- {
408
- value: optionValue,
409
- selected: isSelected,
410
- children: option.label
411
- },
412
- optionValue
413
- );
414
- }) : /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "\u6682\u65E0\u9009\u9879" })
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
- `${field.name}-${value || ""}`
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
- return /* @__PURE__ */ jsxs("div", { className: "w-full", "data-testid": "form-container", "x-data": `{ activeTab: 0 }`, children: [
446
- /* @__PURE__ */ jsxs(
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
- id: "form-loading-indicator",
450
- 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",
378
+ className: "space-y-2",
379
+ "data-testid": `field-${field.name}`,
451
380
  children: [
452
381
  /* @__PURE__ */ jsxs(
453
- "svg",
382
+ "label",
454
383
  {
455
- className: "animate-spin h-4 w-4",
456
- xmlns: "http://www.w3.org/2000/svg",
457
- fill: "none",
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
- "circle",
389
+ "input",
462
390
  {
463
- className: "opacity-25",
464
- cx: "12",
465
- cy: "12",
466
- r: "10",
467
- stroke: "currentColor",
468
- strokeWidth: "4"
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__ */ jsx(
472
- "path",
473
- {
474
- className: "opacity-75",
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("span", { className: "text-sm font-medium", children: "\u63D0\u4EA4\u4E2D..." })
408
+ field.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 ml-8", children: field.description })
483
409
  ]
484
- }
485
- ),
486
- groups && groups.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
487
- /* @__PURE__ */ jsx("div", { className: `sticky top-0 z-10 bg-white border-b border-gray-200 shadow-sm ${isDialog ? "px-6" : "-mx-6 px-6 -mt-6"}`, children: /* @__PURE__ */ jsx("nav", { className: "flex -mb-px w-full", "aria-label": "Tabs", "data-testid": "form-tabs", children: groups.map((group, index) => /* @__PURE__ */ jsx(
488
- "button",
489
- {
490
- type: "button",
491
- "x-on:click": `activeTab = ${index}`,
492
- "x-bind:class": `activeTab === ${index} ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'`,
493
- className: "px-6 py-4 text-sm font-medium border-b-2 transition-colors duration-150 whitespace-nowrap flex-1 text-center",
494
- "data-testid": `form-tab-${index}`,
495
- children: group.label
496
- },
497
- index
498
- )) }) }),
499
- /* @__PURE__ */ jsx(
500
- "form",
501
- {
502
- id: finalFormId,
503
- method: method === "put" ? "post" : method,
504
- action: submitUrl,
505
- "hx-boost": "true",
506
- ...method === "put" ? { "hx-put": submitUrl } : {},
507
- "hx-indicator": "#form-loading-indicator",
508
- "data-testid": "form",
509
- className: isDialog ? "p-6" : "mt-6",
510
- children: /* @__PURE__ */ jsx("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm", children: /* @__PURE__ */ jsx("div", { className: "p-6", children: groups.map((group, index) => /* @__PURE__ */ jsx(
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
- "x-show": `activeTab === ${index}`,
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": `form-tab-content-${index}`,
516
- children: group.fields.map((field) => renderFormField(field, initialData, formFieldRenderers))
517
- },
518
- index
519
- )) }) })
520
- }
521
- )
522
- ] }) : (
523
- /* 平铺模式(向后兼容) */
524
- /* @__PURE__ */ jsx(
525
- "form",
526
- {
527
- id: finalFormId,
528
- method: method === "put" ? "post" : method,
529
- action: submitUrl,
530
- "hx-boost": "true",
531
- ...method === "put" ? { "hx-put": submitUrl } : {},
532
- "hx-indicator": "#form-loading-indicator",
533
- className: "space-y-6",
534
- "data-testid": "form",
535
- children: fields && fields.length > 0 && /* @__PURE__ */ jsx("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm", children: /* @__PURE__ */ jsx("div", { className: "p-6 space-y-6", children: fields.map((field) => renderFormField(field, initialData, formFieldRenderers)) }) })
536
- }
537
- )
538
- )
539
- ] });
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 };