imean-service-engine 2.2.0 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1499,7 +1499,7 @@ declare class PathHelper {
1499
1499
  detail(id: string | number, dialog?: boolean): string;
1500
1500
  /**
1501
1501
  * 编辑页动作
1502
- * 生成路径: {basePath}/{id}/edit
1502
+ * 生成路径: {basePath}/edit/{id}
1503
1503
  * @param dialog 是否在对话框中打开
1504
1504
  */
1505
1505
  edit(id: string | number, dialog?: boolean): string;
@@ -1767,13 +1767,11 @@ declare abstract class DetailPageModule<T = any> extends PageModule {
1767
1767
  * 可以重写 render 方法来自定义渲染,否则使用默认的表单渲染逻辑
1768
1768
  */
1769
1769
  declare abstract class FormPageModule<T = any> extends PageModule {
1770
- /** 数据源 */
1771
- protected datasource: FormDatasource<T>;
1772
1770
  /** ID 字段名(默认 "id") */
1773
1771
  protected idField: string;
1774
1772
  /** Zod Schema(可选,如果提供则自动生成表单字段和校验) */
1775
1773
  protected schema?: z.ZodObject<any>;
1776
- constructor(datasource: FormDatasource<T>, schema?: z.ZodObject<any>);
1774
+ constructor(schema?: z.ZodObject<any>);
1777
1775
  /**
1778
1776
  * 获取数据源(抽象方法,必须实现)
1779
1777
  * 子类必须实现此方法来提供数据源
@@ -1910,6 +1908,11 @@ declare abstract class ListPageModule<T extends {
1910
1908
  * 删除数据(可选,如果数据源支持删除)
1911
1909
  */
1912
1910
  deleteItem(id: string | number): Promise<boolean>;
1911
+ /**
1912
+ * 获取字段标签(可选)
1913
+ * 子类可以重写此方法来自定义字段的中文标签
1914
+ */
1915
+ getFieldLabel?(field: string): string;
1913
1916
  /**
1914
1917
  * 自定义列渲染(可选)
1915
1918
  * 子类可以重写此方法来自定义列的渲染逻辑
package/dist/index.d.ts CHANGED
@@ -1499,7 +1499,7 @@ declare class PathHelper {
1499
1499
  detail(id: string | number, dialog?: boolean): string;
1500
1500
  /**
1501
1501
  * 编辑页动作
1502
- * 生成路径: {basePath}/{id}/edit
1502
+ * 生成路径: {basePath}/edit/{id}
1503
1503
  * @param dialog 是否在对话框中打开
1504
1504
  */
1505
1505
  edit(id: string | number, dialog?: boolean): string;
@@ -1767,13 +1767,11 @@ declare abstract class DetailPageModule<T = any> extends PageModule {
1767
1767
  * 可以重写 render 方法来自定义渲染,否则使用默认的表单渲染逻辑
1768
1768
  */
1769
1769
  declare abstract class FormPageModule<T = any> extends PageModule {
1770
- /** 数据源 */
1771
- protected datasource: FormDatasource<T>;
1772
1770
  /** ID 字段名(默认 "id") */
1773
1771
  protected idField: string;
1774
1772
  /** Zod Schema(可选,如果提供则自动生成表单字段和校验) */
1775
1773
  protected schema?: z.ZodObject<any>;
1776
- constructor(datasource: FormDatasource<T>, schema?: z.ZodObject<any>);
1774
+ constructor(schema?: z.ZodObject<any>);
1777
1775
  /**
1778
1776
  * 获取数据源(抽象方法,必须实现)
1779
1777
  * 子类必须实现此方法来提供数据源
@@ -1910,6 +1908,11 @@ declare abstract class ListPageModule<T extends {
1910
1908
  * 删除数据(可选,如果数据源支持删除)
1911
1909
  */
1912
1910
  deleteItem(id: string | number): Promise<boolean>;
1911
+ /**
1912
+ * 获取字段标签(可选)
1913
+ * 子类可以重写此方法来自定义字段的中文标签
1914
+ */
1915
+ getFieldLabel?(field: string): string;
1913
1916
  /**
1914
1917
  * 自定义列渲染(可选)
1915
1918
  * 子类可以重写此方法来自定义列的渲染逻辑
package/dist/index.js CHANGED
@@ -3908,7 +3908,7 @@ var Button = (props) => {
3908
3908
  if (hxConfirm) htmxAttrs["hx-confirm"] = hxConfirm;
3909
3909
  if (hxHeaders) htmxAttrs["hx-headers"] = hxHeaders;
3910
3910
  const Tag = hxGet || hxPost || hxPut || hxDelete ? "a" : "button";
3911
- const href = hxGet || hxPost || hxPut || hxDelete ? rest.href !== void 0 ? rest.href : "#" : void 0;
3911
+ const href = rest.href ?? hxGet ?? "#";
3912
3912
  return /* @__PURE__ */ jsxRuntime.jsx(
3913
3913
  Tag,
3914
3914
  {
@@ -4090,11 +4090,11 @@ var PathHelper = class {
4090
4090
  }
4091
4091
  /**
4092
4092
  * 编辑页动作
4093
- * 生成路径: {basePath}/{id}/edit
4093
+ * 生成路径: {basePath}/edit/{id}
4094
4094
  * @param dialog 是否在对话框中打开
4095
4095
  */
4096
4096
  edit(id, dialog = false) {
4097
- const url = `${this.basePath}/${id}/edit`;
4097
+ const url = `${this.basePath}/edit/${id}`;
4098
4098
  return dialog ? `${url}?dialog=true` : url;
4099
4099
  }
4100
4100
  /**
@@ -4777,25 +4777,20 @@ function validateFormDataWithSchema(schema, data) {
4777
4777
  }
4778
4778
  }
4779
4779
  var FormPageModule = class extends PageModule {
4780
- /** 数据源 */
4781
- datasource;
4782
4780
  /** ID 字段名(默认 "id") */
4783
4781
  idField = "id";
4784
4782
  /** Zod Schema(可选,如果提供则自动生成表单字段和校验) */
4785
4783
  schema;
4786
- constructor(datasource, schema) {
4784
+ constructor(schema) {
4787
4785
  super();
4788
- this.datasource = datasource;
4789
4786
  this.schema = schema;
4790
4787
  }
4791
4788
  /**
4792
4789
  * 获取单条数据(编辑时使用)
4793
4790
  */
4794
4791
  async getItem(id) {
4795
- if (!this.datasource || !this.datasource.getItem) {
4796
- return null;
4797
- }
4798
- return await this.datasource.getItem(id);
4792
+ const datasource = this.getDatasource();
4793
+ return await datasource.getItem?.(id) || null;
4799
4794
  }
4800
4795
  /**
4801
4796
  * 获取字段标签(可选)
@@ -4938,7 +4933,9 @@ var FormPageModule = class extends PageModule {
4938
4933
  return await this.renderFormPage(adminContext, null, false, body);
4939
4934
  }
4940
4935
  try {
4941
- const newItem = await this.datasource?.createItem?.(body);
4936
+ const newItem = await this.getDatasource().createItem?.(
4937
+ body
4938
+ );
4942
4939
  const basePath = this.context.moduleMetadata.basePath;
4943
4940
  const hasDetail = this.context.moduleMetadata.hasDetail;
4944
4941
  const hasList = this.context.moduleMetadata.hasList;
@@ -5272,6 +5269,127 @@ var Pagination = (props) => {
5272
5269
  ] })
5273
5270
  ] });
5274
5271
  };
5272
+ var STYLES = {
5273
+ header: {
5274
+ container: "px-6 py-4 border-b border-gray-200 flex items-center justify-between",
5275
+ title: "text-lg font-semibold text-gray-900",
5276
+ actions: "flex gap-2"
5277
+ },
5278
+ table: {
5279
+ container: "overflow-x-auto",
5280
+ table: "min-w-full divide-y divide-gray-200",
5281
+ thead: "bg-gray-50",
5282
+ th: {
5283
+ base: "px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider whitespace-nowrap",
5284
+ stickyLeft: "sticky left-0 z-20 bg-gray-50 border-r border-gray-200 whitespace-nowrap",
5285
+ stickyRight: "sticky right-0 z-20 bg-gray-50 border-l border-gray-200 whitespace-nowrap"
5286
+ },
5287
+ tbody: "bg-white divide-y divide-gray-200",
5288
+ tr: "group hover:bg-gray-50 transition-colors",
5289
+ td: {
5290
+ base: "px-6 py-4 whitespace-nowrap text-sm text-gray-900",
5291
+ stickyLeft: "sticky left-0 z-10 bg-white group-hover:bg-gray-50 transition-colors border-r border-gray-200",
5292
+ stickyRight: "sticky right-0 z-10 bg-white group-hover:bg-gray-50 transition-colors border-l border-gray-200"
5293
+ }
5294
+ },
5295
+ actionLink: {
5296
+ container: "flex gap-3",
5297
+ base: "text-sm hover:underline",
5298
+ default: "text-blue-600 hover:text-blue-800",
5299
+ delete: "text-red-600 hover:text-red-800"
5300
+ },
5301
+ actionButton: "flex gap-2 flex-wrap"
5302
+ };
5303
+ function TableHeader(props) {
5304
+ const { title, tableActions } = props;
5305
+ const showHeader = title && title.trim() || tableActions && tableActions.length > 0;
5306
+ if (!showHeader) return null;
5307
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: STYLES.header.container, children: [
5308
+ title && title.trim() ? /* @__PURE__ */ jsxRuntime.jsx("h3", { className: STYLES.header.title, children: title }) : /* @__PURE__ */ jsxRuntime.jsx("div", {}),
5309
+ tableActions && tableActions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: STYLES.header.actions, children: tableActions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsx(
5310
+ Button,
5311
+ {
5312
+ variant: action.variant || "secondary",
5313
+ href: action.href,
5314
+ hxGet: action.hxGet,
5315
+ hxPost: action.hxPost,
5316
+ hxDelete: action.hxDelete,
5317
+ hxConfirm: action.hxConfirm,
5318
+ children: action.label
5319
+ },
5320
+ idx
5321
+ )) })
5322
+ ] });
5323
+ }
5324
+ function TableHeaderRow(props) {
5325
+ const { idColumn, otherColumns, hasActions } = props;
5326
+ return /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
5327
+ idColumn && /* @__PURE__ */ jsxRuntime.jsx("th", { className: `${STYLES.table.th.base} ${STYLES.table.th.stickyLeft}`, children: idColumn.label }),
5328
+ otherColumns.map((col) => /* @__PURE__ */ jsxRuntime.jsx("th", { className: STYLES.table.th.base, children: col.label }, col.key)),
5329
+ hasActions && /* @__PURE__ */ jsxRuntime.jsx(
5330
+ "th",
5331
+ {
5332
+ className: `${STYLES.table.th.base} ${STYLES.table.th.stickyRight}`,
5333
+ children: "\u64CD\u4F5C"
5334
+ }
5335
+ )
5336
+ ] });
5337
+ }
5338
+ function TableCell(props) {
5339
+ const { column, item, isSticky, stickyClass } = props;
5340
+ const value = item[column.key];
5341
+ const content = column.render ? safeRender(column.render(value, item)) : String(value || "");
5342
+ const className = isSticky ? `${STYLES.table.td.base} ${stickyClass || ""}` : STYLES.table.td.base;
5343
+ return /* @__PURE__ */ jsxRuntime.jsx("td", { className, children: content });
5344
+ }
5345
+ function ActionLink(props) {
5346
+ const { action, item } = props;
5347
+ const hrefValue = action.href(item);
5348
+ const isDelete = action.method === "delete";
5349
+ const className = `${STYLES.actionLink.base} ${isDelete ? STYLES.actionLink.delete : STYLES.actionLink.default}`;
5350
+ return /* @__PURE__ */ jsxRuntime.jsx(
5351
+ "a",
5352
+ {
5353
+ href: hrefValue,
5354
+ className,
5355
+ "hx-get": !isDelete ? hrefValue : void 0,
5356
+ "hx-delete": isDelete ? hrefValue : void 0,
5357
+ "hx-confirm": isDelete ? "\u786E\u5B9A\u5220\u9664\u5417\uFF1F" : void 0,
5358
+ children: action.label
5359
+ }
5360
+ );
5361
+ }
5362
+ function ActionCell(props) {
5363
+ const { actions, item, actionStyle } = props;
5364
+ if (!actions || actions.length === 0) return null;
5365
+ return /* @__PURE__ */ jsxRuntime.jsx("td", { className: `${STYLES.table.td.base} ${STYLES.table.td.stickyRight}`, children: actionStyle === "link" ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: STYLES.actionLink.container, children: actions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsx(ActionLink, { action, item }, idx)) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: STYLES.actionButton, children: actions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsx(
5366
+ ActionButton,
5367
+ {
5368
+ label: action.label,
5369
+ href: action.href,
5370
+ method: action.method,
5371
+ className: action.class,
5372
+ item
5373
+ },
5374
+ idx
5375
+ )) }) });
5376
+ }
5377
+ function TableRow(props) {
5378
+ const { item, idColumn, otherColumns, actions, actionStyle } = props;
5379
+ return /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: STYLES.table.tr, children: [
5380
+ idColumn && /* @__PURE__ */ jsxRuntime.jsx(
5381
+ TableCell,
5382
+ {
5383
+ column: idColumn,
5384
+ item,
5385
+ isSticky: true,
5386
+ stickyClass: STYLES.table.td.stickyLeft
5387
+ }
5388
+ ),
5389
+ otherColumns.map((col) => /* @__PURE__ */ jsxRuntime.jsx(TableCell, { column: col, item }, col.key)),
5390
+ actions && actions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(ActionCell, { actions, item, actionStyle })
5391
+ ] });
5392
+ }
5275
5393
  var Table = (props) => {
5276
5394
  const {
5277
5395
  items,
@@ -5284,119 +5402,28 @@ var Table = (props) => {
5284
5402
  } = props;
5285
5403
  const idColumn = columns.find((col) => col.key === "id");
5286
5404
  const otherColumns = columns.filter((col) => col.key !== "id");
5287
- const idColumnWidth = 80;
5288
- const actionsColumnMinWidth = actions && actions.length > 0 ? actions.length * 30 + 48 + (actions.length - 1) * 8 : 0;
5289
- const showHeader = title && title.trim() || tableActions && tableActions.length > 0;
5290
- const hasTitle = title && title.trim();
5405
+ const hasActions = Boolean(actions && actions.length > 0);
5291
5406
  return /* @__PURE__ */ jsxRuntime.jsxs(Card, { shadow: true, noPadding: true, className: "overflow-hidden", children: [
5292
- showHeader && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 border-b border-gray-200 flex items-center justify-between", children: [
5293
- hasTitle ? /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title }) : /* @__PURE__ */ jsxRuntime.jsx("div", {}),
5294
- tableActions && tableActions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-2", children: tableActions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsx(
5295
- Button,
5296
- {
5297
- variant: action.variant || "secondary",
5298
- href: action.href,
5299
- hxGet: action.hxGet,
5300
- hxPost: action.hxPost,
5301
- hxDelete: action.hxDelete,
5302
- hxConfirm: action.hxConfirm,
5303
- children: action.label
5304
- },
5305
- idx
5306
- )) })
5307
- ] }),
5407
+ /* @__PURE__ */ jsxRuntime.jsx(TableHeader, { title, tableActions }),
5308
5408
  items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(EmptyState, { message: "\u6682\u65E0\u6570\u636E" }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5309
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "min-w-full divide-y divide-gray-200", children: [
5310
- /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "bg-gray-50", children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
5311
- idColumn && /* @__PURE__ */ jsxRuntime.jsx(
5312
- "th",
5313
- {
5314
- className: "px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider sticky left-0 z-20 bg-gray-50 border-r border-gray-200",
5315
- style: {
5316
- minWidth: `${idColumnWidth}px`,
5317
- maxWidth: `${idColumnWidth}px`
5318
- },
5319
- children: idColumn.label
5320
- }
5321
- ),
5322
- otherColumns.map((col) => /* @__PURE__ */ jsxRuntime.jsx(
5323
- "th",
5324
- {
5325
- className: "px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider",
5326
- children: col.label
5327
- },
5328
- col.key
5329
- )),
5330
- actions && actions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
5331
- "th",
5332
- {
5333
- className: "px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider sticky right-0 z-20 bg-gray-50 border-l border-gray-200 whitespace-nowrap",
5334
- style: { minWidth: `${actionsColumnMinWidth}px` },
5335
- children: "\u64CD\u4F5C"
5336
- }
5337
- )
5338
- ] }) }),
5339
- /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: "bg-white divide-y divide-gray-200", children: items.map((item, rowIndex) => /* @__PURE__ */ jsxRuntime.jsxs(
5340
- "tr",
5409
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: STYLES.table.container, children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: STYLES.table.table, children: [
5410
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { className: STYLES.table.thead, children: /* @__PURE__ */ jsxRuntime.jsx(
5411
+ TableHeaderRow,
5341
5412
  {
5342
- className: "group hover:bg-gray-50 transition-colors",
5343
- children: [
5344
- idColumn && /* @__PURE__ */ jsxRuntime.jsx(
5345
- "td",
5346
- {
5347
- className: "px-6 py-4 whitespace-nowrap text-sm text-gray-900 sticky left-0 z-10 bg-white group-hover:bg-gray-50 transition-colors border-r border-gray-200",
5348
- style: {
5349
- minWidth: `${idColumnWidth}px`,
5350
- maxWidth: `${idColumnWidth}px`
5351
- },
5352
- children: idColumn.render ? safeRender(
5353
- idColumn.render(item[idColumn.key], item)
5354
- ) : String(item[idColumn.key] || "")
5355
- }
5356
- ),
5357
- otherColumns.map((col) => /* @__PURE__ */ jsxRuntime.jsx(
5358
- "td",
5359
- {
5360
- className: "px-6 py-4 whitespace-nowrap text-sm text-gray-900",
5361
- children: col.render ? safeRender(col.render(item[col.key], item)) : String(item[col.key] || "")
5362
- },
5363
- col.key
5364
- )),
5365
- actions && actions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
5366
- "td",
5367
- {
5368
- className: "px-6 py-4 whitespace-nowrap text-sm sticky right-0 z-10 bg-white group-hover:bg-gray-50 transition-colors border-l border-gray-200",
5369
- style: { minWidth: `${actionsColumnMinWidth}px` },
5370
- children: actionStyle === "link" ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-3", children: actions.map((action, idx) => {
5371
- const hrefValue = typeof action.href === "function" ? action.href(item) : action.href;
5372
- const isDelete = action.method === "delete";
5373
- action.dialog || hrefValue.includes("dialog=true");
5374
- return /* @__PURE__ */ jsxRuntime.jsx(
5375
- "a",
5376
- {
5377
- href: hrefValue,
5378
- className: `text-sm ${isDelete ? "text-red-600 hover:text-red-800" : "text-blue-600 hover:text-blue-800"} hover:underline`,
5379
- "hx-get": !isDelete ? hrefValue : void 0,
5380
- "hx-delete": isDelete ? hrefValue : void 0,
5381
- "hx-confirm": isDelete ? "\u786E\u5B9A\u5220\u9664\u5417\uFF1F" : void 0,
5382
- children: action.label
5383
- },
5384
- idx
5385
- );
5386
- }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-2 flex-wrap", children: actions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsx(
5387
- ActionButton,
5388
- {
5389
- label: action.label,
5390
- href: action.href,
5391
- method: action.method,
5392
- className: action.class,
5393
- item
5394
- },
5395
- idx
5396
- )) })
5397
- }
5398
- )
5399
- ]
5413
+ columns,
5414
+ idColumn,
5415
+ otherColumns,
5416
+ hasActions
5417
+ }
5418
+ ) }),
5419
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { className: STYLES.table.tbody, children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
5420
+ TableRow,
5421
+ {
5422
+ item,
5423
+ idColumn,
5424
+ otherColumns,
5425
+ actions,
5426
+ actionStyle
5400
5427
  },
5401
5428
  item.id
5402
5429
  )) })
