@kine-design/crud 0.0.1-beta.23 → 0.0.1-beta.25

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.
Files changed (37) hide show
  1. package/.vlaude/last-session-id +1 -1
  2. package/components/crudPage/KCrudPage.tsx +12 -5
  3. package/components/editableTable/KEditableTable.tsx +11 -9
  4. package/components/formPage/KApprovalDialog.tsx +24 -23
  5. package/components/formPage/KFormPage.tsx +10 -5
  6. package/components/formPage/KMasterDetailPage.tsx +8 -6
  7. package/components/layout/KSider.tsx +4 -1
  8. package/components/login/KLoginPage.tsx +8 -6
  9. package/components/searchTable/KSearchTable.tsx +5 -2
  10. package/components/upload/KFileList.tsx +16 -8
  11. package/components/upload/KImageUpload.tsx +4 -2
  12. package/components/upload/KUpload.tsx +4 -2
  13. package/composables/error/dispatchError.ts +9 -7
  14. package/composables/form/renderFormField.tsx +6 -4
  15. package/composables/form/useFormPage.ts +3 -1
  16. package/composables/page/__tests__/useAutoPageSize.test.ts +194 -0
  17. package/composables/page/index.ts +2 -0
  18. package/composables/page/types.ts +7 -0
  19. package/composables/page/useAutoPageSize.ts +55 -0
  20. package/composables/page/useCrudPage.ts +18 -3
  21. package/composables/request/requestBuilder.ts +8 -5
  22. package/composables/request/transport/xhrTransport.ts +3 -1
  23. package/composables/request/types.ts +8 -5
  24. package/composables/request/upload.ts +4 -2
  25. package/composables/router/defineCrudRoutes.ts +5 -3
  26. package/dist/components/editableTable/KEditableTable.d.ts +5 -5
  27. package/dist/composables/page/__tests__/useAutoPageSize.test.d.ts +1 -0
  28. package/dist/composables/page/index.d.ts +2 -0
  29. package/dist/composables/page/types.d.ts +10 -0
  30. package/dist/composables/page/useAutoPageSize.d.ts +10 -0
  31. package/dist/composables/page/useCrudPage.d.ts +7 -6
  32. package/dist/crud.js +1240 -699
  33. package/dist/vitest.config.d.ts +10 -0
  34. package/package.json +8 -4
  35. package/setup.ts +11 -12
  36. package/tsconfig.json +12 -12
  37. package/vitest.config.ts +17 -0
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { ref, reactive, computed, onMounted, type Ref } from 'vue';
14
14
  import { useRoute, useRouter } from 'vue-router';
15
+ import { getActiveLocaleMessages } from '@kine-design/core';
15
16
  import { useRequestClient } from '../../setup';
16
17
  import type { FormFieldConfig } from './types';
17
18
 
