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.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/features/base-feature.ts
299
- var BaseFeature = class {
300
- /** Feature 名称 */
301
- name;
302
- /** Feature 类型 */
303
- type;
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
- * 获取动态标题(默认实现:返回 PageMetadata title)
319
- * 子类可以覆盖此方法以提供动态标题
320
- */
321
- async getTitle(context) {
322
- const metadata = context.model.getMetadata();
323
- return metadata.title;
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
- * 获取动态描述(默认实现:返回 PageMetadata 的 description)
327
- * 子类可以覆盖此方法以提供动态描述
328
- */
329
- async getDescription(context) {
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
- async getActions(context) {
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(context, data) {
409
- if (!context.model.modelSchema?.schema) {
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 = context.model.modelSchema.schema._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, context.body);
480
- const validation = context.model.validate(data);
481
- if (!validation.success) {
482
- context.sendError("\u9A8C\u8BC1\u5931\u8D25", validation.error);
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, validation.data);
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 model = context.model;
522
- const fields = model.getFormFields();
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/default-create-feature.tsx
589
- var DefaultCreateFeature = class extends BaseFormFeature {
590
- createItem;
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: "create",
594
- type: "create",
595
- permission: options.permission || `${options.permissionPrefix}.create`,
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.createItem = options.createItem;
602
- }
603
- getFormAction() {
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 model = context.model;
716
- model.getMetadata();
717
- const detailFields = model.getDetailFields();
718
- const fields = detailFields.map((fieldName) => ({
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: model.getFieldLabel(fieldName)
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 Card(props) {
1221
+ function FilterForm(props) {
857
1222
  const {
858
- children,
859
- title,
860
- className = "",
861
- shadow = true,
862
- bordered = false,
863
- noPadding = false
1223
+ fields,
1224
+ listPath,
1225
+ currentFilters = {}
864
1226
  } = props;
865
- const baseClasses = "bg-white rounded-lg";
866
- const shadowClass = shadow ? "shadow-sm hover:shadow-md transition-shadow" : "";
867
- const borderClass = bordered ? "border border-gray-200" : "";
868
- const paddingClass = noPadding ? "" : "p-6";
869
- return /* @__PURE__ */ jsxRuntime.jsxs(
870
- "div",
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
- className: `${baseClasses} ${shadowClass} ${borderClass} ${className}`,
1240
+ method: "get",
1241
+ action: listPath,
1242
+ "hx-get": listPath,
1243
+ className: "space-y-4",
1244
+ "data-testid": "filter-form",
873
1245
  children: [
874
- 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 }) }),
875
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: noPadding ? "" : paddingClass, children })
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("div", { className: STYLES.header.actions, "data-testid": "table-header-actions", children: tableActions.map((action, idx) => {
1050
- const testId = `table-action-${action.label}`;
1051
- return /* @__PURE__ */ jsxRuntime.jsx(
1052
- Button,
1053
- {
1054
- variant: action.variant || "secondary",
1055
- href: action.href,
1056
- hxGet: action.hxGet,
1057
- hxPost: action.hxPost,
1058
- hxDelete: action.hxDelete,
1059
- hxConfirm: action.hxConfirm,
1060
- "data-testid": testId,
1061
- children: action.label
1062
- },
1063
- idx
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("div", { className: STYLES.actionLink.container, "data-testid": "table-actions", children: actions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsx(ActionLink, { action, item }, idx)) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: STYLES.actionButton, "data-testid": "table-actions", children: actions.map((action, idx) => {
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(Card, { shadow: true, noPadding: true, className: "overflow-hidden", "data-testid": "data-table", children: [
1197
- /* @__PURE__ */ jsxRuntime.jsx(TableHeader, { title, tableActions }),
1198
- items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(EmptyState, { message: "\u6682\u65E0\u6570\u636E" }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1199
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: STYLES.table.container, "data-testid": "table-container", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: STYLES.table.table, "data-testid": "table", role: "table", children: [
1200
- /* @__PURE__ */ jsxRuntime.jsx("thead", { className: STYLES.table.thead, "data-testid": "table-head", children: /* @__PURE__ */ jsxRuntime.jsx(
1201
- TableHeaderRow,
1202
- {
1203
- columns,
1204
- idColumn,
1205
- otherColumns,
1206
- hasActions
1207
- }
1208
- ) }),
1209
- /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: STYLES.table.tbody, "data-testid": "table-body", children: items.map((item, rowIndex) => /* @__PURE__ */ jsxRuntime.jsx(
1210
- TableRow,
1211
- {
1212
- item,
1213
- idColumn,
1214
- otherColumns,
1215
- actions,
1216
- actionStyle,
1217
- rowIndex
1218
- },
1219
- item.id
1220
- )) })
1221
- ] }) }),
1222
- pagination && /* @__PURE__ */ jsxRuntime.jsx(Pagination, { ...pagination })
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
- if (createPath) {
1272
- tableActions.push({
1273
- label: "\u65B0\u5EFA",
1274
- href: createPath,
1275
- hxGet: createPath,
1276
- variant: "primary"
1277
- });
1278
- }
1279
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-6", children: /* @__PURE__ */ jsxRuntime.jsx(
1280
- Table,
1281
- {
1282
- items: result.items,
1283
- columns: columns.map((col) => ({
1284
- key: col.key,
1285
- label: col.label,
1286
- render: col.render ? (value, item) => col.render(value, item) : void 0
1287
- })),
1288
- actions: actions.length > 0 ? actions.map((action) => ({
1289
- label: action.label,
1290
- href: (item) => action.href(item),
1291
- method: action.method
1292
- })) : void 0,
1293
- pagination: {
1294
- page: result.page,
1295
- pageSize: result.pageSize,
1296
- total: result.total,
1297
- totalPages: result.totalPages,
1298
- baseUrl: listPath,
1299
- currentParams
1300
- },
1301
- tableActions,
1302
- actionStyle: "link"
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
- const pageType = page.hasModel() ? "model" : "page";
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
  /**