@@ -5528,6 +5555,13 @@ var ListPageModule = class extends PageModule {
5528
5555
  }
5529
5556
  return false;
5530
5557
  }
5558
+ /**
5559
+ * 获取字段标签(可选)
5560
+ * 子类可以重写此方法来自定义字段的中文标签
5561
+ */
5562
+ getFieldLabel(field) {
5563
+ return field;
5564
+ }
5531
5565
  /**
5532
5566
  * 自定义列渲染(可选)
5533
5567
  * 子类可以重写此方法来自定义列的渲染逻辑
@@ -5607,7 +5641,7 @@ var ListPageModule = class extends PageModule {
5607
5641
  const tableTitle = this.getTableTitle ? this.getTableTitle() : pageTitle;
5608
5642
  const columns = result.items.length > 0 ? Object.keys(result.items[0]).map((key) => ({
5609
5643
  key,
5610
- label: key,
5644
+ label: this.getFieldLabel ? this.getFieldLabel(key) : key,
5611
5645
  render: this.renderColumn ? (value, item) => this.renderColumn(key, value, item) : void 0
5612
5646
  })) : [];
5613
5647
  const actions = this.getActions && result.items.length > 0 ? this.getActions(result.items[0]) : [];
@@ -6610,6 +6644,9 @@ var HtmxAdminPlugin = class {
6610
6644
  * 检查并注册模块
6611
6645
  */
