imean-service-engine-htmx-plugin 2.1.1 → 2.3.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.d.mts +99 -14
- package/dist/index.d.ts +99 -14
- package/dist/index.js +1147 -267
- package/dist/index.mjs +1146 -268
- package/docs/README.md +39 -0
- package/docs/alpinejs-interactive-components.md +653 -0
- package/docs/custom-form-field-renderers.md +541 -0
- package/docs/hono-html-best-practices.md +509 -0
- package/package.json +13 -14
package/dist/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { promises } from 'fs';
|
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { jsx, jsxs, Fragment } from 'hono/jsx/jsx-runtime';
|
|
5
5
|
import { getCookie } from 'hono/cookie';
|
|
6
|
+
import { html } from 'hono/html';
|
|
6
7
|
|
|
7
8
|
var __defProp = Object.defineProperty;
|
|
8
9
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -293,144 +294,350 @@ function getFieldValue(field, initialData) {
|
|
|
293
294
|
if (value === null || value === void 0) {
|
|
294
295
|
return "";
|
|
295
296
|
}
|
|
297
|
+
if (typeof value === "object") {
|
|
298
|
+
if (value instanceof Date) {
|
|
299
|
+
return value.toISOString();
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
return JSON.stringify(value);
|
|
303
|
+
} catch (e) {
|
|
304
|
+
return "";
|
|
305
|
+
}
|
|
306
|
+
}
|
|
296
307
|
return String(value);
|
|
297
308
|
}
|
|
298
309
|
return "";
|
|
299
310
|
}
|
|
311
|
+
function isJsonString(value) {
|
|
312
|
+
if (!value || typeof value !== "string") {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
const trimmed = value.trim();
|
|
316
|
+
return trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]");
|
|
317
|
+
}
|
|
318
|
+
function formatJsonString(value) {
|
|
319
|
+
if (!value || typeof value !== "string") {
|
|
320
|
+
return value;
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
const parsed = JSON.parse(value);
|
|
324
|
+
return JSON.stringify(parsed, null, 2);
|
|
325
|
+
} catch (e) {
|
|
326
|
+
return value;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function renderFormField(field, initialData, formFieldRenderers) {
|
|
330
|
+
const value = getFieldValue(field, initialData);
|
|
331
|
+
const customRenderer = formFieldRenderers?.[field.name];
|
|
332
|
+
if (customRenderer) {
|
|
333
|
+
let parsedValue = null;
|
|
334
|
+
if (value && isJsonString(value)) {
|
|
335
|
+
try {
|
|
336
|
+
parsedValue = JSON.parse(value);
|
|
337
|
+
} catch (e) {
|
|
338
|
+
parsedValue = null;
|
|
339
|
+
}
|
|
340
|
+
} else if (value) {
|
|
341
|
+
parsedValue = value;
|
|
342
|
+
}
|
|
343
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", "data-testid": `field-${field.name}`, children: [
|
|
344
|
+
/* @__PURE__ */ jsxs(
|
|
345
|
+
"label",
|
|
346
|
+
{
|
|
347
|
+
htmlFor: field.name,
|
|
348
|
+
className: "block text-sm font-semibold text-gray-700",
|
|
349
|
+
"data-testid": `label-${field.name}`,
|
|
350
|
+
children: [
|
|
351
|
+
field.label,
|
|
352
|
+
field.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", children: "*" })
|
|
353
|
+
]
|
|
354
|
+
}
|
|
355
|
+
),
|
|
356
|
+
/* @__PURE__ */ jsx("div", { children: customRenderer({
|
|
357
|
+
field,
|
|
358
|
+
value: parsedValue,
|
|
359
|
+
initialData,
|
|
360
|
+
fieldName: field.name
|
|
361
|
+
}) }),
|
|
362
|
+
field.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
|
|
363
|
+
] }, field.name);
|
|
364
|
+
}
|
|
365
|
+
const shouldUseTextarea = field.type === "textarea" || value && isJsonString(value) && field.type !== "select";
|
|
366
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", "data-testid": `field-${field.name}`, children: [
|
|
367
|
+
/* @__PURE__ */ jsxs(
|
|
368
|
+
"label",
|
|
369
|
+
{
|
|
370
|
+
htmlFor: field.name,
|
|
371
|
+
className: "block text-sm font-semibold text-gray-700",
|
|
372
|
+
"data-testid": `label-${field.name}`,
|
|
373
|
+
children: [
|
|
374
|
+
field.label,
|
|
375
|
+
field.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", children: "*" })
|
|
376
|
+
]
|
|
377
|
+
}
|
|
378
|
+
),
|
|
379
|
+
shouldUseTextarea ? /* @__PURE__ */ jsx(
|
|
380
|
+
"textarea",
|
|
381
|
+
{
|
|
382
|
+
id: field.name,
|
|
383
|
+
name: field.name,
|
|
384
|
+
required: field.required,
|
|
385
|
+
placeholder: field.placeholder || (isJsonString(value) ? "JSON \u683C\u5F0F\u6570\u636E" : ""),
|
|
386
|
+
rows: isJsonString(value) ? 10 : 4,
|
|
387
|
+
className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 resize-y font-mono text-sm",
|
|
388
|
+
"data-testid": `input-${field.name}`,
|
|
389
|
+
children: isJsonString(value) ? formatJsonString(value) : value
|
|
390
|
+
},
|
|
391
|
+
`${field.name}-${value}`
|
|
392
|
+
) : field.type === "select" ? /* @__PURE__ */ jsxs(
|
|
393
|
+
"select",
|
|
394
|
+
{
|
|
395
|
+
id: field.name,
|
|
396
|
+
name: field.name,
|
|
397
|
+
required: field.required,
|
|
398
|
+
className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 bg-white",
|
|
399
|
+
"data-testid": `select-${field.name}`,
|
|
400
|
+
children: [
|
|
401
|
+
!field.required && /* @__PURE__ */ jsx("option", { value: "", selected: !value || value === "", children: "\u8BF7\u9009\u62E9" }),
|
|
402
|
+
field.options && field.options.length > 0 ? field.options.map((option) => {
|
|
403
|
+
const optionValue = String(option.value);
|
|
404
|
+
const isSelected = value === optionValue;
|
|
405
|
+
return /* @__PURE__ */ jsx(
|
|
406
|
+
"option",
|
|
407
|
+
{
|
|
408
|
+
value: optionValue,
|
|
409
|
+
selected: isSelected,
|
|
410
|
+
children: option.label
|
|
411
|
+
},
|
|
412
|
+
optionValue
|
|
413
|
+
);
|
|
414
|
+
}) : /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "\u6682\u65E0\u9009\u9879" })
|
|
415
|
+
]
|
|
416
|
+
},
|
|
417
|
+
`${field.name}-${value || ""}`
|
|
418
|
+
) : /* @__PURE__ */ jsx(
|
|
419
|
+
"input",
|
|
420
|
+
{
|
|
421
|
+
type: field.type || "text",
|
|
422
|
+
id: field.name,
|
|
423
|
+
name: field.name,
|
|
424
|
+
required: field.required,
|
|
425
|
+
placeholder: field.placeholder,
|
|
426
|
+
step: field.type === "number" ? field.step ?? void 0 : void 0,
|
|
427
|
+
className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200",
|
|
428
|
+
value,
|
|
429
|
+
"data-testid": `input-${field.name}`
|
|
430
|
+
},
|
|
431
|
+
`${field.name}-${value}`
|
|
432
|
+
),
|
|
433
|
+
field.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
|
|
434
|
+
] }, field.name);
|
|
435
|
+
}
|
|
300
436
|
function FormPage(props) {
|
|
301
|
-
const { fields, submitUrl, method = "post", initialData, formId } = props;
|
|
437
|
+
const { fields, groups, submitUrl, method = "post", initialData, formId, isDialog = false, formFieldRenderers } = props;
|
|
302
438
|
const finalFormId = formId || `form-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
303
439
|
if (process.env.NODE_ENV === "development" && initialData) {
|
|
440
|
+
const fieldNames = fields ? fields.map((f) => f.name).join(", ") : groups ? groups.map((g) => g.fields.map((f) => f.name).join(", ")).join(" | ") : "";
|
|
304
441
|
console.log(
|
|
305
|
-
`[FormPage] initialData: ${JSON.stringify(initialData)}, fields: ${
|
|
442
|
+
`[FormPage] initialData: ${JSON.stringify(initialData)}, fields: ${fieldNames}`
|
|
306
443
|
);
|
|
307
444
|
}
|
|
308
|
-
return /* @__PURE__ */
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
445
|
+
return /* @__PURE__ */ jsxs("div", { className: "w-full", "data-testid": "form-container", "x-data": `{ activeTab: 0 }`, children: [
|
|
446
|
+
/* @__PURE__ */ jsxs(
|
|
447
|
+
"div",
|
|
448
|
+
{
|
|
449
|
+
id: "form-loading-indicator",
|
|
450
|
+
className: "htmx-indicator fixed top-4 right-4 bg-blue-600 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 z-50",
|
|
451
|
+
children: [
|
|
452
|
+
/* @__PURE__ */ jsxs(
|
|
453
|
+
"svg",
|
|
454
|
+
{
|
|
455
|
+
className: "animate-spin h-4 w-4",
|
|
456
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
457
|
+
fill: "none",
|
|
458
|
+
viewBox: "0 0 24 24",
|
|
459
|
+
children: [
|
|
460
|
+
/* @__PURE__ */ jsx(
|
|
461
|
+
"circle",
|
|
462
|
+
{
|
|
463
|
+
className: "opacity-25",
|
|
464
|
+
cx: "12",
|
|
465
|
+
cy: "12",
|
|
466
|
+
r: "10",
|
|
467
|
+
stroke: "currentColor",
|
|
468
|
+
strokeWidth: "4"
|
|
469
|
+
}
|
|
470
|
+
),
|
|
471
|
+
/* @__PURE__ */ jsx(
|
|
472
|
+
"path",
|
|
473
|
+
{
|
|
474
|
+
className: "opacity-75",
|
|
475
|
+
fill: "currentColor",
|
|
476
|
+
d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
477
|
+
}
|
|
478
|
+
)
|
|
479
|
+
]
|
|
480
|
+
}
|
|
481
|
+
),
|
|
482
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "\u63D0\u4EA4\u4E2D..." })
|
|
483
|
+
]
|
|
484
|
+
}
|
|
485
|
+
),
|
|
486
|
+
groups && groups.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
487
|
+
/* @__PURE__ */ jsx("div", { className: `sticky top-0 z-10 bg-white border-b border-gray-200 shadow-sm ${isDialog ? "px-6" : "-mx-6 px-6 -mt-6"}`, children: /* @__PURE__ */ jsx("nav", { className: "flex -mb-px w-full", "aria-label": "Tabs", "data-testid": "form-tabs", children: groups.map((group, index) => /* @__PURE__ */ jsx(
|
|
488
|
+
"button",
|
|
489
|
+
{
|
|
490
|
+
type: "button",
|
|
491
|
+
"x-on:click": `activeTab = ${index}`,
|
|
492
|
+
"x-bind:class": `activeTab === ${index} ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'`,
|
|
493
|
+
className: "px-6 py-4 text-sm font-medium border-b-2 transition-colors duration-150 whitespace-nowrap flex-1 text-center",
|
|
494
|
+
"data-testid": `form-tab-${index}`,
|
|
495
|
+
children: group.label
|
|
496
|
+
},
|
|
497
|
+
index
|
|
498
|
+
)) }) }),
|
|
499
|
+
/* @__PURE__ */ jsx(
|
|
500
|
+
"form",
|
|
501
|
+
{
|
|
502
|
+
id: finalFormId,
|
|
503
|
+
method: method === "put" ? "post" : method,
|
|
504
|
+
action: submitUrl,
|
|
505
|
+
"hx-boost": "true",
|
|
506
|
+
...method === "put" ? { "hx-put": submitUrl } : {},
|
|
507
|
+
"hx-indicator": "#form-loading-indicator",
|
|
508
|
+
"data-testid": "form",
|
|
509
|
+
className: isDialog ? "p-6" : "mt-6",
|
|
510
|
+
children: /* @__PURE__ */ jsx("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm", children: /* @__PURE__ */ jsx("div", { className: "p-6", children: groups.map((group, index) => /* @__PURE__ */ jsx(
|
|
511
|
+
"div",
|
|
512
|
+
{
|
|
513
|
+
"x-show": `activeTab === ${index}`,
|
|
514
|
+
className: "space-y-6",
|
|
515
|
+
"data-testid": `form-tab-content-${index}`,
|
|
516
|
+
children: group.fields.map((field) => renderFormField(field, initialData, formFieldRenderers))
|
|
517
|
+
},
|
|
518
|
+
index
|
|
519
|
+
)) }) })
|
|
520
|
+
}
|
|
521
|
+
)
|
|
522
|
+
] }) : (
|
|
523
|
+
/* 平铺模式(向后兼容) */
|
|
524
|
+
/* @__PURE__ */ jsx(
|
|
525
|
+
"form",
|
|
526
|
+
{
|
|
527
|
+
id: finalFormId,
|
|
528
|
+
method: method === "put" ? "post" : method,
|
|
529
|
+
action: submitUrl,
|
|
530
|
+
"hx-boost": "true",
|
|
531
|
+
...method === "put" ? { "hx-put": submitUrl } : {},
|
|
532
|
+
"hx-indicator": "#form-loading-indicator",
|
|
533
|
+
className: "space-y-6",
|
|
534
|
+
"data-testid": "form",
|
|
535
|
+
children: fields && fields.length > 0 && /* @__PURE__ */ jsx("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm", children: /* @__PURE__ */ jsx("div", { className: "p-6 space-y-6", children: fields.map((field) => renderFormField(field, initialData, formFieldRenderers)) }) })
|
|
536
|
+
}
|
|
537
|
+
)
|
|
538
|
+
)
|
|
539
|
+
] });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/utils/form-data-processor.ts
|
|
543
|
+
function preprocessFormData(data, zodSchema) {
|
|
544
|
+
if (!zodSchema) {
|
|
545
|
+
return data;
|
|
546
|
+
}
|
|
547
|
+
const processed = { ...data };
|
|
548
|
+
const def = zodSchema._def;
|
|
549
|
+
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
550
|
+
if (!shape || typeof shape !== "object") {
|
|
551
|
+
return processed;
|
|
552
|
+
}
|
|
553
|
+
for (const [fieldName, fieldSchema] of Object.entries(shape)) {
|
|
554
|
+
const value = processed[fieldName];
|
|
555
|
+
if (value === void 0) {
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (value === null) {
|
|
559
|
+
processed[fieldName] = void 0;
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
if (value === "") {
|
|
563
|
+
processed[fieldName] = void 0;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const fieldDef = fieldSchema._def;
|
|
567
|
+
let typeName = fieldDef?.type || fieldDef?.typeName;
|
|
568
|
+
if (typeName === "optional" || typeName === "ZodOptional") {
|
|
569
|
+
const innerType = fieldDef.innerType;
|
|
570
|
+
if (innerType) {
|
|
571
|
+
const innerDef = innerType._def;
|
|
572
|
+
typeName = innerDef?.type || innerDef?.typeName;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (typeName === "number" || typeName === "ZodNumber") {
|
|
576
|
+
if (typeof value === "string") {
|
|
577
|
+
const trimmed = value.trim();
|
|
578
|
+
if (trimmed !== "") {
|
|
579
|
+
const numValue = Number(trimmed);
|
|
580
|
+
if (!isNaN(numValue)) {
|
|
581
|
+
processed[fieldName] = numValue;
|
|
358
582
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
/* @__PURE__ */ jsxs(
|
|
364
|
-
"label",
|
|
365
|
-
{
|
|
366
|
-
htmlFor: field.name,
|
|
367
|
-
className: "block text-sm font-semibold text-gray-700",
|
|
368
|
-
"data-testid": `label-${field.name}`,
|
|
369
|
-
children: [
|
|
370
|
-
field.label,
|
|
371
|
-
field.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", children: "*" })
|
|
372
|
-
]
|
|
373
|
-
}
|
|
374
|
-
),
|
|
375
|
-
field.type === "textarea" ? /* @__PURE__ */ jsx(
|
|
376
|
-
"textarea",
|
|
377
|
-
{
|
|
378
|
-
id: field.name,
|
|
379
|
-
name: field.name,
|
|
380
|
-
required: field.required,
|
|
381
|
-
placeholder: field.placeholder,
|
|
382
|
-
rows: 4,
|
|
383
|
-
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",
|
|
384
|
-
value,
|
|
385
|
-
"data-testid": `input-${field.name}`
|
|
386
|
-
},
|
|
387
|
-
`${field.name}-${value}`
|
|
388
|
-
) : field.type === "select" ? /* @__PURE__ */ jsxs(
|
|
389
|
-
"select",
|
|
390
|
-
{
|
|
391
|
-
id: field.name,
|
|
392
|
-
name: field.name,
|
|
393
|
-
required: field.required,
|
|
394
|
-
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",
|
|
395
|
-
"data-testid": `select-${field.name}`,
|
|
396
|
-
children: [
|
|
397
|
-
!field.required && /* @__PURE__ */ jsx("option", { value: "", selected: !value || value === "", children: "\u8BF7\u9009\u62E9" }),
|
|
398
|
-
field.options && field.options.length > 0 ? field.options.map((option) => {
|
|
399
|
-
const optionValue = String(option.value);
|
|
400
|
-
const isSelected = value === optionValue;
|
|
401
|
-
return /* @__PURE__ */ jsx(
|
|
402
|
-
"option",
|
|
403
|
-
{
|
|
404
|
-
value: optionValue,
|
|
405
|
-
selected: isSelected,
|
|
406
|
-
children: option.label
|
|
407
|
-
},
|
|
408
|
-
optionValue
|
|
409
|
-
);
|
|
410
|
-
}) : /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "\u6682\u65E0\u9009\u9879" })
|
|
411
|
-
]
|
|
412
|
-
},
|
|
413
|
-
`${field.name}-${value || ""}`
|
|
414
|
-
) : /* @__PURE__ */ jsx(
|
|
415
|
-
"input",
|
|
416
|
-
{
|
|
417
|
-
type: field.type || "text",
|
|
418
|
-
id: field.name,
|
|
419
|
-
name: field.name,
|
|
420
|
-
required: field.required,
|
|
421
|
-
placeholder: field.placeholder,
|
|
422
|
-
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",
|
|
423
|
-
value,
|
|
424
|
-
"data-testid": `input-${field.name}`
|
|
425
|
-
},
|
|
426
|
-
`${field.name}-${value}`
|
|
427
|
-
),
|
|
428
|
-
field.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
|
|
429
|
-
] }, field.name);
|
|
430
|
-
}) }) })
|
|
431
|
-
]
|
|
583
|
+
}
|
|
584
|
+
} else if (typeof value === "number") {
|
|
585
|
+
processed[fieldName] = value;
|
|
586
|
+
}
|
|
432
587
|
}
|
|
433
|
-
|
|
588
|
+
if (typeName === "boolean" || typeName === "ZodBoolean") {
|
|
589
|
+
if (typeof value === "string") {
|
|
590
|
+
const trimmed = value.trim().toLowerCase();
|
|
591
|
+
if (trimmed === "true" || trimmed === "1" || trimmed === "on") {
|
|
592
|
+
processed[fieldName] = true;
|
|
593
|
+
} else if (trimmed === "false" || trimmed === "0" || trimmed === "off" || trimmed === "") {
|
|
594
|
+
processed[fieldName] = false;
|
|
595
|
+
}
|
|
596
|
+
} else if (typeof value === "boolean") {
|
|
597
|
+
processed[fieldName] = value;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (typeName === "array" || typeName === "ZodArray") {
|
|
601
|
+
if (typeof value === "string") {
|
|
602
|
+
const trimmed = value.trim();
|
|
603
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
604
|
+
try {
|
|
605
|
+
const parsed = JSON.parse(trimmed);
|
|
606
|
+
processed[fieldName] = parsed;
|
|
607
|
+
} catch (e) {
|
|
608
|
+
}
|
|
609
|
+
} else {
|
|
610
|
+
const parts = trimmed.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
611
|
+
processed[fieldName] = parts;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (typeName === "object" || typeName === "ZodObject") {
|
|
616
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
617
|
+
try {
|
|
618
|
+
const trimmed = value.trim();
|
|
619
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
620
|
+
const parsed = JSON.parse(trimmed);
|
|
621
|
+
processed[fieldName] = parsed;
|
|
622
|
+
}
|
|
623
|
+
} catch (e) {
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (typeName === "any" || typeName === "ZodAny") {
|
|
628
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
629
|
+
try {
|
|
630
|
+
const trimmed = value.trim();
|
|
631
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
632
|
+
const parsed = JSON.parse(trimmed);
|
|
633
|
+
processed[fieldName] = parsed;
|
|
634
|
+
}
|
|
635
|
+
} catch (e) {
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return processed;
|
|
434
641
|
}
|
|
435
642
|
|
|
436
643
|
// src/utils/schema-utils.ts
|
|
@@ -457,13 +664,14 @@ function parseFieldSchema(fieldName, fieldSchema) {
|
|
|
457
664
|
return null;
|
|
458
665
|
}
|
|
459
666
|
const label = getFieldDescription(fieldSchema) || fieldName;
|
|
460
|
-
const { type, required, options, innerSchema } = analyzeFieldType(fieldSchema);
|
|
667
|
+
const { type, required, options, innerSchema, step } = analyzeFieldType(fieldSchema);
|
|
461
668
|
return {
|
|
462
669
|
name: fieldName,
|
|
463
670
|
label,
|
|
464
671
|
type,
|
|
465
672
|
required,
|
|
466
673
|
options,
|
|
674
|
+
step,
|
|
467
675
|
schema: innerSchema || fieldSchema
|
|
468
676
|
};
|
|
469
677
|
}
|
|
@@ -520,10 +728,20 @@ function analyzeFieldType(schema) {
|
|
|
520
728
|
let fieldType = "text";
|
|
521
729
|
if (def?.checks) {
|
|
522
730
|
const hasEmailCheck = def.checks.some(
|
|
523
|
-
(check) => check.
|
|
731
|
+
(check) => check.format === "email" || check.constructor?.name === "ZodEmail" || check._zod?.def?.format === "email"
|
|
524
732
|
);
|
|
525
733
|
if (hasEmailCheck) {
|
|
526
734
|
fieldType = "email";
|
|
735
|
+
} else {
|
|
736
|
+
const maxLengthCheck = def.checks.find(
|
|
737
|
+
(check) => check.constructor?.name === "$ZodCheckMaxLength" || check._zod?.def?.check === "max_length" || check._zod?.def?.maximum !== void 0
|
|
738
|
+
);
|
|
739
|
+
if (maxLengthCheck) {
|
|
740
|
+
const maxLength = maxLengthCheck._zod?.def?.maximum ?? maxLengthCheck.value ?? maxLengthCheck.maximum;
|
|
741
|
+
if (maxLength !== void 0 && maxLength > 50) {
|
|
742
|
+
fieldType = "textarea";
|
|
743
|
+
}
|
|
744
|
+
}
|
|
527
745
|
}
|
|
528
746
|
}
|
|
529
747
|
return {
|
|
@@ -533,10 +751,22 @@ function analyzeFieldType(schema) {
|
|
|
533
751
|
};
|
|
534
752
|
}
|
|
535
753
|
if (typeName === "number" || typeName === "ZodNumber") {
|
|
754
|
+
let step = void 0;
|
|
755
|
+
if (def?.checks) {
|
|
756
|
+
const hasIntCheck = def.checks.some(
|
|
757
|
+
(check) => check.isInt === true || check.format === "safeint" || check.def?.format === "safeint" || check.kind === "int" || check.constructor?.name === "ZodInt" || check._zod?.def?.check === "int" || check._zod?.def?.kind === "int"
|
|
758
|
+
);
|
|
759
|
+
if (!hasIntCheck) {
|
|
760
|
+
step = "any";
|
|
761
|
+
}
|
|
762
|
+
} else {
|
|
763
|
+
step = "any";
|
|
764
|
+
}
|
|
536
765
|
return {
|
|
537
766
|
type: "number",
|
|
538
767
|
required: true,
|
|
539
|
-
innerSchema: schema
|
|
768
|
+
innerSchema: schema,
|
|
769
|
+
step
|
|
540
770
|
};
|
|
541
771
|
}
|
|
542
772
|
if (typeName === "date" || typeName === "ZodDate") {
|
|
@@ -601,7 +831,8 @@ function modelFieldsToFormFields(fields) {
|
|
|
601
831
|
type: field.type,
|
|
602
832
|
label: field.label,
|
|
603
833
|
required: field.required,
|
|
604
|
-
options: field.options
|
|
834
|
+
options: field.options,
|
|
835
|
+
step: field.step
|
|
605
836
|
}));
|
|
606
837
|
}
|
|
607
838
|
function getFieldNamesFromFields(fields) {
|
|
@@ -619,6 +850,8 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
619
850
|
descriptionGetter;
|
|
620
851
|
/** 当前请求的表单 ID(用于在 render 和 getActions 之间共享) */
|
|
621
852
|
currentFormId;
|
|
853
|
+
/** 自定义表单字段渲染器 */
|
|
854
|
+
formFieldRenderers;
|
|
622
855
|
/**
|
|
623
856
|
* 获取或生成表单 ID(确保在同一个请求中保持一致)
|
|
624
857
|
*/
|
|
@@ -685,59 +918,7 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
685
918
|
if (!this.schema) {
|
|
686
919
|
return data;
|
|
687
920
|
}
|
|
688
|
-
|
|
689
|
-
for (const key of Object.keys(processed)) {
|
|
690
|
-
if (processed[key] === "") {
|
|
691
|
-
processed[key] = void 0;
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
const def = this.schema._def;
|
|
695
|
-
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
696
|
-
if (!shape || typeof shape !== "object") {
|
|
697
|
-
return data;
|
|
698
|
-
}
|
|
699
|
-
for (const [fieldName, fieldSchema] of Object.entries(shape)) {
|
|
700
|
-
const value = processed[fieldName];
|
|
701
|
-
if (value === void 0 || value === null) {
|
|
702
|
-
continue;
|
|
703
|
-
}
|
|
704
|
-
const fieldDef = fieldSchema._def;
|
|
705
|
-
let typeName = fieldDef?.typeName;
|
|
706
|
-
let isOptional = false;
|
|
707
|
-
if (typeName === "ZodOptional") {
|
|
708
|
-
isOptional = true;
|
|
709
|
-
const innerType = fieldDef.innerType?._def;
|
|
710
|
-
typeName = innerType?.typeName;
|
|
711
|
-
}
|
|
712
|
-
if (isOptional && typeof value === "string" && value.trim() === "") {
|
|
713
|
-
if (fieldName === "authorId") {
|
|
714
|
-
logger.info(`[BaseFormFeature] authorId empty optional -> undefined`);
|
|
715
|
-
}
|
|
716
|
-
processed[fieldName] = void 0;
|
|
717
|
-
continue;
|
|
718
|
-
}
|
|
719
|
-
if (typeName === "ZodNumber") {
|
|
720
|
-
if (typeof value === "string") {
|
|
721
|
-
const numValue = Number(value);
|
|
722
|
-
if (fieldName === "authorId") {
|
|
723
|
-
logger.info(
|
|
724
|
-
`[BaseFormFeature] authorId raw value="${value}", numValue=${numValue}, isOptional=${isOptional}`
|
|
725
|
-
);
|
|
726
|
-
}
|
|
727
|
-
if (!isNaN(numValue)) {
|
|
728
|
-
processed[fieldName] = numValue;
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
if (typeName === "ZodArray") {
|
|
733
|
-
if (typeof value === "string") {
|
|
734
|
-
const parts = value.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
735
|
-
processed[fieldName] = parts;
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
logger.info(`[BaseFormFeature] processed form data: ${JSON.stringify(processed)}`);
|
|
740
|
-
return processed;
|
|
921
|
+
return preprocessFormData(data, this.schema);
|
|
741
922
|
}
|
|
742
923
|
/**
|
|
743
924
|
* 处理请求
|
|
@@ -747,12 +928,23 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
747
928
|
this.currentFormId = void 0;
|
|
748
929
|
if (ctx.req.method === "GET") {
|
|
749
930
|
return this.render(context);
|
|
750
|
-
} else if (ctx.req.method === "POST" || ctx.req.method === "PUT") {
|
|
931
|
+
} else if (ctx.req.method === "POST" || ctx.req.method === "PUT" || ctx.req.method === "PATCH") {
|
|
932
|
+
const methodOverride = ctx.req.header("X-HTTP-Method-Override");
|
|
933
|
+
const actualMethod = methodOverride || ctx.req.method;
|
|
934
|
+
const expectedMethod = this.getFormAction() === "edit" ? "PUT" : "POST";
|
|
935
|
+
if (actualMethod.toUpperCase() !== expectedMethod) {
|
|
936
|
+
logger.warn(
|
|
937
|
+
`[BaseFormFeature] Method mismatch: expected ${expectedMethod}, got ${actualMethod} (request method: ${ctx.req.method}, X-HTTP-Method-Override: ${methodOverride || "none"})`
|
|
938
|
+
);
|
|
939
|
+
}
|
|
751
940
|
const originalData = { ...context.body };
|
|
752
941
|
logger.info(
|
|
753
942
|
`[BaseFormFeature] Original body data: ${JSON.stringify(originalData)}`
|
|
754
943
|
);
|
|
755
944
|
let data = this.preprocessFormData(context.body);
|
|
945
|
+
logger.info(
|
|
946
|
+
`[BaseFormFeature] Preprocessed data: ${JSON.stringify(data)}`
|
|
947
|
+
);
|
|
756
948
|
if (!this.schema) {
|
|
757
949
|
throw new Error("Schema is required for form validation");
|
|
758
950
|
}
|
|
@@ -769,7 +961,10 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
769
961
|
);
|
|
770
962
|
return this.render(context, originalData);
|
|
771
963
|
}
|
|
772
|
-
const item = await this.handleSubmit(
|
|
964
|
+
const item = await this.handleSubmit(
|
|
965
|
+
context,
|
|
966
|
+
parseResult.data
|
|
967
|
+
);
|
|
773
968
|
if (!item) {
|
|
774
969
|
context.sendError(
|
|
775
970
|
this.getFormAction() === "create" ? "\u521B\u5EFA\u5931\u8D25" : "\u66F4\u65B0\u5931\u8D25",
|
|
@@ -787,6 +982,9 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
787
982
|
`[BaseFormFeature] Dialog mode: setting refresh to close dialog and refresh list`
|
|
788
983
|
);
|
|
789
984
|
context.setRefresh(true);
|
|
985
|
+
if (context.redirectUrl) {
|
|
986
|
+
context.redirectUrl = void 0;
|
|
987
|
+
}
|
|
790
988
|
return null;
|
|
791
989
|
} else {
|
|
792
990
|
const redirectUrl = this.getSuccessRedirectUrl(context, item);
|
|
@@ -799,12 +997,11 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
799
997
|
}
|
|
800
998
|
}
|
|
801
999
|
formFieldNames;
|
|
1000
|
+
groups;
|
|
802
1001
|
/**
|
|
803
1002
|
* 渲染表单页面
|
|
804
1003
|
*/
|
|
805
1004
|
async render(context, initialData) {
|
|
806
|
-
const filteredFields = this.formFieldNames ? filterFieldsByNames(this.fields || [], this.formFieldNames) : this.fields || [];
|
|
807
|
-
const fields = modelFieldsToFormFields(filteredFields);
|
|
808
1005
|
let formData;
|
|
809
1006
|
if (this.getFormAction() === "edit") {
|
|
810
1007
|
if (initialData) {
|
|
@@ -826,6 +1023,50 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
826
1023
|
}
|
|
827
1024
|
const method = this.getFormAction() === "create" ? "post" : "put";
|
|
828
1025
|
const formId = this.getFormId(context);
|
|
1026
|
+
if (this.groups && this.groups.length > 0) {
|
|
1027
|
+
if (!this.schema) {
|
|
1028
|
+
throw new Error("Schema is required when using groups");
|
|
1029
|
+
}
|
|
1030
|
+
const schema = this.schema;
|
|
1031
|
+
const groupSchemas = this.groups.map((group) => {
|
|
1032
|
+
const pickObject = group.fields.reduce(
|
|
1033
|
+
(acc, fieldName) => {
|
|
1034
|
+
acc[fieldName] = true;
|
|
1035
|
+
return acc;
|
|
1036
|
+
},
|
|
1037
|
+
{}
|
|
1038
|
+
);
|
|
1039
|
+
return {
|
|
1040
|
+
label: group.label,
|
|
1041
|
+
schema: schema.pick(pickObject),
|
|
1042
|
+
fields: group.fields
|
|
1043
|
+
};
|
|
1044
|
+
});
|
|
1045
|
+
const groupFields = groupSchemas.map(
|
|
1046
|
+
({ label, schema: schema2, fields: fieldNames }) => {
|
|
1047
|
+
const groupFields2 = parseSchemaToFields(schema2);
|
|
1048
|
+
const formFields = modelFieldsToFormFields(groupFields2);
|
|
1049
|
+
return {
|
|
1050
|
+
label,
|
|
1051
|
+
fields: formFields
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
);
|
|
1055
|
+
return /* @__PURE__ */ jsx(
|
|
1056
|
+
FormPage,
|
|
1057
|
+
{
|
|
1058
|
+
groups: groupFields,
|
|
1059
|
+
submitUrl,
|
|
1060
|
+
method,
|
|
1061
|
+
initialData: formData,
|
|
1062
|
+
formId,
|
|
1063
|
+
isDialog: context.isDialog,
|
|
1064
|
+
formFieldRenderers: this.formFieldRenderers
|
|
1065
|
+
}
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
const filteredFields = this.formFieldNames ? filterFieldsByNames(this.fields || [], this.formFieldNames) : this.fields || [];
|
|
1069
|
+
const fields = modelFieldsToFormFields(filteredFields);
|
|
829
1070
|
return /* @__PURE__ */ jsx(
|
|
830
1071
|
FormPage,
|
|
831
1072
|
{
|
|
@@ -833,7 +1074,9 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
833
1074
|
submitUrl,
|
|
834
1075
|
method,
|
|
835
1076
|
initialData: formData,
|
|
836
|
-
formId
|
|
1077
|
+
formId,
|
|
1078
|
+
isDialog: context.isDialog,
|
|
1079
|
+
formFieldRenderers: this.formFieldRenderers
|
|
837
1080
|
}
|
|
838
1081
|
);
|
|
839
1082
|
}
|
|
@@ -942,6 +1185,8 @@ var DefaultCreateFeature = class extends BaseFormFeature {
|
|
|
942
1185
|
this.fields = parseSchemaToFields(options.schema);
|
|
943
1186
|
this.createItem = options.createItem;
|
|
944
1187
|
this.formFieldNames = options.formFieldNames;
|
|
1188
|
+
this.groups = options.groups;
|
|
1189
|
+
this.formFieldRenderers = options.formFieldRenderers;
|
|
945
1190
|
}
|
|
946
1191
|
getFormAction() {
|
|
947
1192
|
return "create";
|
|
@@ -1001,12 +1246,119 @@ var DefaultDeleteFeature = class extends BaseFeature {
|
|
|
1001
1246
|
}
|
|
1002
1247
|
}
|
|
1003
1248
|
};
|
|
1249
|
+
function Card(props) {
|
|
1250
|
+
const {
|
|
1251
|
+
children,
|
|
1252
|
+
title,
|
|
1253
|
+
className = "",
|
|
1254
|
+
shadow = true,
|
|
1255
|
+
bordered = false,
|
|
1256
|
+
noPadding = false
|
|
1257
|
+
} = props;
|
|
1258
|
+
const baseClasses = "bg-white rounded-lg";
|
|
1259
|
+
const shadowClass = shadow ? "shadow-sm hover:shadow-md transition-shadow" : "";
|
|
1260
|
+
const borderClass = bordered ? "border border-gray-200" : "";
|
|
1261
|
+
const paddingClass = noPadding ? "" : "p-6";
|
|
1262
|
+
return /* @__PURE__ */ jsxs(
|
|
1263
|
+
"div",
|
|
1264
|
+
{
|
|
1265
|
+
className: `${baseClasses} ${shadowClass} ${borderClass} ${className}`,
|
|
1266
|
+
children: [
|
|
1267
|
+
title && /* @__PURE__ */ jsx("div", { className: "px-6 py-4 border-b border-gray-200", children: /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title }) }),
|
|
1268
|
+
/* @__PURE__ */ jsx("div", { className: noPadding ? "" : paddingClass, children })
|
|
1269
|
+
]
|
|
1270
|
+
}
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
function renderDefaultValue(value) {
|
|
1274
|
+
if (value === null || value === void 0) {
|
|
1275
|
+
return /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "-" });
|
|
1276
|
+
}
|
|
1277
|
+
if (Array.isArray(value)) {
|
|
1278
|
+
if (value.length === 0) {
|
|
1279
|
+
return /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "\u6682\u65E0\u6570\u636E" });
|
|
1280
|
+
}
|
|
1281
|
+
if (value.length > 0 && typeof value[0] === "object" && value[0] !== null) {
|
|
1282
|
+
return /* @__PURE__ */ jsxs("span", { className: "text-gray-600", children: [
|
|
1283
|
+
"\u5305\u542B ",
|
|
1284
|
+
value.length,
|
|
1285
|
+
" \u9879\uFF08\u5BF9\u8C61\u6570\u7EC4\uFF0C\u5EFA\u8BAE\u4F7F\u7528\u81EA\u5B9A\u4E49\u6E32\u67D3\uFF09"
|
|
1286
|
+
] });
|
|
1287
|
+
}
|
|
1288
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: value.map((item, index) => /* @__PURE__ */ jsx(
|
|
1289
|
+
"span",
|
|
1290
|
+
{
|
|
1291
|
+
className: "px-2 py-1 bg-gray-100 text-gray-700 rounded text-sm",
|
|
1292
|
+
children: String(item)
|
|
1293
|
+
},
|
|
1294
|
+
index
|
|
1295
|
+
)) });
|
|
1296
|
+
}
|
|
1297
|
+
if (typeof value === "object") {
|
|
1298
|
+
if (value instanceof Date) {
|
|
1299
|
+
return /* @__PURE__ */ jsx("span", { children: value.toLocaleString() });
|
|
1300
|
+
}
|
|
1301
|
+
return /* @__PURE__ */ jsx("pre", { className: "bg-gray-50 border border-gray-200 rounded p-3 text-xs overflow-x-auto", children: JSON.stringify(value, null, 2) });
|
|
1302
|
+
}
|
|
1303
|
+
if (typeof value === "boolean") {
|
|
1304
|
+
return /* @__PURE__ */ jsx("span", { className: `px-2 py-1 rounded text-sm font-medium ${value ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-600"}`, children: value ? "\u662F" : "\u5426" });
|
|
1305
|
+
}
|
|
1306
|
+
if (typeof value === "number") {
|
|
1307
|
+
return /* @__PURE__ */ jsx("span", { children: value.toLocaleString() });
|
|
1308
|
+
}
|
|
1309
|
+
return /* @__PURE__ */ jsx("span", { children: String(value) });
|
|
1310
|
+
}
|
|
1311
|
+
function renderField(field, value, item) {
|
|
1312
|
+
let content;
|
|
1313
|
+
if (field.render) {
|
|
1314
|
+
const rendered = field.render(value, item);
|
|
1315
|
+
if (rendered === null || rendered === void 0) {
|
|
1316
|
+
content = /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "-" });
|
|
1317
|
+
} else if (typeof rendered === "string" || typeof rendered === "number" || typeof rendered === "boolean") {
|
|
1318
|
+
content = /* @__PURE__ */ jsx("span", { children: String(rendered) });
|
|
1319
|
+
} else {
|
|
1320
|
+
content = rendered;
|
|
1321
|
+
}
|
|
1322
|
+
} else {
|
|
1323
|
+
content = renderDefaultValue(value);
|
|
1324
|
+
}
|
|
1325
|
+
return /* @__PURE__ */ jsxs(
|
|
1326
|
+
"div",
|
|
1327
|
+
{
|
|
1328
|
+
className: "\r\n px-4 py-4 sm:px-6 sm:py-5\r\n flex flex-col\r\n hover:bg-gray-50/50 transition-colors duration-150\r\n group\r\n gap-1.5\r\n ",
|
|
1329
|
+
children: [
|
|
1330
|
+
/* @__PURE__ */ jsx("dt", { className: "\r\n text-xs sm:text-sm font-semibold text-gray-600 sm:text-gray-700\r\n flex items-start\r\n leading-tight sm:leading-5\r\n tracking-wide\r\n ", children: /* @__PURE__ */ jsx("span", { className: "min-w-0 uppercase sm:normal-case", children: field.label }) }),
|
|
1331
|
+
/* @__PURE__ */ jsx("dd", { className: "\r\n text-sm sm:text-base text-gray-900\r\n break-words\r\n leading-relaxed\r\n min-w-0\r\n ", children: /* @__PURE__ */ jsx("div", { className: "min-w-0", children: content }) })
|
|
1332
|
+
]
|
|
1333
|
+
},
|
|
1334
|
+
field.key
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1004
1337
|
function DetailPage(props) {
|
|
1005
|
-
const { item, fields } = props;
|
|
1006
|
-
|
|
1007
|
-
/* @__PURE__ */ jsx("
|
|
1008
|
-
|
|
1009
|
-
|
|
1338
|
+
const { item, fields, groups } = props;
|
|
1339
|
+
if (groups && groups.length > 0) {
|
|
1340
|
+
return /* @__PURE__ */ jsx("div", { className: "space-y-6", children: groups.map((group, groupIndex) => /* @__PURE__ */ jsx(
|
|
1341
|
+
Card,
|
|
1342
|
+
{
|
|
1343
|
+
title: group.label,
|
|
1344
|
+
shadow: true,
|
|
1345
|
+
bordered: true,
|
|
1346
|
+
noPadding: true,
|
|
1347
|
+
children: /* @__PURE__ */ jsx("dl", { className: "divide-y divide-gray-100", children: group.fields.map((field) => {
|
|
1348
|
+
const value = group.values[field.key];
|
|
1349
|
+
return renderField(field, value, item);
|
|
1350
|
+
}) })
|
|
1351
|
+
},
|
|
1352
|
+
groupIndex
|
|
1353
|
+
)) });
|
|
1354
|
+
}
|
|
1355
|
+
if (fields && fields.length > 0) {
|
|
1356
|
+
return /* @__PURE__ */ jsx("div", { className: "bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden", children: /* @__PURE__ */ jsx("dl", { className: "divide-y divide-gray-100", children: fields.map((field) => {
|
|
1357
|
+
const value = item[field.key];
|
|
1358
|
+
return renderField(field, value, item);
|
|
1359
|
+
}) }) });
|
|
1360
|
+
}
|
|
1361
|
+
return null;
|
|
1010
1362
|
}
|
|
1011
1363
|
var DefaultDetailFeature = class extends BaseFeature {
|
|
1012
1364
|
getItem;
|
|
@@ -1014,6 +1366,8 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
1014
1366
|
titleGetter;
|
|
1015
1367
|
descriptionGetter;
|
|
1016
1368
|
detailFieldNames;
|
|
1369
|
+
fieldRenderers;
|
|
1370
|
+
groups;
|
|
1017
1371
|
constructor(options) {
|
|
1018
1372
|
super({
|
|
1019
1373
|
name: "detail",
|
|
@@ -1029,6 +1383,8 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
1029
1383
|
this.titleGetter = options.getTitle;
|
|
1030
1384
|
this.descriptionGetter = options.getDescription;
|
|
1031
1385
|
this.detailFieldNames = options.detailFieldNames;
|
|
1386
|
+
this.fieldRenderers = options.fieldRenderers;
|
|
1387
|
+
this.groups = options.groups;
|
|
1032
1388
|
}
|
|
1033
1389
|
async getTitle(context) {
|
|
1034
1390
|
if (this.titleGetter) {
|
|
@@ -1059,6 +1415,40 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
1059
1415
|
if (!item) {
|
|
1060
1416
|
return context.ctx.json({ error: "Not found" }, 404);
|
|
1061
1417
|
}
|
|
1418
|
+
if (this.groups && this.groups.length > 0) {
|
|
1419
|
+
if (!this.schema) {
|
|
1420
|
+
throw new Error("Schema is required when using groups");
|
|
1421
|
+
}
|
|
1422
|
+
const schema = this.schema;
|
|
1423
|
+
const groupSchemas = this.groups.map((group) => {
|
|
1424
|
+
const pickObject = group.fields.reduce((acc, fieldName) => {
|
|
1425
|
+
acc[fieldName] = true;
|
|
1426
|
+
return acc;
|
|
1427
|
+
}, {});
|
|
1428
|
+
return {
|
|
1429
|
+
label: group.label,
|
|
1430
|
+
schema: schema.pick(pickObject),
|
|
1431
|
+
fields: group.fields
|
|
1432
|
+
};
|
|
1433
|
+
});
|
|
1434
|
+
const groupFields = groupSchemas.map(({ label, schema: schema2, fields: fieldNames }) => {
|
|
1435
|
+
const groupFields2 = parseSchemaToFields(schema2);
|
|
1436
|
+
const detailFields2 = groupFields2.map((field) => ({
|
|
1437
|
+
key: field.name,
|
|
1438
|
+
label: field.label,
|
|
1439
|
+
render: this.fieldRenderers?.[field.name]
|
|
1440
|
+
}));
|
|
1441
|
+
return {
|
|
1442
|
+
label,
|
|
1443
|
+
fields: detailFields2,
|
|
1444
|
+
values: fieldNames.reduce((acc, fieldName) => {
|
|
1445
|
+
acc[fieldName] = item[fieldName];
|
|
1446
|
+
return acc;
|
|
1447
|
+
}, {})
|
|
1448
|
+
};
|
|
1449
|
+
});
|
|
1450
|
+
return /* @__PURE__ */ jsx(DetailPage, { item, groups: groupFields });
|
|
1451
|
+
}
|
|
1062
1452
|
const detailFields = this.detailFieldNames ? filterFieldsByNames(this.fields || [], this.detailFieldNames) : this.fields || [];
|
|
1063
1453
|
if (this.detailFieldNames) {
|
|
1064
1454
|
const systemFields = ["id", "createdAt", "updatedAt"];
|
|
@@ -1077,7 +1467,9 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
1077
1467
|
const detailFieldNames = getFieldNamesFromFields(detailFields);
|
|
1078
1468
|
const fields = detailFieldNames.map((fieldName) => ({
|
|
1079
1469
|
key: fieldName,
|
|
1080
|
-
label: getFieldLabelFromFields(this.fields || [], fieldName) || fieldName
|
|
1470
|
+
label: getFieldLabelFromFields(this.fields || [], fieldName) || fieldName,
|
|
1471
|
+
render: this.fieldRenderers?.[fieldName]
|
|
1472
|
+
// 如果有自定义渲染函数则使用
|
|
1081
1473
|
}));
|
|
1082
1474
|
return /* @__PURE__ */ jsx(DetailPage, { item, fields });
|
|
1083
1475
|
}
|
|
@@ -1155,6 +1547,8 @@ var DefaultEditFeature = class extends BaseFormFeature {
|
|
|
1155
1547
|
this.getItem = options.getItem;
|
|
1156
1548
|
this.updateItem = options.updateItem;
|
|
1157
1549
|
this.formFieldNames = options.formFieldNames;
|
|
1550
|
+
this.groups = options.groups;
|
|
1551
|
+
this.formFieldRenderers = options.formFieldRenderers;
|
|
1158
1552
|
}
|
|
1159
1553
|
getFormAction() {
|
|
1160
1554
|
return "edit";
|
|
@@ -1364,18 +1758,21 @@ function Button(props) {
|
|
|
1364
1758
|
disabled ? "opacity-50 cursor-not-allowed" : "",
|
|
1365
1759
|
className
|
|
1366
1760
|
].filter(Boolean).join(" ");
|
|
1761
|
+
const isNewWindow = rest.target === "_blank";
|
|
1367
1762
|
const htmxAttrs = {};
|
|
1368
|
-
if (
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1763
|
+
if (!isNewWindow) {
|
|
1764
|
+
if (hxGet) htmxAttrs["hx-get"] = hxGet;
|
|
1765
|
+
if (hxPost) htmxAttrs["hx-post"] = hxPost;
|
|
1766
|
+
if (hxPut) htmxAttrs["hx-put"] = hxPut;
|
|
1767
|
+
if (hxDelete) htmxAttrs["hx-delete"] = hxDelete;
|
|
1768
|
+
if (hxTarget) htmxAttrs["hx-target"] = hxTarget;
|
|
1769
|
+
if (hxSwap) htmxAttrs["hx-swap"] = hxSwap;
|
|
1770
|
+
if (hxPushUrl !== void 0)
|
|
1771
|
+
htmxAttrs["hx-push-url"] = hxPushUrl === true ? "true" : hxPushUrl;
|
|
1772
|
+
if (hxIndicator) htmxAttrs["hx-indicator"] = hxIndicator;
|
|
1773
|
+
if (hxConfirm) htmxAttrs["hx-confirm"] = hxConfirm;
|
|
1774
|
+
if (hxHeaders) htmxAttrs["hx-headers"] = hxHeaders;
|
|
1775
|
+
}
|
|
1379
1776
|
const href = rest.href ?? hxGet ?? "#";
|
|
1380
1777
|
const { className: _, ...otherRest } = rest;
|
|
1381
1778
|
return /* @__PURE__ */ jsx(
|
|
@@ -1390,30 +1787,6 @@ function Button(props) {
|
|
|
1390
1787
|
}
|
|
1391
1788
|
);
|
|
1392
1789
|
}
|
|
1393
|
-
function Card(props) {
|
|
1394
|
-
const {
|
|
1395
|
-
children,
|
|
1396
|
-
title,
|
|
1397
|
-
className = "",
|
|
1398
|
-
shadow = true,
|
|
1399
|
-
bordered = false,
|
|
1400
|
-
noPadding = false
|
|
1401
|
-
} = props;
|
|
1402
|
-
const baseClasses = "bg-white rounded-lg";
|
|
1403
|
-
const shadowClass = shadow ? "shadow-sm hover:shadow-md transition-shadow" : "";
|
|
1404
|
-
const borderClass = bordered ? "border border-gray-200" : "";
|
|
1405
|
-
const paddingClass = noPadding ? "" : "p-6";
|
|
1406
|
-
return /* @__PURE__ */ jsxs(
|
|
1407
|
-
"div",
|
|
1408
|
-
{
|
|
1409
|
-
className: `${baseClasses} ${shadowClass} ${borderClass} ${className}`,
|
|
1410
|
-
children: [
|
|
1411
|
-
title && /* @__PURE__ */ jsx("div", { className: "px-6 py-4 border-b border-gray-200", children: /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title }) }),
|
|
1412
|
-
/* @__PURE__ */ jsx("div", { className: noPadding ? "" : paddingClass, children })
|
|
1413
|
-
]
|
|
1414
|
-
}
|
|
1415
|
-
);
|
|
1416
|
-
}
|
|
1417
1790
|
function EmptyState(props) {
|
|
1418
1791
|
const { message = "\u6682\u65E0\u6570\u636E", children } = props;
|
|
1419
1792
|
return /* @__PURE__ */ jsx("div", { className: "text-center py-12", children: children || /* @__PURE__ */ jsx("p", { className: "text-gray-500 text-sm", children: message }) });
|
|
@@ -1597,20 +1970,24 @@ function ActionLink(props) {
|
|
|
1597
1970
|
const { action, item } = props;
|
|
1598
1971
|
const hrefValue = action.href(item);
|
|
1599
1972
|
const isDelete = action.method === "delete";
|
|
1973
|
+
const isNewWindow = action.target === "_blank";
|
|
1600
1974
|
const className = `${STYLES.actionLink.base} ${isDelete ? STYLES.actionLink.delete : STYLES.actionLink.default}`;
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1975
|
+
const linkProps = {
|
|
1976
|
+
href: hrefValue,
|
|
1977
|
+
className,
|
|
1978
|
+
"data-testid": `table-action-${action.label}`,
|
|
1979
|
+
"aria-label": `${action.label}\uFF1A${item.id || item}`
|
|
1980
|
+
};
|
|
1981
|
+
if (isNewWindow) {
|
|
1982
|
+
linkProps.target = "_blank";
|
|
1983
|
+
linkProps.rel = "noopener noreferrer";
|
|
1984
|
+
} else if (isDelete) {
|
|
1985
|
+
linkProps["hx-delete"] = hrefValue;
|
|
1986
|
+
linkProps["hx-confirm"] = "\u786E\u5B9A\u5220\u9664\u5417\uFF1F";
|
|
1987
|
+
} else {
|
|
1988
|
+
linkProps["hx-get"] = hrefValue;
|
|
1989
|
+
}
|
|
1990
|
+
return /* @__PURE__ */ jsx("a", { ...linkProps, children: action.label });
|
|
1614
1991
|
}
|
|
1615
1992
|
function ActionCell(props) {
|
|
1616
1993
|
const { actions, item, actionStyle } = props;
|
|
@@ -1631,15 +2008,18 @@ function ActionCell(props) {
|
|
|
1631
2008
|
) : /* @__PURE__ */ jsx("div", { className: STYLES.actionButton, "data-testid": "table-actions", children: actions.map((action, idx) => {
|
|
1632
2009
|
const hrefValue = action.href(item);
|
|
1633
2010
|
const isDelete = action.method === "delete";
|
|
2011
|
+
const isNewWindow = action.target === "_blank";
|
|
1634
2012
|
return /* @__PURE__ */ jsx(
|
|
1635
2013
|
Button,
|
|
1636
2014
|
{
|
|
1637
2015
|
variant: isDelete ? "danger" : "secondary",
|
|
1638
2016
|
size: "sm",
|
|
1639
2017
|
href: hrefValue,
|
|
1640
|
-
"hx-get": !isDelete ? hrefValue : void 0,
|
|
2018
|
+
"hx-get": !isDelete && !isNewWindow ? hrefValue : void 0,
|
|
1641
2019
|
"hx-delete": isDelete ? hrefValue : void 0,
|
|
1642
2020
|
"hx-confirm": isDelete ? "\u786E\u5B9A\u5220\u9664\u5417\uFF1F" : void 0,
|
|
2021
|
+
target: isNewWindow ? "_blank" : void 0,
|
|
2022
|
+
rel: isNewWindow ? "noopener noreferrer" : void 0,
|
|
1643
2023
|
className: action.class,
|
|
1644
2024
|
"data-testid": `table-action-${action.label}`,
|
|
1645
2025
|
"aria-label": `${action.label}\uFF1A${item.id || item}`,
|
|
@@ -1736,19 +2116,24 @@ function ListPage(props) {
|
|
|
1736
2116
|
editPath,
|
|
1737
2117
|
deletePath,
|
|
1738
2118
|
listPath,
|
|
1739
|
-
filterFields
|
|
2119
|
+
filterFields,
|
|
2120
|
+
openMode
|
|
1740
2121
|
} = props;
|
|
1741
2122
|
const actions = [];
|
|
1742
2123
|
if (detailPath) {
|
|
2124
|
+
const mode = openMode?.detail || "dialog";
|
|
1743
2125
|
actions.push({
|
|
1744
2126
|
label: "\u67E5\u770B",
|
|
1745
|
-
href: detailPath
|
|
2127
|
+
href: detailPath,
|
|
2128
|
+
target: mode === "newWindow" ? "_blank" : void 0
|
|
1746
2129
|
});
|
|
1747
2130
|
}
|
|
1748
2131
|
if (editPath) {
|
|
2132
|
+
const mode = openMode?.edit || "dialog";
|
|
1749
2133
|
actions.push({
|
|
1750
2134
|
label: "\u7F16\u8F91",
|
|
1751
|
-
href: editPath
|
|
2135
|
+
href: editPath,
|
|
2136
|
+
target: mode === "newWindow" ? "_blank" : void 0
|
|
1752
2137
|
});
|
|
1753
2138
|
}
|
|
1754
2139
|
if (deletePath) {
|
|
@@ -1793,7 +2178,8 @@ function ListPage(props) {
|
|
|
1793
2178
|
actions: actions.length > 0 ? actions.map((action) => ({
|
|
1794
2179
|
label: action.label,
|
|
1795
2180
|
href: (item) => action.href(item),
|
|
1796
|
-
method: action.method
|
|
2181
|
+
method: action.method,
|
|
2182
|
+
target: action.target
|
|
1797
2183
|
})) : void 0,
|
|
1798
2184
|
pagination: {
|
|
1799
2185
|
page: result.page,
|
|
@@ -1834,6 +2220,7 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
1834
2220
|
listFieldNames;
|
|
1835
2221
|
filterSchema;
|
|
1836
2222
|
columnRenderers;
|
|
2223
|
+
openMode;
|
|
1837
2224
|
constructor(options) {
|
|
1838
2225
|
super({
|
|
1839
2226
|
name: "list",
|
|
@@ -1849,6 +2236,7 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
1849
2236
|
this.listFieldNames = options.listFieldNames;
|
|
1850
2237
|
this.filterSchema = options.filterSchema;
|
|
1851
2238
|
this.columnRenderers = options.columnRenderers;
|
|
2239
|
+
this.openMode = options.openMode;
|
|
1852
2240
|
}
|
|
1853
2241
|
getRoutes() {
|
|
1854
2242
|
return [{ method: "get", path: "/list" }];
|
|
@@ -1869,9 +2257,12 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
1869
2257
|
const prefix = context.prefix || "";
|
|
1870
2258
|
const basePath = `${prefix}/${model.modelName}`;
|
|
1871
2259
|
const listPath = `${basePath}/list`;
|
|
1872
|
-
const
|
|
1873
|
-
const
|
|
1874
|
-
const
|
|
2260
|
+
const createMode = this.openMode?.create || "dialog";
|
|
2261
|
+
const detailMode = this.openMode?.detail || "dialog";
|
|
2262
|
+
const editMode = this.openMode?.edit || "dialog";
|
|
2263
|
+
const createPath = model.features.get("create") ? createMode === "dialog" ? `${basePath}/new?dialog=true` : `${basePath}/new` : void 0;
|
|
2264
|
+
const detailPath = model.features.get("detail") ? (item) => detailMode === "dialog" ? `${basePath}/detail/${item.id}?dialog=true` : `${basePath}/detail/${item.id}` : void 0;
|
|
2265
|
+
const editPath = model.features.get("edit") ? (item) => editMode === "dialog" ? `${basePath}/edit/${item.id}?dialog=true` : `${basePath}/edit/${item.id}` : void 0;
|
|
1875
2266
|
const deletePath = this.deleteItem && model.features.get("delete") ? (item) => `${basePath}/${item.id}` : void 0;
|
|
1876
2267
|
return /* @__PURE__ */ jsx(
|
|
1877
2268
|
ListPage,
|
|
@@ -1884,7 +2275,8 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
1884
2275
|
editPath,
|
|
1885
2276
|
deletePath,
|
|
1886
2277
|
listPath,
|
|
1887
|
-
filterFields
|
|
2278
|
+
filterFields,
|
|
2279
|
+
openMode: this.openMode
|
|
1888
2280
|
}
|
|
1889
2281
|
);
|
|
1890
2282
|
}
|
|
@@ -1895,11 +2287,22 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
1895
2287
|
const hasCreate = model.features.get("create") !== void 0;
|
|
1896
2288
|
const actions = [];
|
|
1897
2289
|
if (hasCreate) {
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
2290
|
+
const createMode = this.openMode?.create || "dialog";
|
|
2291
|
+
const createUrl = createMode === "dialog" ? `${basePath}/new?dialog=true` : `${basePath}/new`;
|
|
2292
|
+
if (createMode === "newWindow") {
|
|
2293
|
+
actions.push({
|
|
2294
|
+
label: "\u65B0\u5EFA",
|
|
2295
|
+
href: createUrl,
|
|
2296
|
+
variant: "primary",
|
|
2297
|
+
target: "_blank"
|
|
2298
|
+
});
|
|
2299
|
+
} else {
|
|
2300
|
+
actions.push({
|
|
2301
|
+
label: "\u65B0\u5EFA",
|
|
2302
|
+
hxGet: createUrl,
|
|
2303
|
+
variant: "primary"
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
1903
2306
|
}
|
|
1904
2307
|
return actions;
|
|
1905
2308
|
}
|
|
@@ -2305,7 +2708,8 @@ function Dialog(props) {
|
|
|
2305
2708
|
className = "",
|
|
2306
2709
|
size = "lg",
|
|
2307
2710
|
closeOnBackdropClick = true,
|
|
2308
|
-
actions = []
|
|
2711
|
+
actions = [],
|
|
2712
|
+
fixedContentHeight = false
|
|
2309
2713
|
} = props;
|
|
2310
2714
|
const sizeClasses = {
|
|
2311
2715
|
sm: "max-w-md",
|
|
@@ -2364,7 +2768,13 @@ function Dialog(props) {
|
|
|
2364
2768
|
}
|
|
2365
2769
|
)
|
|
2366
2770
|
] }),
|
|
2367
|
-
/* @__PURE__ */ jsx(
|
|
2771
|
+
/* @__PURE__ */ jsx(
|
|
2772
|
+
"div",
|
|
2773
|
+
{
|
|
2774
|
+
className: `${fixedContentHeight ? "h-[70vh]" : "flex-1"} overflow-y-auto ${fixedContentHeight ? "p-0" : "p-6"} bg-gray-50`,
|
|
2775
|
+
children
|
|
2776
|
+
}
|
|
2777
|
+
),
|
|
2368
2778
|
actions.length > 0 && /* @__PURE__ */ jsx("div", { className: "px-6 py-4 border-t border-gray-200 bg-white flex justify-end gap-2", children: actions.map((action, index) => renderActionButton(action, index)) })
|
|
2369
2779
|
]
|
|
2370
2780
|
}
|
|
@@ -2625,6 +3035,13 @@ function BaseLayout(props) {
|
|
|
2625
3035
|
{
|
|
2626
3036
|
dangerouslySetInnerHTML: {
|
|
2627
3037
|
__html: `
|
|
3038
|
+
/* \u5BB9\u5668\u67E5\u8BE2\u652F\u6301 - \u5982\u679C Tailwind CDN \u4E0D\u652F\u6301\uFF0C\u4F7F\u7528\u539F\u751F CSS \u5BB9\u5668\u67E5\u8BE2 */
|
|
3039
|
+
@supports (container-type: inline-size) {
|
|
3040
|
+
.\\@container {
|
|
3041
|
+
container-type: inline-size;
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
|
|
2628
3045
|
@keyframes fadeIn {
|
|
2629
3046
|
from { opacity: 0;}
|
|
2630
3047
|
to { opacity: 1;}
|
|
@@ -2741,7 +3158,8 @@ function renderActionButton2(action, index) {
|
|
|
2741
3158
|
submit,
|
|
2742
3159
|
formId,
|
|
2743
3160
|
onClick,
|
|
2744
|
-
className = ""
|
|
3161
|
+
className = "",
|
|
3162
|
+
target
|
|
2745
3163
|
} = action;
|
|
2746
3164
|
if (submit && formId) {
|
|
2747
3165
|
const variantStyles = {
|
|
@@ -2793,18 +3211,21 @@ function renderActionButton2(action, index) {
|
|
|
2793
3211
|
} else if (label === "\u53D6\u6D88") {
|
|
2794
3212
|
testId = "cancel-button";
|
|
2795
3213
|
}
|
|
3214
|
+
const isNewWindow = target === "_blank";
|
|
2796
3215
|
return /* @__PURE__ */ jsx(
|
|
2797
3216
|
Button,
|
|
2798
3217
|
{
|
|
2799
3218
|
variant,
|
|
2800
3219
|
href,
|
|
2801
|
-
hxGet,
|
|
2802
|
-
hxPost,
|
|
2803
|
-
hxPut,
|
|
2804
|
-
hxDelete,
|
|
2805
|
-
hxConfirm: confirm,
|
|
3220
|
+
hxGet: !isNewWindow ? hxGet : void 0,
|
|
3221
|
+
hxPost: !isNewWindow ? hxPost : void 0,
|
|
3222
|
+
hxPut: !isNewWindow ? hxPut : void 0,
|
|
3223
|
+
hxDelete: !isNewWindow ? hxDelete : void 0,
|
|
3224
|
+
hxConfirm: !isNewWindow ? confirm : void 0,
|
|
2806
3225
|
className,
|
|
2807
3226
|
"data-testid": testId,
|
|
3227
|
+
target,
|
|
3228
|
+
rel: target === "_blank" ? "noopener noreferrer" : void 0,
|
|
2808
3229
|
children: label
|
|
2809
3230
|
},
|
|
2810
3231
|
index
|
|
@@ -3147,9 +3568,9 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3147
3568
|
headers
|
|
3148
3569
|
);
|
|
3149
3570
|
}
|
|
3150
|
-
if (context.redirectUrl) {
|
|
3571
|
+
if (context.redirectUrl && !context.refresh) {
|
|
3151
3572
|
logger.info(
|
|
3152
|
-
`[ResponseRenderer] Redirect URL found: ${context.redirectUrl} (isHtmxRequest: ${context.isHtmxRequest})`
|
|
3573
|
+
`[ResponseRenderer] Redirect URL found: ${context.redirectUrl} (isHtmxRequest: ${context.isHtmxRequest}, isDialog: ${context.isDialog})`
|
|
3153
3574
|
);
|
|
3154
3575
|
if (context.isHtmxRequest) {
|
|
3155
3576
|
return ctx.html(/* @__PURE__ */ jsx("div", {}), 200, {
|
|
@@ -3158,7 +3579,11 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3158
3579
|
} else {
|
|
3159
3580
|
return ctx.redirect(context.redirectUrl);
|
|
3160
3581
|
}
|
|
3161
|
-
} else {
|
|
3582
|
+
} else if (context.redirectUrl && context.refresh) {
|
|
3583
|
+
logger.info(
|
|
3584
|
+
`[ResponseRenderer] Both redirect URL and refresh are set, using refresh (isDialog: ${context.isDialog})`
|
|
3585
|
+
);
|
|
3586
|
+
} else if (!context.redirectUrl) {
|
|
3162
3587
|
logger.info(
|
|
3163
3588
|
`[ResponseRenderer] No redirect URL found (result: ${result === null ? "null" : typeof result}, isHtmxRequest: ${context.isHtmxRequest})`
|
|
3164
3589
|
);
|
|
@@ -3209,6 +3634,8 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3209
3634
|
if (context.isDialog) {
|
|
3210
3635
|
const dialogSize = renderOptions.feature?.dialogSize || "lg";
|
|
3211
3636
|
const closeOnBackdropClick = renderOptions.feature?.closeOnBackdropClick ?? true;
|
|
3637
|
+
const isFormFeature = renderOptions.feature?.type === "create" || renderOptions.feature?.type === "edit";
|
|
3638
|
+
const fixedContentHeight = isFormFeature;
|
|
3212
3639
|
return ctx.html(
|
|
3213
3640
|
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3214
3641
|
/* @__PURE__ */ jsx(
|
|
@@ -3218,6 +3645,7 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3218
3645
|
size: dialogSize,
|
|
3219
3646
|
closeOnBackdropClick,
|
|
3220
3647
|
actions,
|
|
3648
|
+
fixedContentHeight,
|
|
3221
3649
|
children: result
|
|
3222
3650
|
}
|
|
3223
3651
|
),
|
|
@@ -3478,6 +3906,28 @@ function registerPageRoutes(page, options) {
|
|
|
3478
3906
|
});
|
|
3479
3907
|
};
|
|
3480
3908
|
options.hono[route.method](fullPath, handler);
|
|
3909
|
+
if (route.method === "put" || route.method === "delete") {
|
|
3910
|
+
const postHandler = async (ctx) => {
|
|
3911
|
+
const methodOverride = ctx.req.header("X-HTTP-Method-Override");
|
|
3912
|
+
const expectedMethod = route.method.toUpperCase();
|
|
3913
|
+
if (methodOverride === expectedMethod) {
|
|
3914
|
+
logger.info(
|
|
3915
|
+
`[HtmxAdminPlugin] Method override detected: POST ${fullPath} -> ${expectedMethod} (feature: ${feature.name})`
|
|
3916
|
+
);
|
|
3917
|
+
return handleRequest(ctx, page, feature, {
|
|
3918
|
+
options: options.options
|
|
3919
|
+
});
|
|
3920
|
+
}
|
|
3921
|
+
logger.warn(
|
|
3922
|
+
`[HtmxAdminPlugin] POST request to ${fullPath} without matching X-HTTP-Method-Override header (got: ${methodOverride || "none"}, expected: ${expectedMethod})`
|
|
3923
|
+
);
|
|
3924
|
+
return ctx.text("Method Not Allowed", 405);
|
|
3925
|
+
};
|
|
3926
|
+
logger.info(
|
|
3927
|
+
`[HtmxAdminPlugin] Registering POST route for method override: POST ${fullPath} (actual method: ${route.method.toUpperCase()}, feature: ${feature.name})`
|
|
3928
|
+
);
|
|
3929
|
+
options.hono.post(fullPath, postHandler);
|
|
3930
|
+
}
|
|
3481
3931
|
}
|
|
3482
3932
|
}
|
|
3483
3933
|
}
|
|
@@ -3567,5 +4017,433 @@ var HtmxAdminPlugin = class {
|
|
|
3567
4017
|
registerHomeRedirect(this.pages, routeOptions);
|
|
3568
4018
|
}
|
|
3569
4019
|
};
|
|
4020
|
+
function ObjectEditor(props) {
|
|
4021
|
+
const { value, fieldName, objectSchema } = props;
|
|
4022
|
+
if (!objectSchema) {
|
|
4023
|
+
return /* @__PURE__ */ jsx("div", { className: "p-4 border border-yellow-300 rounded-lg bg-yellow-50 text-yellow-800 text-sm", children: "\u8BF7\u63D0\u4F9B objectSchema \u53C2\u6570\u4EE5\u4F7F\u7528\u5BF9\u8C61\u7F16\u8F91\u5668" });
|
|
4024
|
+
}
|
|
4025
|
+
const fields = parseSchemaToFields(objectSchema);
|
|
4026
|
+
const initialObject = value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
|
|
4027
|
+
fields.forEach((field) => {
|
|
4028
|
+
if (!(field.name in initialObject)) {
|
|
4029
|
+
if (field.type === "number") {
|
|
4030
|
+
initialObject[field.name] = field.required ? 0 : void 0;
|
|
4031
|
+
} else if (field.type === "checkbox") {
|
|
4032
|
+
initialObject[field.name] = field.required ? false : void 0;
|
|
4033
|
+
} else {
|
|
4034
|
+
initialObject[field.name] = field.required ? "" : void 0;
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
});
|
|
4038
|
+
const fieldNames = fields.map((f) => f.name);
|
|
4039
|
+
const fieldNamesJson = JSON.stringify(fieldNames);
|
|
4040
|
+
const initialValueJson = JSON.stringify(initialObject);
|
|
4041
|
+
const xDataContent = `{
|
|
4042
|
+
obj: {},
|
|
4043
|
+
init() {
|
|
4044
|
+
const dataAttr = this.$el.getAttribute('data-initial-value');
|
|
4045
|
+
if (dataAttr) {
|
|
4046
|
+
try {
|
|
4047
|
+
this.obj = JSON.parse(dataAttr);
|
|
4048
|
+
} catch (e) {
|
|
4049
|
+
console.error('Failed to parse initial value:', e);
|
|
4050
|
+
this.obj = {};
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
const fieldNames = ${fieldNamesJson};
|
|
4054
|
+
fieldNames.forEach(fieldName => {
|
|
4055
|
+
if (!(fieldName in this.obj)) {
|
|
4056
|
+
this.obj[fieldName] = undefined;
|
|
4057
|
+
}
|
|
4058
|
+
});
|
|
4059
|
+
this.updateHiddenField();
|
|
4060
|
+
},
|
|
4061
|
+
updateHiddenField() {
|
|
4062
|
+
const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
|
|
4063
|
+
if (hiddenInput) {
|
|
4064
|
+
hiddenInput.value = JSON.stringify(this.obj);
|
|
4065
|
+
}
|
|
4066
|
+
},
|
|
4067
|
+
updateField(fieldName, value, fieldType, required) {
|
|
4068
|
+
let convertedValue = value;
|
|
4069
|
+
if (fieldType === 'number') {
|
|
4070
|
+
convertedValue = value === '' ? (required ? 0 : undefined) : Number(value);
|
|
4071
|
+
if (isNaN(convertedValue)) convertedValue = required ? 0 : undefined;
|
|
4072
|
+
} else if (fieldType === 'checkbox') {
|
|
4073
|
+
convertedValue = value === 'true' || value === true || value === '1' || value === 1;
|
|
4074
|
+
} else {
|
|
4075
|
+
convertedValue = value || (required ? '' : undefined);
|
|
4076
|
+
}
|
|
4077
|
+
if (convertedValue === undefined && !required) {
|
|
4078
|
+
delete this.obj[fieldName];
|
|
4079
|
+
} else {
|
|
4080
|
+
this.obj[fieldName] = convertedValue;
|
|
4081
|
+
}
|
|
4082
|
+
this.updateHiddenField();
|
|
4083
|
+
}
|
|
4084
|
+
}`;
|
|
4085
|
+
const generateField = (field) => {
|
|
4086
|
+
const fieldId = `${fieldName}-${field.name}`;
|
|
4087
|
+
const fieldNameVar = `obj.${field.name}`;
|
|
4088
|
+
const requiredValue = field.required ? "true" : "false";
|
|
4089
|
+
const fieldNameForJs = JSON.stringify(field.name);
|
|
4090
|
+
let inputElement;
|
|
4091
|
+
if (field.type === "text") {
|
|
4092
|
+
inputElement = html`
|
|
4093
|
+
<input
|
|
4094
|
+
type="text"
|
|
4095
|
+
id="${fieldId}"
|
|
4096
|
+
x-bind:value="${fieldNameVar} || ''"
|
|
4097
|
+
x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
|
|
4098
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
4099
|
+
data-testid="${fieldName}-input-${field.name}"
|
|
4100
|
+
${field.required ? "required" : ""}
|
|
4101
|
+
/>
|
|
4102
|
+
`;
|
|
4103
|
+
} else if (field.type === "textarea") {
|
|
4104
|
+
inputElement = html`
|
|
4105
|
+
<textarea
|
|
4106
|
+
id="${fieldId}"
|
|
4107
|
+
x-bind:value="${fieldNameVar} || ''"
|
|
4108
|
+
x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
|
|
4109
|
+
rows="4"
|
|
4110
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-y"
|
|
4111
|
+
data-testid="${fieldName}-input-${field.name}"
|
|
4112
|
+
${field.required ? "required" : ""}
|
|
4113
|
+
></textarea>
|
|
4114
|
+
`;
|
|
4115
|
+
} else if (field.type === "number") {
|
|
4116
|
+
const step = field.step || (field.step === void 0 ? "1" : "any");
|
|
4117
|
+
inputElement = html`
|
|
4118
|
+
<input
|
|
4119
|
+
type="number"
|
|
4120
|
+
id="${fieldId}"
|
|
4121
|
+
x-bind:value="${fieldNameVar} != null ? ${fieldNameVar} : ''"
|
|
4122
|
+
x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'number', ${requiredValue})"
|
|
4123
|
+
step="${step}"
|
|
4124
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
4125
|
+
data-testid="${fieldName}-input-${field.name}"
|
|
4126
|
+
${field.required ? "required" : ""}
|
|
4127
|
+
/>
|
|
4128
|
+
`;
|
|
4129
|
+
} else if (field.type === "date") {
|
|
4130
|
+
inputElement = html`
|
|
4131
|
+
<input
|
|
4132
|
+
type="date"
|
|
4133
|
+
id="${fieldId}"
|
|
4134
|
+
x-bind:value="${fieldNameVar} || ''"
|
|
4135
|
+
x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'date', ${requiredValue})"
|
|
4136
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
4137
|
+
data-testid="${fieldName}-input-${field.name}"
|
|
4138
|
+
${field.required ? "required" : ""}
|
|
4139
|
+
/>
|
|
4140
|
+
`;
|
|
4141
|
+
} else if (field.type === "email") {
|
|
4142
|
+
inputElement = html`
|
|
4143
|
+
<input
|
|
4144
|
+
type="email"
|
|
4145
|
+
id="${fieldId}"
|
|
4146
|
+
x-bind:value="${fieldNameVar} || ''"
|
|
4147
|
+
x-on:input="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
|
|
4148
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
4149
|
+
data-testid="${fieldName}-input-${field.name}"
|
|
4150
|
+
${field.required ? "required" : ""}
|
|
4151
|
+
/>
|
|
4152
|
+
`;
|
|
4153
|
+
} else if (field.type === "select" && field.options) {
|
|
4154
|
+
inputElement = html`
|
|
4155
|
+
<select
|
|
4156
|
+
id="${fieldId}"
|
|
4157
|
+
x-bind:value="${fieldNameVar} || ''"
|
|
4158
|
+
x-on:change="updateField(${fieldNameForJs}, $event.target.value, 'text', ${requiredValue})"
|
|
4159
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
|
|
4160
|
+
data-testid="${fieldName}-select-${field.name}"
|
|
4161
|
+
${field.required ? "required" : ""}
|
|
4162
|
+
>
|
|
4163
|
+
${!field.required ? html`<option value="">请选择</option>` : ""}
|
|
4164
|
+
${field.options.map(
|
|
4165
|
+
(option) => html`
|
|
4166
|
+
<option value="${String(option.value)}">${option.label}</option>
|
|
4167
|
+
`
|
|
4168
|
+
)}
|
|
4169
|
+
</select>
|
|
4170
|
+
`;
|
|
4171
|
+
} else if (field.type === "checkbox") {
|
|
4172
|
+
inputElement = html`
|
|
4173
|
+
<div class="flex items-center">
|
|
4174
|
+
<input
|
|
4175
|
+
type="checkbox"
|
|
4176
|
+
id="${fieldId}"
|
|
4177
|
+
x-bind:checked="${fieldNameVar} === true || ${fieldNameVar} === 'true' || ${fieldNameVar} === 1 || ${fieldNameVar} === '1'"
|
|
4178
|
+
x-on:change="updateField(${fieldNameForJs}, $event.target.checked, 'checkbox', ${requiredValue})"
|
|
4179
|
+
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
|
4180
|
+
data-testid="${fieldName}-checkbox-${field.name}"
|
|
4181
|
+
/>
|
|
4182
|
+
<label for="${fieldId}" class="ml-2 text-sm text-gray-700">
|
|
4183
|
+
${field.label}
|
|
4184
|
+
</label>
|
|
4185
|
+
</div>
|
|
4186
|
+
`;
|
|
4187
|
+
}
|
|
4188
|
+
return html`
|
|
4189
|
+
<div class="space-y-2" data-testid="${fieldName}-field-${field.name}">
|
|
4190
|
+
${field.type !== "checkbox" ? html`
|
|
4191
|
+
<label
|
|
4192
|
+
for="${fieldId}"
|
|
4193
|
+
class="block text-sm font-semibold text-gray-700"
|
|
4194
|
+
data-testid="${fieldName}-label-${field.name}"
|
|
4195
|
+
>
|
|
4196
|
+
${field.label}
|
|
4197
|
+
${field.required ? html`<span class="text-red-500 ml-1">*</span>` : ""}
|
|
4198
|
+
</label>
|
|
4199
|
+
` : ""}
|
|
4200
|
+
${inputElement}
|
|
4201
|
+
</div>
|
|
4202
|
+
`;
|
|
4203
|
+
};
|
|
4204
|
+
return html`
|
|
4205
|
+
<div
|
|
4206
|
+
x-data="${xDataContent}"
|
|
4207
|
+
data-initial-value="${initialValueJson}"
|
|
4208
|
+
x-init="init()"
|
|
4209
|
+
class="space-y-4"
|
|
4210
|
+
>
|
|
4211
|
+
<input
|
|
4212
|
+
type="hidden"
|
|
4213
|
+
name="${fieldName}"
|
|
4214
|
+
value=""
|
|
4215
|
+
data-testid="hidden-${fieldName}"
|
|
4216
|
+
/>
|
|
4217
|
+
<div class="space-y-4">
|
|
4218
|
+
${fields.map((field) => generateField(field))}
|
|
4219
|
+
</div>
|
|
4220
|
+
</div>
|
|
4221
|
+
`;
|
|
4222
|
+
}
|
|
4223
|
+
function StringArrayEditor(props) {
|
|
4224
|
+
const {
|
|
4225
|
+
value,
|
|
4226
|
+
fieldName,
|
|
4227
|
+
placeholder = "\u8BF7\u8F93\u5165\u5185\u5BB9",
|
|
4228
|
+
allowEmpty = false
|
|
4229
|
+
} = props;
|
|
4230
|
+
const initialItems = value || [];
|
|
4231
|
+
const initialValueJson = JSON.stringify(initialItems);
|
|
4232
|
+
const xDataContent = `{
|
|
4233
|
+
items: ${initialValueJson},
|
|
4234
|
+
draggedIndex: null,
|
|
4235
|
+
draggedOverIndex: null,
|
|
4236
|
+
fieldName: ${JSON.stringify(fieldName)},
|
|
4237
|
+
placeholder: ${JSON.stringify(placeholder)},
|
|
4238
|
+
allowEmpty: ${allowEmpty},
|
|
4239
|
+
init() {
|
|
4240
|
+
const dataAttr = this.$el.getAttribute('data-initial-value');
|
|
4241
|
+
if (dataAttr) {
|
|
4242
|
+
try {
|
|
4243
|
+
const parsed = JSON.parse(dataAttr);
|
|
4244
|
+
if (Array.isArray(parsed)) {
|
|
4245
|
+
this.items = parsed;
|
|
4246
|
+
} else {
|
|
4247
|
+
this.items = [];
|
|
4248
|
+
}
|
|
4249
|
+
} catch (e) {
|
|
4250
|
+
console.error('Failed to parse initial value:', e);
|
|
4251
|
+
this.items = [];
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
this.updateHiddenField();
|
|
4255
|
+
},
|
|
4256
|
+
updateHiddenField() {
|
|
4257
|
+
const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
|
|
4258
|
+
if (hiddenInput) {
|
|
4259
|
+
hiddenInput.value = JSON.stringify(this.items);
|
|
4260
|
+
}
|
|
4261
|
+
},
|
|
4262
|
+
addItem() {
|
|
4263
|
+
this.items.push('');
|
|
4264
|
+
this.updateHiddenField();
|
|
4265
|
+
this.$nextTick(() => {
|
|
4266
|
+
const keyInputs = this.$el.querySelectorAll('input[data-testid*="-input-"]');
|
|
4267
|
+
if (keyInputs.length > 0) {
|
|
4268
|
+
const lastInput = keyInputs[keyInputs.length - 1];
|
|
4269
|
+
if (lastInput && lastInput.focus) {
|
|
4270
|
+
lastInput.focus();
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
});
|
|
4274
|
+
},
|
|
4275
|
+
removeItem(index) {
|
|
4276
|
+
this.items.splice(index, 1);
|
|
4277
|
+
this.updateHiddenField();
|
|
4278
|
+
},
|
|
4279
|
+
updateItem(index, value) {
|
|
4280
|
+
this.items[index] = value;
|
|
4281
|
+
this.updateHiddenField();
|
|
4282
|
+
},
|
|
4283
|
+
handleDragStart(index, event) {
|
|
4284
|
+
this.draggedIndex = index;
|
|
4285
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
4286
|
+
event.dataTransfer.setData('text/plain', index.toString());
|
|
4287
|
+
const target = event.currentTarget || event.target.closest('[draggable="true"]');
|
|
4288
|
+
if (target) {
|
|
4289
|
+
target.style.opacity = '0.5';
|
|
4290
|
+
}
|
|
4291
|
+
},
|
|
4292
|
+
handleDragEnd(event) {
|
|
4293
|
+
const target = event.currentTarget || event.target.closest('[draggable="true"]');
|
|
4294
|
+
if (target) {
|
|
4295
|
+
target.style.opacity = '';
|
|
4296
|
+
}
|
|
4297
|
+
this.draggedIndex = null;
|
|
4298
|
+
this.draggedOverIndex = null;
|
|
4299
|
+
},
|
|
4300
|
+
handleDragOver(index, event) {
|
|
4301
|
+
event.preventDefault();
|
|
4302
|
+
event.dataTransfer.dropEffect = 'move';
|
|
4303
|
+
this.draggedOverIndex = index;
|
|
4304
|
+
},
|
|
4305
|
+
handleDragLeave() {
|
|
4306
|
+
this.draggedOverIndex = null;
|
|
4307
|
+
},
|
|
4308
|
+
handleDrop(index, event) {
|
|
4309
|
+
event.preventDefault();
|
|
4310
|
+
if (this.draggedIndex !== null && this.draggedIndex !== index) {
|
|
4311
|
+
const draggedItem = this.items[this.draggedIndex];
|
|
4312
|
+
this.items.splice(this.draggedIndex, 1);
|
|
4313
|
+
this.items.splice(index, 0, draggedItem);
|
|
4314
|
+
this.updateHiddenField();
|
|
4315
|
+
}
|
|
4316
|
+
this.draggedIndex = null;
|
|
4317
|
+
this.draggedOverIndex = null;
|
|
4318
|
+
}
|
|
4319
|
+
}`;
|
|
4320
|
+
return html`
|
|
4321
|
+
<div
|
|
4322
|
+
x-data="${xDataContent}"
|
|
4323
|
+
data-initial-value="${initialValueJson}"
|
|
4324
|
+
x-init="init()"
|
|
4325
|
+
class="space-y-3"
|
|
4326
|
+
>
|
|
4327
|
+
<input
|
|
4328
|
+
type="hidden"
|
|
4329
|
+
name="${fieldName}"
|
|
4330
|
+
value=""
|
|
4331
|
+
data-testid="hidden-${fieldName}"
|
|
4332
|
+
/>
|
|
4333
|
+
<div class="space-y-3">
|
|
4334
|
+
<!-- 头部:显示数量和添加按钮 -->
|
|
4335
|
+
<div class="flex items-center justify-between">
|
|
4336
|
+
<span class="text-sm text-gray-600">
|
|
4337
|
+
共 <span x-text="items.length">0</span> 项
|
|
4338
|
+
</span>
|
|
4339
|
+
<button
|
|
4340
|
+
type="button"
|
|
4341
|
+
x-on:click="addItem()"
|
|
4342
|
+
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium flex items-center gap-2"
|
|
4343
|
+
data-testid="${fieldName}-add-button"
|
|
4344
|
+
>
|
|
4345
|
+
<svg
|
|
4346
|
+
class="w-4 h-4"
|
|
4347
|
+
fill="none"
|
|
4348
|
+
stroke="currentColor"
|
|
4349
|
+
viewBox="0 0 24 24"
|
|
4350
|
+
>
|
|
4351
|
+
<path
|
|
4352
|
+
stroke-linecap="round"
|
|
4353
|
+
stroke-linejoin="round"
|
|
4354
|
+
stroke-width="2"
|
|
4355
|
+
d="M12 4v16m8-8H4"
|
|
4356
|
+
/>
|
|
4357
|
+
</svg>
|
|
4358
|
+
添加项
|
|
4359
|
+
</button>
|
|
4360
|
+
</div>
|
|
4361
|
+
|
|
4362
|
+
<!-- 列表项 -->
|
|
4363
|
+
<div class="space-y-2" x-show="items.length > 0">
|
|
4364
|
+
<template x-for="(item, index) in items" x-bind:key="index">
|
|
4365
|
+
<div
|
|
4366
|
+
class="flex items-center gap-2 group"
|
|
4367
|
+
x-bind:class="{
|
|
4368
|
+
'opacity-50': draggedIndex === index,
|
|
4369
|
+
'border-blue-300 bg-blue-50': draggedOverIndex === index && draggedIndex !== null && draggedIndex !== index
|
|
4370
|
+
}"
|
|
4371
|
+
draggable="true"
|
|
4372
|
+
x-on:dragstart="handleDragStart(index, $event)"
|
|
4373
|
+
x-on:dragend="handleDragEnd($event)"
|
|
4374
|
+
x-on:dragover="handleDragOver(index, $event)"
|
|
4375
|
+
x-on:dragleave="handleDragLeave()"
|
|
4376
|
+
x-on:drop="handleDrop(index, $event)"
|
|
4377
|
+
x-bind:data-testid="fieldName + '-item-' + index"
|
|
4378
|
+
>
|
|
4379
|
+
<!-- 拖拽手柄 -->
|
|
4380
|
+
<div
|
|
4381
|
+
class="flex-shrink-0 cursor-move text-gray-400 hover:text-gray-600 transition-colors p-1"
|
|
4382
|
+
x-bind:data-testid="fieldName + '-drag-handle-' + index"
|
|
4383
|
+
title="拖拽排序"
|
|
4384
|
+
>
|
|
4385
|
+
<svg
|
|
4386
|
+
class="w-5 h-5"
|
|
4387
|
+
fill="none"
|
|
4388
|
+
stroke="currentColor"
|
|
4389
|
+
viewBox="0 0 24 24"
|
|
4390
|
+
>
|
|
4391
|
+
<path
|
|
4392
|
+
stroke-linecap="round"
|
|
4393
|
+
stroke-linejoin="round"
|
|
4394
|
+
stroke-width="2"
|
|
4395
|
+
d="M4 8h16M4 16h16"
|
|
4396
|
+
/>
|
|
4397
|
+
</svg>
|
|
4398
|
+
</div>
|
|
4399
|
+
|
|
4400
|
+
<!-- 输入框 -->
|
|
4401
|
+
<input
|
|
4402
|
+
type="text"
|
|
4403
|
+
x-bind:value="items[index] || ''"
|
|
4404
|
+
x-on:input="updateItem(index, $event.target.value)"
|
|
4405
|
+
x-bind:placeholder="placeholder + ' ' + (index + 1)"
|
|
4406
|
+
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
4407
|
+
x-bind:data-testid="fieldName + '-input-' + index"
|
|
4408
|
+
x-bind:required="!allowEmpty"
|
|
4409
|
+
/>
|
|
4410
|
+
|
|
4411
|
+
<!-- 删除按钮 -->
|
|
4412
|
+
<button
|
|
4413
|
+
type="button"
|
|
4414
|
+
x-on:click="removeItem(index)"
|
|
4415
|
+
class="flex-shrink-0 px-3 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
|
4416
|
+
x-bind:data-testid="fieldName + '-remove-button-' + index"
|
|
4417
|
+
title="删除此项"
|
|
4418
|
+
>
|
|
4419
|
+
<svg
|
|
4420
|
+
class="w-5 h-5"
|
|
4421
|
+
fill="none"
|
|
4422
|
+
stroke="currentColor"
|
|
4423
|
+
viewBox="0 0 24 24"
|
|
4424
|
+
>
|
|
4425
|
+
<path
|
|
4426
|
+
stroke-linecap="round"
|
|
4427
|
+
stroke-linejoin="round"
|
|
4428
|
+
stroke-width="2"
|
|
4429
|
+
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
4430
|
+
/>
|
|
4431
|
+
</svg>
|
|
4432
|
+
</button>
|
|
4433
|
+
</div>
|
|
4434
|
+
</template>
|
|
4435
|
+
</div>
|
|
4436
|
+
|
|
4437
|
+
<!-- 空状态提示 -->
|
|
4438
|
+
<div
|
|
4439
|
+
x-show="items.length === 0"
|
|
4440
|
+
class="text-center py-8 text-gray-400 text-sm border border-dashed border-gray-300 rounded-lg"
|
|
4441
|
+
>
|
|
4442
|
+
暂无项,点击"添加项"按钮添加
|
|
4443
|
+
</div>
|
|
4444
|
+
</div>
|
|
4445
|
+
</div>
|
|
4446
|
+
`;
|
|
4447
|
+
}
|
|
3570
4448
|
|
|
3571
|
-
export { BaseFeature, CustomFeature, DefaultCreateFeature, DefaultDeleteFeature, DefaultDetailFeature, DefaultEditFeature, DefaultListFeature, Dialog, ErrorAlert, HtmxAdminPlugin, LoadingBar, PageModel, checkUserPermission, getUserInfo, modelNameToPath, parseListParams };
|
|
4449
|
+
export { BaseFeature, CustomFeature, DefaultCreateFeature, DefaultDeleteFeature, DefaultDetailFeature, DefaultEditFeature, DefaultListFeature, Dialog, ErrorAlert, HtmxAdminPlugin, LoadingBar, ObjectEditor, PageModel, StringArrayEditor, checkUserPermission, getUserInfo, modelNameToPath, parseListParams };
|