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