imean-service-engine-htmx-plugin 2.3.0 → 2.4.0

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