@@ -114,7 +115,8 @@ export function useFormPage(options: UseFormPageOptions): UseFormPageReturn {
114
115
  // 必填校验
115
116
  if (field.required) {
116
117
  if (value === undefined || value === null || value === '') {
117
- const msg = `请输入${field.label}`;
118
+ const t = getActiveLocaleMessages();
119
+ const msg = t.form.inputPlaceholder(field.label);
118
120
  errors.value[param] = msg;
119
121
  return msg;
120
122
  }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * @description useAutoPageSize 单元测试
3
+ * @author 阿怪
4
+ * @date 2026/5/7
5
+ * @version v1.0.0
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
10
+ import { ref } from 'vue';
11
+ import { useAutoPageSize } from '../useAutoPageSize';
12
+
13
+ function mockContainer(clientHeight: number) {
14
+ return ref({ clientHeight } as HTMLElement);
15
+ }
16
+
17
+ // Fix #5: use queueMicrotask to preserve one async tick, return incrementing ID
18
+ let rafId = 0;
19
+ beforeEach(() => {
20
+ rafId = 0;
21
+ vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
22
+ const id = ++rafId;
23
+ queueMicrotask(() => cb(0));
24
+ return id;
25
+ });
26
+ });
27
+
28
+ afterEach(() => {
29
+ vi.unstubAllGlobals();
30
+ });
31
+
32
+ describe('useAutoPageSize', () => {
33
+ describe('初始状态', () => {
34
+ it('pageSize 初始为 0,resolved 初始为 false', () => {
35
+ const container = mockContainer(500);
36
+ const { pageSize, resolved } = useAutoPageSize(container);
37
+ expect(pageSize.value).toBe(0);
38
+ expect(resolved.value).toBe(false);
39
+ });
40
+ });
41
+
42
+ describe('calculate 默认参数', () => {
43
+ it('标准高度:(500 - 40) / 48 = 9', async () => {
44
+ const container = mockContainer(500);
45
+ const { waitForLayout } = useAutoPageSize(container);
46
+ const size = await waitForLayout();
47
+ expect(size).toBe(9);
48
+ });
49
+
50
+ it('大屏高度:(900 - 40) / 48 = 17', async () => {
51
+ const container = mockContainer(900);
52
+ const { waitForLayout } = useAutoPageSize(container);
53
+ const size = await waitForLayout();
54
+ expect(size).toBe(17);
55
+ });
56
+
57
+ it('刚好整除:(520 - 40) / 48 = 10', async () => {
58
+ const container = mockContainer(520);
59
+ const { waitForLayout } = useAutoPageSize(container);
60
+ expect(await waitForLayout()).toBe(10);
61
+ });
62
+ });
63
+
64
+ describe('自定义 rowHeight / headerHeight', () => {
65
+ it('自定义行高:(500 - 40) / 60 = 7', async () => {
66
+ const container = mockContainer(500);
67
+ const { waitForLayout } = useAutoPageSize(container, { rowHeight: 60 });
68
+ expect(await waitForLayout()).toBe(7);
69
+ });
70
+
71
+ it('自定义表头高度:(500 - 56) / 48 = 9', async () => {
72
+ const container = mockContainer(500);
73
+ const { waitForLayout } = useAutoPageSize(container, { headerHeight: 56 });
74
+ expect(await waitForLayout()).toBe(9);
75
+ });
76
+
77
+ it('同时自定义:(600 - 50) / 55 = 10', async () => {
78
+ const container = mockContainer(600);
79
+ const { waitForLayout } = useAutoPageSize(container, { rowHeight: 55, headerHeight: 50 });
80
+ expect(await waitForLayout()).toBe(10);
81
+ });
82
+ });
83
+
84
+ // Fix #2: nullish-coalescing fallback paths
85
+ describe('nullish 参数回退到默认值', () => {
86
+ it('rowHeight 为 undefined 时回退默认 48', async () => {
87
+ const container = mockContainer(500);
88
+ const { waitForLayout } = useAutoPageSize(container, { rowHeight: undefined });
89
+ // (500 - 40) / 48 = 9.58 → floor = 9, same as no options
90
+ expect(await waitForLayout()).toBe(9);
91
+ });
92
+
93
+ it('headerHeight 为 undefined 时回退默认 40', async () => {
94
+ const container = mockContainer(500);
95
+ const { waitForLayout } = useAutoPageSize(container, { headerHeight: undefined });
96
+ // (500 - 40) / 48 = 9.58 → floor = 9, same as no options
97
+ expect(await waitForLayout()).toBe(9);
98
+ });
99
+ });
100
+
101
+ describe('边界:MIN_PAGE_SIZE 兜底', () => {
102
+ it('容器 ref 为 undefined → 返回 5', async () => {
103
+ const container = ref<HTMLElement | undefined>(undefined);
104
+ const { waitForLayout } = useAutoPageSize(container);
105
+ expect(await waitForLayout()).toBe(5);
106
+ });
107
+
108
+ it('容器高度为 0 → 返回 5', async () => {
109
+ const container = mockContainer(0);
110
+ const { waitForLayout } = useAutoPageSize(container);
111
+ expect(await waitForLayout()).toBe(5);
112
+ });
113
+
114
+ it('可用高度为负(容器比表头还矮)→ 返回 5', async () => {
115
+ const container = mockContainer(20);
116
+ const { waitForLayout } = useAutoPageSize(container);
117
+ expect(await waitForLayout()).toBe(5);
118
+ });
119
+
120
+ it('可用高度不够一行但大于 0 → 返回 5', async () => {
121
+ const container = mockContainer(60);
122
+ const { waitForLayout } = useAutoPageSize(container);
123
+ // (60 - 40) / 48 = 0.41 → floor = 0 → max(0, 5) = 5
124
+ expect(await waitForLayout()).toBe(5);
125
+ });
126
+
127
+ // Fix #3: near-integer floor truncation
128
+ it('接近整数但不到:(519 - 40) / 48 = 9.979 → floor 为 9', async () => {
129
+ const container = mockContainer(519);
130
+ const { waitForLayout } = useAutoPageSize(container);
131
+ // (519 - 40) / 48 = 479 / 48 = 9.979… → floor = 9, not rounded to 10
132
+ expect(await waitForLayout()).toBe(9);
133
+ });
134
+ });
135
+
136
+ describe('waitForLayout 状态更新', () => {
137
+ // Fix #1: concrete assertion instead of self-referential check
138
+ it('resolve 后 pageSize 和 resolved 同步更新', async () => {
139
+ const container = mockContainer(500);
140
+ const { pageSize, resolved, waitForLayout } = useAutoPageSize(container);
141
+
142
+ const size = await waitForLayout();
143
+ // (500 - 40) / 48 = 9.58 → floor = 9
144
+ expect(size).toBe(9);
145
+ expect(pageSize.value).toBe(9);
146
+ expect(resolved.value).toBe(true);
147
+ });
148
+
149
+ // Fix #1: test a different container height with concrete value
150
+ it('不同容器高度 resolve 值正确:(700 - 40) / 48 = 13', async () => {
151
+ const container = mockContainer(700);
152
+ const { pageSize, waitForLayout } = useAutoPageSize(container);
153
+
154
+ const size = await waitForLayout();
155
+ // (700 - 40) / 48 = 13.75 → floor = 13
156
+ expect(size).toBe(13);
157
+ expect(pageSize.value).toBe(13);
158
+ });
159
+ });
160
+
161
+ // Fix #4: multiple waitForLayout calls
162
+ describe('多次调用 waitForLayout', () => {
163
+ it('同一实例多次调用 waitForLayout 会重新计算', async () => {
164
+ const container = mockContainer(500);
165
+ const { pageSize, waitForLayout } = useAutoPageSize(container);
166
+
167
+ const first = await waitForLayout();
168
+ expect(first).toBe(9);
169
+ expect(pageSize.value).toBe(9);
170
+
171
+ // second call on same instance, same container → same result
172
+ const second = await waitForLayout();
173
+ expect(second).toBe(9);
174
+ expect(pageSize.value).toBe(9);
175
+ });
176
+
177
+ it('容器高度变化后再次调用 waitForLayout 得到新值', async () => {
178
+ const container = ref({ clientHeight: 500 } as HTMLElement);
179
+ const { pageSize, waitForLayout } = useAutoPageSize(container);
180
+
181
+ const first = await waitForLayout();
182
+ expect(first).toBe(9);
183
+ expect(pageSize.value).toBe(9);
184
+
185
+ // mutate container height
186
+ container.value = { clientHeight: 900 } as HTMLElement;
187
+
188
+ const second = await waitForLayout();
189
+ // (900 - 40) / 48 = 17.91 → floor = 17
190
+ expect(second).toBe(17);
191
+ expect(pageSize.value).toBe(17);
192
+ });
193
+ });
194
+ });
@@ -8,4 +8,6 @@
8
8
  */
