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