6612
6646
  checkAndRegisterModules(modules) {
6647
+ modules = modules.filter(
6648
+ (module) => module.clazz instanceof PageModule || module.clazz.prototype instanceof PageModule
6649
+ );
6613
6650
  for (const module of modules) {
6614
6651
  const options = module.options;
6615
6652
  const moduleClass = module.clazz;
@@ -6688,31 +6725,25 @@ var HtmxAdminPlugin = class {
6688
6725
  description: moduleInfo.options.description ?? ""
6689
6726
  }
6690
6727
  });
6691
- if (type === "list") {
6692
- this.hono.get(`${basePath}/list`, async (ctx) => {
6693
- return routeHandler.handle(ctx);
6694
- });
6695
- } else if (type === "detail") {
6696
- this.hono.get(`${basePath}/detail/:id`, async (ctx) => {
6697
- return routeHandler.handle(ctx);
6698
- });
6699
- } else if (type === "form") {
6700
- this.hono.get(`${basePath}/new`, async (ctx) => {
6701
- return routeHandler.handle(ctx);
6702
- });
6703
- this.hono.post(basePath, async (ctx) => {
6704
- return routeHandler.handle(ctx);
6705
- });
6706
- this.hono.put(`${basePath}/:id`, async (ctx) => {
6707
- return routeHandler.handle(ctx);
6708
- });
6709
- this.hono.delete(`${basePath}/:id`, async (ctx) => {
6710
- return routeHandler.handle(ctx);
6711
- });
6712
- } else if (type === "custom") {
6713
- this.hono.get(basePath, async (ctx) => {
6714
- return routeHandler.handle(ctx);
6715
- });
6728
+ const routeConfig = {
6729
+ list: [{ method: "get", path: `${basePath}/list` }],
6730
+ detail: [{ method: "get", path: `${basePath}/detail/:id` }],
6731
+ form: [
6732
+ { method: "get", path: `${basePath}/new` },
6733
+ { method: "get", path: `${basePath}/edit/:id` },
6734
+ { method: "post", path: basePath },
6735
+ { method: "put", path: `${basePath}/:id` },
6736
+ { method: "delete", path: `${basePath}/:id` }
6737
+ ],
6738
+ custom: [{ method: "get", path: basePath }]
6739
+ };
6740
+ const routes = routeConfig[type];
6741
+ for (const route of routes) {
6742
+ logger_default.info(
6743
+ `[HtmxAdminPlugin] Registering ${type} route: ${route.method.toUpperCase()} ${route.path}`
6744
+ );
6745
+ const handler = async (ctx) => routeHandler.handle(ctx);
6746
+ this.hono[route.method](route.path, handler);
6716
6747
  }
6717
6748
  }
