imean-service-engine-htmx-plugin 2.1.1 → 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 +110 -14
- package/dist/index.d.ts +110 -14
- package/dist/index.js +712 -223
- package/dist/index.mjs +711 -224
- 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.js
CHANGED
|
@@ -295,144 +295,350 @@ function getFieldValue(field, initialData) {
|
|
|
295
295
|
if (value === null || value === void 0) {
|
|
296
296
|
return "";
|
|
297
297
|
}
|
|
298
|
+
if (typeof value === "object") {
|
|
299
|
+
if (value instanceof Date) {
|
|
300
|
+
return value.toISOString();
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
return JSON.stringify(value);
|
|
304
|
+
} catch (e) {
|
|
305
|
+
return "";
|
|
306
|
+
}
|
|
307
|
+
}
|
|
298
308
|
return String(value);
|
|
299
309
|
}
|
|
300
310
|
return "";
|
|
301
311
|
}
|
|
312
|
+
function isJsonString(value) {
|
|
313
|
+
if (!value || typeof value !== "string") {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
const trimmed = value.trim();
|
|
317
|
+
return trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]");
|
|
318
|
+
}
|
|
319
|
+
function formatJsonString(value) {
|
|
320
|
+
if (!value || typeof value !== "string") {
|
|
321
|
+
return value;
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const parsed = JSON.parse(value);
|
|
325
|
+
return JSON.stringify(parsed, null, 2);
|
|
326
|
+
} catch (e) {
|
|
327
|
+
return value;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function renderFormField(field, initialData, formFieldRenderers) {
|
|
331
|
+
const value = getFieldValue(field, initialData);
|
|
332
|
+
const customRenderer = formFieldRenderers?.[field.name];
|
|
333
|
+
if (customRenderer) {
|
|
334
|
+
let parsedValue = null;
|
|
335
|
+
if (value && isJsonString(value)) {
|
|
336
|
+
try {
|
|
337
|
+
parsedValue = JSON.parse(value);
|
|
338
|
+
} catch (e) {
|
|
339
|
+
parsedValue = null;
|
|
340
|
+
}
|
|
341
|
+
} else if (value) {
|
|
342
|
+
parsedValue = value;
|
|
343
|
+
}
|
|
344
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", "data-testid": `field-${field.name}`, children: [
|
|
345
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
346
|
+
"label",
|
|
347
|
+
{
|
|
348
|
+
htmlFor: field.name,
|
|
349
|
+
className: "block text-sm font-semibold text-gray-700",
|
|
350
|
+
"data-testid": `label-${field.name}`,
|
|
351
|
+
children: [
|
|
352
|
+
field.label,
|
|
353
|
+
field.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", children: "*" })
|
|
354
|
+
]
|
|
355
|
+
}
|
|
356
|
+
),
|
|
357
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: customRenderer({
|
|
358
|
+
field,
|
|
359
|
+
value: parsedValue,
|
|
360
|
+
initialData,
|
|
361
|
+
fieldName: field.name
|
|
362
|
+
}) }),
|
|
363
|
+
field.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
|
|
364
|
+
] }, field.name);
|
|
365
|
+
}
|
|
366
|
+
const shouldUseTextarea = field.type === "textarea" || value && isJsonString(value) && field.type !== "select";
|
|
367
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", "data-testid": `field-${field.name}`, children: [
|
|
368
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
369
|
+
"label",
|
|
370
|
+
{
|
|
371
|
+
htmlFor: field.name,
|
|
372
|
+
className: "block text-sm font-semibold text-gray-700",
|
|
373
|
+
"data-testid": `label-${field.name}`,
|
|
374
|
+
children: [
|
|
375
|
+
field.label,
|
|
376
|
+
field.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", children: "*" })
|
|
377
|
+
]
|
|
378
|
+
}
|
|
379
|
+
),
|
|
380
|
+
shouldUseTextarea ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
381
|
+
"textarea",
|
|
382
|
+
{
|
|
383
|
+
id: field.name,
|
|
384
|
+
name: field.name,
|
|
385
|
+
required: field.required,
|
|
386
|
+
placeholder: field.placeholder || (isJsonString(value) ? "JSON \u683C\u5F0F\u6570\u636E" : ""),
|
|
387
|
+
rows: isJsonString(value) ? 10 : 4,
|
|
388
|
+
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",
|
|
389
|
+
"data-testid": `input-${field.name}`,
|
|
390
|
+
children: isJsonString(value) ? formatJsonString(value) : value
|
|
391
|
+
},
|
|
392
|
+
`${field.name}-${value}`
|
|
393
|
+
) : field.type === "select" ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
394
|
+
"select",
|
|
395
|
+
{
|
|
396
|
+
id: field.name,
|
|
397
|
+
name: field.name,
|
|
398
|
+
required: field.required,
|
|
399
|
+
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",
|
|
400
|
+
"data-testid": `select-${field.name}`,
|
|
401
|
+
children: [
|
|
402
|
+
!field.required && /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", selected: !value || value === "", children: "\u8BF7\u9009\u62E9" }),
|
|
403
|
+
field.options && field.options.length > 0 ? field.options.map((option) => {
|
|
404
|
+
const optionValue = String(option.value);
|
|
405
|
+
const isSelected = value === optionValue;
|
|
406
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
407
|
+
"option",
|
|
408
|
+
{
|
|
409
|
+
value: optionValue,
|
|
410
|
+
selected: isSelected,
|
|
411
|
+
children: option.label
|
|
412
|
+
},
|
|
413
|
+
optionValue
|
|
414
|
+
);
|
|
415
|
+
}) : /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", disabled: true, children: "\u6682\u65E0\u9009\u9879" })
|
|
416
|
+
]
|
|
417
|
+
},
|
|
418
|
+
`${field.name}-${value || ""}`
|
|
419
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
420
|
+
"input",
|
|
421
|
+
{
|
|
422
|
+
type: field.type || "text",
|
|
423
|
+
id: field.name,
|
|
424
|
+
name: field.name,
|
|
425
|
+
required: field.required,
|
|
426
|
+
placeholder: field.placeholder,
|
|
427
|
+
step: field.type === "number" ? field.step ?? void 0 : void 0,
|
|
428
|
+
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",
|
|
429
|
+
value,
|
|
430
|
+
"data-testid": `input-${field.name}`
|
|
431
|
+
},
|
|
432
|
+
`${field.name}-${value}`
|
|
433
|
+
),
|
|
434
|
+
field.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
|
|
435
|
+
] }, field.name);
|
|
436
|
+
}
|
|
302
437
|
function FormPage(props) {
|
|
303
|
-
const { fields, submitUrl, method = "post", initialData, formId } = props;
|
|
438
|
+
const { fields, groups, submitUrl, method = "post", initialData, formId, isDialog = false, formFieldRenderers } = props;
|
|
304
439
|
const finalFormId = formId || `form-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
305
440
|
if (process.env.NODE_ENV === "development" && initialData) {
|
|
441
|
+
const fieldNames = fields ? fields.map((f) => f.name).join(", ") : groups ? groups.map((g) => g.fields.map((f) => f.name).join(", ")).join(" | ") : "";
|
|
306
442
|
console.log(
|
|
307
|
-
`[FormPage] initialData: ${JSON.stringify(initialData)}, fields: ${
|
|
443
|
+
`[FormPage] initialData: ${JSON.stringify(initialData)}, fields: ${fieldNames}`
|
|
308
444
|
);
|
|
309
445
|
}
|
|
310
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
446
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full", "data-testid": "form-container", "x-data": `{ activeTab: 0 }`, children: [
|
|
447
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
448
|
+
"div",
|
|
449
|
+
{
|
|
450
|
+
id: "form-loading-indicator",
|
|
451
|
+
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",
|
|
452
|
+
children: [
|
|
453
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
454
|
+
"svg",
|
|
455
|
+
{
|
|
456
|
+
className: "animate-spin h-4 w-4",
|
|
457
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
458
|
+
fill: "none",
|
|
459
|
+
viewBox: "0 0 24 24",
|
|
460
|
+
children: [
|
|
461
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
462
|
+
"circle",
|
|
463
|
+
{
|
|
464
|
+
className: "opacity-25",
|
|
465
|
+
cx: "12",
|
|
466
|
+
cy: "12",
|
|
467
|
+
r: "10",
|
|
468
|
+
stroke: "currentColor",
|
|
469
|
+
strokeWidth: "4"
|
|
470
|
+
}
|
|
471
|
+
),
|
|
472
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
473
|
+
"path",
|
|
474
|
+
{
|
|
475
|
+
className: "opacity-75",
|
|
476
|
+
fill: "currentColor",
|
|
477
|
+
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"
|
|
478
|
+
}
|
|
479
|
+
)
|
|
480
|
+
]
|
|
481
|
+
}
|
|
482
|
+
),
|
|
483
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: "\u63D0\u4EA4\u4E2D..." })
|
|
484
|
+
]
|
|
485
|
+
}
|
|
486
|
+
),
|
|
487
|
+
groups && groups.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
488
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: `sticky top-0 z-10 bg-white border-b border-gray-200 shadow-sm ${isDialog ? "px-6" : "-mx-6 px-6 -mt-6"}`, children: /* @__PURE__ */ jsxRuntime.jsx("nav", { className: "flex -mb-px w-full", "aria-label": "Tabs", "data-testid": "form-tabs", children: groups.map((group, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
489
|
+
"button",
|
|
490
|
+
{
|
|
491
|
+
type: "button",
|
|
492
|
+
"x-on:click": `activeTab = ${index}`,
|
|
493
|
+
"x-bind:class": `activeTab === ${index} ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'`,
|
|
494
|
+
className: "px-6 py-4 text-sm font-medium border-b-2 transition-colors duration-150 whitespace-nowrap flex-1 text-center",
|
|
495
|
+
"data-testid": `form-tab-${index}`,
|
|
496
|
+
children: group.label
|
|
497
|
+
},
|
|
498
|
+
index
|
|
499
|
+
)) }) }),
|
|
500
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
501
|
+
"form",
|
|
502
|
+
{
|
|
503
|
+
id: finalFormId,
|
|
504
|
+
method: method === "put" ? "post" : method,
|
|
505
|
+
action: submitUrl,
|
|
506
|
+
"hx-boost": "true",
|
|
507
|
+
...method === "put" ? { "hx-put": submitUrl } : {},
|
|
508
|
+
"hx-indicator": "#form-loading-indicator",
|
|
509
|
+
"data-testid": "form",
|
|
510
|
+
className: isDialog ? "p-6" : "mt-6",
|
|
511
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6", children: groups.map((group, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
512
|
+
"div",
|
|
513
|
+
{
|
|
514
|
+
"x-show": `activeTab === ${index}`,
|
|
515
|
+
className: "space-y-6",
|
|
516
|
+
"data-testid": `form-tab-content-${index}`,
|
|
517
|
+
children: group.fields.map((field) => renderFormField(field, initialData, formFieldRenderers))
|
|
518
|
+
},
|
|
519
|
+
index
|
|
520
|
+
)) }) })
|
|
521
|
+
}
|
|
522
|
+
)
|
|
523
|
+
] }) : (
|
|
524
|
+
/* 平铺模式(向后兼容) */
|
|
525
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
526
|
+
"form",
|
|
527
|
+
{
|
|
528
|
+
id: finalFormId,
|
|
529
|
+
method: method === "put" ? "post" : method,
|
|
530
|
+
action: submitUrl,
|
|
531
|
+
"hx-boost": "true",
|
|
532
|
+
...method === "put" ? { "hx-put": submitUrl } : {},
|
|
533
|
+
"hx-indicator": "#form-loading-indicator",
|
|
534
|
+
className: "space-y-6",
|
|
535
|
+
"data-testid": "form",
|
|
536
|
+
children: fields && fields.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6 space-y-6", children: fields.map((field) => renderFormField(field, initialData, formFieldRenderers)) }) })
|
|
537
|
+
}
|
|
538
|
+
)
|
|
539
|
+
)
|
|
540
|
+
] });
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/utils/form-data-processor.ts
|
|
544
|
+
function preprocessFormData(data, zodSchema) {
|
|
545
|
+
if (!zodSchema) {
|
|
546
|
+
return data;
|
|
547
|
+
}
|
|
548
|
+
const processed = { ...data };
|
|
549
|
+
const def = zodSchema._def;
|
|
550
|
+
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
551
|
+
if (!shape || typeof shape !== "object") {
|
|
552
|
+
return processed;
|
|
553
|
+
}
|
|
554
|
+
for (const [fieldName, fieldSchema] of Object.entries(shape)) {
|
|
555
|
+
const value = processed[fieldName];
|
|
556
|
+
if (value === void 0) {
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (value === null) {
|
|
560
|
+
processed[fieldName] = void 0;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (value === "") {
|
|
564
|
+
processed[fieldName] = void 0;
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
const fieldDef = fieldSchema._def;
|
|
568
|
+
let typeName = fieldDef?.type || fieldDef?.typeName;
|
|
569
|
+
if (typeName === "optional" || typeName === "ZodOptional") {
|
|
570
|
+
const innerType = fieldDef.innerType;
|
|
571
|
+
if (innerType) {
|
|
572
|
+
const innerDef = innerType._def;
|
|
573
|
+
typeName = innerDef?.type || innerDef?.typeName;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (typeName === "number" || typeName === "ZodNumber") {
|
|
577
|
+
if (typeof value === "string") {
|
|
578
|
+
const trimmed = value.trim();
|
|
579
|
+
if (trimmed !== "") {
|
|
580
|
+
const numValue = Number(trimmed);
|
|
581
|
+
if (!isNaN(numValue)) {
|
|
582
|
+
processed[fieldName] = numValue;
|
|
360
583
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
366
|
-
"label",
|
|
367
|
-
{
|
|
368
|
-
htmlFor: field.name,
|
|
369
|
-
className: "block text-sm font-semibold text-gray-700",
|
|
370
|
-
"data-testid": `label-${field.name}`,
|
|
371
|
-
children: [
|
|
372
|
-
field.label,
|
|
373
|
-
field.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 ml-1", children: "*" })
|
|
374
|
-
]
|
|
375
|
-
}
|
|
376
|
-
),
|
|
377
|
-
field.type === "textarea" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
378
|
-
"textarea",
|
|
379
|
-
{
|
|
380
|
-
id: field.name,
|
|
381
|
-
name: field.name,
|
|
382
|
-
required: field.required,
|
|
383
|
-
placeholder: field.placeholder,
|
|
384
|
-
rows: 4,
|
|
385
|
-
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",
|
|
386
|
-
value,
|
|
387
|
-
"data-testid": `input-${field.name}`
|
|
388
|
-
},
|
|
389
|
-
`${field.name}-${value}`
|
|
390
|
-
) : field.type === "select" ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
391
|
-
"select",
|
|
392
|
-
{
|
|
393
|
-
id: field.name,
|
|
394
|
-
name: field.name,
|
|
395
|
-
required: field.required,
|
|
396
|
-
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",
|
|
397
|
-
"data-testid": `select-${field.name}`,
|
|
398
|
-
children: [
|
|
399
|
-
!field.required && /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", selected: !value || value === "", children: "\u8BF7\u9009\u62E9" }),
|
|
400
|
-
field.options && field.options.length > 0 ? field.options.map((option) => {
|
|
401
|
-
const optionValue = String(option.value);
|
|
402
|
-
const isSelected = value === optionValue;
|
|
403
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
404
|
-
"option",
|
|
405
|
-
{
|
|
406
|
-
value: optionValue,
|
|
407
|
-
selected: isSelected,
|
|
408
|
-
children: option.label
|
|
409
|
-
},
|
|
410
|
-
optionValue
|
|
411
|
-
);
|
|
412
|
-
}) : /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", disabled: true, children: "\u6682\u65E0\u9009\u9879" })
|
|
413
|
-
]
|
|
414
|
-
},
|
|
415
|
-
`${field.name}-${value || ""}`
|
|
416
|
-
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
417
|
-
"input",
|
|
418
|
-
{
|
|
419
|
-
type: field.type || "text",
|
|
420
|
-
id: field.name,
|
|
421
|
-
name: field.name,
|
|
422
|
-
required: field.required,
|
|
423
|
-
placeholder: field.placeholder,
|
|
424
|
-
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",
|
|
425
|
-
value,
|
|
426
|
-
"data-testid": `input-${field.name}`
|
|
427
|
-
},
|
|
428
|
-
`${field.name}-${value}`
|
|
429
|
-
),
|
|
430
|
-
field.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
|
|
431
|
-
] }, field.name);
|
|
432
|
-
}) }) })
|
|
433
|
-
]
|
|
584
|
+
}
|
|
585
|
+
} else if (typeof value === "number") {
|
|
586
|
+
processed[fieldName] = value;
|
|
587
|
+
}
|
|
434
588
|
}
|
|
435
|
-
|
|
589
|
+
if (typeName === "boolean" || typeName === "ZodBoolean") {
|
|
590
|
+
if (typeof value === "string") {
|
|
591
|
+
const trimmed = value.trim().toLowerCase();
|
|
592
|
+
if (trimmed === "true" || trimmed === "1" || trimmed === "on") {
|
|
593
|
+
processed[fieldName] = true;
|
|
594
|
+
} else if (trimmed === "false" || trimmed === "0" || trimmed === "off" || trimmed === "") {
|
|
595
|
+
processed[fieldName] = false;
|
|
596
|
+
}
|
|
597
|
+
} else if (typeof value === "boolean") {
|
|
598
|
+
processed[fieldName] = value;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (typeName === "array" || typeName === "ZodArray") {
|
|
602
|
+
if (typeof value === "string") {
|
|
603
|
+
const trimmed = value.trim();
|
|
604
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
605
|
+
try {
|
|
606
|
+
const parsed = JSON.parse(trimmed);
|
|
607
|
+
processed[fieldName] = parsed;
|
|
608
|
+
} catch (e) {
|
|
609
|
+
}
|
|
610
|
+
} else {
|
|
611
|
+
const parts = trimmed.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
612
|
+
processed[fieldName] = parts;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (typeName === "object" || typeName === "ZodObject") {
|
|
617
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
618
|
+
try {
|
|
619
|
+
const trimmed = value.trim();
|
|
620
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
621
|
+
const parsed = JSON.parse(trimmed);
|
|
622
|
+
processed[fieldName] = parsed;
|
|
623
|
+
}
|
|
624
|
+
} catch (e) {
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (typeName === "any" || typeName === "ZodAny") {
|
|
629
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
630
|
+
try {
|
|
631
|
+
const trimmed = value.trim();
|
|
632
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
633
|
+
const parsed = JSON.parse(trimmed);
|
|
634
|
+
processed[fieldName] = parsed;
|
|
635
|
+
}
|
|
636
|
+
} catch (e) {
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return processed;
|
|
436
642
|
}
|
|
437
643
|
|
|
438
644
|
// src/utils/schema-utils.ts
|
|
@@ -459,13 +665,14 @@ function parseFieldSchema(fieldName, fieldSchema) {
|
|
|
459
665
|
return null;
|
|
460
666
|
}
|
|
461
667
|
const label = getFieldDescription(fieldSchema) || fieldName;
|
|
462
|
-
const { type, required, options, innerSchema } = analyzeFieldType(fieldSchema);
|
|
668
|
+
const { type, required, options, innerSchema, step } = analyzeFieldType(fieldSchema);
|
|
463
669
|
return {
|
|
464
670
|
name: fieldName,
|
|
465
671
|
label,
|
|
466
672
|
type,
|
|
467
673
|
required,
|
|
468
674
|
options,
|
|
675
|
+
step,
|
|
469
676
|
schema: innerSchema || fieldSchema
|
|
470
677
|
};
|
|
471
678
|
}
|
|
@@ -522,10 +729,20 @@ function analyzeFieldType(schema) {
|
|
|
522
729
|
let fieldType = "text";
|
|
523
730
|
if (def?.checks) {
|
|
524
731
|
const hasEmailCheck = def.checks.some(
|
|
525
|
-
(check) => check.
|
|
732
|
+
(check) => check.format === "email" || check.constructor?.name === "ZodEmail" || check._zod?.def?.format === "email"
|
|
526
733
|
);
|
|
527
734
|
if (hasEmailCheck) {
|
|
528
735
|
fieldType = "email";
|
|
736
|
+
} else {
|
|
737
|
+
const maxLengthCheck = def.checks.find(
|
|
738
|
+
(check) => check.constructor?.name === "$ZodCheckMaxLength" || check._zod?.def?.check === "max_length" || check._zod?.def?.maximum !== void 0
|
|
739
|
+
);
|
|
740
|
+
if (maxLengthCheck) {
|
|
741
|
+
const maxLength = maxLengthCheck._zod?.def?.maximum ?? maxLengthCheck.value ?? maxLengthCheck.maximum;
|
|
742
|
+
if (maxLength !== void 0 && maxLength > 50) {
|
|
743
|
+
fieldType = "textarea";
|
|
744
|
+
}
|
|
745
|
+
}
|
|
529
746
|
}
|
|
530
747
|
}
|
|
531
748
|
return {
|
|
@@ -535,10 +752,22 @@ function analyzeFieldType(schema) {
|
|
|
535
752
|
};
|
|
536
753
|
}
|
|
537
754
|
if (typeName === "number" || typeName === "ZodNumber") {
|
|
755
|
+
let step = void 0;
|
|
756
|
+
if (def?.checks) {
|
|
757
|
+
const hasIntCheck = def.checks.some(
|
|
758
|
+
(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"
|
|
759
|
+
);
|
|
760
|
+
if (!hasIntCheck) {
|
|
761
|
+
step = "any";
|
|
762
|
+
}
|
|
763
|
+
} else {
|
|
764
|
+
step = "any";
|
|
765
|
+
}
|
|
538
766
|
return {
|
|
539
767
|
type: "number",
|
|
540
768
|
required: true,
|
|
541
|
-
innerSchema: schema
|
|
769
|
+
innerSchema: schema,
|
|
770
|
+
step
|
|
542
771
|
};
|
|
543
772
|
}
|
|
544
773
|
if (typeName === "date" || typeName === "ZodDate") {
|
|
@@ -603,7 +832,8 @@ function modelFieldsToFormFields(fields) {
|
|
|
603
832
|
type: field.type,
|
|
604
833
|
label: field.label,
|
|
605
834
|
required: field.required,
|
|
606
|
-
options: field.options
|
|
835
|
+
options: field.options,
|
|
836
|
+
step: field.step
|
|
607
837
|
}));
|
|
608
838
|
}
|
|
609
839
|
function getFieldNamesFromFields(fields) {
|
|
@@ -621,6 +851,8 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
621
851
|
descriptionGetter;
|
|
622
852
|
/** 当前请求的表单 ID(用于在 render 和 getActions 之间共享) */
|
|
623
853
|
currentFormId;
|
|
854
|
+
/** 自定义表单字段渲染器 */
|
|
855
|
+
formFieldRenderers;
|
|
624
856
|
/**
|
|
625
857
|
* 获取或生成表单 ID(确保在同一个请求中保持一致)
|
|
626
858
|
*/
|
|
@@ -687,59 +919,7 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
687
919
|
if (!this.schema) {
|
|
688
920
|
return data;
|
|
689
921
|
}
|
|
690
|
-
|
|
691
|
-
for (const key of Object.keys(processed)) {
|
|
692
|
-
if (processed[key] === "") {
|
|
693
|
-
processed[key] = void 0;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
const def = this.schema._def;
|
|
697
|
-
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
698
|
-
if (!shape || typeof shape !== "object") {
|
|
699
|
-
return data;
|
|
700
|
-
}
|
|
701
|
-
for (const [fieldName, fieldSchema] of Object.entries(shape)) {
|
|
702
|
-
const value = processed[fieldName];
|
|
703
|
-
if (value === void 0 || value === null) {
|
|
704
|
-
continue;
|
|
705
|
-
}
|
|
706
|
-
const fieldDef = fieldSchema._def;
|
|
707
|
-
let typeName = fieldDef?.typeName;
|
|
708
|
-
let isOptional = false;
|
|
709
|
-
if (typeName === "ZodOptional") {
|
|
710
|
-
isOptional = true;
|
|
711
|
-
const innerType = fieldDef.innerType?._def;
|
|
712
|
-
typeName = innerType?.typeName;
|
|
713
|
-
}
|
|
714
|
-
if (isOptional && typeof value === "string" && value.trim() === "") {
|
|
715
|
-
if (fieldName === "authorId") {
|
|
716
|
-
imeanServiceEngine.logger.info(`[BaseFormFeature] authorId empty optional -> undefined`);
|
|
717
|
-
}
|
|
718
|
-
processed[fieldName] = void 0;
|
|
719
|
-
continue;
|
|
720
|
-
}
|
|
721
|
-
if (typeName === "ZodNumber") {
|
|
722
|
-
if (typeof value === "string") {
|
|
723
|
-
const numValue = Number(value);
|
|
724
|
-
if (fieldName === "authorId") {
|
|
725
|
-
imeanServiceEngine.logger.info(
|
|
726
|
-
`[BaseFormFeature] authorId raw value="${value}", numValue=${numValue}, isOptional=${isOptional}`
|
|
727
|
-
);
|
|
728
|
-
}
|
|
729
|
-
if (!isNaN(numValue)) {
|
|
730
|
-
processed[fieldName] = numValue;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
if (typeName === "ZodArray") {
|
|
735
|
-
if (typeof value === "string") {
|
|
736
|
-
const parts = value.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
737
|
-
processed[fieldName] = parts;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
imeanServiceEngine.logger.info(`[BaseFormFeature] processed form data: ${JSON.stringify(processed)}`);
|
|
742
|
-
return processed;
|
|
922
|
+
return preprocessFormData(data, this.schema);
|
|
743
923
|
}
|
|
744
924
|
/**
|
|
745
925
|
* 处理请求
|
|
@@ -749,12 +929,23 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
749
929
|
this.currentFormId = void 0;
|
|
750
930
|
if (ctx.req.method === "GET") {
|
|
751
931
|
return this.render(context);
|
|
752
|
-
} else if (ctx.req.method === "POST" || ctx.req.method === "PUT") {
|
|
932
|
+
} else if (ctx.req.method === "POST" || ctx.req.method === "PUT" || ctx.req.method === "PATCH") {
|
|
933
|
+
const methodOverride = ctx.req.header("X-HTTP-Method-Override");
|
|
934
|
+
const actualMethod = methodOverride || ctx.req.method;
|
|
935
|
+
const expectedMethod = this.getFormAction() === "edit" ? "PUT" : "POST";
|
|
936
|
+
if (actualMethod.toUpperCase() !== expectedMethod) {
|
|
937
|
+
imeanServiceEngine.logger.warn(
|
|
938
|
+
`[BaseFormFeature] Method mismatch: expected ${expectedMethod}, got ${actualMethod} (request method: ${ctx.req.method}, X-HTTP-Method-Override: ${methodOverride || "none"})`
|
|
939
|
+
);
|
|
940
|
+
}
|
|
753
941
|
const originalData = { ...context.body };
|
|
754
942
|
imeanServiceEngine.logger.info(
|
|
755
943
|
`[BaseFormFeature] Original body data: ${JSON.stringify(originalData)}`
|
|
756
944
|
);
|
|
757
945
|
let data = this.preprocessFormData(context.body);
|
|
946
|
+
imeanServiceEngine.logger.info(
|
|
947
|
+
`[BaseFormFeature] Preprocessed data: ${JSON.stringify(data)}`
|
|
948
|
+
);
|
|
758
949
|
if (!this.schema) {
|
|
759
950
|
throw new Error("Schema is required for form validation");
|
|
760
951
|
}
|
|
@@ -771,7 +962,10 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
771
962
|
);
|
|
772
963
|
return this.render(context, originalData);
|
|
773
964
|
}
|
|
774
|
-
const item = await this.handleSubmit(
|
|
965
|
+
const item = await this.handleSubmit(
|
|
966
|
+
context,
|
|
967
|
+
parseResult.data
|
|
968
|
+
);
|
|
775
969
|
if (!item) {
|
|
776
970
|
context.sendError(
|
|
777
971
|
this.getFormAction() === "create" ? "\u521B\u5EFA\u5931\u8D25" : "\u66F4\u65B0\u5931\u8D25",
|
|
@@ -789,6 +983,9 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
789
983
|
`[BaseFormFeature] Dialog mode: setting refresh to close dialog and refresh list`
|
|
790
984
|
);
|
|
791
985
|
context.setRefresh(true);
|
|
986
|
+
if (context.redirectUrl) {
|
|
987
|
+
context.redirectUrl = void 0;
|
|
988
|
+
}
|
|
792
989
|
return null;
|
|
793
990
|
} else {
|
|
794
991
|
const redirectUrl = this.getSuccessRedirectUrl(context, item);
|
|
@@ -801,12 +998,11 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
801
998
|
}
|
|
802
999
|
}
|
|
803
1000
|
formFieldNames;
|
|
1001
|
+
groups;
|
|
804
1002
|
/**
|
|
805
1003
|
* 渲染表单页面
|
|
806
1004
|
*/
|
|
807
1005
|
async render(context, initialData) {
|
|
808
|
-
const filteredFields = this.formFieldNames ? filterFieldsByNames(this.fields || [], this.formFieldNames) : this.fields || [];
|
|
809
|
-
const fields = modelFieldsToFormFields(filteredFields);
|
|
810
1006
|
let formData;
|
|
811
1007
|
if (this.getFormAction() === "edit") {
|
|
812
1008
|
if (initialData) {
|
|
@@ -828,6 +1024,50 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
828
1024
|
}
|
|
829
1025
|
const method = this.getFormAction() === "create" ? "post" : "put";
|
|
830
1026
|
const formId = this.getFormId(context);
|
|
1027
|
+
if (this.groups && this.groups.length > 0) {
|
|
1028
|
+
if (!this.schema) {
|
|
1029
|
+
throw new Error("Schema is required when using groups");
|
|
1030
|
+
}
|
|
1031
|
+
const schema = this.schema;
|
|
1032
|
+
const groupSchemas = this.groups.map((group) => {
|
|
1033
|
+
const pickObject = group.fields.reduce(
|
|
1034
|
+
(acc, fieldName) => {
|
|
1035
|
+
acc[fieldName] = true;
|
|
1036
|
+
return acc;
|
|
1037
|
+
},
|
|
1038
|
+
{}
|
|
1039
|
+
);
|
|
1040
|
+
return {
|
|
1041
|
+
label: group.label,
|
|
1042
|
+
schema: schema.pick(pickObject),
|
|
1043
|
+
fields: group.fields
|
|
1044
|
+
};
|
|
1045
|
+
});
|
|
1046
|
+
const groupFields = groupSchemas.map(
|
|
1047
|
+
({ label, schema: schema2, fields: fieldNames }) => {
|
|
1048
|
+
const groupFields2 = parseSchemaToFields(schema2);
|
|
1049
|
+
const formFields = modelFieldsToFormFields(groupFields2);
|
|
1050
|
+
return {
|
|
1051
|
+
label,
|
|
1052
|
+
fields: formFields
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
);
|
|
1056
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1057
|
+
FormPage,
|
|
1058
|
+
{
|
|
1059
|
+
groups: groupFields,
|
|
1060
|
+
submitUrl,
|
|
1061
|
+
method,
|
|
1062
|
+
initialData: formData,
|
|
1063
|
+
formId,
|
|
1064
|
+
isDialog: context.isDialog,
|
|
1065
|
+
formFieldRenderers: this.formFieldRenderers
|
|
1066
|
+
}
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
const filteredFields = this.formFieldNames ? filterFieldsByNames(this.fields || [], this.formFieldNames) : this.fields || [];
|
|
1070
|
+
const fields = modelFieldsToFormFields(filteredFields);
|
|
831
1071
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
832
1072
|
FormPage,
|
|
833
1073
|
{
|
|
@@ -835,7 +1075,9 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
835
1075
|
submitUrl,
|
|
836
1076
|
method,
|
|
837
1077
|
initialData: formData,
|
|
838
|
-
formId
|
|
1078
|
+
formId,
|
|
1079
|
+
isDialog: context.isDialog,
|
|
1080
|
+
formFieldRenderers: this.formFieldRenderers
|
|
839
1081
|
}
|
|
840
1082
|
);
|
|
841
1083
|
}
|
|
@@ -944,6 +1186,8 @@ var DefaultCreateFeature = class extends BaseFormFeature {
|
|
|
944
1186
|
this.fields = parseSchemaToFields(options.schema);
|
|
945
1187
|
this.createItem = options.createItem;
|
|
946
1188
|
this.formFieldNames = options.formFieldNames;
|
|
1189
|
+
this.groups = options.groups;
|
|
1190
|
+
this.formFieldRenderers = options.formFieldRenderers;
|
|
947
1191
|
}
|
|
948
1192
|
getFormAction() {
|
|
949
1193
|
return "create";
|
|
@@ -1003,12 +1247,119 @@ var DefaultDeleteFeature = class extends BaseFeature {
|
|
|
1003
1247
|
}
|
|
1004
1248
|
}
|
|
1005
1249
|
};
|
|
1250
|
+
function Card(props) {
|
|
1251
|
+
const {
|
|
1252
|
+
children,
|
|
1253
|
+
title,
|
|
1254
|
+
className = "",
|
|
1255
|
+
shadow = true,
|
|
1256
|
+
bordered = false,
|
|
1257
|
+
noPadding = false
|
|
1258
|
+
} = props;
|
|
1259
|
+
const baseClasses = "bg-white rounded-lg";
|
|
1260
|
+
const shadowClass = shadow ? "shadow-sm hover:shadow-md transition-shadow" : "";
|
|
1261
|
+
const borderClass = bordered ? "border border-gray-200" : "";
|
|
1262
|
+
const paddingClass = noPadding ? "" : "p-6";
|
|
1263
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1264
|
+
"div",
|
|
1265
|
+
{
|
|
1266
|
+
className: `${baseClasses} ${shadowClass} ${borderClass} ${className}`,
|
|
1267
|
+
children: [
|
|
1268
|
+
title && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 border-b border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title }) }),
|
|
1269
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: noPadding ? "" : paddingClass, children })
|
|
1270
|
+
]
|
|
1271
|
+
}
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
function renderDefaultValue(value) {
|
|
1275
|
+
if (value === null || value === void 0) {
|
|
1276
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400", children: "-" });
|
|
1277
|
+
}
|
|
1278
|
+
if (Array.isArray(value)) {
|
|
1279
|
+
if (value.length === 0) {
|
|
1280
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400", children: "\u6682\u65E0\u6570\u636E" });
|
|
1281
|
+
}
|
|
1282
|
+
if (value.length > 0 && typeof value[0] === "object" && value[0] !== null) {
|
|
1283
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-gray-600", children: [
|
|
1284
|
+
"\u5305\u542B ",
|
|
1285
|
+
value.length,
|
|
1286
|
+
" \u9879\uFF08\u5BF9\u8C61\u6570\u7EC4\uFF0C\u5EFA\u8BAE\u4F7F\u7528\u81EA\u5B9A\u4E49\u6E32\u67D3\uFF09"
|
|
1287
|
+
] });
|
|
1288
|
+
}
|
|
1289
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: value.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1290
|
+
"span",
|
|
1291
|
+
{
|
|
1292
|
+
className: "px-2 py-1 bg-gray-100 text-gray-700 rounded text-sm",
|
|
1293
|
+
children: String(item)
|
|
1294
|
+
},
|
|
1295
|
+
index
|
|
1296
|
+
)) });
|
|
1297
|
+
}
|
|
1298
|
+
if (typeof value === "object") {
|
|
1299
|
+
if (value instanceof Date) {
|
|
1300
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { children: value.toLocaleString() });
|
|
1301
|
+
}
|
|
1302
|
+
return /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "bg-gray-50 border border-gray-200 rounded p-3 text-xs overflow-x-auto", children: JSON.stringify(value, null, 2) });
|
|
1303
|
+
}
|
|
1304
|
+
if (typeof value === "boolean") {
|
|
1305
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `px-2 py-1 rounded text-sm font-medium ${value ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-600"}`, children: value ? "\u662F" : "\u5426" });
|
|
1306
|
+
}
|
|
1307
|
+
if (typeof value === "number") {
|
|
1308
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { children: value.toLocaleString() });
|
|
1309
|
+
}
|
|
1310
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { children: String(value) });
|
|
1311
|
+
}
|
|
1312
|
+
function renderField(field, value, item) {
|
|
1313
|
+
let content;
|
|
1314
|
+
if (field.render) {
|
|
1315
|
+
const rendered = field.render(value, item);
|
|
1316
|
+
if (rendered === null || rendered === void 0) {
|
|
1317
|
+
content = /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400", children: "-" });
|
|
1318
|
+
} else if (typeof rendered === "string" || typeof rendered === "number" || typeof rendered === "boolean") {
|
|
1319
|
+
content = /* @__PURE__ */ jsxRuntime.jsx("span", { children: String(rendered) });
|
|
1320
|
+
} else {
|
|
1321
|
+
content = rendered;
|
|
1322
|
+
}
|
|
1323
|
+
} else {
|
|
1324
|
+
content = renderDefaultValue(value);
|
|
1325
|
+
}
|
|
1326
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1327
|
+
"div",
|
|
1328
|
+
{
|
|
1329
|
+
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 ",
|
|
1330
|
+
children: [
|
|
1331
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { className: "\r\n text-xs sm:text-sm font-semibold text-gray-600 sm:text-gray-700\r\n flex items-start\r\n leading-tight sm:leading-5\r\n tracking-wide\r\n ", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 uppercase sm:normal-case", children: field.label }) }),
|
|
1332
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { className: "\r\n text-sm sm:text-base text-gray-900\r\n break-words\r\n leading-relaxed\r\n min-w-0\r\n ", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0", children: content }) })
|
|
1333
|
+
]
|
|
1334
|
+
},
|
|
1335
|
+
field.key
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1006
1338
|
function DetailPage(props) {
|
|
1007
|
-
const { item, fields } = props;
|
|
1008
|
-
|
|
1009
|
-
/* @__PURE__ */ jsxRuntime.jsx("
|
|
1010
|
-
|
|
1011
|
-
|
|
1339
|
+
const { item, fields, groups } = props;
|
|
1340
|
+
if (groups && groups.length > 0) {
|
|
1341
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-6", children: groups.map((group, groupIndex) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1342
|
+
Card,
|
|
1343
|
+
{
|
|
1344
|
+
title: group.label,
|
|
1345
|
+
shadow: true,
|
|
1346
|
+
bordered: true,
|
|
1347
|
+
noPadding: true,
|
|
1348
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("dl", { className: "divide-y divide-gray-100", children: group.fields.map((field) => {
|
|
1349
|
+
const value = group.values[field.key];
|
|
1350
|
+
return renderField(field, value, item);
|
|
1351
|
+
}) })
|
|
1352
|
+
},
|
|
1353
|
+
groupIndex
|
|
1354
|
+
)) });
|
|
1355
|
+
}
|
|
1356
|
+
if (fields && fields.length > 0) {
|
|
1357
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx("dl", { className: "divide-y divide-gray-100", children: fields.map((field) => {
|
|
1358
|
+
const value = item[field.key];
|
|
1359
|
+
return renderField(field, value, item);
|
|
1360
|
+
}) }) });
|
|
1361
|
+
}
|
|
1362
|
+
return null;
|
|
1012
1363
|
}
|
|
1013
1364
|
var DefaultDetailFeature = class extends BaseFeature {
|
|
1014
1365
|
getItem;
|
|
@@ -1016,6 +1367,8 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
1016
1367
|
titleGetter;
|
|
1017
1368
|
descriptionGetter;
|
|
1018
1369
|
detailFieldNames;
|
|
1370
|
+
fieldRenderers;
|
|
1371
|
+
groups;
|
|
1019
1372
|
constructor(options) {
|
|
1020
1373
|
super({
|
|
1021
1374
|
name: "detail",
|
|
@@ -1031,6 +1384,8 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
1031
1384
|
this.titleGetter = options.getTitle;
|
|
1032
1385
|
this.descriptionGetter = options.getDescription;
|
|
1033
1386
|
this.detailFieldNames = options.detailFieldNames;
|
|
1387
|
+
this.fieldRenderers = options.fieldRenderers;
|
|
1388
|
+
this.groups = options.groups;
|
|
1034
1389
|
}
|
|
1035
1390
|
async getTitle(context) {
|
|
1036
1391
|
if (this.titleGetter) {
|
|
@@ -1061,6 +1416,40 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
1061
1416
|
if (!item) {
|
|
1062
1417
|
return context.ctx.json({ error: "Not found" }, 404);
|
|
1063
1418
|
}
|
|
1419
|
+
if (this.groups && this.groups.length > 0) {
|
|
1420
|
+
if (!this.schema) {
|
|
1421
|
+
throw new Error("Schema is required when using groups");
|
|
1422
|
+
}
|
|
1423
|
+
const schema = this.schema;
|
|
1424
|
+
const groupSchemas = this.groups.map((group) => {
|
|
1425
|
+
const pickObject = group.fields.reduce((acc, fieldName) => {
|
|
1426
|
+
acc[fieldName] = true;
|
|
1427
|
+
return acc;
|
|
1428
|
+
}, {});
|
|
1429
|
+
return {
|
|
1430
|
+
label: group.label,
|
|
1431
|
+
schema: schema.pick(pickObject),
|
|
1432
|
+
fields: group.fields
|
|
1433
|
+
};
|
|
1434
|
+
});
|
|
1435
|
+
const groupFields = groupSchemas.map(({ label, schema: schema2, fields: fieldNames }) => {
|
|
1436
|
+
const groupFields2 = parseSchemaToFields(schema2);
|
|
1437
|
+
const detailFields2 = groupFields2.map((field) => ({
|
|
1438
|
+
key: field.name,
|
|
1439
|
+
label: field.label,
|
|
1440
|
+
render: this.fieldRenderers?.[field.name]
|
|
1441
|
+
}));
|
|
1442
|
+
return {
|
|
1443
|
+
label,
|
|
1444
|
+
fields: detailFields2,
|
|
1445
|
+
values: fieldNames.reduce((acc, fieldName) => {
|
|
1446
|
+
acc[fieldName] = item[fieldName];
|
|
1447
|
+
return acc;
|
|
1448
|
+
}, {})
|
|
1449
|
+
};
|
|
1450
|
+
});
|
|
1451
|
+
return /* @__PURE__ */ jsxRuntime.jsx(DetailPage, { item, groups: groupFields });
|
|
1452
|
+
}
|
|
1064
1453
|
const detailFields = this.detailFieldNames ? filterFieldsByNames(this.fields || [], this.detailFieldNames) : this.fields || [];
|
|
1065
1454
|
if (this.detailFieldNames) {
|
|
1066
1455
|
const systemFields = ["id", "createdAt", "updatedAt"];
|
|
@@ -1079,7 +1468,9 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
1079
1468
|
const detailFieldNames = getFieldNamesFromFields(detailFields);
|
|
1080
1469
|
const fields = detailFieldNames.map((fieldName) => ({
|
|
1081
1470
|
key: fieldName,
|
|
1082
|
-
label: getFieldLabelFromFields(this.fields || [], fieldName) || fieldName
|
|
1471
|
+
label: getFieldLabelFromFields(this.fields || [], fieldName) || fieldName,
|
|
1472
|
+
render: this.fieldRenderers?.[fieldName]
|
|
1473
|
+
// 如果有自定义渲染函数则使用
|
|
1083
1474
|
}));
|
|
1084
1475
|
return /* @__PURE__ */ jsxRuntime.jsx(DetailPage, { item, fields });
|
|
1085
1476
|
}
|
|
@@ -1157,6 +1548,8 @@ var DefaultEditFeature = class extends BaseFormFeature {
|
|
|
1157
1548
|
this.getItem = options.getItem;
|
|
1158
1549
|
this.updateItem = options.updateItem;
|
|
1159
1550
|
this.formFieldNames = options.formFieldNames;
|
|
1551
|
+
this.groups = options.groups;
|
|
1552
|
+
this.formFieldRenderers = options.formFieldRenderers;
|
|
1160
1553
|
}
|
|
1161
1554
|
getFormAction() {
|
|
1162
1555
|
return "edit";
|
|
@@ -1392,30 +1785,6 @@ function Button(props) {
|
|
|
1392
1785
|
}
|
|
1393
1786
|
);
|
|
1394
1787
|
}
|
|
1395
|
-
function Card(props) {
|
|
1396
|
-
const {
|
|
1397
|
-
children,
|
|
1398
|
-
title,
|
|
1399
|
-
className = "",
|
|
1400
|
-
shadow = true,
|
|
1401
|
-
bordered = false,
|
|
1402
|
-
noPadding = false
|
|
1403
|
-
} = props;
|
|
1404
|
-
const baseClasses = "bg-white rounded-lg";
|
|
1405
|
-
const shadowClass = shadow ? "shadow-sm hover:shadow-md transition-shadow" : "";
|
|
1406
|
-
const borderClass = bordered ? "border border-gray-200" : "";
|
|
1407
|
-
const paddingClass = noPadding ? "" : "p-6";
|
|
1408
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1409
|
-
"div",
|
|
1410
|
-
{
|
|
1411
|
-
className: `${baseClasses} ${shadowClass} ${borderClass} ${className}`,
|
|
1412
|
-
children: [
|
|
1413
|
-
title && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 border-b border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title }) }),
|
|
1414
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: noPadding ? "" : paddingClass, children })
|
|
1415
|
-
]
|
|
1416
|
-
}
|
|
1417
|
-
);
|
|
1418
|
-
}
|
|
1419
1788
|
function EmptyState(props) {
|
|
1420
1789
|
const { message = "\u6682\u65E0\u6570\u636E", children } = props;
|
|
1421
1790
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-12", children: children || /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-500 text-sm", children: message }) });
|
|
@@ -2307,7 +2676,8 @@ function Dialog(props) {
|
|
|
2307
2676
|
className = "",
|
|
2308
2677
|
size = "lg",
|
|
2309
2678
|
closeOnBackdropClick = true,
|
|
2310
|
-
actions = []
|
|
2679
|
+
actions = [],
|
|
2680
|
+
fixedContentHeight = false
|
|
2311
2681
|
} = props;
|
|
2312
2682
|
const sizeClasses = {
|
|
2313
2683
|
sm: "max-w-md",
|
|
@@ -2366,7 +2736,13 @@ function Dialog(props) {
|
|
|
2366
2736
|
}
|
|
2367
2737
|
)
|
|
2368
2738
|
] }),
|
|
2369
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2739
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2740
|
+
"div",
|
|
2741
|
+
{
|
|
2742
|
+
className: `${fixedContentHeight ? "h-[70vh]" : "flex-1"} overflow-y-auto ${fixedContentHeight ? "p-0" : "p-6"} bg-gray-50`,
|
|
2743
|
+
children
|
|
2744
|
+
}
|
|
2745
|
+
),
|
|
2370
2746
|
actions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 border-t border-gray-200 bg-white flex justify-end gap-2", children: actions.map((action, index) => renderActionButton(action, index)) })
|
|
2371
2747
|
]
|
|
2372
2748
|
}
|
|
@@ -2627,6 +3003,13 @@ function BaseLayout(props) {
|
|
|
2627
3003
|
{
|
|
2628
3004
|
dangerouslySetInnerHTML: {
|
|
2629
3005
|
__html: `
|
|
3006
|
+
/* \u5BB9\u5668\u67E5\u8BE2\u652F\u6301 - \u5982\u679C Tailwind CDN \u4E0D\u652F\u6301\uFF0C\u4F7F\u7528\u539F\u751F CSS \u5BB9\u5668\u67E5\u8BE2 */
|
|
3007
|
+
@supports (container-type: inline-size) {
|
|
3008
|
+
.\\@container {
|
|
3009
|
+
container-type: inline-size;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
|
|
2630
3013
|
@keyframes fadeIn {
|
|
2631
3014
|
from { opacity: 0;}
|
|
2632
3015
|
to { opacity: 1;}
|
|
@@ -3149,9 +3532,9 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3149
3532
|
headers
|
|
3150
3533
|
);
|
|
3151
3534
|
}
|
|
3152
|
-
if (context.redirectUrl) {
|
|
3535
|
+
if (context.redirectUrl && !context.refresh) {
|
|
3153
3536
|
imeanServiceEngine.logger.info(
|
|
3154
|
-
`[ResponseRenderer] Redirect URL found: ${context.redirectUrl} (isHtmxRequest: ${context.isHtmxRequest})`
|
|
3537
|
+
`[ResponseRenderer] Redirect URL found: ${context.redirectUrl} (isHtmxRequest: ${context.isHtmxRequest}, isDialog: ${context.isDialog})`
|
|
3155
3538
|
);
|
|
3156
3539
|
if (context.isHtmxRequest) {
|
|
3157
3540
|
return ctx.html(/* @__PURE__ */ jsxRuntime.jsx("div", {}), 200, {
|
|
@@ -3160,7 +3543,11 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3160
3543
|
} else {
|
|
3161
3544
|
return ctx.redirect(context.redirectUrl);
|
|
3162
3545
|
}
|
|
3163
|
-
} else {
|
|
3546
|
+
} else if (context.redirectUrl && context.refresh) {
|
|
3547
|
+
imeanServiceEngine.logger.info(
|
|
3548
|
+
`[ResponseRenderer] Both redirect URL and refresh are set, using refresh (isDialog: ${context.isDialog})`
|
|
3549
|
+
);
|
|
3550
|
+
} else if (!context.redirectUrl) {
|
|
3164
3551
|
imeanServiceEngine.logger.info(
|
|
3165
3552
|
`[ResponseRenderer] No redirect URL found (result: ${result === null ? "null" : typeof result}, isHtmxRequest: ${context.isHtmxRequest})`
|
|
3166
3553
|
);
|
|
@@ -3211,6 +3598,8 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3211
3598
|
if (context.isDialog) {
|
|
3212
3599
|
const dialogSize = renderOptions.feature?.dialogSize || "lg";
|
|
3213
3600
|
const closeOnBackdropClick = renderOptions.feature?.closeOnBackdropClick ?? true;
|
|
3601
|
+
const isFormFeature = renderOptions.feature?.type === "create" || renderOptions.feature?.type === "edit";
|
|
3602
|
+
const fixedContentHeight = isFormFeature;
|
|
3214
3603
|
return ctx.html(
|
|
3215
3604
|
/* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3216
3605
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -3220,6 +3609,7 @@ async function renderResult(ctx, context, result, renderOptions) {
|
|
|
3220
3609
|
size: dialogSize,
|
|
3221
3610
|
closeOnBackdropClick,
|
|
3222
3611
|
actions,
|
|
3612
|
+
fixedContentHeight,
|
|
3223
3613
|
children: result
|
|
3224
3614
|
}
|
|
3225
3615
|
),
|
|
@@ -3480,6 +3870,28 @@ function registerPageRoutes(page, options) {
|
|
|
3480
3870
|
});
|
|
3481
3871
|
};
|
|
3482
3872
|
options.hono[route.method](fullPath, handler);
|
|
3873
|
+
if (route.method === "put" || route.method === "delete") {
|
|
3874
|
+
const postHandler = async (ctx) => {
|
|
3875
|
+
const methodOverride = ctx.req.header("X-HTTP-Method-Override");
|
|
3876
|
+
const expectedMethod = route.method.toUpperCase();
|
|
3877
|
+
if (methodOverride === expectedMethod) {
|
|
3878
|
+
imeanServiceEngine.logger.info(
|
|
3879
|
+
`[HtmxAdminPlugin] Method override detected: POST ${fullPath} -> ${expectedMethod} (feature: ${feature.name})`
|
|
3880
|
+
);
|
|
3881
|
+
return handleRequest(ctx, page, feature, {
|
|
3882
|
+
options: options.options
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3885
|
+
imeanServiceEngine.logger.warn(
|
|
3886
|
+
`[HtmxAdminPlugin] POST request to ${fullPath} without matching X-HTTP-Method-Override header (got: ${methodOverride || "none"}, expected: ${expectedMethod})`
|
|
3887
|
+
);
|
|
3888
|
+
return ctx.text("Method Not Allowed", 405);
|
|
3889
|
+
};
|
|
3890
|
+
imeanServiceEngine.logger.info(
|
|
3891
|
+
`[HtmxAdminPlugin] Registering POST route for method override: POST ${fullPath} (actual method: ${route.method.toUpperCase()}, feature: ${feature.name})`
|
|
3892
|
+
);
|
|
3893
|
+
options.hono.post(fullPath, postHandler);
|
|
3894
|
+
}
|
|
3483
3895
|
}
|
|
3484
3896
|
}
|
|
3485
3897
|
}
|
|
@@ -3569,6 +3981,81 @@ var HtmxAdminPlugin = class {
|
|
|
3569
3981
|
registerHomeRedirect(this.pages, routeOptions);
|
|
3570
3982
|
}
|
|
3571
3983
|
};
|
|
3984
|
+
function createFormFieldXData(options) {
|
|
3985
|
+
const {
|
|
3986
|
+
fieldName,
|
|
3987
|
+
dataKey,
|
|
3988
|
+
defaultValue = [],
|
|
3989
|
+
customData = {},
|
|
3990
|
+
customMethods = {}
|
|
3991
|
+
} = options;
|
|
3992
|
+
const dataEntries = [];
|
|
3993
|
+
dataEntries.push(`${dataKey}: ${JSON.stringify(defaultValue)}`);
|
|
3994
|
+
for (const [key, value] of Object.entries(customData)) {
|
|
3995
|
+
dataEntries.push(`${key}: ${JSON.stringify(value)}`);
|
|
3996
|
+
}
|
|
3997
|
+
const methodEntries = [];
|
|
3998
|
+
methodEntries.push(`init() {
|
|
3999
|
+
const dataAttr = this.$el.getAttribute('data-initial-value');
|
|
4000
|
+
if (dataAttr) {
|
|
4001
|
+
try {
|
|
4002
|
+
this.${dataKey} = JSON.parse(dataAttr);
|
|
4003
|
+
} catch (e) {
|
|
4004
|
+
console.error('Failed to parse initial value:', e);
|
|
4005
|
+
this.${dataKey} = ${JSON.stringify(defaultValue)};
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
this.updateHiddenField();
|
|
4009
|
+
}`);
|
|
4010
|
+
methodEntries.push(`updateHiddenField() {
|
|
4011
|
+
const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
|
|
4012
|
+
if (hiddenInput) {
|
|
4013
|
+
hiddenInput.value = JSON.stringify(this.${dataKey});
|
|
4014
|
+
}
|
|
4015
|
+
}`);
|
|
4016
|
+
for (const [methodName, methodBody] of Object.entries(customMethods)) {
|
|
4017
|
+
methodEntries.push(`${methodName}${methodBody}`);
|
|
4018
|
+
}
|
|
4019
|
+
const dataStr = dataEntries.join(",\n ");
|
|
4020
|
+
const methodsStr = methodEntries.join(",\n ");
|
|
4021
|
+
return `{
|
|
4022
|
+
${dataStr},
|
|
4023
|
+
${methodsStr}
|
|
4024
|
+
}`;
|
|
4025
|
+
}
|
|
4026
|
+
function FormFieldWrapper(props) {
|
|
4027
|
+
const {
|
|
4028
|
+
fieldName,
|
|
4029
|
+
initialValue,
|
|
4030
|
+
xData,
|
|
4031
|
+
autoSync = false,
|
|
4032
|
+
children,
|
|
4033
|
+
className = "space-y-4"
|
|
4034
|
+
} = props;
|
|
4035
|
+
const initialValueJson = JSON.stringify(initialValue);
|
|
4036
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4037
|
+
"div",
|
|
4038
|
+
{
|
|
4039
|
+
"x-data": xData,
|
|
4040
|
+
"data-initial-value": initialValueJson,
|
|
4041
|
+
"x-init": "init()",
|
|
4042
|
+
...autoSync ? { "x-effect": "updateHiddenField()" } : {},
|
|
4043
|
+
className,
|
|
4044
|
+
children: [
|
|
4045
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4046
|
+
"input",
|
|
4047
|
+
{
|
|
4048
|
+
type: "hidden",
|
|
4049
|
+
name: fieldName,
|
|
4050
|
+
value: "",
|
|
4051
|
+
"data-testid": `hidden-${fieldName}`
|
|
4052
|
+
}
|
|
4053
|
+
),
|
|
4054
|
+
children
|
|
4055
|
+
]
|
|
4056
|
+
}
|
|
4057
|
+
);
|
|
4058
|
+
}
|
|
3572
4059
|
|
|
3573
4060
|
exports.BaseFeature = BaseFeature;
|
|
3574
4061
|
exports.CustomFeature = CustomFeature;
|
|
@@ -3579,10 +4066,12 @@ exports.DefaultEditFeature = DefaultEditFeature;
|
|
|
3579
4066
|
exports.DefaultListFeature = DefaultListFeature;
|
|
3580
4067
|
exports.Dialog = Dialog;
|
|
3581
4068
|
exports.ErrorAlert = ErrorAlert;
|
|
4069
|
+
exports.FormFieldWrapper = FormFieldWrapper;
|
|
3582
4070
|
exports.HtmxAdminPlugin = HtmxAdminPlugin;
|
|
3583
4071
|
exports.LoadingBar = LoadingBar;
|
|
3584
4072
|
exports.PageModel = PageModel;
|
|
3585
4073
|
exports.checkUserPermission = checkUserPermission;
|
|
4074
|
+
exports.createFormFieldXData = createFormFieldXData;
|
|
3586
4075
|
exports.getUserInfo = getUserInfo;
|
|
3587
4076
|
exports.modelNameToPath = modelNameToPath;
|
|
3588
4077
|
exports.parseListParams = parseListParams;
|