imean-service-engine-htmx-plugin 2.1.0 → 2.2.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 +113 -14
- package/dist/index.d.ts +113 -14
- package/dist/index.js +717 -224
- package/dist/index.mjs +716 -225
- package/docs/README.md +25 -0
- package/docs/custom-form-field-renderers.md +541 -0
- package/docs/jsx-alpine-best-practices.md +197 -0
- package/package.json +13 -14
package/dist/index.mjs
CHANGED
|
@@ -293,144 +293,350 @@ function getFieldValue(field, initialData) {
|
|
|
293
293
|
if (value === null || value === void 0) {
|
|
294
294
|
return "";
|
|
295
295
|
}
|
|
296
|
+
if (typeof value === "object") {
|
|
297
|
+
if (value instanceof Date) {
|
|
298
|
+
return value.toISOString();
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
return JSON.stringify(value);
|
|
302
|
+
} catch (e) {
|
|
303
|
+
return "";
|
|
304
|
+
}
|
|
305
|
+
}
|
|
296
306
|
return String(value);
|
|
297
307
|
}
|
|
298
308
|
return "";
|
|
299
309
|
}
|
|
310
|
+
function isJsonString(value) {
|
|
311
|
+
if (!value || typeof value !== "string") {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
const trimmed = value.trim();
|
|
315
|
+
return trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]");
|
|
316
|
+
}
|
|
317
|
+
function formatJsonString(value) {
|
|
318
|
+
if (!value || typeof value !== "string") {
|
|
319
|
+
return value;
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
const parsed = JSON.parse(value);
|
|
323
|
+
return JSON.stringify(parsed, null, 2);
|
|
324
|
+
} catch (e) {
|
|
325
|
+
return value;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function renderFormField(field, initialData, formFieldRenderers) {
|
|
329
|
+
const value = getFieldValue(field, initialData);
|
|
330
|
+
const customRenderer = formFieldRenderers?.[field.name];
|
|
331
|
+
if (customRenderer) {
|
|
332
|
+
let parsedValue = null;
|
|
333
|
+
if (value && isJsonString(value)) {
|
|
334
|
+
try {
|
|
335
|
+
parsedValue = JSON.parse(value);
|
|
336
|
+
} catch (e) {
|
|
337
|
+
parsedValue = null;
|
|
338
|
+
}
|
|
339
|
+
} else if (value) {
|
|
340
|
+
parsedValue = value;
|
|
341
|
+
}
|
|
342
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", "data-testid": `field-${field.name}`, children: [
|
|
343
|
+
/* @__PURE__ */ jsxs(
|
|
344
|
+
"label",
|
|
345
|
+
{
|
|
346
|
+
htmlFor: field.name,
|
|
347
|
+
className: "block text-sm font-semibold text-gray-700",
|
|
348
|
+
"data-testid": `label-${field.name}`,
|
|
349
|
+
children: [
|
|
350
|
+
field.label,
|
|
351
|
+
field.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", children: "*" })
|
|
352
|
+
]
|
|
353
|
+
}
|
|
354
|
+
),
|
|
355
|
+
/* @__PURE__ */ jsx("div", { children: customRenderer({
|
|
356
|
+
field,
|
|
357
|
+
value: parsedValue,
|
|
358
|
+
initialData,
|
|
359
|
+
fieldName: field.name
|
|
360
|
+
}) }),
|
|
361
|
+
field.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
|
|
362
|
+
] }, field.name);
|
|
363
|
+
}
|
|
364
|
+
const shouldUseTextarea = field.type === "textarea" || value && isJsonString(value) && field.type !== "select";
|
|
365
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", "data-testid": `field-${field.name}`, children: [
|
|
366
|
+
/* @__PURE__ */ jsxs(
|
|
367
|
+
"label",
|
|
368
|
+
{
|
|
369
|
+
htmlFor: field.name,
|
|
370
|
+
className: "block text-sm font-semibold text-gray-700",
|
|
371
|
+
"data-testid": `label-${field.name}`,
|
|
372
|
+
children: [
|
|
373
|
+
field.label,
|
|
374
|
+
field.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", children: "*" })
|
|
375
|
+
]
|
|
376
|
+
}
|
|
377
|
+
),
|
|
378
|
+
shouldUseTextarea ? /* @__PURE__ */ jsx(
|
|
379
|
+
"textarea",
|
|
380
|
+
{
|
|
381
|
+
id: field.name,
|
|
382
|
+
name: field.name,
|
|
383
|
+
required: field.required,
|
|
384
|
+
placeholder: field.placeholder || (isJsonString(value) ? "JSON \u683C\u5F0F\u6570\u636E" : ""),
|
|
385
|
+
rows: isJsonString(value) ? 10 : 4,
|
|
386
|
+
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",
|
|
387
|
+
"data-testid": `input-${field.name}`,
|
|
388
|
+
children: isJsonString(value) ? formatJsonString(value) : value
|
|
389
|
+
},
|
|
390
|
+
`${field.name}-${value}`
|
|
391
|
+
) : field.type === "select" ? /* @__PURE__ */ jsxs(
|
|
392
|
+
"select",
|
|
393
|
+
{
|
|
394
|
+
id: field.name,
|
|
395
|
+
name: field.name,
|
|
396
|
+
required: field.required,
|
|
397
|
+
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",
|
|
398
|
+
"data-testid": `select-${field.name}`,
|
|
399
|
+
children: [
|
|
400
|
+
!field.required && /* @__PURE__ */ jsx("option", { value: "", selected: !value || value === "", children: "\u8BF7\u9009\u62E9" }),
|
|
401
|
+
field.options && field.options.length > 0 ? field.options.map((option) => {
|
|
402
|
+
const optionValue = String(option.value);
|
|
403
|
+
const isSelected = value === optionValue;
|
|
404
|
+
return /* @__PURE__ */ jsx(
|
|
405
|
+
"option",
|
|
406
|
+
{
|
|
407
|
+
value: optionValue,
|
|
408
|
+
selected: isSelected,
|
|
409
|
+
children: option.label
|
|
410
|
+
},
|
|
411
|
+
optionValue
|
|
412
|
+
);
|
|
413
|
+
}) : /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "\u6682\u65E0\u9009\u9879" })
|
|
414
|
+
]
|
|
415
|
+
},
|
|
416
|
+
`${field.name}-${value || ""}`
|
|
417
|
+
) : /* @__PURE__ */ jsx(
|
|
418
|
+
"input",
|
|
419
|
+
{
|
|
420
|
+
type: field.type || "text",
|
|
421
|
+
id: field.name,
|
|
422
|
+
name: field.name,
|
|
423
|
+
required: field.required,
|
|
424
|
+
placeholder: field.placeholder,
|
|
425
|
+
step: field.type === "number" ? field.step ?? void 0 : void 0,
|
|
426
|
+
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",
|
|
427
|
+
value,
|
|
428
|
+
"data-testid": `input-${field.name}`
|
|
429
|
+
},
|
|
430
|
+
`${field.name}-${value}`
|
|
431
|
+
),
|
|
432
|
+
field.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
|
|
433
|
+
] }, field.name);
|
|
434
|
+
}
|
|
300
435
|
function FormPage(props) {
|
|
301
|
-
const { fields, submitUrl, method = "post", initialData, formId } = props;
|
|
436
|
+
const { fields, groups, submitUrl, method = "post", initialData, formId, isDialog = false, formFieldRenderers } = props;
|
|
302
437
|
const finalFormId = formId || `form-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
303
438
|
if (process.env.NODE_ENV === "development" && initialData) {
|
|
439
|
+
const fieldNames = fields ? fields.map((f) => f.name).join(", ") : groups ? groups.map((g) => g.fields.map((f) => f.name).join(", ")).join(" | ") : "";
|
|
304
440
|
console.log(
|
|
305
|
-
`[FormPage] initialData: ${JSON.stringify(initialData)}, fields: ${
|
|
441
|
+
`[FormPage] initialData: ${JSON.stringify(initialData)}, fields: ${fieldNames}`
|
|
306
442
|
);
|
|
307
443
|
}
|
|
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
|
-
|
|
444
|
+
return /* @__PURE__ */ jsxs("div", { className: "w-full", "data-testid": "form-container", "x-data": `{ activeTab: 0 }`, children: [
|
|
445
|
+
/* @__PURE__ */ jsxs(
|
|
446
|
+
"div",
|
|
447
|
+
{
|
|
448
|
+
id: "form-loading-indicator",
|
|
449
|
+
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",
|
|
450
|
+
children: [
|
|
451
|
+
/* @__PURE__ */ jsxs(
|
|
452
|
+
"svg",
|
|
453
|
+
{
|
|
454
|
+
className: "animate-spin h-4 w-4",
|
|
455
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
456
|
+
fill: "none",
|
|
457
|
+
viewBox: "0 0 24 24",
|
|
458
|
+
children: [
|
|
459
|
+
/* @__PURE__ */ jsx(
|
|
460
|
+
"circle",
|
|
461
|
+
{
|
|
462
|
+
className: "opacity-25",
|
|
463
|
+
cx: "12",
|
|
464
|
+
cy: "12",
|
|
465
|
+
r: "10",
|
|
466
|
+
stroke: "currentColor",
|
|
467
|
+
strokeWidth: "4"
|
|
468
|
+
}
|
|
469
|
+
),
|
|
470
|
+
/* @__PURE__ */ jsx(
|
|
471
|
+
"path",
|
|
472
|
+
{
|
|
473
|
+
className: "opacity-75",
|
|
474
|
+
fill: "currentColor",
|
|
475
|
+
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"
|
|
476
|
+
}
|
|
477
|
+
)
|
|
478
|
+
]
|
|
479
|
+
}
|
|
480
|
+
),
|
|
481
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "\u63D0\u4EA4\u4E2D..." })
|
|
482
|
+
]
|
|
483
|
+
}
|
|
484
|
+
),
|
|
485
|
+
groups && groups.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
486
|
+
/* @__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(
|
|
487
|
+
"button",
|
|
488
|
+
{
|
|
489
|
+
type: "button",
|
|
490
|
+
"x-on:click": `activeTab = ${index}`,
|
|
491
|
+
"x-bind:class": `activeTab === ${index} ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'`,
|
|
492
|
+
className: "px-6 py-4 text-sm font-medium border-b-2 transition-colors duration-150 whitespace-nowrap flex-1 text-center",
|
|
493
|
+
"data-testid": `form-tab-${index}`,
|
|
494
|
+
children: group.label
|
|
495
|
+
},
|
|
496
|
+
index
|
|
497
|
+
)) }) }),
|
|
498
|
+
/* @__PURE__ */ jsx(
|
|
499
|
+
"form",
|
|
500
|
+
{
|
|
501
|
+
id: finalFormId,
|
|
502
|
+
method: method === "put" ? "post" : method,
|
|
503
|
+
action: submitUrl,
|
|
504
|
+
"hx-boost": "true",
|
|
505
|
+
...method === "put" ? { "hx-put": submitUrl } : {},
|
|
506
|
+
"hx-indicator": "#form-loading-indicator",
|
|
507
|
+
"data-testid": "form",
|
|
508
|
+
className: isDialog ? "p-6" : "mt-6",
|
|
509
|
+
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(
|
|
510
|
+
"div",
|
|
511
|
+
{
|
|
512
|
+
"x-show": `activeTab === ${index}`,
|
|
513
|
+
className: "space-y-6",
|
|
514
|
+
"data-testid": `form-tab-content-${index}`,
|
|
515
|
+
children: group.fields.map((field) => renderFormField(field, initialData, formFieldRenderers))
|
|
516
|
+
},
|
|
517
|
+
index
|
|
518
|
+
)) }) })
|
|
519
|
+
}
|
|
520
|
+
)
|
|
521
|
+
] }) : (
|
|
522
|
+
/* 平铺模式(向后兼容) */
|
|
523
|
+
/* @__PURE__ */ jsx(
|
|
524
|
+
"form",
|
|
525
|
+
{
|
|
526
|
+
id: finalFormId,
|
|
527
|
+
method: method === "put" ? "post" : method,
|
|
528
|
+
action: submitUrl,
|
|
529
|
+
"hx-boost": "true",
|
|
530
|
+
...method === "put" ? { "hx-put": submitUrl } : {},
|
|
531
|
+
"hx-indicator": "#form-loading-indicator",
|
|
532
|
+
className: "space-y-6",
|
|
533
|
+
"data-testid": "form",
|
|
534
|
+
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)) }) })
|
|
535
|
+
}
|
|
536
|
+
)
|
|
537
|
+
)
|
|
538
|
+
] });
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/utils/form-data-processor.ts
|
|
542
|
+
function preprocessFormData(data, zodSchema) {
|
|
543
|
+
if (!zodSchema) {
|
|
544
|
+
return data;
|
|
545
|
+
}
|
|
546
|
+
const processed = { ...data };
|
|
547
|
+
const def = zodSchema._def;
|
|
548
|
+
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
549
|
+
if (!shape || typeof shape !== "object") {
|
|
550
|
+
return processed;
|
|
551
|
+
}
|
|
552
|
+
for (const [fieldName, fieldSchema] of Object.entries(shape)) {
|
|
553
|
+
const value = processed[fieldName];
|
|
554
|
+
if (value === void 0) {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
if (value === null) {
|
|
558
|
+
processed[fieldName] = void 0;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (value === "") {
|
|
562
|
+
processed[fieldName] = void 0;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const fieldDef = fieldSchema._def;
|
|
566
|
+
let typeName = fieldDef?.type || fieldDef?.typeName;
|
|
567
|
+
if (typeName === "optional" || typeName === "ZodOptional") {
|
|
568
|
+
const innerType = fieldDef.innerType;
|
|
569
|
+
if (innerType) {
|
|
570
|
+
const innerDef = innerType._def;
|
|
571
|
+
typeName = innerDef?.type || innerDef?.typeName;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (typeName === "number" || typeName === "ZodNumber") {
|
|
575
|
+
if (typeof value === "string") {
|
|
576
|
+
const trimmed = value.trim();
|
|
577
|
+
if (trimmed !== "") {
|
|
578
|
+
const numValue = Number(trimmed);
|
|
579
|
+
if (!isNaN(numValue)) {
|
|
580
|
+
processed[fieldName] = numValue;
|
|
358
581
|
}
|
|
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
|
-
]
|
|
582
|
+
}
|
|
583
|
+
} else if (typeof value === "number") {
|
|
584
|
+
processed[fieldName] = value;
|
|
585
|
+
}
|
|
432
586
|
}
|
|
433
|
-
|
|
587
|
+
if (typeName === "boolean" || typeName === "ZodBoolean") {
|
|
588
|
+
if (typeof value === "string") {
|
|
589
|
+
const trimmed = value.trim().toLowerCase();
|
|
590
|
+
if (trimmed === "true" || trimmed === "1" || trimmed === "on") {
|
|
591
|
+
processed[fieldName] = true;
|
|
592
|
+
} else if (trimmed === "false" || trimmed === "0" || trimmed === "off" || trimmed === "") {
|
|
593
|
+
processed[fieldName] = false;
|
|
594
|
+
}
|
|
595
|
+
} else if (typeof value === "boolean") {
|
|
596
|
+
processed[fieldName] = value;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (typeName === "array" || typeName === "ZodArray") {
|
|
600
|
+
if (typeof value === "string") {
|
|
601
|
+
const trimmed = value.trim();
|
|
602
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
603
|
+
try {
|
|
604
|
+
const parsed = JSON.parse(trimmed);
|
|
605
|
+
processed[fieldName] = parsed;
|
|
606
|
+
} catch (e) {
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
const parts = trimmed.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
610
|
+
processed[fieldName] = parts;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (typeName === "object" || typeName === "ZodObject") {
|
|
615
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
616
|
+
try {
|
|
617
|
+
const trimmed = value.trim();
|
|
618
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
619
|
+
const parsed = JSON.parse(trimmed);
|
|
620
|
+
processed[fieldName] = parsed;
|
|
621
|
+
}
|
|
622
|
+
} catch (e) {
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (typeName === "any" || typeName === "ZodAny") {
|
|
627
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
628
|
+
try {
|
|
629
|
+
const trimmed = value.trim();
|
|
630
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
631
|
+
const parsed = JSON.parse(trimmed);
|
|
632
|
+
processed[fieldName] = parsed;
|
|
633
|
+
}
|
|
634
|
+
} catch (e) {
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return processed;
|
|
434
640
|
}
|
|
435
641
|
|
|
436
642
|
// src/utils/schema-utils.ts
|
|
@@ -457,13 +663,14 @@ function parseFieldSchema(fieldName, fieldSchema) {
|
|
|
457
663
|
return null;
|
|
458
664
|
}
|
|
459
665
|
const label = getFieldDescription(fieldSchema) || fieldName;
|
|
460
|
-
const { type, required, options, innerSchema } = analyzeFieldType(fieldSchema);
|
|
666
|
+
const { type, required, options, innerSchema, step } = analyzeFieldType(fieldSchema);
|
|
461
667
|
return {
|
|
462
668
|
name: fieldName,
|
|
463
669
|
label,
|
|
464
670
|
type,
|
|
465
671
|
required,
|
|
466
672
|
options,
|
|
673
|
+
step,
|
|
467
674
|
schema: innerSchema || fieldSchema
|
|
468
675
|
};
|
|
469
676
|
}
|
|
@@ -520,10 +727,20 @@ function analyzeFieldType(schema) {
|
|
|
520
727
|
let fieldType = "text";
|
|
521
728
|
if (def?.checks) {
|
|
522
729
|
const hasEmailCheck = def.checks.some(
|
|
523
|
-
(check) => check.
|
|
730
|
+
(check) => check.format === "email" || check.constructor?.name === "ZodEmail" || check._zod?.def?.format === "email"
|
|
524
731
|
);
|
|
525
732
|
if (hasEmailCheck) {
|
|
526
733
|
fieldType = "email";
|
|
734
|
+
} else {
|
|
735
|
+
const maxLengthCheck = def.checks.find(
|
|
736
|
+
(check) => check.constructor?.name === "$ZodCheckMaxLength" || check._zod?.def?.check === "max_length" || check._zod?.def?.maximum !== void 0
|
|
737
|
+
);
|
|
738
|
+
if (maxLengthCheck) {
|
|
739
|
+
const maxLength = maxLengthCheck._zod?.def?.maximum ?? maxLengthCheck.value ?? maxLengthCheck.maximum;
|
|
740
|
+
if (maxLength !== void 0 && maxLength > 50) {
|
|
741
|
+
fieldType = "textarea";
|
|
742
|
+
}
|
|
743
|
+
}
|
|
527
744
|
}
|
|
528
745
|
}
|
|
529
746
|
return {
|
|
@@ -533,10 +750,22 @@ function analyzeFieldType(schema) {
|
|
|
533
750
|
};
|
|
534
751
|
}
|
|
535
752
|
if (typeName === "number" || typeName === "ZodNumber") {
|
|
753
|
+
let step = void 0;
|
|
754
|
+
if (def?.checks) {
|
|
755
|
+
const hasIntCheck = def.checks.some(
|
|
756
|
+
(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"
|
|
757
|
+
);
|
|
758
|
+
if (!hasIntCheck) {
|
|
759
|
+
step = "any";
|
|
760
|
+
}
|
|
761
|
+
} else {
|
|
762
|
+
step = "any";
|
|
763
|
+
}
|
|
536
764
|
return {
|
|
537
765
|
type: "number",
|
|
538
766
|
required: true,
|
|
539
|
-
innerSchema: schema
|
|
767
|
+
innerSchema: schema,
|
|
768
|
+
step
|
|
540
769
|
};
|
|
541
770
|
}
|
|
542
771
|
if (typeName === "date" || typeName === "ZodDate") {
|
|
@@ -601,7 +830,8 @@ function modelFieldsToFormFields(fields) {
|
|
|
601
830
|
type: field.type,
|
|
602
831
|
label: field.label,
|
|
603
832
|
required: field.required,
|
|
604
|
-
options: field.options
|
|
833
|
+
options: field.options,
|
|
834
|
+
step: field.step
|
|
605
835
|
}));
|
|
606
836
|
}
|
|
607
837
|
function getFieldNamesFromFields(fields) {
|
|
@@ -619,6 +849,8 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
619
849
|
descriptionGetter;
|
|
620
850
|
/** 当前请求的表单 ID(用于在 render 和 getActions 之间共享) */
|
|
621
851
|
currentFormId;
|
|
852
|
+
/** 自定义表单字段渲染器 */
|
|
853
|
+
formFieldRenderers;
|
|
622
854
|
/**
|
|
623
855
|
* 获取或生成表单 ID(确保在同一个请求中保持一致)
|
|
624
856
|
*/
|
|
@@ -685,59 +917,7 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
685
917
|
if (!this.schema) {
|
|
686
918
|
return data;
|
|
687
919
|
}
|
|
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;
|
|
920
|
+
return preprocessFormData(data, this.schema);
|
|
741
921
|
}
|
|
742
922
|
/**
|
|
743
923
|
* 处理请求
|
|
@@ -747,12 +927,23 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
747
927
|
this.currentFormId = void 0;
|
|
748
928
|
if (ctx.req.method === "GET") {
|
|
749
929
|
return this.render(context);
|
|
750
|
-
} else if (ctx.req.method === "POST" || ctx.req.method === "PUT") {
|
|
930
|
+
} else if (ctx.req.method === "POST" || ctx.req.method === "PUT" || ctx.req.method === "PATCH") {
|
|
931
|
+
const methodOverride = ctx.req.header("X-HTTP-Method-Override");
|
|
932
|
+
const actualMethod = methodOverride || ctx.req.method;
|
|
933
|
+
const expectedMethod = this.getFormAction() === "edit" ? "PUT" : "POST";
|
|
934
|
+
if (actualMethod.toUpperCase() !== expectedMethod) {
|
|
935
|
+
logger.warn(
|
|
936
|
+
`[BaseFormFeature] Method mismatch: expected ${expectedMethod}, got ${actualMethod} (request method: ${ctx.req.method}, X-HTTP-Method-Override: ${methodOverride || "none"})`
|
|
937
|
+
);
|
|
938
|
+
}
|
|
751
939
|
const originalData = { ...context.body };
|
|
752
940
|
logger.info(
|
|
753
941
|
`[BaseFormFeature] Original body data: ${JSON.stringify(originalData)}`
|
|
754
942
|
);
|
|
755
943
|
let data = this.preprocessFormData(context.body);
|
|
944
|
+
logger.info(
|
|
945
|
+
`[BaseFormFeature] Preprocessed data: ${JSON.stringify(data)}`
|
|
946
|
+
);
|
|
756
947
|
if (!this.schema) {
|
|
757
948
|
throw new Error("Schema is required for form validation");
|
|
758
949
|
}
|
|
@@ -769,7 +960,10 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
769
960
|
);
|
|
770
961
|
return this.render(context, originalData);
|
|
771
962
|
}
|
|
772
|
-
const item = await this.handleSubmit(
|
|
963
|
+
const item = await this.handleSubmit(
|
|
964
|
+
context,
|
|
965
|
+
parseResult.data
|
|
966
|
+
);
|
|
773
967
|
if (!item) {
|
|
774
968
|
context.sendError(
|
|
775
969
|
this.getFormAction() === "create" ? "\u521B\u5EFA\u5931\u8D25" : "\u66F4\u65B0\u5931\u8D25",
|
|
@@ -787,6 +981,9 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
787
981
|
`[BaseFormFeature] Dialog mode: setting refresh to close dialog and refresh list`
|
|
788
982
|
);
|
|
789
983
|
context.setRefresh(true);
|
|
984
|
+
if (context.redirectUrl) {
|
|
985
|
+
context.redirectUrl = void 0;
|
|
986
|
+
}
|
|
790
987
|
return null;
|
|
791
988
|
} else {
|
|
792
989
|
const redirectUrl = this.getSuccessRedirectUrl(context, item);
|
|
@@ -799,12 +996,11 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
799
996
|
}
|
|
800
997
|
}
|
|
801
998
|
formFieldNames;
|
|
999
|
+
groups;
|
|
802
1000
|
/**
|
|
803
1001
|
* 渲染表单页面
|
|
804
1002
|
*/
|
|
805
1003
|
async render(context, initialData) {
|
|
806
|
-
const filteredFields = this.formFieldNames ? filterFieldsByNames(this.fields || [], this.formFieldNames) : this.fields || [];
|
|
807
|
-
const fields = modelFieldsToFormFields(filteredFields);
|
|
808
1004
|
let formData;
|
|
809
1005
|
if (this.getFormAction() === "edit") {
|
|
810
1006
|
if (initialData) {
|
|
@@ -826,6 +1022,50 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
826
1022
|
}
|
|
827
1023
|
const method = this.getFormAction() === "create" ? "post" : "put";
|
|
828
1024
|
const formId = this.getFormId(context);
|
|
1025
|
+
if (this.groups && this.groups.length > 0) {
|
|
1026
|
+
if (!this.schema) {
|
|
1027
|
+
throw new Error("Schema is required when using groups");
|
|
1028
|
+
}
|
|
1029
|
+
const schema = this.schema;
|
|
1030
|
+
const groupSchemas = this.groups.map((group) => {
|
|
1031
|
+
const pickObject = group.fields.reduce(
|
|
1032
|
+
(acc, fieldName) => {
|
|
1033
|
+
acc[fieldName] = true;
|
|
1034
|
+
return acc;
|
|
1035
|
+
},
|
|
1036
|
+
{}
|
|
1037
|
+
);
|
|
1038
|
+
return {
|
|
1039
|
+
label: group.label,
|
|
1040
|
+
schema: schema.pick(pickObject),
|
|
1041
|
+
fields: group.fields
|
|
1042
|
+
};
|
|
1043
|
+
});
|
|
1044
|
+
const groupFields = groupSchemas.map(
|
|
1045
|
+
({ label, schema: schema2, fields: fieldNames }) => {
|
|
1046
|
+
const groupFields2 = parseSchemaToFields(schema2);
|
|
1047
|
+
const formFields = modelFieldsToFormFields(groupFields2);
|
|
1048
|
+
return {
|
|
1049
|
+
label,
|
|
1050
|
+
fields: formFields
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
);
|
|
1054
|
+
return /* @__PURE__ */ jsx(
|
|
1055
|
+
FormPage,
|
|
1056
|
+
{
|
|
1057
|
+
groups: groupFields,
|
|
1058
|
+
submitUrl,
|
|
1059
|
+
method,
|
|
1060
|
+
initialData: formData,
|
|
1061
|
+
formId,
|
|
1062
|
+
isDialog: context.isDialog,
|
|
1063
|
+
formFieldRenderers: this.formFieldRenderers
|
|
1064
|
+
}
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
const filteredFields = this.formFieldNames ? filterFieldsByNames(this.fields || [], this.formFieldNames) : this.fields || [];
|
|
1068
|
+
const fields = modelFieldsToFormFields(filteredFields);
|
|
829
1069
|
return /* @__PURE__ */ jsx(
|
|
830
1070
|
FormPage,
|
|
831
1071
|
{
|
|
@@ -833,7 +1073,9 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
833
1073
|
submitUrl,
|
|
834
1074
|
method,
|
|
835
1075
|
initialData: formData,
|
|
836
|
-
formId
|
|
1076
|
+
formId,
|
|
1077
|
+
isDialog: context.isDialog,
|
|
1078
|
+
formFieldRenderers: this.formFieldRenderers
|
|
837
1079
|
}
|
|
838
1080
|
);
|
|
839
1081
|
}
|
|
@@ -942,6 +1184,8 @@ var DefaultCreateFeature = class extends BaseFormFeature {
|
|
|
942
1184
|
this.fields = parseSchemaToFields(options.schema);
|
|
943
1185
|
this.createItem = options.createItem;
|
|
944
1186
|
this.formFieldNames = options.formFieldNames;
|
|
1187
|
+
this.groups = options.groups;
|
|
1188
|
+
this.formFieldRenderers = options.formFieldRenderers;
|
|
945
1189
|
}
|
|
946
1190
|
getFormAction() {
|
|
947
1191
|
return "create";
|
|
@@ -1001,12 +1245,119 @@ var DefaultDeleteFeature = class extends BaseFeature {
|
|
|
1001
1245
|
}
|
|
1002
1246
|
}
|
|
1003
1247
|
};
|
|
1248
|
+
function Card(props) {
|
|
1249
|
+
const {
|
|
1250
|
+
children,
|
|
1251
|
+
title,
|
|
1252
|
+
className = "",
|
|
1253
|
+
shadow = true,
|
|
1254
|
+
bordered = false,
|
|
1255
|
+
noPadding = false
|
|
1256
|
+
} = props;
|
|
1257
|
+
const baseClasses = "bg-white rounded-lg";
|
|
1258
|
+
const shadowClass = shadow ? "shadow-sm hover:shadow-md transition-shadow" : "";
|
|
1259
|
+
const borderClass = bordered ? "border border-gray-200" : "";
|
|
1260
|
+
const paddingClass = noPadding ? "" : "p-6";
|
|
1261
|
+
return /* @__PURE__ */ jsxs(
|
|
1262
|
+
"div",
|
|
1263
|
+
{
|
|
1264
|
+
className: `${baseClasses} ${shadowClass} ${borderClass} ${className}`,
|
|
1265
|
+
children: [
|
|
1266
|
+
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 }) }),
|
|
1267
|
+
/* @__PURE__ */ jsx("div", { className: noPadding ? "" : paddingClass, children })
|
|
1268
|
+
]
|
|
1269
|
+
}
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
function renderDefaultValue(value) {
|
|
1273
|
+
if (value === null || value === void 0) {
|
|
1274
|
+
return /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "-" });
|
|
1275
|
+
}
|
|
1276
|
+
if (Array.isArray(value)) {
|
|
1277
|
+
if (value.length === 0) {
|
|
1278
|
+
return /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "\u6682\u65E0\u6570\u636E" });
|
|
1279
|
+
}
|
|
1280
|
+
if (value.length > 0 && typeof value[0] === "object" && value[0] !== null) {
|
|
1281
|
+
return /* @__PURE__ */ jsxs("span", { className: "text-gray-600", children: [
|
|
1282
|
+
"\u5305\u542B ",
|
|
1283
|
+
value.length,
|
|
1284
|
+
" \u9879\uFF08\u5BF9\u8C61\u6570\u7EC4\uFF0C\u5EFA\u8BAE\u4F7F\u7528\u81EA\u5B9A\u4E49\u6E32\u67D3\uFF09"
|
|
1285
|
+
] });
|
|
1286
|
+
}
|
|
1287
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: value.map((item, index) => /* @__PURE__ */ jsx(
|
|
1288
|
+
"span",
|
|
1289
|
+
{
|
|
1290
|
+
className: "px-2 py-1 bg-gray-100 text-gray-700 rounded text-sm",
|
|
1291
|
+
children: String(item)
|
|
1292
|
+
},
|
|
1293
|
+
index
|
|
1294
|
+
)) });
|
|
1295
|
+
}
|
|
1296
|
+
if (typeof value === "object") {
|
|
1297
|
+
if (value instanceof Date) {
|
|
1298
|
+
return /* @__PURE__ */ jsx("span", { children: value.toLocaleString() });
|
|
1299
|
+
}
|
|
1300
|
+
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) });
|
|
1301
|
+
}
|
|
1302
|
+
if (typeof value === "boolean") {
|
|
1303
|
+
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" });
|
|
1304
|
+
}
|
|
1305
|
+
if (typeof value === "number") {
|
|
1306
|
+
return /* @__PURE__ */ jsx("span", { children: value.toLocaleString() });
|
|
1307
|
+
}
|
|
1308
|
+
return /* @__PURE__ */ jsx("span", { children: String(value) });
|
|
1309
|
+
}
|
|
1310
|
+
function renderField(field, value, item) {
|
|
1311
|
+
let content;
|
|
1312
|
+
if (field.render) {
|
|
1313
|
+
const rendered = field.render(value, item);
|
|
1314
|
+
if (rendered === null || rendered === void 0) {
|
|
1315
|
+
content = /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "-" });
|
|
1316
|
+
} else if (typeof rendered === "string" || typeof rendered === "number" || typeof rendered === "boolean") {
|
|
1317
|
+
content = /* @__PURE__ */ jsx("span", { children: String(rendered) });
|
|
1318
|
+
} else {
|
|
1319
|
+
content = rendered;
|
|
1320
|
+
}
|
|
1321
|
+
} else {
|
|
1322
|
+
content = renderDefaultValue(value);
|
|
1323
|
+
}
|
|
1324
|
+
return /* @__PURE__ */ jsxs(
|
|
1325
|
+
"div",
|
|
1326
|
+
{
|
|
1327
|
+
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 ",
|
|
1328
|
+
children: [
|
|
1329
|
+
/* @__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 }) }),
|
|
1330
|
+
/* @__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 }) })
|
|
1331
|
+
]
|
|
1332
|
+
},
|
|
1333
|
+
field.key
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1004
1336
|
function DetailPage(props) {
|
|
1005
|
-
const { item, fields } = props;
|
|
1006
|
-
|
|
1007
|
-
/* @__PURE__ */ jsx("
|
|
1008
|
-
|
|
1009
|
-
|
|
1337
|
+
const { item, fields, groups } = props;
|
|
1338
|
+
if (groups && groups.length > 0) {
|
|
1339
|
+
return /* @__PURE__ */ jsx("div", { className: "space-y-6", children: groups.map((group, groupIndex) => /* @__PURE__ */ jsx(
|
|
1340
|
+
Card,
|
|
1341
|
+
{
|
|
1342
|
+
title: group.label,
|
|
1343
|
+
shadow: true,
|
|
1344
|
+
bordered: true,
|
|
1345
|
+
noPadding: true,
|
|
1346
|
+
children: /* @__PURE__ */ jsx("dl", { className: "divide-y divide-gray-100", children: group.fields.map((field) => {
|
|
1347
|
+
const value = group.values[field.key];
|
|
1348
|
+
return renderField(field, value, item);
|
|
1349
|
+
}) })
|
|
1350
|
+
},
|
|
1351
|
+
groupIndex
|
|
1352
|
+
)) });
|
|
1353
|
+
}
|
|
1354
|
+
if (fields && fields.length > 0) {
|
|
1355
|
+
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) => {
|
|
1356
|
+
const value = item[field.key];
|
|
1357
|
+
return renderField(field, value, item);
|
|
1358
|
+
}) }) });
|
|
1359
|
+
}
|
|
1360
|
+
return null;
|
|
1010
1361
|
}
|
|
1011
1362
|
var DefaultDetailFeature = class extends BaseFeature {
|
|
1012
1363
|
getItem;
|
|
@@ -1014,6 +1365,8 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
1014
1365
|
titleGetter;
|
|
1015
1366
|
descriptionGetter;
|
|
1016
1367
|
detailFieldNames;
|
|
1368
|
+
fieldRenderers;
|
|
1369
|
+
groups;
|
|
1017
1370
|
constructor(options) {
|
|
1018
1371
|
super({
|
|
1019
1372
|
name: "detail",
|
|
@@ -1029,6 +1382,8 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
1029
1382
|
this.titleGetter = options.getTitle;
|
|
1030
1383
|
this.descriptionGetter = options.getDescription;
|
|
1031
1384
|
this.detailFieldNames = options.detailFieldNames;
|
|
1385
|
+
this.fieldRenderers = options.fieldRenderers;
|
|
1386
|
+
this.groups = options.groups;
|
|
1032
1387
|
}
|
|
1033
1388
|
async getTitle(context) {
|
|
1034
1389
|
if (this.titleGetter) {
|
|
@@ -1059,6 +1414,40 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
1059
1414
|
if (!item) {
|
|
1060
1415
|
return context.ctx.json({ error: "Not found" }, 404);
|
|
1061
1416
|
}
|
|
1417
|
+
if (this.groups && this.groups.length > 0) {
|
|
1418
|
+
if (!this.schema) {
|
|
1419
|
+
throw new Error("Schema is required when using groups");
|
|
1420
|
+
}
|
|
1421
|
+
const schema = this.schema;
|
|
1422
|
+
const groupSchemas = this.groups.map((group) => {
|
|
1423
|
+
const pickObject = group.fields.reduce((acc, fieldName) => {
|
|
1424
|
+
acc[fieldName] = true;
|
|
1425
|
+
return acc;
|
|
1426
|
+
}, {});
|
|
1427
|
+
return {
|
|
1428
|
+
label: group.label,
|
|
1429
|
+
schema: schema.pick(pickObject),
|
|
1430
|
+
fields: group.fields
|
|
1431
|
+
};
|
|
1432
|
+
});
|
|
1433
|
+
const groupFields = groupSchemas.map(({ label, schema: schema2, fields: fieldNames }) => {
|
|
1434
|
+
const groupFields2 = parseSchemaToFields(schema2);
|
|
1435
|
+
const detailFields2 = groupFields2.map((field) => ({
|
|
1436
|
+
key: field.name,
|
|
1437
|
+
label: field.label,
|
|
1438
|
+
render: this.fieldRenderers?.[field.name]
|
|
1439
|
+
}));
|
|
1440
|
+
return {
|
|
1441
|
+
label,
|
|
1442
|
+
fields: detailFields2,
|
|
1443
|
+
values: fieldNames.reduce((acc, fieldName) => {
|
|
1444
|
+
acc[fieldName] = item[fieldName];
|
|
1445
|
+
return acc;
|
|
1446
|
+
}, {})
|
|
1447
|
+
};
|
|
1448
|
+
});
|
|
1449
|
+
return /* @__PURE__ */ jsx(DetailPage, { item, groups: groupFields });
|
|
1450
|
+
}
|
|
1062
1451
|
const detailFields = this.detailFieldNames ? filterFieldsByNames(this.fields || [], this.detailFieldNames) : this.fields || [];
|
|
1063
1452
|
if (this.detailFieldNames) {
|
|
1064
1453
|
const systemFields = ["id", "createdAt", "updatedAt"];
|
|
@@ -1077,7 +1466,9 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
1077
1466
|
const detailFieldNames = getFieldNamesFromFields(detailFields);
|
|
1078
1467
|
const fields = detailFieldNames.map((fieldName) => ({
|
|
1079
1468
|
key: fieldName,
|
|
1080
|
-
label: getFieldLabelFromFields(this.fields || [], fieldName) || fieldName
|
|
1469
|
+
label: getFieldLabelFromFields(this.fields || [], fieldName) || fieldName,
|
|
1470
|
+
render: this.fieldRenderers?.[fieldName]
|
|
1471
|
+
// 如果有自定义渲染函数则使用
|
|
1081
1472
|
}));
|
|
1082
1473
|
return /* @__PURE__ */ jsx(DetailPage, { item, fields });
|
|
1083
1474
|
}
|
|
@@ -1155,6 +1546,8 @@ var DefaultEditFeature = class extends BaseFormFeature {
|
|
|
1155
1546
|
this.getItem = options.getItem;
|
|
1156
1547
|
this.updateItem = options.updateItem;
|
|
1157
1548
|
this.formFieldNames = options.formFieldNames;
|
|
1549
|
+
this.groups = options.groups;
|
|
1550
|
+
this.formFieldRenderers = options.formFieldRenderers;
|
|
1158
1551
|
}
|
|
1159
1552
|
getFormAction() {
|
|
1160
1553
|
return "edit";
|
|
@@ -1390,30 +1783,6 @@ function Button(props) {
|
|
|
1390
1783
|
}
|
|
1391
1784
|
);
|
|
1392
1785
|
}
|
|
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
1786
|
function EmptyState(props) {
|
|
1418
1787
|
const { message = "\u6682\u65E0\u6570\u636E", children } = props;
|
|
1419
1788
|
return /* @__PURE__ */ jsx("div", { className: "text-center py-12", children: children || /* @__PURE__ */ jsx("p", { className: "text-gray-500 text-sm", children: message }) });
|
|
@@ -1833,6 +2202,7 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
1833
2202
|
deleteItem;
|
|
1834
2203
|
listFieldNames;
|
|
1835
2204
|
filterSchema;
|
|
2205
|
+
columnRenderers;
|
|
1836
2206
|
constructor(options) {
|
|
1837
2207
|
super({
|
|
1838
2208
|
name: "list",
|
|
@@ -1847,6 +2217,7 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
1847
2217
|
this.deleteItem = options.deleteItem;
|
|
1848
2218
|
this.listFieldNames = options.listFieldNames;
|
|
1849
2219
|
this.filterSchema = options.filterSchema;
|
|
2220
|
+
this.columnRenderers = options.columnRenderers;
|
|
1850
2221
|
}
|
|
1851
2222
|
getRoutes() {
|
|
1852
2223
|
return [{ method: "get", path: "/list" }];
|
|
@@ -1859,7 +2230,9 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
1859
2230
|
const filterFields = this.filterSchema ? modelFieldsToFormFields(parseSchemaToFields(this.filterSchema)) : [];
|
|
1860
2231
|
const columns = listFieldNames.map((fieldName) => ({
|
|
1861
2232
|
key: fieldName,
|
|
1862
|
-
label: getFieldLabelFromFields(this.fields || [], fieldName)
|
|
2233
|
+
label: getFieldLabelFromFields(this.fields || [], fieldName),
|
|
2234
|
+
render: this.columnRenderers?.[fieldName]
|
|
2235
|
+
// 如果有自定义渲染函数则使用
|
|
1863
2236
|
}));
|
|
1864
2237
|
const model = context.model;
|
|
1865
2238
|
const prefix = context.prefix || "";
|
|
@@ -2301,7 +2674,8 @@ function Dialog(props) {
|
|
|
2301
2674
|
className = "",
|
|
2302
2675
|
size = "lg",
|
|
2303
2676
|
closeOnBackdropClick = true,
|
|
2304
|
-
actions = []
|
|
2677
|
+
actions = [],
|
|
2678
|
+
fixedContentHeight = false
|
|
2305
2679
|
} = props;
|
|
2306
2680
|
const sizeClasses = {
|
|
2307
2681
|
sm: "max-w-md",
|
|
@@ -2360,7 +2734,13 @@ function Dialog(props) {
|
|
|
2360
2734
|
}
|
|
2361
2735
|
)
|
|
2362
2736
|
] }),
|
|
2363
|
-
/* @__PURE__ */ jsx(
|
|
2737
|
+
/* @__PURE__ */ jsx(
|
|
2738
|
+
"div",
|
|
2739
|
+
{
|
|
2740
|
+
className: `${fixedContentHeight ? "h-[70vh]" : "flex-1"} overflow-y-auto ${fixedContentHeight ? "p-0" : "p-6"} bg-gray-50`,
|
|
2741
|
+
children
|
|
2742
|
+
}
|
|
2743
|
+
),
|
|
2364
2744
|
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)) })
|
|
2365
2745
|
]
|
|
2366
2746
|
}
|
|
@@ -2621,6 +3001,13 @@ function BaseLayout(props) {
|
|
|
2621
3001
|
{
|
|
2622
3002
|
dangerouslySetInnerHTML: {
|
|
2623
3003
|
__html: `
|
|
3004
|
+
/* \u5BB9\u5668\u67E5\u8BE2\u652F\u6301 - \u5982\u679C Tailwind CDN \u4E0D\u652F\u6301\uFF0C\u4F7F\u7528\u539F\u751F CSS \u5BB9\u5668\u67E5\u8BE2 */
|
|
3005
|
+
@supports (container-type: inline-size) {
|
|
3006
|
+
.\\@container {
|
|
3007
|
+
container-type: inline-size;
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
|
|
2624
3011
|
@keyframes fadeIn {
|
|
2625
3012
|
from { opacity: 0;}
|
|
2626
3013
|
to { opacity: 1;}
|
|
@@ -3143,9 +3530,9 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3143
3530
|
headers
|
|
3144
3531
|
);
|
|
3145
3532
|
}
|
|
3146
|
-
if (context.redirectUrl) {
|
|
3533
|
+
if (context.redirectUrl && !context.refresh) {
|
|
3147
3534
|
logger.info(
|
|
3148
|
-
`[ResponseRenderer] Redirect URL found: ${context.redirectUrl} (isHtmxRequest: ${context.isHtmxRequest})`
|
|
3535
|
+
`[ResponseRenderer] Redirect URL found: ${context.redirectUrl} (isHtmxRequest: ${context.isHtmxRequest}, isDialog: ${context.isDialog})`
|
|
3149
3536
|
);
|
|
3150
3537
|
if (context.isHtmxRequest) {
|
|
3151
3538
|
return ctx.html(/* @__PURE__ */ jsx("div", {}), 200, {
|
|
@@ -3154,7 +3541,11 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3154
3541
|
} else {
|
|
3155
3542
|
return ctx.redirect(context.redirectUrl);
|
|
3156
3543
|
}
|
|
3157
|
-
} else {
|
|
3544
|
+
} else if (context.redirectUrl && context.refresh) {
|
|
3545
|
+
logger.info(
|
|
3546
|
+
`[ResponseRenderer] Both redirect URL and refresh are set, using refresh (isDialog: ${context.isDialog})`
|
|
3547
|
+
);
|
|
3548
|
+
} else if (!context.redirectUrl) {
|
|
3158
3549
|
logger.info(
|
|
3159
3550
|
`[ResponseRenderer] No redirect URL found (result: ${result === null ? "null" : typeof result}, isHtmxRequest: ${context.isHtmxRequest})`
|
|
3160
3551
|
);
|
|
@@ -3205,6 +3596,8 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3205
3596
|
if (context.isDialog) {
|
|
3206
3597
|
const dialogSize = renderOptions.feature?.dialogSize || "lg";
|
|
3207
3598
|
const closeOnBackdropClick = renderOptions.feature?.closeOnBackdropClick ?? true;
|
|
3599
|
+
const isFormFeature = renderOptions.feature?.type === "create" || renderOptions.feature?.type === "edit";
|
|
3600
|
+
const fixedContentHeight = isFormFeature;
|
|
3208
3601
|
return ctx.html(
|
|
3209
3602
|
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3210
3603
|
/* @__PURE__ */ jsx(
|
|
@@ -3214,6 +3607,7 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3214
3607
|
size: dialogSize,
|
|
3215
3608
|
closeOnBackdropClick,
|
|
3216
3609
|
actions,
|
|
3610
|
+
fixedContentHeight,
|
|
3217
3611
|
children: result
|
|
3218
3612
|
}
|
|
3219
3613
|
),
|
|
@@ -3474,6 +3868,28 @@ function registerPageRoutes(page, options) {
|
|
|
3474
3868
|
});
|
|
3475
3869
|
};
|
|
3476
3870
|
options.hono[route.method](fullPath, handler);
|
|
3871
|
+
if (route.method === "put" || route.method === "delete") {
|
|
3872
|
+
const postHandler = async (ctx) => {
|
|
3873
|
+
const methodOverride = ctx.req.header("X-HTTP-Method-Override");
|
|
3874
|
+
const expectedMethod = route.method.toUpperCase();
|
|
3875
|
+
if (methodOverride === expectedMethod) {
|
|
3876
|
+
logger.info(
|
|
3877
|
+
`[HtmxAdminPlugin] Method override detected: POST ${fullPath} -> ${expectedMethod} (feature: ${feature.name})`
|
|
3878
|
+
);
|
|
3879
|
+
return handleRequest(ctx, page, feature, {
|
|
3880
|
+
options: options.options
|
|
3881
|
+
});
|
|
3882
|
+
}
|
|
3883
|
+
logger.warn(
|
|
3884
|
+
`[HtmxAdminPlugin] POST request to ${fullPath} without matching X-HTTP-Method-Override header (got: ${methodOverride || "none"}, expected: ${expectedMethod})`
|
|
3885
|
+
);
|
|
3886
|
+
return ctx.text("Method Not Allowed", 405);
|
|
3887
|
+
};
|
|
3888
|
+
logger.info(
|
|
3889
|
+
`[HtmxAdminPlugin] Registering POST route for method override: POST ${fullPath} (actual method: ${route.method.toUpperCase()}, feature: ${feature.name})`
|
|
3890
|
+
);
|
|
3891
|
+
options.hono.post(fullPath, postHandler);
|
|
3892
|
+
}
|
|
3477
3893
|
}
|
|
3478
3894
|
}
|
|
3479
3895
|
}
|
|
@@ -3563,5 +3979,80 @@ var HtmxAdminPlugin = class {
|
|
|
3563
3979
|
registerHomeRedirect(this.pages, routeOptions);
|
|
3564
3980
|
}
|
|
3565
3981
|
};
|
|
3982
|
+
function createFormFieldXData(options) {
|
|
3983
|
+
const {
|
|
3984
|
+
fieldName,
|
|
3985
|
+
dataKey,
|
|
3986
|
+
defaultValue = [],
|
|
3987
|
+
customData = {},
|
|
3988
|
+
customMethods = {}
|
|
3989
|
+
} = options;
|
|
3990
|
+
const dataEntries = [];
|
|
3991
|
+
dataEntries.push(`${dataKey}: ${JSON.stringify(defaultValue)}`);
|
|
3992
|
+
for (const [key, value] of Object.entries(customData)) {
|
|
3993
|
+
dataEntries.push(`${key}: ${JSON.stringify(value)}`);
|
|
3994
|
+
}
|
|
3995
|
+
const methodEntries = [];
|
|
3996
|
+
methodEntries.push(`init() {
|
|
3997
|
+
const dataAttr = this.$el.getAttribute('data-initial-value');
|
|
3998
|
+
if (dataAttr) {
|
|
3999
|
+
try {
|
|
4000
|
+
this.${dataKey} = JSON.parse(dataAttr);
|
|
4001
|
+
} catch (e) {
|
|
4002
|
+
console.error('Failed to parse initial value:', e);
|
|
4003
|
+
this.${dataKey} = ${JSON.stringify(defaultValue)};
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
this.updateHiddenField();
|
|
4007
|
+
}`);
|
|
4008
|
+
methodEntries.push(`updateHiddenField() {
|
|
4009
|
+
const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
|
|
4010
|
+
if (hiddenInput) {
|
|
4011
|
+
hiddenInput.value = JSON.stringify(this.${dataKey});
|
|
4012
|
+
}
|
|
4013
|
+
}`);
|
|
4014
|
+
for (const [methodName, methodBody] of Object.entries(customMethods)) {
|
|
4015
|
+
methodEntries.push(`${methodName}${methodBody}`);
|
|
4016
|
+
}
|
|
4017
|
+
const dataStr = dataEntries.join(",\n ");
|
|
4018
|
+
const methodsStr = methodEntries.join(",\n ");
|
|
4019
|
+
return `{
|
|
4020
|
+
${dataStr},
|
|
4021
|
+
${methodsStr}
|
|
4022
|
+
}`;
|
|
4023
|
+
}
|
|
4024
|
+
function FormFieldWrapper(props) {
|
|
4025
|
+
const {
|
|
4026
|
+
fieldName,
|
|
4027
|
+
initialValue,
|
|
4028
|
+
xData,
|
|
4029
|
+
autoSync = false,
|
|
4030
|
+
children,
|
|
4031
|
+
className = "space-y-4"
|
|
4032
|
+
} = props;
|
|
4033
|
+
const initialValueJson = JSON.stringify(initialValue);
|
|
4034
|
+
return /* @__PURE__ */ jsxs(
|
|
4035
|
+
"div",
|
|
4036
|
+
{
|
|
4037
|
+
"x-data": xData,
|
|
4038
|
+
"data-initial-value": initialValueJson,
|
|
4039
|
+
"x-init": "init()",
|
|
4040
|
+
...autoSync ? { "x-effect": "updateHiddenField()" } : {},
|
|
4041
|
+
className,
|
|
4042
|
+
children: [
|
|
4043
|
+
/* @__PURE__ */ jsx(
|
|
4044
|
+
"input",
|
|
4045
|
+
{
|
|
4046
|
+
type: "hidden",
|
|
4047
|
+
name: fieldName,
|
|
4048
|
+
value: "",
|
|
4049
|
+
"data-testid": `hidden-${fieldName}`
|
|
4050
|
+
}
|
|
4051
|
+
),
|
|
4052
|
+
children
|
|
4053
|
+
]
|
|
4054
|
+
}
|
|
4055
|
+
);
|
|
4056
|
+
}
|
|
3566
4057
|
|
|
3567
|
-
export { BaseFeature, CustomFeature, DefaultCreateFeature, DefaultDeleteFeature, DefaultDetailFeature, DefaultEditFeature, DefaultListFeature, Dialog, ErrorAlert, HtmxAdminPlugin, LoadingBar, PageModel, checkUserPermission, getUserInfo, modelNameToPath, parseListParams };
|
|
4058
|
+
export { BaseFeature, CustomFeature, DefaultCreateFeature, DefaultDeleteFeature, DefaultDetailFeature, DefaultEditFeature, DefaultListFeature, Dialog, ErrorAlert, FormFieldWrapper, HtmxAdminPlugin, LoadingBar, PageModel, checkUserPermission, createFormFieldXData, getUserInfo, modelNameToPath, parseListParams };
|