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