6718
6749
  }
package/dist/index.mjs CHANGED
@@ -3883,7 +3883,7 @@ var Button = (props) => {
3883
3883
  if (hxConfirm) htmxAttrs["hx-confirm"] = hxConfirm;
3884
3884
  if (hxHeaders) htmxAttrs["hx-headers"] = hxHeaders;
3885
3885
  const Tag = hxGet || hxPost || hxPut || hxDelete ? "a" : "button";
3886
- const href = hxGet || hxPost || hxPut || hxDelete ? rest.href !== void 0 ? rest.href : "#" : void 0;
3886
+ const href = rest.href ?? hxGet ?? "#";
3887
3887
  return /* @__PURE__ */ jsx(
3888
3888
  Tag,
3889
3889
  {
@@ -4065,11 +4065,11 @@ var PathHelper = class {
4065
4065
  }
4066
4066
  /**
4067
4067
  * 编辑页动作
4068
- * 生成路径: {basePath}/{id}/edit
4068
+ * 生成路径: {basePath}/edit/{id}
4069
4069
  * @param dialog 是否在对话框中打开
4070
4070
  */
4071
4071
  edit(id, dialog = false) {
4072
- const url = `${this.basePath}/${id}/edit`;
4072
+ const url = `${this.basePath}/edit/${id}`;
4073
4073
  return dialog ? `${url}?dialog=true` : url;
4074
4074
  }
4075
4075
  /**
@@ -4752,25 +4752,20 @@ function validateFormDataWithSchema(schema, data) {
4752
4752
  }
4753
4753
  }
4754
4754
  var FormPageModule = class extends PageModule {
4755
- /** 数据源 */
4756
- datasource;
4757
4755
  /** ID 字段名(默认 "id") */
4758
4756
  idField = "id";
4759
4757
  /** Zod Schema(可选,如果提供则自动生成表单字段和校验) */
4760
4758
  schema;
4761
- constructor(datasource, schema) {
4759
+ constructor(schema) {
4762
4760
  super();
4763
- this.datasource = datasource;
4764
4761
  this.schema = schema;
4765
4762
  }
4766
4763
  /**
4767
4764
  * 获取单条数据(编辑时使用)
4768
4765
  */
4769
4766
  async getItem(id) {
4770
- if (!this.datasource || !this.datasource.getItem) {
4771
- return null;
4772
- }
4773
- return await this.datasource.getItem(id);
4767
+ const datasource = this.getDatasource();
4768
+ return await datasource.getItem?.(id) || null;
4774
4769
  }
4775
4770
  /**
4776
4771
  * 获取字段标签(可选)
@@ -4913,7 +4908,9 @@ var FormPageModule = class extends PageModule {
4913
4908
  return await this.renderFormPage(adminContext, null, false, body);
4914
4909
  }
4915
4910
  try {
4916
- const newItem = await this.datasource?.createItem?.(body);
4911
+ const newItem = await this.getDatasource().createItem?.(
4912
+ body
4913
+ );
4917
4914
  const basePath = this.context.moduleMetadata.basePath;
4918
4915
  const hasDetail = this.context.moduleMetadata.hasDetail;
4919
4916
  const hasList = this.context.moduleMetadata.hasList;
@@ -5247,6 +5244,127 @@ var Pagination = (props) => {
5247
5244
  ] })
5248
5245
  ] });
5249
5246
  };
5247
+ var STYLES = {
5248
+ header: {
5249
+ container: "px-6 py-4 border-b border-gray-200 flex items-center justify-between",
5250
+ title: "text-lg font-semibold text-gray-900",
5251
+ actions: "flex gap-2"
5252
+ },
5253
+ table: {
5254
+ container: "overflow-x-auto",
5255
+ table: "min-w-full divide-y divide-gray-200",
5256
+ thead: "bg-gray-50",
5257
+ th: {
5258
+ base: "px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider whitespace-nowrap",
5259
+ stickyLeft: "sticky left-0 z-20 bg-gray-50 border-r border-gray-200 whitespace-nowrap",
5260
+ stickyRight: "sticky right-0 z-20 bg-gray-50 border-l border-gray-200 whitespace-nowrap"
5261
+ },
5262
+ tbody: "bg-white divide-y divide-gray-200",
5263
+ tr: "group hover:bg-gray-50 transition-colors",
5264
+ td: {
5265
+ base: "px-6 py-4 whitespace-nowrap text-sm text-gray-900",
5266
+ stickyLeft: "sticky left-0 z-10 bg-white group-hover:bg-gray-50 transition-colors border-r border-gray-200",
5267
+ stickyRight: "sticky right-0 z-10 bg-white group-hover:bg-gray-50 transition-colors border-l border-gray-200"
5268
+ }
5269
+ },
5270
+ actionLink: {
5271
+ container: "flex gap-3",
5272
+ base: "text-sm hover:underline",
5273
+ default: "text-blue-600 hover:text-blue-800",
5274
+ delete: "text-red-600 hover:text-red-800"
5275
+ },
5276
+ actionButton: "flex gap-2 flex-wrap"
5277
+ };
5278
+ function TableHeader(props) {
5279
+ const { title, tableActions } = props;
5280
+ const showHeader = title && title.trim() || tableActions && tableActions.length > 0;
5281
+ if (!showHeader) return null;
5282
+ return /* @__PURE__ */ jsxs("div", { className: STYLES.header.container, children: [
5283
+ title && title.trim() ? /* @__PURE__ */ jsx("h3", { className: STYLES.header.title, children: title }) : /* @__PURE__ */ jsx("div", {}),
5284
+ tableActions && tableActions.length > 0 && /* @__PURE__ */ jsx("div", { className: STYLES.header.actions, children: tableActions.map((action, idx) => /* @__PURE__ */ jsx(
5285
+ Button,
5286
+ {
5287
+ variant: action.variant || "secondary",
5288
+ href: action.href,
5289
+ hxGet: action.hxGet,
5290
+ hxPost: action.hxPost,
5291
+ hxDelete: action.hxDelete,
5292
+ hxConfirm: action.hxConfirm,
5293
+ children: action.label
5294
+ },
5295
+ idx
5296
+ )) })
5297
+ ] });
5298
+ }
5299
+ function TableHeaderRow(props) {
5300
+ const { idColumn, otherColumns, hasActions } = props;
5301
+ return /* @__PURE__ */ jsxs("tr", { children: [
5302
+ idColumn && /* @__PURE__ */ jsx("th", { className: `${STYLES.table.th.base} ${STYLES.table.th.stickyLeft}`, children: idColumn.label }),
5303
+ otherColumns.map((col) => /* @__PURE__ */ jsx("th", { className: STYLES.table.th.base, children: col.label }, col.key)),
5304
+ hasActions && /* @__PURE__ */ jsx(
5305
+ "th",
5306
+ {
5307
+ className: `${STYLES.table.th.base} ${STYLES.table.th.stickyRight}`,
5308
+ children: "\u64CD\u4F5C"
5309
+ }
5310
+ )
5311
+ ] });
5312
+ }
5313
+ function TableCell(props) {
5314
+ const { column, item, isSticky, stickyClass } = props;
5315
+ const value = item[column.key];
5316
+ const content = column.render ? safeRender(column.render(value, item)) : String(value || "");
5317
+ const className = isSticky ? `${STYLES.table.td.base} ${stickyClass || ""}` : STYLES.table.td.base;
5318
+ return /* @__PURE__ */ jsx("td", { className, children: content });
5319
+ }
5320
+ function ActionLink(props) {
5321
+ const { action, item } = props;
5322
+ const hrefValue = action.href(item);
5323
+ const isDelete = action.method === "delete";
5324
+ const className = `${STYLES.actionLink.base} ${isDelete ? STYLES.actionLink.delete : STYLES.actionLink.default}`;
5325
+ return /* @__PURE__ */ jsx(
5326
+ "a",
5327
+ {
5328
+ href: hrefValue,
5329
+ className,
5330
+ "hx-get": !isDelete ? hrefValue : void 0,
5331
+ "hx-delete": isDelete ? hrefValue : void 0,
5332
+ "hx-confirm": isDelete ? "\u786E\u5B9A\u5220\u9664\u5417\uFF1F" : void 0,
5333
+ children: action.label
5334
+ }
5335
+ );
5336
+ }
5337
+ function ActionCell(props) {
5338
+ const { actions, item, actionStyle } = props;
5339
+ if (!actions || actions.length === 0) return null;
5340
+ return /* @__PURE__ */ jsx("td", { className: `${STYLES.table.td.base} ${STYLES.table.td.stickyRight}`, children: actionStyle === "link" ? /* @__PURE__ */ jsx("div", { className: STYLES.actionLink.container, children: actions.map((action, idx) => /* @__PURE__ */ jsx(ActionLink, { action, item }, idx)) }) : /* @__PURE__ */ jsx("div", { className: STYLES.actionButton, children: actions.map((action, idx) => /* @__PURE__ */ jsx(
5341
+ ActionButton,
5342
+ {
5343
+ label: action.label,
5344
+ href: action.href,
5345
+ method: action.method,
5346
+ className: action.class,
5347
+ item
5348
+ },
5349
+ idx
5350
+ )) }) });
5351
+ }
5352
+ function TableRow(props) {
5353
+ const { item, idColumn, otherColumns, actions, actionStyle } = props;
5354
+ return /* @__PURE__ */ jsxs("tr", { className: STYLES.table.tr, children: [
5355
+ idColumn && /* @__PURE__ */ jsx(
5356
+ TableCell,
5357
+ {
5358
+ column: idColumn,
5359
+ item,
5360
+ isSticky: true,
5361
+ stickyClass: STYLES.table.td.stickyLeft
5362
+ }
5363
+ ),
5364
+ otherColumns.map((col) => /* @__PURE__ */ jsx(TableCell, { column: col, item }, col.key)),
5365
+ actions && actions.length > 0 && /* @__PURE__ */ jsx(ActionCell, { actions, item, actionStyle })
5366
+ ] });
5367
+ }
5250
5368
  var Table = (props) => {
5251
5369
  const {
5252
5370
  items,
@@ -5259,119 +5377,28 @@ var Table = (props) => {
5259
5377
  } = props;
5260
5378
  const idColumn = columns.find((col) => col.key === "id");
5261
5379
  const otherColumns = columns.filter((col) => col.key !== "id");
5262
- const idColumnWidth = 80;
5263
- const actionsColumnMinWidth = actions && actions.length > 0 ? actions.length * 30 + 48 + (actions.length - 1) * 8 : 0;
5264
- const showHeader = title && title.trim() || tableActions && tableActions.length > 0;
5265
- const hasTitle = title && title.trim();
5380
+ const hasActions = Boolean(actions && actions.length > 0);
5266
5381
  return /* @__PURE__ */ jsxs(Card, { shadow: true, noPadding: true, className: "overflow-hidden", children: [
5267
- showHeader && /* @__PURE__ */ jsxs("div", { className: "px-6 py-4 border-b border-gray-200 flex items-center justify-between", children: [
5268
- hasTitle ? /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title }) : /* @__PURE__ */ jsx("div", {}),
5269
- tableActions && tableActions.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex gap-2", children: tableActions.map((action, idx) => /* @__PURE__ */ jsx(
5270
- Button,
5271
- {
5272
- variant: action.variant || "secondary",
5273
- href: action.href,
5274
- hxGet: action.hxGet,
5275
- hxPost: action.hxPost,
5276
- hxDelete: action.hxDelete,
5277
- hxConfirm: action.hxConfirm,
5278
- children: action.label
5279
- },
5280
- idx
5281
- )) })
5282
- ] }),
5382
+ /* @__PURE__ */ jsx(TableHeader, { title, tableActions }),
5283
5383
  items.length === 0 ? /* @__PURE__ */ jsx(EmptyState, { message: "\u6682\u65E0\u6570\u636E" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
5284
- /* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: "min-w-full divide-y divide-gray-200", children: [
5285
- /* @__PURE__ */ jsx("thead", { className: "bg-gray-50", children: /* @__PURE__ */ jsxs("tr", { children: [
5286
- idColumn && /* @__PURE__ */ jsx(
5287
- "th",
5288
- {
5289
- className: "px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider sticky left-0 z-20 bg-gray-50 border-r border-gray-200",
5290
- style: {
5291
- minWidth: `${idColumnWidth}px`,
5292
- maxWidth: `${idColumnWidth}px`
5293
- },
5294
- children: idColumn.label
5295
- }
5296
- ),
5297
- otherColumns.map((col) => /* @__PURE__ */ jsx(
5298
- "th",
5299
- {
5300
- className: "px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider",
5301
- children: col.label
5302
- },
5303
- col.key
5304
- )),
5305
- actions && actions.length > 0 && /* @__PURE__ */ jsx(
5306
- "th",
5307
- {
5308
- className: "px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider sticky right-0 z-20 bg-gray-50 border-l border-gray-200 whitespace-nowrap",
5309
- style: { minWidth: `${actionsColumnMinWidth}px` },
5310
- children: "\u64CD\u4F5C"
5311
- }
5312
- )
5313
- ] }) }),
5314
- /* @__PURE__ */ jsx("tbody", { className: "bg-white divide-y divide-gray-200", children: items.map((item, rowIndex) => /* @__PURE__ */ jsxs(
5315
- "tr",
5384
+ /* @__PURE__ */ jsx("div", { className: STYLES.table.container, children: /* @__PURE__ */ jsxs("table", { className: STYLES.table.table, children: [
5385
+ /* @__PURE__ */ jsx("thead", { className: STYLES.table.thead, children: /* @__PURE__ */ jsx(
5386
+ TableHeaderRow,
5316
5387
  {
5317
- className: "group hover:bg-gray-50 transition-colors",
5318
- children: [
5319
- idColumn && /* @__PURE__ */ jsx(
5320
- "td",
5321
- {
5322
- className: "px-6 py-4 whitespace-nowrap text-sm text-gray-900 sticky left-0 z-10 bg-white group-hover:bg-gray-50 transition-colors border-r border-gray-200",
5323
- style: {
5324
- minWidth: `${idColumnWidth}px`,
5325
- maxWidth: `${idColumnWidth}px`
5326
- },
5327
- children: idColumn.render ? safeRender(
5328
- idColumn.render(item[idColumn.key], item)
5329
- ) : String(item[idColumn.key] || "")
5330
- }
5331
- ),
5332
- otherColumns.map((col) => /* @__PURE__ */ jsx(
5333
- "td",
5334
- {
5335
- className: "px-6 py-4 whitespace-nowrap text-sm text-gray-900",
5336
- children: col.render ? safeRender(col.render(item[col.key], item)) : String(item[col.key] || "")
5337
- },
5338
- col.key
5339
- )),
5340
- actions && actions.length > 0 && /* @__PURE__ */ jsx(
5341
- "td",
5342
- {
5343
- className: "px-6 py-4 whitespace-nowrap text-sm sticky right-0 z-10 bg-white group-hover:bg-gray-50 transition-colors border-l border-gray-200",
5344
- style: { minWidth: `${actionsColumnMinWidth}px` },
5345
- children: actionStyle === "link" ? /* @__PURE__ */ jsx("div", { className: "flex gap-3", children: actions.map((action, idx) => {
5346
- const hrefValue = typeof action.href === "function" ? action.href(item) : action.href;
5347
- const isDelete = action.method === "delete";
5348
- action.dialog || hrefValue.includes("dialog=true");
5349
- return /* @__PURE__ */ jsx(
5350
- "a",
5351
- {
5352
- href: hrefValue,
5353
- className: `text-sm ${isDelete ? "text-red-600 hover:text-red-800" : "text-blue-600 hover:text-blue-800"} hover:underline`,
5354
- "hx-get": !isDelete ? hrefValue : void 0,
5355
- "hx-delete": isDelete ? hrefValue : void 0,
5356
- "hx-confirm": isDelete ? "\u786E\u5B9A\u5220\u9664\u5417\uFF1F" : void 0,
5357
- children: action.label
5358
- },
5359
- idx
5360
- );
5361
- }) }) : /* @__PURE__ */ jsx("div", { className: "flex gap-2 flex-wrap", children: actions.map((action, idx) => /* @__PURE__ */ jsx(
5362
- ActionButton,
5363
- {
5364
- label: action.label,
5365
- href: action.href,
5366
- method: action.method,
5367
- className: action.class,
5368
- item
5369
- },
5370
- idx
5371
- )) })
5372
- }
5373
- )
5374
- ]
5388
+ columns,
5389
+ idColumn,
5390
+ otherColumns,
5391
+ hasActions
5392
+ }
5393
+ ) }),
5394
+ /* @__PURE__ */ jsx("tbody", { className: STYLES.table.tbody, children: items.map((item) => /* @__PURE__ */ jsx(
5395
+ TableRow,
5396
+ {
5397
+ item,
5398
+ idColumn,
5399
+ otherColumns,
5400
+ actions,
5401
+ actionStyle
5375
5402
  },
5376
5403
  item.id
5377
5404
  )) })
@@ -5503,6 +5530,13 @@ var ListPageModule = class extends PageModule {
5503
5530
  }
5504
5531
  return false;
5505
5532
  }
5533
+ /**
5534
+ * 获取字段标签(可选)
5535
+ * 子类可以重写此方法来自定义字段的中文标签
5536
+ */
5537
+ getFieldLabel(field) {
5538
+ return field;
5539
+ }
5506
5540
  /**
5507
5541
  * 自定义列渲染(可选)
5508
5542
  * 子类可以重写此方法来自定义列的渲染逻辑
@@ -5582,7 +5616,7 @@ var ListPageModule = class extends PageModule {
5582
5616
  const tableTitle = this.getTableTitle ? this.getTableTitle() : pageTitle;
5583
5617
  const columns = result.items.length > 0 ? Object.keys(result.items[0]).map((key) => ({
5584
5618
  key,
5585
- label: key,
5619
+ label: this.getFieldLabel ? this.getFieldLabel(key) : key,
5586
5620
  render: this.renderColumn ? (value, item) => this.renderColumn(key, value, item) : void 0
5587
5621
  })) : [];
5588
5622
  const actions = this.getActions && result.items.length > 0 ? this.getActions(result.items[0]) : [];
@@ -6585,6 +6619,9 @@ var HtmxAdminPlugin = class {
6585
6619
  * 检查并注册模块
6586
6620
  */
6587
6621
  checkAndRegisterModules(modules) {
6622
+ modules = modules.filter(
6623
+ (module) => module.clazz instanceof PageModule || module.clazz.prototype instanceof PageModule
6624
+ );
6588
6625
  for (const module of modules) {
6589
6626
  const options = module.options;
6590
6627
  const moduleClass = module.clazz;
@@ -6663,31 +6700,25 @@ var HtmxAdminPlugin = class {
6663
6700
  description: moduleInfo.options.description ?? ""
6664
6701
  }
6665
6702
  });
6666
- if (type === "list") {
6667
- this.hono.get(`${basePath}/list`, async (ctx) => {
6668
- return routeHandler.handle(ctx);
6669
- });
6670
- } else if (type === "detail") {
6671
- this.hono.get(`${basePath}/detail/:id`, async (ctx) => {
6672
- return routeHandler.handle(ctx);
6673
- });
6674
- } else if (type === "form") {
6675
- this.hono.get(`${basePath}/new`, async (ctx) => {
6676
- return routeHandler.handle(ctx);
6677
- });
6678
- this.hono.post(basePath, async (ctx) => {
6679
- return routeHandler.handle(ctx);
6680
- });
6681
- this.hono.put(`${basePath}/:id`, async (ctx) => {
6682
- return routeHandler.handle(ctx);
6683
- });
6684
- this.hono.delete(`${basePath}/:id`, async (ctx) => {
6685
- return routeHandler.handle(ctx);
6686
- });
6687
- } else if (type === "custom") {
6688
- this.hono.get(basePath, async (ctx) => {
6689
- return routeHandler.handle(ctx);
6690
- });
6703
+ const routeConfig = {
6704
+ list: [{ method: "get", path: `${basePath}/list` }],
6705
+ detail: [{ method: "get", path: `${basePath}/detail/:id` }],
6706
+ form: [
6707
+ { method: "get", path: `${basePath}/new` },
6708
+ { method: "get", path: `${basePath}/edit/:id` },
6709
+ { method: "post", path: basePath },
6710
+ { method: "put", path: `${basePath}/:id` },
6711
+ { method: "delete", path: `${basePath}/:id` }
6712
+ ],
6713
+ custom: [{ method: "get", path: basePath }]
6714
+ };
6715
+ const routes = routeConfig[type];
6716
+ for (const route of routes) {
6717
+ logger_default.info(
6718
+ `[HtmxAdminPlugin] Registering ${type} route: ${route.method.toUpperCase()} ${route.path}`
6719
+ );
6720
+ const handler = async (ctx) => routeHandler.handle(ctx);
6721
+ this.hono[route.method](route.path, handler);
6691
6722
  }
6692
6723
  }
6693
6724
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imean-service-engine",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "基于Hono的轻量级微服务引擎框架",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",