9
9
 
10
10
  export { useCrudPage } from './useCrudPage';
11
+ export { useAutoPageSize } from './useAutoPageSize';
12
+ export type { AutoPageSizeOptions } from './useAutoPageSize';
11
13
  export type { CrudPageConfig, CrudColumnConfig, CrudFilterConfig, StatusMapItem } from './types';
@@ -53,6 +53,13 @@ export interface CrudPageConfig {
53
53
  statusMap?: Record<string, StatusMapItem>;
54
54
  /** 每页条数,默认 20 */
55
55
  pageSize?: number;
56
+ /**
57
+ * 根据表格容器高度自动计算 pageSize(首次计算,后续锁定)。
58
+ * - true: 使用默认行高(48px)
59
+ * - { rowHeight, headerHeight }: 自定义行高参数
60
+ * 启用后 pageSize 字段作为 fallback 使用。
61
+ */
62
+ autoPageSize?: boolean | { rowHeight?: number; headerHeight?: number };
56
63
  /** 行主键字段,默认 'id' */
57
64
  rowKey?: string;
58
65
  /** 详情页路由前缀,用于构建查看/编辑路由:`${detailPath}/${row[rowKey]}` */
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @description 根据容器可用高度自动计算 pageSize(首次计算,后续锁定)
3
+ * @author 阿怪
4
+ * @date 2026/5/7
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+
10
+ import { ref, type Ref } from 'vue';
11
+
12
+ export interface AutoPageSizeOptions {
13
+ rowHeight?: number;
14
+ headerHeight?: number;
15
+ }
16
+
17
+ const DEFAULT_ROW_HEIGHT = 48;
18
+ const DEFAULT_HEADER_HEIGHT = 40;
19
+ const MIN_PAGE_SIZE = 5;
20
+
21
+ export function useAutoPageSize(
22
+ containerRef: Ref<HTMLElement | undefined>,
23
+ options: AutoPageSizeOptions = {},
24
+ ) {
25
+ const rowHeight = options.rowHeight ?? DEFAULT_ROW_HEIGHT;
26
+ const headerHeight = options.headerHeight ?? DEFAULT_HEADER_HEIGHT;
27
+
28
+ const pageSize = ref(0);
29
+ const resolved = ref(false);
30
+
31
+ function calculate(): number {
32
+ const el = containerRef.value;
33
+ if (!el) return MIN_PAGE_SIZE;
34
+
35
+ const available = el.clientHeight - headerHeight;
36
+ return Math.max(Math.floor(available / rowHeight), MIN_PAGE_SIZE);
37
+ }
38
+
39
+ function waitForLayout(): Promise<number> {
40
+ return new Promise<number>(resolve => {
41
+ requestAnimationFrame(() => {
42
+ const size = calculate();
43
+ pageSize.value = size;
44
+ resolved.value = true;
45
+ resolve(size);
46
+ });
47
+ });
48
+ }
49
+
50
+ return {
51
+ pageSize,
52
+ resolved,
53
+ waitForLayout,
54
+ };
55
+ }
@@ -7,11 +7,12 @@
7
7
  * 江湖的业务千篇一律,复杂的代码好几百行。
8
8
  */
9
9
 
10
- import { ref, reactive, computed, onMounted } from 'vue';
10
+ import { ref, reactive, computed, onMounted, type Ref } from 'vue';
11
11
  import { useRequestClient } from '../../setup';
12
12
  import type { CrudPageConfig } from './types';
13
+ import { useAutoPageSize, type AutoPageSizeOptions } from './useAutoPageSize';
13
14
 
14
- export function useCrudPage(config: CrudPageConfig) {
15
+ export function useCrudPage(config: CrudPageConfig, tableBodyRef?: Ref<HTMLElement | undefined>) {
15
16
  const client = useRequestClient();
16
17
 
17
18
  const page = ref(1);
@@ -20,6 +21,14 @@ export function useCrudPage(config: CrudPageConfig) {
20
21
  const list = ref<Record<string, unknown>[]>([]);
21
22
  const loading = ref(false);
22
23
 
24
+ const autoOptions: AutoPageSizeOptions | undefined = config.autoPageSize
25
+ ? (typeof config.autoPageSize === 'object' ? config.autoPageSize : {})
26
+ : undefined;
27
+
28
+ const auto = autoOptions && tableBodyRef
29
+ ? useAutoPageSize(tableBodyRef, autoOptions)
30
+ : undefined;
31
+
23
32
  // 筛选表单状态
24
33
  const filters = reactive<Record<string, unknown>>({});
25
34
  if (config.filters) {
@@ -70,7 +79,13 @@ export function useCrudPage(config: CrudPageConfig) {
70
79
  fetchData();
71
80
  }
72
81
 
73
- onMounted(fetchData);
82
+ onMounted(async () => {
83
+ if (auto) {
84
+ const size = await auto.waitForLayout();
85
+ pageSize.value = size;
86
+ }
87
+ fetchData();
88
+ });
74
89
 
75
90
  return {
76
91
  page,
@@ -21,6 +21,7 @@ import type {
21
21
  } from './types';
22
22
  import { BusinessError, NetworkRequestError } from './types';
23
23
  import { buildCacheKey, ControlGate } from './controlGate';
24
+ import { getActiveLocaleMessages } from '@kine-design/core';
24
25
 
25
26
  // ────────────────────────────────────────────────────────────────────────────
26
27
  // 内部:缓存存储
@@ -89,6 +90,7 @@ function toQueryString(params: Record<string, unknown>): string {
89
90
  async function unwrapResponse<T>(
90
91
  response: { status: number; statusText: string; headers: Record<string, string>; text(): Promise<string>; json<R>(): Promise<R> },
91
92
  ): Promise<T> {
93
+ const t = getActiveLocaleMessages();
92
94
  // HTTP 错误
93
95
  if (response.status < 200 || response.status >= 300) {
94
96
  // 4xx/5xx:尝试解析 body,提取业务错误信息
@@ -98,7 +100,7 @@ async function unwrapResponse<T>(
98
100
  try {
99
101
  const json = await response.json<WrappedResponse<unknown>>();
100
102
  if (isWrappedResponse(json) && !json.success) {
101
- throw new BusinessError(json.message || `请求失败(${response.status})`);
103
+ throw new BusinessError(json.message || t.request.failedWithStatus(response.status));
102
104
  }
103
105
  } catch (e) {
104
106
  if (e instanceof BusinessError) throw e;
@@ -121,7 +123,7 @@ async function unwrapResponse<T>(
121
123
  // 检查是否为 WrappedResponse 结构
122
124
  if (isWrappedResponse<T>(json)) {
123
125
  if (!json.success) {
124
- throw new BusinessError(json.message || '请求失败');
126
+ throw new BusinessError(json.message || t.request.failed);
125
127
  }
126
128
  return json.data;
127
129
  }
@@ -277,6 +279,7 @@ export class RequestBuilder {
277
279
  // ── 执行 ────────────────────────────────────────────────────────────────
278
280
 
279
281
  async execute<T>(): Promise<T> {
282
+ const t = getActiveLocaleMessages();
280
283
  const { transport, controlGate, baseURL, defaultHeaders, getToken, onUnauthorized, responseInterceptor, registerAbort } = this.context;
281
284
 
282
285
  // 构建完整 URL
@@ -364,7 +367,7 @@ export class RequestBuilder {
364
367
 
365
368
  // 写操作成功后自动反馈
366
369
  if (this.context.feedback && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(this.method)) {
367
- this.context.feedback.showSuccess('操作成功');
370
+ this.context.feedback.showSuccess(t.request.success);
368
371
  }
369
372
 
370
373
  return finalResult as T;
@@ -376,8 +379,8 @@ export class RequestBuilder {
376
379
  // 写操作失败自动反馈错误信息
377
380
  if (this.context.feedback) {
378
381
  const msg = error instanceof BusinessError ? error.message
379
- : error instanceof NetworkRequestError ? `请求失败: ${error.message}`
380
- : '操作失败';
382
+ : error instanceof NetworkRequestError ? t.request.failedWithMsg(error.message)
383
+ : t.request.opFailed;
381
384
  this.context.feedback.showError(msg);
382
385
  }
383
386
  throw error;
@@ -7,6 +7,7 @@
7
7
  * 江湖的业务千篇一律,复杂的代码好几百行。
8
8
  */
9
9
 
10
+ import { getActiveLocaleMessages } from '@kine-design/core';
10
11
  import type { Transport, TransportRequest, TransportResponse } from '../types';
11
12
 
12
13
  /** 解析 XHR 响应头字符串为 Record */
@@ -27,6 +28,7 @@ function parseResponseHeaders(rawHeaders: string): Record<string, string> {
27
28
 
28
29
  export class XhrTransport implements Transport {
29
30
  send(request: TransportRequest): Promise<TransportResponse> {
31
+ const t = getActiveLocaleMessages();
30
32
  return new Promise((resolve, reject) => {
31
33
  const xhr = new XMLHttpRequest();
32
34
 
@@ -83,7 +85,7 @@ export class XhrTransport implements Transport {
83
85
  };
84
86
 
85
87
  xhr.onerror = () => {
86
- reject({ type: 'unknown' as const, error: new Error('网络错误') });
88
+ reject({ type: 'unknown' as const, error: new Error(t.request.networkError) });
87
89
  };
88
90
 
89
91
  xhr.ontimeout = () => {
@@ -7,6 +7,8 @@
7
7
  * 江湖的业务千篇一律,复杂的代码好几百行。
8
8
  */
9
9
 
10
+ import { getActiveLocaleMessages } from '@kine-design/core';
11
+
10
12
  // ────────────────────────────────────────────────────────────────────────────
11
13
  // 请求方法
12
14
  // ────────────────────────────────────────────────────────────────────────────
@@ -111,13 +113,14 @@ export class NetworkRequestError extends Error {
111
113
  }
112
114
 
113
115
  function networkErrorMessage(e: NetworkError): string {
116
+ const t = getActiveLocaleMessages();
114
117
  switch (e.type) {
115
- case 'invalidURL': return `无效的 URL: ${e.url}`;
116
- case 'timeout': return '请求超时';
118
+ case 'invalidURL': return t.request.invalidUrl(e.url);
119
+ case 'timeout': return t.request.timeout;
117
120
  case 'httpError': return `HTTP ${e.status} ${e.statusText}`;
118
- case 'decodingFailed': return `解码失败: ${e.reason}`;
119
- case 'aborted': return '请求已取消';
120
- case 'unknown': return `未知错误: ${String(e.error)}`;
121
+ case 'decodingFailed': return t.request.decodeFailed(e.reason);
122
+ case 'aborted': return t.request.canceled;
123
+ case 'unknown': return t.request.unknownError(String(e.error));
121
124
  }
122
125
  }
123
126
 
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { ref, type Ref } from 'vue';
11
+ import { getActiveLocaleMessages } from '@kine-design/core';
11
12
  import type { UploaderOptions, UploadOptions, WrappedResponse } from './types';
12
13
  import { BusinessError, NetworkRequestError } from './types';
13
14
  import { XhrTransport } from './transport/xhrTransport';
@@ -55,6 +56,7 @@ export function createUploader(uploaderOptions: UploaderOptions = {}): Uploader
55
56
  }
56
57
 
57
58
  function doUpload<T>(formData: FormData, options: UploadOptions): UploadHandle<T> {
59
+ const t = getActiveLocaleMessages();
58
60
  const progress = ref(0);
59
61
  const abortController = new AbortController();
60
62
 
@@ -83,7 +85,7 @@ export function createUploader(uploaderOptions: UploaderOptions = {}): Uploader
83
85
  if (contentType.includes('application/json')) {
84
86
  const json = await response.json<WrappedResponse<T> | T>();
85
87
  if (isWrappedResponse<T>(json)) {
86
- if (!json.success) throw new BusinessError(json.message || '上传失败');
88
+ if (!json.success) throw new BusinessError(json.message || t.request.uploadFailed);
87
89
  return json.data;
88
90
  }
89
91
  return json as T;
@@ -94,7 +96,7 @@ export function createUploader(uploaderOptions: UploaderOptions = {}): Uploader
94
96
 
95
97
  return {
96
98
  progress,
97
- abort: () => abortController.abort('上传已取消'),
99
+ abort: () => abortController.abort(t.request.uploadCanceled),
98
100
  promise,
99
101
  };
100
102
  }
@@ -7,6 +7,7 @@
7
7
  * 江湖的业务千篇一律,复杂的代码好几百行。
8
8
  */
9
9
 
10
+ import { getActiveLocaleMessages } from '@kine-design/core';
10
11
  import type { CrudRouteConfig, CrudRouteRecord } from './types';
11
12
 
12
13
  // ────────────────────────────────────────────────────────────────────────────
@@ -62,15 +63,16 @@ interface OpSpec {
62
63
  export function defineCrudRoutes(config: CrudRouteConfig): CrudRouteRecord[] {
63
64
  const { name, path, permission, title, icon } = config;
64
65
  const resource = permission ?? name.toLowerCase();
66
+ const t = getActiveLocaleMessages();
65
67
 
66
68
  // 规范化 basePath,确保以 / 开头、不以 / 结尾
67
69
  const basePath = path.replace(/\/+$/, '');
68
70
 
69
71
  const opSpecs: OpSpec[] = [
70
72
  { op: 'list', suffix: '', action: 'read', component: config.list, titleSuffix: '', keepAlive: true, hidden: false },
71
- { op: 'new', suffix: '/new', action: 'create', component: config.new, titleSuffix: ' - 新建', keepAlive: false, hidden: true },
72
- { op: 'modify', suffix: '/:id/edit', action: 'update', component: config.modify, titleSuffix: ' - 编辑', keepAlive: false, hidden: true },
73
- { op: 'detail', suffix: '/:id', action: 'read', component: config.detail, titleSuffix: ' - 详情', keepAlive: false, hidden: true },
73
+ { op: 'new', suffix: '/new', action: 'create', component: config.new, titleSuffix: t.routes.createSuffix, keepAlive: false, hidden: true },
74
+ { op: 'modify', suffix: '/:id/edit', action: 'update', component: config.modify, titleSuffix: t.routes.editSuffix, keepAlive: false, hidden: true },
75
+ { op: 'detail', suffix: '/:id', action: 'read', component: config.detail, titleSuffix: t.routes.detailSuffix, keepAlive: false, hidden: true },
74
76
  ];
75
77
 
76
78
  const routes: CrudRouteRecord[] = [];
@@ -60,12 +60,12 @@ declare const _default: import('vue').DefineComponent<import('vue').ExtractPropT
60
60
  /** 汇总行标签 */
61
61
  summaryLabel: {
62
62
  type: StringConstructor;
63
- default: string;
63
+ default: undefined;
64
64
  };
65
65
  /** 新增行按钮文本 */
66
66
  addText: {
67
67
  type: StringConstructor;
68
- default: string;
68
+ default: undefined;
69
69
  };
70
70
  /** 是否可新增 */
71
71
  addable: {
@@ -77,7 +77,7 @@ declare const _default: import('vue').DefineComponent<import('vue').ExtractPropT
77
77
  type: StringConstructor;
78
78
  default: string;
79
79
  };
80
- }>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, ("add" | "update:modelValue" | "remove")[], "add" | "update:modelValue" | "remove", import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
80
+ }>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, ("update:modelValue" | "add" | "remove")[], "update:modelValue" | "add" | "remove", import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
81
81
  /** 列定义 */
82
82
  columns: {
83
83
  type: PropType<EditableColumn[]>;
@@ -111,12 +111,12 @@ declare const _default: import('vue').DefineComponent<import('vue').ExtractPropT
111
111
  /** 汇总行标签 */
112
112
  summaryLabel: {
113
113
  type: StringConstructor;
114
- default: string;
114
+ default: undefined;
115
115
  };
116
116
  /** 新增行按钮文本 */
117
117
  addText: {
118
118
  type: StringConstructor;
119
- default: string;
119
+ default: undefined;
120
120
  };
121
121
  /** 是否可新增 */
122
122
  addable: {
@@ -7,4 +7,6 @@
7
7
  * 江湖的业务千篇一律,复杂的代码好几百行。
8
8
  */
9
9
  export { useCrudPage } from './useCrudPage';
10
+ export { useAutoPageSize } from './useAutoPageSize';
11
+ export type { AutoPageSizeOptions } from './useAutoPageSize';
10
12
  export type { CrudPageConfig, CrudColumnConfig, CrudFilterConfig, StatusMapItem } from './types';
@@ -52,6 +52,16 @@ export interface CrudPageConfig {
52
52
  statusMap?: Record<string, StatusMapItem>;
53
53
  /** 每页条数,默认 20 */
54
54
  pageSize?: number;
55
+ /**
56
+ * 根据表格容器高度自动计算 pageSize(首次计算,后续锁定)。
57
+ * - true: 使用默认行高(48px)
58
+ * - { rowHeight, headerHeight }: 自定义行高参数
59
+ * 启用后 pageSize 字段作为 fallback 使用。
60
+ */
61
+ autoPageSize?: boolean | {
62
+ rowHeight?: number;
63
+ headerHeight?: number;
64
+ };
55
65
  /** 行主键字段,默认 'id' */
56
66
  rowKey?: string;
57
67
  /** 详情页路由前缀,用于构建查看/编辑路由:`${detailPath}/${row[rowKey]}` */
@@ -0,0 +1,10 @@
1
+ import { Ref } from 'vue';
2
+ export interface AutoPageSizeOptions {
3
+ rowHeight?: number;
4
+ headerHeight?: number;
5
+ }
6
+ export declare function useAutoPageSize(containerRef: Ref<HTMLElement | undefined>, options?: AutoPageSizeOptions): {
7
+ pageSize: Ref<number, number>;
8
+ resolved: Ref<boolean, boolean>;
9
+ waitForLayout: () => Promise<number>;
10
+ };
@@ -1,11 +1,12 @@
1
+ import { Ref } from 'vue';
1
2
  import { CrudPageConfig } from './types';
2
- export declare function useCrudPage(config: CrudPageConfig): {
3
- page: import('vue').Ref<number, number>;
4
- pageSize: import('vue').Ref<number, number>;
5
- total: import('vue').Ref<number, number>;
3
+ export declare function useCrudPage(config: CrudPageConfig, tableBodyRef?: Ref<HTMLElement | undefined>): {
4
+ page: Ref<number, number>;
5
+ pageSize: Ref<number, number>;
6
+ total: Ref<number, number>;
6
7
  totalPages: import('vue').ComputedRef<number>;
7
- list: import('vue').Ref<Record<string, unknown>[], Record<string, unknown>[]>;
8
- loading: import('vue').Ref<boolean, boolean>;
8
+ list: Ref<Record<string, unknown>[], Record<string, unknown>[]>;
9
+ loading: Ref<boolean, boolean>;
9
10
  filters: Record<string, unknown>;
10
11
  fetchData: () => Promise<void>;
11
12
  onPageChange: (p: number) => void;