imean-service-engine-htmx-plugin 2.0.0 → 2.1.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 +62 -146
- package/dist/index.d.ts +62 -146
- package/dist/index.js +692 -697
- package/dist/index.mjs +692 -697
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -149,6 +149,146 @@ var init_cdn_cache = __esm({
|
|
|
149
149
|
cache = /* @__PURE__ */ new Map();
|
|
150
150
|
}
|
|
151
151
|
});
|
|
152
|
+
|
|
153
|
+
// src/feature-registry.ts
|
|
154
|
+
var FeatureRegistry = class {
|
|
155
|
+
features = /* @__PURE__ */ new Map();
|
|
156
|
+
model;
|
|
157
|
+
constructor(model) {
|
|
158
|
+
this.model = model;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 注册 Feature
|
|
162
|
+
*/
|
|
163
|
+
register(name, feature) {
|
|
164
|
+
this.features.set(name, feature);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 快捷方法:注册列表 Feature
|
|
168
|
+
*/
|
|
169
|
+
list(feature) {
|
|
170
|
+
this.register("list", feature);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* 快捷方法:注册详情 Feature
|
|
174
|
+
*/
|
|
175
|
+
detail(feature) {
|
|
176
|
+
this.register("detail", feature);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 快捷方法:注册创建 Feature
|
|
180
|
+
*/
|
|
181
|
+
create(feature) {
|
|
182
|
+
this.register("create", feature);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* 快捷方法:注册编辑 Feature
|
|
186
|
+
*/
|
|
187
|
+
edit(feature) {
|
|
188
|
+
this.register("edit", feature);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 快捷方法:注册删除 Feature
|
|
192
|
+
*/
|
|
193
|
+
delete(feature) {
|
|
194
|
+
this.register("delete", feature);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* 快捷方法:注册自定义 Feature
|
|
198
|
+
*/
|
|
199
|
+
custom(name, feature) {
|
|
200
|
+
this.register(name, feature);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* 获取所有注册的 Feature
|
|
204
|
+
*/
|
|
205
|
+
getAll() {
|
|
206
|
+
return Array.from(this.features.values());
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* 获取指定 Feature
|
|
210
|
+
*/
|
|
211
|
+
get(name) {
|
|
212
|
+
return this.features.get(name);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// src/page-model.ts
|
|
217
|
+
var PageModel = class {
|
|
218
|
+
modelName;
|
|
219
|
+
features;
|
|
220
|
+
metadata;
|
|
221
|
+
/**
|
|
222
|
+
* 构造函数
|
|
223
|
+
* @param modelName 模型/页面名称(用于路由和权限)
|
|
224
|
+
* @param metadata 页面元数据(可选,如果提供则不需要实现 getMetadata)
|
|
225
|
+
*/
|
|
226
|
+
constructor(modelName, metadata) {
|
|
227
|
+
this.modelName = modelName;
|
|
228
|
+
this.metadata = metadata || {
|
|
229
|
+
title: modelName,
|
|
230
|
+
description: "",
|
|
231
|
+
useAdminLayout: true
|
|
232
|
+
};
|
|
233
|
+
this.features = new FeatureRegistry(this);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* 获取页面元数据
|
|
237
|
+
* 如果构造函数提供了 metadata,直接返回
|
|
238
|
+
* 否则调用子类实现
|
|
239
|
+
*/
|
|
240
|
+
getMetadata() {
|
|
241
|
+
return this.metadata;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// src/features/base-feature.ts
|
|
246
|
+
var BaseFeature = class {
|
|
247
|
+
/** Feature 名称 */
|
|
248
|
+
name;
|
|
249
|
+
/** Feature 类型 */
|
|
250
|
+
type;
|
|
251
|
+
/** 所需权限(格式:modelName.featureName 或自定义,null 表示开放访问) */
|
|
252
|
+
permission;
|
|
253
|
+
/** 弹窗大小(当 Feature 在弹窗中打开时使用,默认 "lg") */
|
|
254
|
+
dialogSize;
|
|
255
|
+
/** 是否允许点击遮罩区域关闭弹窗(默认 true,设置为 false 时只能通过关闭按钮关闭) */
|
|
256
|
+
closeOnBackdropClick;
|
|
257
|
+
/** Schema(可选,由子类在构造函数中设置) */
|
|
258
|
+
schema;
|
|
259
|
+
/** 解析后的字段列表(可选,由子类在构造函数中设置) */
|
|
260
|
+
fields;
|
|
261
|
+
constructor(options) {
|
|
262
|
+
this.name = options.name;
|
|
263
|
+
this.type = options.type;
|
|
264
|
+
this.permission = options.permission;
|
|
265
|
+
this.dialogSize = options.dialogSize;
|
|
266
|
+
this.closeOnBackdropClick = options.closeOnBackdropClick;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* 获取动态标题(默认实现:返回 PageMetadata 的 title)
|
|
270
|
+
* 子类可以覆盖此方法以提供动态标题
|
|
271
|
+
*/
|
|
272
|
+
async getTitle(context) {
|
|
273
|
+
const metadata = context.model.getMetadata();
|
|
274
|
+
return metadata.title;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* 获取动态描述(默认实现:返回 PageMetadata 的 description)
|
|
278
|
+
* 子类可以覆盖此方法以提供动态描述
|
|
279
|
+
*/
|
|
280
|
+
async getDescription(context) {
|
|
281
|
+
const metadata = context.model.getMetadata();
|
|
282
|
+
return metadata.description;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* 获取操作按钮(默认实现:返回空数组)
|
|
286
|
+
* 子类可以覆盖此方法以提供操作按钮
|
|
287
|
+
*/
|
|
288
|
+
async getActions(context) {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
};
|
|
152
292
|
function getFieldValue(field, initialData) {
|
|
153
293
|
if (initialData && Object.prototype.hasOwnProperty.call(initialData, field.name)) {
|
|
154
294
|
const value = initialData[field.name];
|
|
@@ -295,49 +435,187 @@ function FormPage(props) {
|
|
|
295
435
|
) });
|
|
296
436
|
}
|
|
297
437
|
|
|
298
|
-
// src/
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
/** 所需权限(格式:modelName.featureName 或自定义,null 表示开放访问) */
|
|
305
|
-
permission;
|
|
306
|
-
/** 弹窗大小(当 Feature 在弹窗中打开时使用,默认 "lg") */
|
|
307
|
-
dialogSize;
|
|
308
|
-
/** 是否允许点击遮罩区域关闭弹窗(默认 true,设置为 false 时只能通过关闭按钮关闭) */
|
|
309
|
-
closeOnBackdropClick;
|
|
310
|
-
constructor(options) {
|
|
311
|
-
this.name = options.name;
|
|
312
|
-
this.type = options.type;
|
|
313
|
-
this.permission = options.permission;
|
|
314
|
-
this.dialogSize = options.dialogSize;
|
|
315
|
-
this.closeOnBackdropClick = options.closeOnBackdropClick;
|
|
438
|
+
// src/utils/schema-utils.ts
|
|
439
|
+
function parseSchemaToFields(schema) {
|
|
440
|
+
const def = schema._def;
|
|
441
|
+
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
442
|
+
if (!shape || typeof shape !== "object") {
|
|
443
|
+
return [];
|
|
316
444
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
|
|
445
|
+
const fields = [];
|
|
446
|
+
for (const [fieldName, fieldSchema] of Object.entries(shape)) {
|
|
447
|
+
if (["id", "createdAt", "updatedAt"].includes(fieldName)) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const field = parseFieldSchema(fieldName, fieldSchema);
|
|
451
|
+
if (field) {
|
|
452
|
+
fields.push(field);
|
|
453
|
+
}
|
|
324
454
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const metadata = context.model.getMetadata();
|
|
331
|
-
return metadata.description;
|
|
455
|
+
return fields;
|
|
456
|
+
}
|
|
457
|
+
function parseFieldSchema(fieldName, fieldSchema) {
|
|
458
|
+
if (!fieldSchema || !fieldSchema._def) {
|
|
459
|
+
return null;
|
|
332
460
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
461
|
+
const label = getFieldDescription(fieldSchema) || fieldName;
|
|
462
|
+
const { type, required, options, innerSchema } = analyzeFieldType(fieldSchema);
|
|
463
|
+
return {
|
|
464
|
+
name: fieldName,
|
|
465
|
+
label,
|
|
466
|
+
type,
|
|
467
|
+
required,
|
|
468
|
+
options,
|
|
469
|
+
schema: innerSchema || fieldSchema
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function getFieldDescription(schema) {
|
|
473
|
+
if (schema?.description) {
|
|
474
|
+
return schema.description;
|
|
475
|
+
}
|
|
476
|
+
const schemaDef = schema._def;
|
|
477
|
+
if (schemaDef?.description) {
|
|
478
|
+
return schemaDef.description;
|
|
479
|
+
}
|
|
480
|
+
if (schemaDef?.innerType) {
|
|
481
|
+
return getFieldDescription(schemaDef.innerType);
|
|
482
|
+
}
|
|
483
|
+
return void 0;
|
|
484
|
+
}
|
|
485
|
+
function analyzeFieldType(schema) {
|
|
486
|
+
const def = schema._def;
|
|
487
|
+
const typeName = def?.type || def?.typeName;
|
|
488
|
+
if (typeName === "optional" || typeName === "ZodOptional") {
|
|
489
|
+
const inner = analyzeFieldType(def.innerType);
|
|
490
|
+
return {
|
|
491
|
+
...inner,
|
|
492
|
+
required: false,
|
|
493
|
+
innerSchema: def.innerType
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
if (typeName === "nullable" || typeName === "ZodNullable") {
|
|
497
|
+
const inner = analyzeFieldType(def.innerType);
|
|
498
|
+
return {
|
|
499
|
+
...inner,
|
|
500
|
+
innerSchema: def.innerType
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
if (typeName === "enum" || typeName === "ZodEnum") {
|
|
504
|
+
const options = extractEnumValues(def);
|
|
505
|
+
return {
|
|
506
|
+
type: "select",
|
|
507
|
+
required: true,
|
|
508
|
+
options,
|
|
509
|
+
innerSchema: schema
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
if (typeName === "nativeEnum" || typeName === "ZodNativeEnum") {
|
|
513
|
+
const options = extractNativeEnumValues(def);
|
|
514
|
+
return {
|
|
515
|
+
type: "select",
|
|
516
|
+
required: true,
|
|
517
|
+
options,
|
|
518
|
+
innerSchema: schema
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
if (typeName === "string" || typeName === "ZodString") {
|
|
522
|
+
let fieldType = "text";
|
|
523
|
+
if (def?.checks) {
|
|
524
|
+
const hasEmailCheck = def.checks.some(
|
|
525
|
+
(check) => check.kind === "email"
|
|
526
|
+
);
|
|
527
|
+
if (hasEmailCheck) {
|
|
528
|
+
fieldType = "email";
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
type: fieldType,
|
|
533
|
+
required: true,
|
|
534
|
+
innerSchema: schema
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
if (typeName === "number" || typeName === "ZodNumber") {
|
|
538
|
+
return {
|
|
539
|
+
type: "number",
|
|
540
|
+
required: true,
|
|
541
|
+
innerSchema: schema
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
if (typeName === "date" || typeName === "ZodDate") {
|
|
545
|
+
return {
|
|
546
|
+
type: "date",
|
|
547
|
+
required: true,
|
|
548
|
+
innerSchema: schema
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
if (typeName === "boolean" || typeName === "ZodBoolean") {
|
|
552
|
+
return {
|
|
553
|
+
type: "checkbox",
|
|
554
|
+
required: true,
|
|
555
|
+
innerSchema: schema
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
type: "text",
|
|
560
|
+
required: true,
|
|
561
|
+
innerSchema: schema
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function extractEnumValues(def) {
|
|
565
|
+
let values;
|
|
566
|
+
if (def?.values && Array.isArray(def.values)) {
|
|
567
|
+
values = def.values;
|
|
568
|
+
} else if (def?.entries && typeof def.entries === "object") {
|
|
569
|
+
values = Object.keys(def.entries);
|
|
570
|
+
}
|
|
571
|
+
if (values && values.length > 0) {
|
|
572
|
+
return values.map((value) => ({
|
|
573
|
+
value,
|
|
574
|
+
label: String(value)
|
|
575
|
+
}));
|
|
576
|
+
}
|
|
577
|
+
return void 0;
|
|
578
|
+
}
|
|
579
|
+
function extractNativeEnumValues(def) {
|
|
580
|
+
const enumValues = def.values || def._def?.values || {};
|
|
581
|
+
if (enumValues && typeof enumValues === "object") {
|
|
582
|
+
return Object.entries(enumValues).filter(
|
|
583
|
+
([_, value]) => typeof value === "string" || typeof value === "number"
|
|
584
|
+
).map(([key, value]) => ({
|
|
585
|
+
value,
|
|
586
|
+
label: key
|
|
587
|
+
}));
|
|
588
|
+
}
|
|
589
|
+
return void 0;
|
|
590
|
+
}
|
|
591
|
+
function filterFieldsByNames(fields, fieldNames) {
|
|
592
|
+
if (fieldNames === void 0) {
|
|
593
|
+
return fields;
|
|
594
|
+
}
|
|
595
|
+
if (fieldNames.length === 0) {
|
|
338
596
|
return [];
|
|
339
597
|
}
|
|
340
|
-
|
|
598
|
+
return fields.filter((field) => fieldNames.includes(field.name));
|
|
599
|
+
}
|
|
600
|
+
function modelFieldsToFormFields(fields) {
|
|
601
|
+
return fields.map((field) => ({
|
|
602
|
+
name: field.name,
|
|
603
|
+
type: field.type,
|
|
604
|
+
label: field.label,
|
|
605
|
+
required: field.required,
|
|
606
|
+
options: field.options
|
|
607
|
+
}));
|
|
608
|
+
}
|
|
609
|
+
function getFieldNamesFromFields(fields) {
|
|
610
|
+
return fields.map((field) => field.name);
|
|
611
|
+
}
|
|
612
|
+
function getFieldByName(fields, fieldName) {
|
|
613
|
+
return fields.find((field) => field.name === fieldName);
|
|
614
|
+
}
|
|
615
|
+
function getFieldLabelFromFields(fields, fieldName) {
|
|
616
|
+
const field = getFieldByName(fields, fieldName);
|
|
617
|
+
return field?.label || fieldName;
|
|
618
|
+
}
|
|
341
619
|
var BaseFormFeature = class extends BaseFeature {
|
|
342
620
|
titleGetter;
|
|
343
621
|
descriptionGetter;
|
|
@@ -405,8 +683,8 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
405
683
|
* 预处理表单数据:根据 schema 类型转换数据
|
|
406
684
|
* 表单数据都是字符串,需要转换为正确的类型(数字、布尔等)
|
|
407
685
|
*/
|
|
408
|
-
preprocessFormData(
|
|
409
|
-
if (!
|
|
686
|
+
preprocessFormData(data) {
|
|
687
|
+
if (!this.schema) {
|
|
410
688
|
return data;
|
|
411
689
|
}
|
|
412
690
|
const processed = { ...data };
|
|
@@ -415,7 +693,7 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
415
693
|
processed[key] = void 0;
|
|
416
694
|
}
|
|
417
695
|
}
|
|
418
|
-
const def =
|
|
696
|
+
const def = this.schema._def;
|
|
419
697
|
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
420
698
|
if (!shape || typeof shape !== "object") {
|
|
421
699
|
return data;
|
|
@@ -476,16 +754,24 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
476
754
|
imeanServiceEngine.logger.info(
|
|
477
755
|
`[BaseFormFeature] Original body data: ${JSON.stringify(originalData)}`
|
|
478
756
|
);
|
|
479
|
-
let data = this.preprocessFormData(context
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
757
|
+
let data = this.preprocessFormData(context.body);
|
|
758
|
+
if (!this.schema) {
|
|
759
|
+
throw new Error("Schema is required for form validation");
|
|
760
|
+
}
|
|
761
|
+
const parseResult = this.schema.safeParse(data);
|
|
762
|
+
if (!parseResult.success) {
|
|
763
|
+
const issues = parseResult.error.issues || [];
|
|
764
|
+
const firstError = issues[0];
|
|
765
|
+
const fieldName = firstError.path && firstError.path.length > 0 ? firstError.path.join(".") : "";
|
|
766
|
+
const errorMessage = firstError.message;
|
|
767
|
+
const errorText = fieldName ? `${fieldName}: ${errorMessage}` : errorMessage;
|
|
768
|
+
context.sendError("\u9A8C\u8BC1\u5931\u8D25", errorText);
|
|
483
769
|
imeanServiceEngine.logger.info(
|
|
484
770
|
`[BaseFormFeature] Validation failed, returning form with originalData: ${JSON.stringify(originalData)}`
|
|
485
771
|
);
|
|
486
772
|
return this.render(context, originalData);
|
|
487
773
|
}
|
|
488
|
-
const item = await this.handleSubmit(context,
|
|
774
|
+
const item = await this.handleSubmit(context, parseResult.data);
|
|
489
775
|
if (!item) {
|
|
490
776
|
context.sendError(
|
|
491
777
|
this.getFormAction() === "create" ? "\u521B\u5EFA\u5931\u8D25" : "\u66F4\u65B0\u5931\u8D25",
|
|
@@ -514,12 +800,13 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
514
800
|
}
|
|
515
801
|
}
|
|
516
802
|
}
|
|
803
|
+
formFieldNames;
|
|
517
804
|
/**
|
|
518
805
|
* 渲染表单页面
|
|
519
806
|
*/
|
|
520
807
|
async render(context, initialData) {
|
|
521
|
-
const
|
|
522
|
-
const fields =
|
|
808
|
+
const filteredFields = this.formFieldNames ? filterFieldsByNames(this.fields || [], this.formFieldNames) : this.fields || [];
|
|
809
|
+
const fields = modelFieldsToFormFields(filteredFields);
|
|
523
810
|
let formData;
|
|
524
811
|
if (this.getFormAction() === "edit") {
|
|
525
812
|
if (initialData) {
|
|
@@ -585,22 +872,80 @@ var BaseFormFeature = class extends BaseFeature {
|
|
|
585
872
|
}
|
|
586
873
|
};
|
|
587
874
|
|
|
588
|
-
// src/features/
|
|
589
|
-
var
|
|
590
|
-
|
|
875
|
+
// src/features/custom-feature.ts
|
|
876
|
+
var CustomFeature = class extends BaseFeature {
|
|
877
|
+
routes;
|
|
878
|
+
handlerFn;
|
|
879
|
+
renderFn;
|
|
880
|
+
titleGetter;
|
|
881
|
+
descriptionGetter;
|
|
591
882
|
constructor(options) {
|
|
592
883
|
super({
|
|
593
|
-
name:
|
|
594
|
-
type: "
|
|
595
|
-
permission: options.permission
|
|
884
|
+
name: options.name,
|
|
885
|
+
type: "custom",
|
|
886
|
+
permission: options.permission ?? null,
|
|
596
887
|
dialogSize: options.dialogSize,
|
|
597
|
-
closeOnBackdropClick: options.closeOnBackdropClick
|
|
598
|
-
getTitle: options.getTitle ? (context) => options.getTitle(context) : void 0,
|
|
599
|
-
getDescription: options.getDescription ? (context) => options.getDescription(context) : void 0
|
|
888
|
+
closeOnBackdropClick: options.closeOnBackdropClick
|
|
600
889
|
});
|
|
601
|
-
this.
|
|
602
|
-
|
|
603
|
-
|
|
890
|
+
this.routes = options.routes;
|
|
891
|
+
this.handlerFn = options.handler;
|
|
892
|
+
this.renderFn = options.render;
|
|
893
|
+
this.titleGetter = options.getTitle;
|
|
894
|
+
this.descriptionGetter = options.getDescription;
|
|
895
|
+
if (!this.handlerFn && !this.renderFn) {
|
|
896
|
+
throw new Error(
|
|
897
|
+
`CustomFeature "${options.name}" must provide either "handler" or "render" method`
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
async getTitle(context) {
|
|
902
|
+
if (this.titleGetter) {
|
|
903
|
+
return await this.titleGetter(context);
|
|
904
|
+
}
|
|
905
|
+
return super.getTitle(context);
|
|
906
|
+
}
|
|
907
|
+
async getDescription(context) {
|
|
908
|
+
if (this.descriptionGetter) {
|
|
909
|
+
return await this.descriptionGetter(context);
|
|
910
|
+
}
|
|
911
|
+
return super.getDescription(context);
|
|
912
|
+
}
|
|
913
|
+
getRoutes() {
|
|
914
|
+
return this.routes;
|
|
915
|
+
}
|
|
916
|
+
async handle(context) {
|
|
917
|
+
if (this.handlerFn) {
|
|
918
|
+
return await this.handlerFn(context);
|
|
919
|
+
}
|
|
920
|
+
return void 0;
|
|
921
|
+
}
|
|
922
|
+
async render(context) {
|
|
923
|
+
if (this.renderFn) {
|
|
924
|
+
return await this.renderFn(context);
|
|
925
|
+
}
|
|
926
|
+
return void 0;
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
// src/features/default-create-feature.tsx
|
|
931
|
+
var DefaultCreateFeature = class extends BaseFormFeature {
|
|
932
|
+
createItem;
|
|
933
|
+
constructor(options) {
|
|
934
|
+
super({
|
|
935
|
+
name: "create",
|
|
936
|
+
type: "create",
|
|
937
|
+
permission: options.permission || `${options.permissionPrefix}.create`,
|
|
938
|
+
dialogSize: options.dialogSize,
|
|
939
|
+
closeOnBackdropClick: options.closeOnBackdropClick,
|
|
940
|
+
getTitle: options.getTitle ? (context) => options.getTitle(context) : void 0,
|
|
941
|
+
getDescription: options.getDescription ? (context) => options.getDescription(context) : void 0
|
|
942
|
+
});
|
|
943
|
+
this.schema = options.schema;
|
|
944
|
+
this.fields = parseSchemaToFields(options.schema);
|
|
945
|
+
this.createItem = options.createItem;
|
|
946
|
+
this.formFieldNames = options.formFieldNames;
|
|
947
|
+
}
|
|
948
|
+
getFormAction() {
|
|
604
949
|
return "create";
|
|
605
950
|
}
|
|
606
951
|
getRoutes() {
|
|
@@ -670,6 +1015,7 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
670
1015
|
deleteItem;
|
|
671
1016
|
titleGetter;
|
|
672
1017
|
descriptionGetter;
|
|
1018
|
+
detailFieldNames;
|
|
673
1019
|
constructor(options) {
|
|
674
1020
|
super({
|
|
675
1021
|
name: "detail",
|
|
@@ -678,10 +1024,13 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
678
1024
|
dialogSize: options.dialogSize,
|
|
679
1025
|
closeOnBackdropClick: options.closeOnBackdropClick
|
|
680
1026
|
});
|
|
1027
|
+
this.schema = options.schema;
|
|
1028
|
+
this.fields = parseSchemaToFields(options.schema);
|
|
681
1029
|
this.getItem = options.getItem;
|
|
682
1030
|
this.deleteItem = options.deleteItem;
|
|
683
1031
|
this.titleGetter = options.getTitle;
|
|
684
1032
|
this.descriptionGetter = options.getDescription;
|
|
1033
|
+
this.detailFieldNames = options.detailFieldNames;
|
|
685
1034
|
}
|
|
686
1035
|
async getTitle(context) {
|
|
687
1036
|
if (this.titleGetter) {
|
|
@@ -712,12 +1061,25 @@ var DefaultDetailFeature = class extends BaseFeature {
|
|
|
712
1061
|
if (!item) {
|
|
713
1062
|
return context.ctx.json({ error: "Not found" }, 404);
|
|
714
1063
|
}
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
1064
|
+
const detailFields = this.detailFieldNames ? filterFieldsByNames(this.fields || [], this.detailFieldNames) : this.fields || [];
|
|
1065
|
+
if (this.detailFieldNames) {
|
|
1066
|
+
const systemFields = ["id", "createdAt", "updatedAt"];
|
|
1067
|
+
for (const sysField of systemFields) {
|
|
1068
|
+
if (this.detailFieldNames.includes(sysField) && !detailFields.find((f) => f.name === sysField)) {
|
|
1069
|
+
detailFields.push({
|
|
1070
|
+
name: sysField,
|
|
1071
|
+
label: sysField,
|
|
1072
|
+
type: "text",
|
|
1073
|
+
required: false,
|
|
1074
|
+
schema: null
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
const detailFieldNames = getFieldNamesFromFields(detailFields);
|
|
1080
|
+
const fields = detailFieldNames.map((fieldName) => ({
|
|
719
1081
|
key: fieldName,
|
|
720
|
-
label:
|
|
1082
|
+
label: getFieldLabelFromFields(this.fields || [], fieldName) || fieldName
|
|
721
1083
|
}));
|
|
722
1084
|
return /* @__PURE__ */ jsxRuntime.jsx(DetailPage, { item, fields });
|
|
723
1085
|
}
|
|
@@ -790,8 +1152,11 @@ var DefaultEditFeature = class extends BaseFormFeature {
|
|
|
790
1152
|
return void 0;
|
|
791
1153
|
} : void 0
|
|
792
1154
|
});
|
|
1155
|
+
this.schema = options.schema;
|
|
1156
|
+
this.fields = parseSchemaToFields(options.schema);
|
|
793
1157
|
this.getItem = options.getItem;
|
|
794
1158
|
this.updateItem = options.updateItem;
|
|
1159
|
+
this.formFieldNames = options.formFieldNames;
|
|
795
1160
|
}
|
|
796
1161
|
getFormAction() {
|
|
797
1162
|
return "edit";
|
|
@@ -853,33 +1218,115 @@ var DefaultEditFeature = class extends BaseFormFeature {
|
|
|
853
1218
|
}
|
|
854
1219
|
}
|
|
855
1220
|
};
|
|
856
|
-
function
|
|
1221
|
+
function FilterForm(props) {
|
|
857
1222
|
const {
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
shadow = true,
|
|
862
|
-
bordered = false,
|
|
863
|
-
noPadding = false
|
|
1223
|
+
fields,
|
|
1224
|
+
listPath,
|
|
1225
|
+
currentFilters = {}
|
|
864
1226
|
} = props;
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1227
|
+
function getFieldValue2(field) {
|
|
1228
|
+
const value = currentFilters[field.name];
|
|
1229
|
+
if (value === null || value === void 0 || value === "") {
|
|
1230
|
+
return "";
|
|
1231
|
+
}
|
|
1232
|
+
return String(value);
|
|
1233
|
+
}
|
|
1234
|
+
if (fields.length === 0) {
|
|
1235
|
+
return null;
|
|
1236
|
+
}
|
|
1237
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white border border-gray-200 rounded-lg shadow-sm p-4 mb-6", "data-testid": "filter-form-container", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1238
|
+
"form",
|
|
871
1239
|
{
|
|
872
|
-
|
|
1240
|
+
method: "get",
|
|
1241
|
+
action: listPath,
|
|
1242
|
+
"hx-get": listPath,
|
|
1243
|
+
className: "space-y-4",
|
|
1244
|
+
"data-testid": "filter-form",
|
|
873
1245
|
children: [
|
|
874
|
-
|
|
875
|
-
|
|
1246
|
+
fields.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4", children: fields.map((field) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", "data-testid": `filter-field-${field.name}`, children: [
|
|
1247
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1248
|
+
"label",
|
|
1249
|
+
{
|
|
1250
|
+
htmlFor: `filter-${field.name}`,
|
|
1251
|
+
className: "block text-sm font-semibold text-gray-700",
|
|
1252
|
+
"data-testid": `filter-label-${field.name}`,
|
|
1253
|
+
children: field.label
|
|
1254
|
+
}
|
|
1255
|
+
),
|
|
1256
|
+
field.type === "select" ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1257
|
+
"select",
|
|
1258
|
+
{
|
|
1259
|
+
id: `filter-${field.name}`,
|
|
1260
|
+
name: field.name,
|
|
1261
|
+
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",
|
|
1262
|
+
"data-testid": `filter-select-${field.name}`,
|
|
1263
|
+
children: [
|
|
1264
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", selected: getFieldValue2(field) === "", children: "\u5168\u90E8" }),
|
|
1265
|
+
field.options?.map((option) => {
|
|
1266
|
+
const optionValue = String(option.value);
|
|
1267
|
+
const isSelected = getFieldValue2(field) === optionValue;
|
|
1268
|
+
return /* @__PURE__ */ jsxRuntime.jsx("option", { value: optionValue, selected: isSelected, children: option.label }, optionValue);
|
|
1269
|
+
})
|
|
1270
|
+
]
|
|
1271
|
+
}
|
|
1272
|
+
) : field.type === "date" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1273
|
+
"input",
|
|
1274
|
+
{
|
|
1275
|
+
id: `filter-${field.name}`,
|
|
1276
|
+
name: field.name,
|
|
1277
|
+
type: "date",
|
|
1278
|
+
value: getFieldValue2(field),
|
|
1279
|
+
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",
|
|
1280
|
+
"data-testid": `filter-input-${field.name}`
|
|
1281
|
+
}
|
|
1282
|
+
) : field.type === "number" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1283
|
+
"input",
|
|
1284
|
+
{
|
|
1285
|
+
id: `filter-${field.name}`,
|
|
1286
|
+
name: field.name,
|
|
1287
|
+
type: "number",
|
|
1288
|
+
value: getFieldValue2(field),
|
|
1289
|
+
placeholder: field.placeholder || `\u8BF7\u8F93\u5165${field.label}`,
|
|
1290
|
+
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",
|
|
1291
|
+
"data-testid": `filter-input-${field.name}`
|
|
1292
|
+
}
|
|
1293
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
1294
|
+
"input",
|
|
1295
|
+
{
|
|
1296
|
+
id: `filter-${field.name}`,
|
|
1297
|
+
name: field.name,
|
|
1298
|
+
type: "text",
|
|
1299
|
+
value: getFieldValue2(field),
|
|
1300
|
+
placeholder: field.placeholder || `\u8BF7\u8F93\u5165${field.label}`,
|
|
1301
|
+
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",
|
|
1302
|
+
"data-testid": `filter-input-${field.name}`
|
|
1303
|
+
}
|
|
1304
|
+
)
|
|
1305
|
+
] }, field.name)) }),
|
|
1306
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 pt-2", children: [
|
|
1307
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1308
|
+
"button",
|
|
1309
|
+
{
|
|
1310
|
+
type: "submit",
|
|
1311
|
+
className: "px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium",
|
|
1312
|
+
"data-testid": "filter-submit-button",
|
|
1313
|
+
children: "\u7B5B\u9009"
|
|
1314
|
+
}
|
|
1315
|
+
),
|
|
1316
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1317
|
+
"a",
|
|
1318
|
+
{
|
|
1319
|
+
href: listPath,
|
|
1320
|
+
"hx-get": listPath,
|
|
1321
|
+
className: "px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition-colors font-medium",
|
|
1322
|
+
"data-testid": "filter-reset-button",
|
|
1323
|
+
children: "\u91CD\u7F6E"
|
|
1324
|
+
}
|
|
1325
|
+
)
|
|
1326
|
+
] })
|
|
876
1327
|
]
|
|
877
1328
|
}
|
|
878
|
-
);
|
|
879
|
-
}
|
|
880
|
-
function EmptyState(props) {
|
|
881
|
-
const { message = "\u6682\u65E0\u6570\u636E", children } = props;
|
|
882
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-12", children: children || /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-500 text-sm", children: message }) });
|
|
1329
|
+
) });
|
|
883
1330
|
}
|
|
884
1331
|
function Button(props) {
|
|
885
1332
|
const {
|
|
@@ -945,6 +1392,34 @@ function Button(props) {
|
|
|
945
1392
|
}
|
|
946
1393
|
);
|
|
947
1394
|
}
|
|
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
|
+
function EmptyState(props) {
|
|
1420
|
+
const { message = "\u6682\u65E0\u6570\u636E", children } = props;
|
|
1421
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-12", children: children || /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-500 text-sm", children: message }) });
|
|
1422
|
+
}
|
|
948
1423
|
function Pagination(props) {
|
|
949
1424
|
const {
|
|
950
1425
|
page,
|
|
@@ -1046,23 +1521,30 @@ function TableHeader(props) {
|
|
|
1046
1521
|
if (!showHeader) return null;
|
|
1047
1522
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: STYLES.header.container, children: [
|
|
1048
1523
|
title && title.trim() ? /* @__PURE__ */ jsxRuntime.jsx("h3", { className: STYLES.header.title, children: title }) : /* @__PURE__ */ jsxRuntime.jsx("div", {}),
|
|
1049
|
-
tableActions && tableActions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1524
|
+
tableActions && tableActions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1525
|
+
"div",
|
|
1526
|
+
{
|
|
1527
|
+
className: STYLES.header.actions,
|
|
1528
|
+
"data-testid": "table-header-actions",
|
|
1529
|
+
children: tableActions.map((action, idx) => {
|
|
1530
|
+
const testId = `table-action-${action.label}`;
|
|
1531
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1532
|
+
Button,
|
|
1533
|
+
{
|
|
1534
|
+
variant: action.variant || "secondary",
|
|
1535
|
+
href: action.href,
|
|
1536
|
+
hxGet: action.hxGet,
|
|
1537
|
+
hxPost: action.hxPost,
|
|
1538
|
+
hxDelete: action.hxDelete,
|
|
1539
|
+
hxConfirm: action.hxConfirm,
|
|
1540
|
+
"data-testid": testId,
|
|
1541
|
+
children: action.label
|
|
1542
|
+
},
|
|
1543
|
+
idx
|
|
1544
|
+
);
|
|
1545
|
+
})
|
|
1546
|
+
}
|
|
1547
|
+
)
|
|
1066
1548
|
] });
|
|
1067
1549
|
}
|
|
1068
1550
|
function TableHeaderRow(props) {
|
|
@@ -1141,7 +1623,14 @@ function ActionCell(props) {
|
|
|
1141
1623
|
className: `${STYLES.table.td.base} ${STYLES.table.td.stickyRight}`,
|
|
1142
1624
|
"data-testid": "table-cell-actions",
|
|
1143
1625
|
role: "cell",
|
|
1144
|
-
children: actionStyle === "link" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1626
|
+
children: actionStyle === "link" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1627
|
+
"div",
|
|
1628
|
+
{
|
|
1629
|
+
className: STYLES.actionLink.container,
|
|
1630
|
+
"data-testid": "table-actions",
|
|
1631
|
+
children: actions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsx(ActionLink, { action, item }, idx))
|
|
1632
|
+
}
|
|
1633
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: STYLES.actionButton, "data-testid": "table-actions", children: actions.map((action, idx) => {
|
|
1145
1634
|
const hrefValue = action.href(item);
|
|
1146
1635
|
const isDelete = action.method === "delete";
|
|
1147
1636
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -1193,35 +1682,51 @@ function Table(props) {
|
|
|
1193
1682
|
const idColumn = columns.find((col) => col.key === "id");
|
|
1194
1683
|
const otherColumns = columns.filter((col) => col.key !== "id");
|
|
1195
1684
|
const hasActions = Boolean(actions && actions.length > 0);
|
|
1196
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1685
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1686
|
+
Card,
|
|
1687
|
+
{
|
|
1688
|
+
shadow: true,
|
|
1689
|
+
noPadding: true,
|
|
1690
|
+
className: "overflow-hidden",
|
|
1691
|
+
"data-testid": "data-table",
|
|
1692
|
+
children: [
|
|
1693
|
+
/* @__PURE__ */ jsxRuntime.jsx(TableHeader, { title, tableActions }),
|
|
1694
|
+
items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(EmptyState, { message: "\u6682\u65E0\u6570\u636E" }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1695
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: STYLES.table.container, "data-testid": "table-container", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1696
|
+
"table",
|
|
1697
|
+
{
|
|
1698
|
+
className: STYLES.table.table,
|
|
1699
|
+
"data-testid": "table",
|
|
1700
|
+
role: "table",
|
|
1701
|
+
children: [
|
|
1702
|
+
/* @__PURE__ */ jsxRuntime.jsx("thead", { className: STYLES.table.thead, "data-testid": "table-head", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1703
|
+
TableHeaderRow,
|
|
1704
|
+
{
|
|
1705
|
+
columns,
|
|
1706
|
+
idColumn,
|
|
1707
|
+
otherColumns,
|
|
1708
|
+
hasActions
|
|
1709
|
+
}
|
|
1710
|
+
) }),
|
|
1711
|
+
/* @__PURE__ */ jsxRuntime.jsx("tbody", { className: STYLES.table.tbody, "data-testid": "table-body", children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1712
|
+
TableRow,
|
|
1713
|
+
{
|
|
1714
|
+
item,
|
|
1715
|
+
idColumn,
|
|
1716
|
+
otherColumns,
|
|
1717
|
+
actions,
|
|
1718
|
+
actionStyle
|
|
1719
|
+
},
|
|
1720
|
+
item.id
|
|
1721
|
+
)) })
|
|
1722
|
+
]
|
|
1723
|
+
}
|
|
1724
|
+
) }),
|
|
1725
|
+
pagination && /* @__PURE__ */ jsxRuntime.jsx(Pagination, { ...pagination })
|
|
1726
|
+
] })
|
|
1727
|
+
]
|
|
1728
|
+
}
|
|
1729
|
+
);
|
|
1225
1730
|
}
|
|
1226
1731
|
function ListPage(props) {
|
|
1227
1732
|
const {
|
|
@@ -1232,7 +1737,8 @@ function ListPage(props) {
|
|
|
1232
1737
|
detailPath,
|
|
1233
1738
|
editPath,
|
|
1234
1739
|
deletePath,
|
|
1235
|
-
listPath
|
|
1740
|
+
listPath,
|
|
1741
|
+
filterFields
|
|
1236
1742
|
} = props;
|
|
1237
1743
|
const actions = [];
|
|
1238
1744
|
if (detailPath) {
|
|
@@ -1268,40 +1774,42 @@ function ListPage(props) {
|
|
|
1268
1774
|
variant: "primary"
|
|
1269
1775
|
}
|
|
1270
1776
|
];
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1777
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-6", children: [
|
|
1778
|
+
filterFields && filterFields.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1779
|
+
FilterForm,
|
|
1780
|
+
{
|
|
1781
|
+
fields: filterFields,
|
|
1782
|
+
listPath,
|
|
1783
|
+
currentFilters: params.filters
|
|
1784
|
+
}
|
|
1785
|
+
),
|
|
1786
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1787
|
+
Table,
|
|
1788
|
+
{
|
|
1789
|
+
items: result.items,
|
|
1790
|
+
columns: columns.map((col) => ({
|
|
1791
|
+
key: col.key,
|
|
1792
|
+
label: col.label,
|
|
1793
|
+
render: col.render ? (value, item) => col.render(value, item) : void 0
|
|
1794
|
+
})),
|
|
1795
|
+
actions: actions.length > 0 ? actions.map((action) => ({
|
|
1796
|
+
label: action.label,
|
|
1797
|
+
href: (item) => action.href(item),
|
|
1798
|
+
method: action.method
|
|
1799
|
+
})) : void 0,
|
|
1800
|
+
pagination: {
|
|
1801
|
+
page: result.page,
|
|
1802
|
+
pageSize: result.pageSize,
|
|
1803
|
+
total: result.total,
|
|
1804
|
+
totalPages: result.totalPages,
|
|
1805
|
+
baseUrl: listPath,
|
|
1806
|
+
currentParams
|
|
1807
|
+
},
|
|
1808
|
+
tableActions,
|
|
1809
|
+
actionStyle: "link"
|
|
1810
|
+
}
|
|
1811
|
+
)
|
|
1812
|
+
] });
|
|
1305
1813
|
}
|
|
1306
1814
|
|
|
1307
1815
|
// src/utils/params.ts
|
|
@@ -1325,6 +1833,8 @@ function parseListParams(ctx) {
|
|
|
1325
1833
|
var DefaultListFeature = class extends BaseFeature {
|
|
1326
1834
|
getList;
|
|
1327
1835
|
deleteItem;
|
|
1836
|
+
listFieldNames;
|
|
1837
|
+
filterSchema;
|
|
1328
1838
|
constructor(options) {
|
|
1329
1839
|
super({
|
|
1330
1840
|
name: "list",
|
|
@@ -1333,8 +1843,12 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
1333
1843
|
dialogSize: options.dialogSize,
|
|
1334
1844
|
closeOnBackdropClick: options.closeOnBackdropClick
|
|
1335
1845
|
});
|
|
1846
|
+
this.schema = options.schema;
|
|
1847
|
+
this.fields = parseSchemaToFields(options.schema);
|
|
1336
1848
|
this.getList = options.getList;
|
|
1337
1849
|
this.deleteItem = options.deleteItem;
|
|
1850
|
+
this.listFieldNames = options.listFieldNames;
|
|
1851
|
+
this.filterSchema = options.filterSchema;
|
|
1338
1852
|
}
|
|
1339
1853
|
getRoutes() {
|
|
1340
1854
|
return [{ method: "get", path: "/list" }];
|
|
@@ -1342,16 +1856,14 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
1342
1856
|
async render(context) {
|
|
1343
1857
|
const params = parseListParams(context.ctx);
|
|
1344
1858
|
const result = await this.getList(params);
|
|
1859
|
+
const listFields = this.listFieldNames ? filterFieldsByNames(this.fields || [], this.listFieldNames) : this.fields || [];
|
|
1860
|
+
const listFieldNames = getFieldNamesFromFields(listFields);
|
|
1861
|
+
const filterFields = this.filterSchema ? modelFieldsToFormFields(parseSchemaToFields(this.filterSchema)) : [];
|
|
1862
|
+
const columns = listFieldNames.map((fieldName) => ({
|
|
1863
|
+
key: fieldName,
|
|
1864
|
+
label: getFieldLabelFromFields(this.fields || [], fieldName)
|
|
1865
|
+
}));
|
|
1345
1866
|
const model = context.model;
|
|
1346
|
-
const listFields = model.getListFields();
|
|
1347
|
-
const columns = listFields.map((fieldName) => {
|
|
1348
|
-
const fieldMetadata = model.modelSchema?.fields?.[fieldName];
|
|
1349
|
-
return {
|
|
1350
|
-
key: fieldName,
|
|
1351
|
-
label: model.getFieldLabel(fieldName),
|
|
1352
|
-
render: fieldMetadata?.render ? (value, item) => fieldMetadata.render(value, item) : void 0
|
|
1353
|
-
};
|
|
1354
|
-
});
|
|
1355
1867
|
const prefix = context.prefix || "";
|
|
1356
1868
|
const basePath = `${prefix}/${model.modelName}`;
|
|
1357
1869
|
const listPath = `${basePath}/list`;
|
|
@@ -1369,7 +1881,8 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
1369
1881
|
detailPath,
|
|
1370
1882
|
editPath,
|
|
1371
1883
|
deletePath,
|
|
1372
|
-
listPath
|
|
1884
|
+
listPath,
|
|
1885
|
+
filterFields
|
|
1373
1886
|
}
|
|
1374
1887
|
);
|
|
1375
1888
|
}
|
|
@@ -1390,523 +1903,6 @@ var DefaultListFeature = class extends BaseFeature {
|
|
|
1390
1903
|
}
|
|
1391
1904
|
};
|
|
1392
1905
|
|
|
1393
|
-
// src/feature-registry.ts
|
|
1394
|
-
var FeatureRegistry = class {
|
|
1395
|
-
features = /* @__PURE__ */ new Map();
|
|
1396
|
-
model;
|
|
1397
|
-
constructor(model) {
|
|
1398
|
-
this.model = model;
|
|
1399
|
-
}
|
|
1400
|
-
/**
|
|
1401
|
-
* 注册 Feature
|
|
1402
|
-
*/
|
|
1403
|
-
register(name, feature) {
|
|
1404
|
-
this.features.set(name, feature);
|
|
1405
|
-
}
|
|
1406
|
-
/**
|
|
1407
|
-
* 快捷方法:注册列表 Feature
|
|
1408
|
-
*/
|
|
1409
|
-
list(feature) {
|
|
1410
|
-
this.register("list", feature);
|
|
1411
|
-
}
|
|
1412
|
-
/**
|
|
1413
|
-
* 快捷方法:注册详情 Feature
|
|
1414
|
-
*/
|
|
1415
|
-
detail(feature) {
|
|
1416
|
-
this.register("detail", feature);
|
|
1417
|
-
}
|
|
1418
|
-
/**
|
|
1419
|
-
* 快捷方法:注册创建 Feature
|
|
1420
|
-
*/
|
|
1421
|
-
create(feature) {
|
|
1422
|
-
this.register("create", feature);
|
|
1423
|
-
}
|
|
1424
|
-
/**
|
|
1425
|
-
* 快捷方法:注册编辑 Feature
|
|
1426
|
-
*/
|
|
1427
|
-
edit(feature) {
|
|
1428
|
-
this.register("edit", feature);
|
|
1429
|
-
}
|
|
1430
|
-
/**
|
|
1431
|
-
* 快捷方法:注册删除 Feature
|
|
1432
|
-
*/
|
|
1433
|
-
delete(feature) {
|
|
1434
|
-
this.register("delete", feature);
|
|
1435
|
-
}
|
|
1436
|
-
/**
|
|
1437
|
-
* 快捷方法:注册自定义 Feature
|
|
1438
|
-
*/
|
|
1439
|
-
custom(name, feature) {
|
|
1440
|
-
this.register(name, feature);
|
|
1441
|
-
}
|
|
1442
|
-
/**
|
|
1443
|
-
* 一键启用所有 CRUD Feature
|
|
1444
|
-
* 类型通过 model.modelSchema.schema 的 z.infer 推断
|
|
1445
|
-
*/
|
|
1446
|
-
crud(options) {
|
|
1447
|
-
const {
|
|
1448
|
-
permissionPrefix,
|
|
1449
|
-
getList,
|
|
1450
|
-
getItem,
|
|
1451
|
-
createItem,
|
|
1452
|
-
updateItem,
|
|
1453
|
-
deleteItem,
|
|
1454
|
-
permissions = {},
|
|
1455
|
-
features = {},
|
|
1456
|
-
dialogSizes = {},
|
|
1457
|
-
closeOnBackdropClick = {},
|
|
1458
|
-
getTitles = {},
|
|
1459
|
-
getDescriptions = {}
|
|
1460
|
-
} = options;
|
|
1461
|
-
if (features.list !== false) {
|
|
1462
|
-
this.list(
|
|
1463
|
-
new DefaultListFeature({
|
|
1464
|
-
getList,
|
|
1465
|
-
deleteItem,
|
|
1466
|
-
permissionPrefix,
|
|
1467
|
-
permission: permissions.list,
|
|
1468
|
-
dialogSize: dialogSizes.list,
|
|
1469
|
-
closeOnBackdropClick: closeOnBackdropClick.list
|
|
1470
|
-
})
|
|
1471
|
-
);
|
|
1472
|
-
}
|
|
1473
|
-
if (features.detail !== false) {
|
|
1474
|
-
this.detail(
|
|
1475
|
-
new DefaultDetailFeature({
|
|
1476
|
-
getItem,
|
|
1477
|
-
deleteItem,
|
|
1478
|
-
permissionPrefix,
|
|
1479
|
-
permission: permissions.read,
|
|
1480
|
-
dialogSize: dialogSizes.detail,
|
|
1481
|
-
closeOnBackdropClick: closeOnBackdropClick.detail,
|
|
1482
|
-
getTitle: getTitles.detail,
|
|
1483
|
-
getDescription: getDescriptions.detail
|
|
1484
|
-
})
|
|
1485
|
-
);
|
|
1486
|
-
}
|
|
1487
|
-
if (features.create !== false) {
|
|
1488
|
-
this.create(
|
|
1489
|
-
new DefaultCreateFeature({
|
|
1490
|
-
createItem,
|
|
1491
|
-
permissionPrefix,
|
|
1492
|
-
permission: permissions.create,
|
|
1493
|
-
dialogSize: dialogSizes.create,
|
|
1494
|
-
closeOnBackdropClick: closeOnBackdropClick.create,
|
|
1495
|
-
getTitle: getTitles.create,
|
|
1496
|
-
getDescription: getDescriptions.create
|
|
1497
|
-
})
|
|
1498
|
-
);
|
|
1499
|
-
}
|
|
1500
|
-
if (features.edit !== false) {
|
|
1501
|
-
this.edit(
|
|
1502
|
-
new DefaultEditFeature({
|
|
1503
|
-
getItem,
|
|
1504
|
-
updateItem,
|
|
1505
|
-
permissionPrefix,
|
|
1506
|
-
permission: permissions.edit,
|
|
1507
|
-
dialogSize: dialogSizes.edit,
|
|
1508
|
-
closeOnBackdropClick: closeOnBackdropClick.edit,
|
|
1509
|
-
getTitle: getTitles.edit,
|
|
1510
|
-
getDescription: getDescriptions.edit
|
|
1511
|
-
})
|
|
1512
|
-
);
|
|
1513
|
-
}
|
|
1514
|
-
if (features.delete !== false && deleteItem) {
|
|
1515
|
-
this.delete(
|
|
1516
|
-
new DefaultDeleteFeature({
|
|
1517
|
-
deleteItem,
|
|
1518
|
-
permissionPrefix,
|
|
1519
|
-
permission: permissions.delete
|
|
1520
|
-
})
|
|
1521
|
-
);
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
/**
|
|
1525
|
-
* 获取所有注册的 Feature
|
|
1526
|
-
*/
|
|
1527
|
-
getAll() {
|
|
1528
|
-
return Array.from(this.features.values());
|
|
1529
|
-
}
|
|
1530
|
-
/**
|
|
1531
|
-
* 获取指定 Feature
|
|
1532
|
-
*/
|
|
1533
|
-
get(name) {
|
|
1534
|
-
return this.features.get(name);
|
|
1535
|
-
}
|
|
1536
|
-
};
|
|
1537
|
-
|
|
1538
|
-
// src/page-model.ts
|
|
1539
|
-
var PageModel = class {
|
|
1540
|
-
modelName;
|
|
1541
|
-
features;
|
|
1542
|
-
modelSchema;
|
|
1543
|
-
metadata;
|
|
1544
|
-
/**
|
|
1545
|
-
* 构造函数
|
|
1546
|
-
* @param modelName 模型/页面名称(用于路由和权限)
|
|
1547
|
-
* @param schema 模型 Schema(可选,不提供则为普通页面)
|
|
1548
|
-
* @param metadata 页面元数据(可选,如果提供则不需要实现 getMetadata)
|
|
1549
|
-
*/
|
|
1550
|
-
constructor(modelName, schema, metadata) {
|
|
1551
|
-
this.modelName = modelName;
|
|
1552
|
-
this.modelSchema = schema;
|
|
1553
|
-
this.metadata = metadata || {
|
|
1554
|
-
title: modelName,
|
|
1555
|
-
description: "",
|
|
1556
|
-
useAdminLayout: true
|
|
1557
|
-
};
|
|
1558
|
-
this.features = new FeatureRegistry(this);
|
|
1559
|
-
}
|
|
1560
|
-
/**
|
|
1561
|
-
* 获取推断的类型(如果有 schema)
|
|
1562
|
-
* 使用方式:type ModelType = z.infer<typeof pageModel.getSchema()!.schema>
|
|
1563
|
-
*/
|
|
1564
|
-
getInferredType() {
|
|
1565
|
-
if (!this.modelSchema?.schema) {
|
|
1566
|
-
return void 0;
|
|
1567
|
-
}
|
|
1568
|
-
return this.modelSchema.schema;
|
|
1569
|
-
}
|
|
1570
|
-
/**
|
|
1571
|
-
* 获取页面元数据
|
|
1572
|
-
* 如果构造函数提供了 metadata,直接返回
|
|
1573
|
-
* 否则调用子类实现
|
|
1574
|
-
*/
|
|
1575
|
-
getMetadata() {
|
|
1576
|
-
return this.metadata;
|
|
1577
|
-
}
|
|
1578
|
-
/**
|
|
1579
|
-
* 判断是否有数据模型(有 Schema)
|
|
1580
|
-
*/
|
|
1581
|
-
hasModel() {
|
|
1582
|
-
return !!this.modelSchema;
|
|
1583
|
-
}
|
|
1584
|
-
/**
|
|
1585
|
-
* 获取模型 Schema(可选)
|
|
1586
|
-
*/
|
|
1587
|
-
getSchema() {
|
|
1588
|
-
return this.modelSchema;
|
|
1589
|
-
}
|
|
1590
|
-
/**
|
|
1591
|
-
* 验证数据(仅用于有数据模型的场景)
|
|
1592
|
-
*/
|
|
1593
|
-
validate(data) {
|
|
1594
|
-
if (!this.modelSchema) {
|
|
1595
|
-
return { success: true, data };
|
|
1596
|
-
}
|
|
1597
|
-
const result = this.modelSchema.schema.safeParse(data);
|
|
1598
|
-
if (result.success) {
|
|
1599
|
-
return { success: true, data: result.data };
|
|
1600
|
-
} else {
|
|
1601
|
-
const issues = result.error.issues || [];
|
|
1602
|
-
if (issues.length === 0) {
|
|
1603
|
-
return { success: false, error: "\u9A8C\u8BC1\u5931\u8D25" };
|
|
1604
|
-
}
|
|
1605
|
-
const firstError = issues[0];
|
|
1606
|
-
const fieldName = firstError.path && firstError.path.length > 0 ? firstError.path.join(".") : "";
|
|
1607
|
-
const errorMessage = firstError.message;
|
|
1608
|
-
return {
|
|
1609
|
-
success: false,
|
|
1610
|
-
error: fieldName ? `${fieldName}: ${errorMessage}` : errorMessage
|
|
1611
|
-
};
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
/**
|
|
1615
|
-
* 获取表单字段(仅用于有数据模型的场景)
|
|
1616
|
-
*/
|
|
1617
|
-
getFormFields() {
|
|
1618
|
-
if (!this.modelSchema) {
|
|
1619
|
-
return [];
|
|
1620
|
-
}
|
|
1621
|
-
if (this.modelSchema.formFields) {
|
|
1622
|
-
return this.modelSchema.formFields.map((fieldName) => this.createFormField(fieldName)).filter((field) => field !== null);
|
|
1623
|
-
}
|
|
1624
|
-
const def = this.modelSchema.schema._def;
|
|
1625
|
-
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
1626
|
-
if (!shape || typeof shape !== "object") {
|
|
1627
|
-
return [];
|
|
1628
|
-
}
|
|
1629
|
-
const fields = [];
|
|
1630
|
-
for (const fieldName of Object.keys(shape)) {
|
|
1631
|
-
if (["id", "createdAt", "updatedAt"].includes(fieldName)) {
|
|
1632
|
-
continue;
|
|
1633
|
-
}
|
|
1634
|
-
const field = this.createFormField(fieldName);
|
|
1635
|
-
if (field) {
|
|
1636
|
-
fields.push(field);
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
return fields;
|
|
1640
|
-
}
|
|
1641
|
-
/**
|
|
1642
|
-
* 创建表单字段
|
|
1643
|
-
*/
|
|
1644
|
-
createFormField(fieldName) {
|
|
1645
|
-
if (!this.modelSchema?.schema) {
|
|
1646
|
-
return null;
|
|
1647
|
-
}
|
|
1648
|
-
const def = this.modelSchema.schema._def;
|
|
1649
|
-
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
1650
|
-
if (!shape || typeof shape !== "object") {
|
|
1651
|
-
return null;
|
|
1652
|
-
}
|
|
1653
|
-
const fieldSchema = shape[fieldName];
|
|
1654
|
-
if (!fieldSchema) {
|
|
1655
|
-
return null;
|
|
1656
|
-
}
|
|
1657
|
-
const fieldMetadata = this.modelSchema.fields?.[fieldName];
|
|
1658
|
-
const label = fieldMetadata?.label || this.getFieldLabel(fieldName);
|
|
1659
|
-
const description = fieldMetadata?.description;
|
|
1660
|
-
const placeholder = fieldMetadata?.placeholder;
|
|
1661
|
-
const required = this.isRequiredField(fieldSchema);
|
|
1662
|
-
const type = fieldMetadata?.type || this.inferFieldType(fieldSchema);
|
|
1663
|
-
let options;
|
|
1664
|
-
if (fieldMetadata?.options) {
|
|
1665
|
-
options = fieldMetadata.options;
|
|
1666
|
-
} else {
|
|
1667
|
-
const inferredType = this.inferFieldType(fieldSchema);
|
|
1668
|
-
if (type === "select" || inferredType === "select") {
|
|
1669
|
-
options = this.extractEnumOptions(fieldSchema);
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
return {
|
|
1673
|
-
name: fieldName,
|
|
1674
|
-
type,
|
|
1675
|
-
label,
|
|
1676
|
-
required,
|
|
1677
|
-
description,
|
|
1678
|
-
placeholder,
|
|
1679
|
-
options
|
|
1680
|
-
};
|
|
1681
|
-
}
|
|
1682
|
-
/**
|
|
1683
|
-
* 从 Zod enum schema 中提取选项
|
|
1684
|
-
*/
|
|
1685
|
-
extractEnumOptions(schema) {
|
|
1686
|
-
if (!schema || !schema._def) {
|
|
1687
|
-
return void 0;
|
|
1688
|
-
}
|
|
1689
|
-
const def = schema._def;
|
|
1690
|
-
const typeName = def?.typeName || def?.type;
|
|
1691
|
-
if (typeName === "ZodOptional" || typeName === "optional") {
|
|
1692
|
-
return this.extractEnumOptions(def.innerType);
|
|
1693
|
-
}
|
|
1694
|
-
if (typeName === "ZodNullable" || typeName === "nullable") {
|
|
1695
|
-
return this.extractEnumOptions(def.innerType);
|
|
1696
|
-
}
|
|
1697
|
-
if (typeName === "ZodEnum" || typeName === "enum" || def?.type === "enum") {
|
|
1698
|
-
let values;
|
|
1699
|
-
if (def?.values && Array.isArray(def.values)) {
|
|
1700
|
-
values = def.values;
|
|
1701
|
-
} else if (def?.entries && typeof def.entries === "object") {
|
|
1702
|
-
values = Object.keys(def.entries);
|
|
1703
|
-
}
|
|
1704
|
-
if (values && values.length > 0) {
|
|
1705
|
-
return values.map((value) => ({
|
|
1706
|
-
value,
|
|
1707
|
-
label: String(value)
|
|
1708
|
-
}));
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
if (typeName === "ZodNativeEnum" || typeName === "nativeEnum") {
|
|
1712
|
-
const enumValues = def.values || def._def?.values || {};
|
|
1713
|
-
if (enumValues && typeof enumValues === "object") {
|
|
1714
|
-
return Object.entries(enumValues).filter(
|
|
1715
|
-
([_, value]) => typeof value === "string" || typeof value === "number"
|
|
1716
|
-
).map(([key, value]) => ({
|
|
1717
|
-
value,
|
|
1718
|
-
label: key
|
|
1719
|
-
}));
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
return void 0;
|
|
1723
|
-
}
|
|
1724
|
-
/**
|
|
1725
|
-
* 判断字段是否为必填
|
|
1726
|
-
*/
|
|
1727
|
-
isRequiredField(schema) {
|
|
1728
|
-
const def = schema._def;
|
|
1729
|
-
const typeName = def?.type || def?.typeName;
|
|
1730
|
-
if (typeName === "optional" || typeName === "ZodOptional") {
|
|
1731
|
-
return false;
|
|
1732
|
-
}
|
|
1733
|
-
if (typeName === "nullable" || typeName === "ZodNullable") {
|
|
1734
|
-
return this.isRequiredField(def.innerType);
|
|
1735
|
-
}
|
|
1736
|
-
return true;
|
|
1737
|
-
}
|
|
1738
|
-
/**
|
|
1739
|
-
* 推断字段类型
|
|
1740
|
-
*/
|
|
1741
|
-
inferFieldType(schema) {
|
|
1742
|
-
const def = schema._def;
|
|
1743
|
-
const typeName = def?.type || def?.typeName;
|
|
1744
|
-
if (typeName === "optional" || typeName === "ZodOptional") {
|
|
1745
|
-
return this.inferFieldType(def.innerType);
|
|
1746
|
-
}
|
|
1747
|
-
if (typeName === "nullable" || typeName === "ZodNullable") {
|
|
1748
|
-
return this.inferFieldType(def.innerType);
|
|
1749
|
-
}
|
|
1750
|
-
if (typeName === "enum" || typeName === "ZodEnum") {
|
|
1751
|
-
return "select";
|
|
1752
|
-
}
|
|
1753
|
-
if (typeName === "nativeEnum" || typeName === "ZodNativeEnum") {
|
|
1754
|
-
return "select";
|
|
1755
|
-
}
|
|
1756
|
-
if (typeName === "string" || typeName === "ZodString") {
|
|
1757
|
-
if (def?.checks) {
|
|
1758
|
-
const hasEmailCheck = def.checks.some(
|
|
1759
|
-
(check) => check.kind === "email"
|
|
1760
|
-
);
|
|
1761
|
-
if (hasEmailCheck) {
|
|
1762
|
-
return "email";
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
return "text";
|
|
1766
|
-
}
|
|
1767
|
-
if (typeName === "number" || typeName === "ZodNumber") {
|
|
1768
|
-
return "number";
|
|
1769
|
-
}
|
|
1770
|
-
if (typeName === "date" || typeName === "ZodDate") {
|
|
1771
|
-
return "date";
|
|
1772
|
-
}
|
|
1773
|
-
if (typeName === "boolean" || typeName === "ZodBoolean") {
|
|
1774
|
-
return "checkbox";
|
|
1775
|
-
}
|
|
1776
|
-
return "text";
|
|
1777
|
-
}
|
|
1778
|
-
/**
|
|
1779
|
-
* 获取列表字段(仅用于有数据模型的场景)
|
|
1780
|
-
*/
|
|
1781
|
-
getListFields() {
|
|
1782
|
-
if (this.modelSchema?.listFields) {
|
|
1783
|
-
return this.modelSchema.listFields;
|
|
1784
|
-
}
|
|
1785
|
-
if (this.modelSchema?.fields) {
|
|
1786
|
-
return Object.entries(this.modelSchema.fields).filter(([_, meta]) => meta.showInList !== false).map(([key]) => key);
|
|
1787
|
-
}
|
|
1788
|
-
if (this.modelSchema?.schema) {
|
|
1789
|
-
const def = this.modelSchema.schema._def;
|
|
1790
|
-
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
1791
|
-
if (shape && typeof shape === "object") {
|
|
1792
|
-
return Object.keys(shape).filter(
|
|
1793
|
-
(key) => !["id", "createdAt", "updatedAt"].includes(key)
|
|
1794
|
-
);
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
return [];
|
|
1798
|
-
}
|
|
1799
|
-
/**
|
|
1800
|
-
* 获取详情字段(仅用于有数据模型的场景)
|
|
1801
|
-
*/
|
|
1802
|
-
getDetailFields() {
|
|
1803
|
-
if (this.modelSchema?.detailFields) {
|
|
1804
|
-
return this.modelSchema.detailFields;
|
|
1805
|
-
}
|
|
1806
|
-
if (this.modelSchema?.fields) {
|
|
1807
|
-
return Object.entries(this.modelSchema.fields).filter(([_, meta]) => meta.showInDetail !== false).map(([key]) => key);
|
|
1808
|
-
}
|
|
1809
|
-
if (this.modelSchema?.schema) {
|
|
1810
|
-
const def = this.modelSchema.schema._def;
|
|
1811
|
-
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
1812
|
-
if (shape && typeof shape === "object") {
|
|
1813
|
-
return Object.keys(shape);
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
return [];
|
|
1817
|
-
}
|
|
1818
|
-
/**
|
|
1819
|
-
* 获取字段标签
|
|
1820
|
-
*/
|
|
1821
|
-
getFieldLabel(fieldName) {
|
|
1822
|
-
if (this.modelSchema?.fields?.[fieldName]?.label) {
|
|
1823
|
-
return this.modelSchema.fields[fieldName].label;
|
|
1824
|
-
}
|
|
1825
|
-
if (this.modelSchema?.schema) {
|
|
1826
|
-
const def = this.modelSchema.schema._def;
|
|
1827
|
-
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
1828
|
-
if (shape && typeof shape === "object") {
|
|
1829
|
-
const fieldSchema = shape[fieldName];
|
|
1830
|
-
if (fieldSchema) {
|
|
1831
|
-
const getDescription = (schema) => {
|
|
1832
|
-
if (schema?.description) {
|
|
1833
|
-
return schema.description;
|
|
1834
|
-
}
|
|
1835
|
-
const schemaDef = schema._def;
|
|
1836
|
-
if (schemaDef?.description) {
|
|
1837
|
-
return schemaDef.description;
|
|
1838
|
-
}
|
|
1839
|
-
if (schemaDef?.innerType) {
|
|
1840
|
-
return getDescription(schemaDef.innerType);
|
|
1841
|
-
}
|
|
1842
|
-
return void 0;
|
|
1843
|
-
};
|
|
1844
|
-
const description = getDescription(fieldSchema);
|
|
1845
|
-
if (description) {
|
|
1846
|
-
return description;
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
return fieldName;
|
|
1852
|
-
}
|
|
1853
|
-
};
|
|
1854
|
-
|
|
1855
|
-
// src/features/custom-feature.ts
|
|
1856
|
-
var CustomFeature = class extends BaseFeature {
|
|
1857
|
-
routes;
|
|
1858
|
-
handlerFn;
|
|
1859
|
-
renderFn;
|
|
1860
|
-
titleGetter;
|
|
1861
|
-
descriptionGetter;
|
|
1862
|
-
constructor(options) {
|
|
1863
|
-
super({
|
|
1864
|
-
name: options.name,
|
|
1865
|
-
type: "custom",
|
|
1866
|
-
permission: options.permission ?? null,
|
|
1867
|
-
dialogSize: options.dialogSize,
|
|
1868
|
-
closeOnBackdropClick: options.closeOnBackdropClick
|
|
1869
|
-
});
|
|
1870
|
-
this.routes = options.routes;
|
|
1871
|
-
this.handlerFn = options.handler;
|
|
1872
|
-
this.renderFn = options.render;
|
|
1873
|
-
this.titleGetter = options.getTitle;
|
|
1874
|
-
this.descriptionGetter = options.getDescription;
|
|
1875
|
-
if (!this.handlerFn && !this.renderFn) {
|
|
1876
|
-
throw new Error(
|
|
1877
|
-
`CustomFeature "${options.name}" must provide either "handler" or "render" method`
|
|
1878
|
-
);
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
async getTitle(context) {
|
|
1882
|
-
if (this.titleGetter) {
|
|
1883
|
-
return await this.titleGetter(context);
|
|
1884
|
-
}
|
|
1885
|
-
return super.getTitle(context);
|
|
1886
|
-
}
|
|
1887
|
-
async getDescription(context) {
|
|
1888
|
-
if (this.descriptionGetter) {
|
|
1889
|
-
return await this.descriptionGetter(context);
|
|
1890
|
-
}
|
|
1891
|
-
return super.getDescription(context);
|
|
1892
|
-
}
|
|
1893
|
-
getRoutes() {
|
|
1894
|
-
return this.routes;
|
|
1895
|
-
}
|
|
1896
|
-
async handle(context) {
|
|
1897
|
-
if (this.handlerFn) {
|
|
1898
|
-
return await this.handlerFn(context);
|
|
1899
|
-
}
|
|
1900
|
-
return void 0;
|
|
1901
|
-
}
|
|
1902
|
-
async render(context) {
|
|
1903
|
-
if (this.renderFn) {
|
|
1904
|
-
return await this.renderFn(context);
|
|
1905
|
-
}
|
|
1906
|
-
return void 0;
|
|
1907
|
-
}
|
|
1908
|
-
};
|
|
1909
|
-
|
|
1910
1906
|
// src/utils/path.ts
|
|
1911
1907
|
function modelNameToPath(modelName) {
|
|
1912
1908
|
return `/${modelName.replace(/([A-Z])/g, "-$1").toLowerCase()}`;
|
|
@@ -3528,8 +3524,7 @@ var HtmxAdminPlugin = class {
|
|
|
3528
3524
|
);
|
|
3529
3525
|
}
|
|
3530
3526
|
this.pages.set(page.modelName, page);
|
|
3531
|
-
|
|
3532
|
-
imeanServiceEngine.logger.info(`[HtmxAdminPlugin] Registered ${pageType}: ${page.modelName}`);
|
|
3527
|
+
imeanServiceEngine.logger.info(`[HtmxAdminPlugin] Registered page: ${page.modelName}`);
|
|
3533
3528
|
return this;
|
|
3534
3529
|
}
|
|
3535
3530
|
/**
|