@kine-design/crud 0.0.1-beta.21 → 0.0.1-beta.24

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.
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * 一个配置出一整个页面:标题 + 筛选 + 表格 + 分页 + 状态标签。
10
10
  */
11
- import { defineComponent, type PropType } from 'vue';
11
+ import { defineComponent, ref, computed, type PropType } from 'vue';
12
12
  import KTableColumn from 'kine-ui/components/tableColumn/KTableColumn.tsx';
13
13
  import KTag from 'kine-ui/components/tag/KTag.tsx';
14
14
  import KImage from 'kine-ui/components/image/KImage.tsx';
@@ -31,10 +31,15 @@ export default defineComponent({
31
31
  },
32
32
 
33
33
  setup(props, { slots }) {
34
+ const rootRef = ref<HTMLElement>();
35
+ const tableBodyRef = computed(
36
+ () => rootRef.value?.querySelector('.k-search-table-body') as HTMLElement | undefined,
37
+ );
38
+
34
39
  const {
35
40
  page, pageSize, total, list, loading,
36
41
  filters, onPageChange, onSearch, onReset,
37
- } = useCrudPage(props.config);
42
+ } = useCrudPage(props.config, tableBodyRef);
38
43
 
39
44
  /** 格式化日期 */
40
45
  const formatDate = (val: unknown) => {
@@ -131,7 +136,7 @@ export default defineComponent({
131
136
  };
132
137
 
133
138
  return () => (
134
- <div class="k-crud-page">
139
+ <div class="k-crud-page" ref={rootRef}>
135
140
  <KPageHeader title={props.config.title}>
136
141
  {{ extra: slots.headerExtra }}
137
142
  </KPageHeader>
@@ -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,
@@ -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;
package/dist/crud.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Comment, Fragment, Teleport, computed, createApp, createTextVNode, createVNode, defineComponent, h, inject, isRef, mergeProps, nextTick, onBeforeUnmount, onMounted, onUnmounted, provide, reactive, ref, resolveComponent, shallowRef, toRef, triggerRef, watch } from "vue";
1
+ import { Comment, Fragment, Teleport, cloneVNode, computed, createApp, createTextVNode, createVNode, defineComponent, h, inject, isRef, mergeProps, nextTick, onBeforeUnmount, onMounted, onUnmounted, provide, reactive, ref, resolveComponent, shallowRef, toRef, triggerRef, watch } from "vue";
2
2
  import { QueryClient, VueQueryPlugin, useMutation, useQuery, useQueryClient } from "@tanstack/vue-query";
3
3
  import { createPinia, defineStore } from "pinia";
4
4
  import { useRoute, useRouter } from "vue-router";
@@ -283,22 +283,127 @@ function useTable() {
283
283
  };
284
284
  }
285
285
  //#endregion
286
- //#region ../ui/components/table/KTable.tsx
286
+ //#region ../core/components/template/table/index.ts
287
287
  /**
288
- * @description kine-ui table 组件
288
+ * @description table core 导出
289
289
  * @author 阿怪
290
- * @date 2026/2/26
290
+ * @date 2026/2/25
291
291
  * @version v1.0.0
292
292
  *
293
293
  * 江湖的业务千篇一律,复杂的代码好几百行。
294
294
  */
295
- var { props: props$9 } = {
295
+ var TableCore = {
296
296
  props: props$10,
297
297
  useTable
298
298
  };
299
- var KTable_default = /* @__PURE__ */ defineComponent((_props, { slots }) => {
299
+ //#endregion
300
+ //#region ../core/compositions/common/testAnchor.ts
301
+ /**
302
+ * @description Test anchor system: provide/inject keys, types, and kDefineComponent wrapper
303
+ * @author kine-design
304
+ * @date 2026/4/28
305
+ * @version v1.0.0
306
+ *
307
+ * Renders business-semantic `data-k` attributes on DOM elements when testAnchor is enabled,
308
+ * allowing E2E tests to target components by stable identifiers.
309
+ */
310
+ /** Global switch provided by KConfigProvider to enable data-k rendering */
311
+ var K_TEST_ANCHOR_KEY = Symbol("k-test-anchor");
312
+ /** Field identity provided by KFormItem so child inputs inherit field name */
313
+ var K_FIELD_KEY = Symbol("k-field");
314
+ /**
315
+ * Extract text content from a VNode tree (for slotText mode).
316
+ * Walks children recursively, concatenating string segments.
317
+ */
318
+ function extractSlotText(nodes) {
319
+ if (!nodes) return "";
320
+ let text = "";
321
+ for (const node of nodes) if (typeof node.children === "string") text += node.children;
322
+ else if (Array.isArray(node.children)) text += extractSlotText(node.children);
323
+ return text.trim();
324
+ }
325
+ /**
326
+ * Resolve the data-k value for a component instance.
327
+ * Returns undefined when testAnchor is off or no identity can be derived.
328
+ */
329
+ function resolveDataK(config, props, attrs, fieldFromParent, slotNodes) {
330
+ const override = attrs[`k-${config.type}`];
331
+ if (typeof override === "string" && override) return `${config.type}:${override}`;
332
+ if (config.prop) {
333
+ const val = props[config.prop];
334
+ if (typeof val === "string" && val) return `${config.type}:${val}`;
335
+ }
336
+ if (config.slotText && slotNodes) {
337
+ const text = extractSlotText(slotNodes);
338
+ if (text) return `${config.type}:${text}`;
339
+ }
340
+ if (config.slotText && typeof props.text === "string" && props.text) return `${config.type}:${props.text}`;
341
+ if (config.type === "field" && !config.prop && fieldFromParent) return `${config.type}:${fieldFromParent}`;
342
+ }
343
+ /**
344
+ * Inject data-k attribute into a VNode.
345
+ * Uses cloneVNode to avoid mutating the original.
346
+ */
347
+ function injectDataK(vnode, dataK) {
348
+ return cloneVNode(vnode, { "data-k": dataK });
349
+ }
350
+ /**
351
+ * Wrapper around Vue's defineComponent that automatically injects `data-k`
352
+ * test anchor attributes when testAnchor is enabled via KConfigProvider.
353
+ *
354
+ * Usage:
355
+ * ```tsx
356
+ * export default kDefineComponent((_props, ctx) => {
357
+ * return () => <div class="k-form-item">...</div>;
358
+ * }, {
359
+ * name: 'KFormItem',
360
+ * props: FormCore.formItemProps,
361
+ * kAnchor: { type: 'field', prop: 'prop' },
362
+ * });
363
+ * ```
364
+ */
365
+ function kDefineComponent(setup, options) {
366
+ const { kAnchor, ...defineOptions } = options;
367
+ return defineComponent((rawProps, ctx) => {
368
+ const render = setup(rawProps, ctx);
369
+ if (!kAnchor) return render;
370
+ const testAnchor = inject(K_TEST_ANCHOR_KEY, void 0);
371
+ const parentField = kAnchor.type === "field" && !kAnchor.prop ? inject(K_FIELD_KEY, void 0) : void 0;
372
+ return () => {
373
+ const vnode = render();
374
+ if (!testAnchor?.value) return vnode;
375
+ const props = rawProps;
376
+ const slotNodes = kAnchor.slotText ? ctx.slots.default?.() ?? void 0 : void 0;
377
+ const dataK = resolveDataK(kAnchor, props, ctx.attrs, parentField?.value, slotNodes);
378
+ if (!dataK) return vnode;
379
+ if (vnode === null || vnode === void 0) return vnode;
380
+ if (Array.isArray(vnode)) {
381
+ if (vnode.length === 0) return vnode;
382
+ const first = vnode[0];
383
+ if (first && typeof first === "object") return [injectDataK(first, dataK), ...vnode.slice(1)];
384
+ return vnode;
385
+ }
386
+ return injectDataK(vnode, dataK);
387
+ };
388
+ }, defineOptions);
389
+ }
390
+ //#endregion
391
+ //#region ../ui/components/table/KTable.tsx
392
+ /**
393
+ * @description kine-ui table 组件
394
+ * @author 阿怪
395
+ * @date 2026/2/26
396
+ * @version v1.1.0
397
+ *
398
+ * 江湖的业务千篇一律,复杂的代码好几百行。
399
+ *
400
+ * v1.1.0 migrate to kDefineComponent for data-k test anchor 2026/4/28
401
+ */
402
+ var { props: props$9 } = TableCore;
403
+ var KTable_default = kDefineComponent((_props, { slots }) => {
300
404
  const p = _props;
301
405
  const { initTable } = useTable();
406
+ const testAnchor = inject(K_TEST_ANCHOR_KEY, void 0);
302
407
  return () => {
303
408
  const columns = [];
304
409
  (slots.default?.() ?? []).forEach((s) => {
@@ -314,6 +419,7 @@ var KTable_default = /* @__PURE__ */ defineComponent((_props, { slots }) => {
314
419
  empty: createVNode("tbody", { "class": "m-table-empty k-table-empty" }, [createVNode("tr", null, [createVNode("th", { "colspan": columns.length }, [slots.empty?.() ?? "暂无数据"])])]),
315
420
  tbodyTr: ({ data, param, slot, style: cellStyle, slotInfo }) => createVNode("td", {
316
421
  "style": cellStyle,
422
+ "data-k": testAnchor?.value && param ? `col:${param}` : void 0,
317
423
  "class": [
318
424
  "m-td",
319
425
  "k-td",
@@ -360,7 +466,11 @@ var KTable_default = /* @__PURE__ */ defineComponent((_props, { slots }) => {
360
466
  };
361
467
  }, {
362
468
  name: "KTable",
363
- props: props$9
469
+ props: props$9,
470
+ kAnchor: {
471
+ type: "table",
472
+ prop: "name"
473
+ }
364
474
  });
365
475
  //#endregion
366
476
  //#region ../core/components/base/input/api.ts
@@ -7068,11 +7178,13 @@ function useComponentSize(props) {
7068
7178
  * @description kine-ui button 组件
7069
7179
  * @author 阿怪
7070
7180
  * @date 2026/2/27
7071
- * @version v1.1.0
7181
+ * @version v1.2.0
7072
7182
  *
7073
7183
  * 江湖的业务千篇一律,复杂的代码好几百行。
7184
+ *
7185
+ * v1.2.0 migrate to kDefineComponent for data-k test anchor 2026/4/28
7074
7186
  */
7075
- var KButton_default = /* @__PURE__ */ defineComponent((_props, ctx) => {
7187
+ var KButton_default = kDefineComponent((_props, ctx) => {
7076
7188
  const props = _props;
7077
7189
  const componentSize = useComponentSize(props);
7078
7190
  return () => {
@@ -7095,7 +7207,11 @@ var KButton_default = /* @__PURE__ */ defineComponent((_props, ctx) => {
7095
7207
  };
7096
7208
  }, {
7097
7209
  name: "KButton",
7098
- props: ButtonCore.props
7210
+ props: ButtonCore.props,
7211
+ kAnchor: {
7212
+ type: "action",
7213
+ slotText: true
7214
+ }
7099
7215
  });
7100
7216
  //#endregion
7101
7217
  //#region components/searchTable/KSearchTable.tsx
@@ -7396,11 +7512,13 @@ var KImage_default = /* @__PURE__ */ defineComponent((_props, { slots, emit }) =
7396
7512
  * @description kine-ui input 组件
7397
7513
  * @author 阿怪
7398
7514
  * @date 2026/2/26
7399
- * @version v1.0.0
7515
+ * @version v1.1.0
7400
7516
  *
7401
7517
  * 江湖的业务千篇一律,复杂的代码好几百行。
7518
+ *
7519
+ * v1.1.0 migrate to kDefineComponent for data-k test anchor 2026/4/28
7402
7520
  */
7403
- var KInput_default = /* @__PURE__ */ defineComponent((_props, ctx) => {
7521
+ var KInput_default = kDefineComponent((_props, ctx) => {
7404
7522
  const props = _props;
7405
7523
  const size = useComponentSize(props);
7406
7524
  return () => {
@@ -7447,7 +7565,8 @@ var KInput_default = /* @__PURE__ */ defineComponent((_props, ctx) => {
7447
7565
  "focus",
7448
7566
  "blur",
7449
7567
  "clear"
7450
- ]
7568
+ ],
7569
+ kAnchor: { type: "field" }
7451
7570
  });
7452
7571
  //#endregion
7453
7572
  //#region ../ui/constants.ts
@@ -7465,14 +7584,15 @@ var DROPDOWN_HIDDEN_STYLE = {
7465
7584
  * @description kine-ui select 组件
7466
7585
  * @author 阿怪
7467
7586
  * @date 2026/2/26
7468
- * @version v1.0.1
7587
+ * @version v1.1.0
7469
7588
  *
7470
7589
  * 江湖的业务千篇一律,复杂的代码好几百行。
7471
7590
  *
7472
7591
  * v1.0.1 消费 inputReadonly,支持 filterable 可搜索模式 阿怪 2026/2/27
7592
+ * v1.1.0 migrate to kDefineComponent for data-k test anchor 2026/4/28
7473
7593
  */
7474
7594
  var { props: selectProps, useSelect } = SelectCore;
7475
- var KSelect_default = /* @__PURE__ */ defineComponent((_props, _ctx) => {
7595
+ var KSelect_default = kDefineComponent((_props, _ctx) => {
7476
7596
  const props = _props;
7477
7597
  const { slots, expose } = _ctx;
7478
7598
  const componentSize = useComponentSize(props);
@@ -7657,7 +7777,8 @@ var KSelect_default = /* @__PURE__ */ defineComponent((_props, _ctx) => {
7657
7777
  "select",
7658
7778
  "focus",
7659
7779
  "blur"
7660
- ]
7780
+ ],
7781
+ kAnchor: { type: "field" }
7661
7782
  });
7662
7783
  //#endregion
7663
7784
  //#region components/pageHeader/KPageHeader.tsx
@@ -8751,7 +8872,6 @@ function createCrudAppWithOptions(rootComponent, options) {
8751
8872
  options.router.beforeEach(guard);
8752
8873
  }
8753
8874
  }
8754
- if (options.router) app.use(options.router);
8755
8875
  setupCrud(app, options.query);
8756
8876
  if (options.request) {
8757
8877
  const requestOptions = { ...options.request };
@@ -8763,7 +8883,10 @@ function createCrudAppWithOptions(rootComponent, options) {
8763
8883
  if (options.error) createErrorHandler(app, options.error);
8764
8884
  const enhancedApp = Object.create(app);
8765
8885
  enhancedApp.mount = (rootContainer) => {
8766
- const doMount = () => app.mount(rootContainer);
8886
+ const doMount = () => {
8887
+ if (options.router) app.use(options.router);
8888
+ app.mount(rootContainer);
8889
+ };
8767
8890
  if (auth) auth.restore().finally(doMount);
8768
8891
  else doMount();
8769
8892
  return app;
@@ -8771,6 +8894,46 @@ function createCrudAppWithOptions(rootComponent, options) {
8771
8894
  return enhancedApp;
8772
8895
  }
8773
8896
  //#endregion
8897
+ //#region composables/page/useAutoPageSize.ts
8898
+ /**
8899
+ * @description 根据容器可用高度自动计算 pageSize(首次计算,后续锁定)
8900
+ * @author 阿怪
8901
+ * @date 2026/5/7
8902
+ * @version v0.0.1
8903
+ *
8904
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8905
+ */
8906
+ var DEFAULT_ROW_HEIGHT = 48;
8907
+ var DEFAULT_HEADER_HEIGHT = 40;
8908
+ var MIN_PAGE_SIZE = 5;
8909
+ function useAutoPageSize(containerRef, options = {}) {
8910
+ const rowHeight = options.rowHeight ?? DEFAULT_ROW_HEIGHT;
8911
+ const headerHeight = options.headerHeight ?? DEFAULT_HEADER_HEIGHT;
8912
+ const pageSize = ref(0);
8913
+ const resolved = ref(false);
8914
+ function calculate() {
8915
+ const el = containerRef.value;
8916
+ if (!el) return MIN_PAGE_SIZE;
8917
+ const available = el.clientHeight - headerHeight;
8918
+ return Math.max(Math.floor(available / rowHeight), MIN_PAGE_SIZE);
8919
+ }
8920
+ function waitForLayout() {
8921
+ return new Promise((resolve) => {
8922
+ requestAnimationFrame(() => {
8923
+ const size = calculate();
8924
+ pageSize.value = size;
8925
+ resolved.value = true;
8926
+ resolve(size);
8927
+ });
8928
+ });
8929
+ }
8930
+ return {
8931
+ pageSize,
8932
+ resolved,
8933
+ waitForLayout
8934
+ };
8935
+ }
8936
+ //#endregion
8774
8937
  //#region composables/page/useCrudPage.ts
8775
8938
  /**
8776
8939
  * @description CrudPage composable — 配置驱动的列表页数据层
@@ -8780,13 +8943,15 @@ function createCrudAppWithOptions(rootComponent, options) {
8780
8943
  *
8781
8944
  * 江湖的业务千篇一律,复杂的代码好几百行。
8782
8945
  */
8783
- function useCrudPage(config) {
8946
+ function useCrudPage(config, tableBodyRef) {
8784
8947
  const client = useRequestClient();
8785
8948
  const page = ref(1);
8786
8949
  const pageSize = ref(config.pageSize ?? 20);
8787
8950
  const total = ref(0);
8788
8951
  const list = ref([]);
8789
8952
  const loading = ref(false);
8953
+ const autoOptions = config.autoPageSize ? typeof config.autoPageSize === "object" ? config.autoPageSize : {} : void 0;
8954
+ const auto = autoOptions && tableBodyRef ? useAutoPageSize(tableBodyRef, autoOptions) : void 0;
8790
8955
  const filters = reactive({});
8791
8956
  if (config.filters) for (const f of config.filters) filters[f.param] = void 0;
8792
8957
  const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)));
@@ -8819,7 +8984,10 @@ function useCrudPage(config) {
8819
8984
  page.value = 1;
8820
8985
  fetchData();
8821
8986
  }
8822
- onMounted(fetchData);
8987
+ onMounted(async () => {
8988
+ if (auto) pageSize.value = await auto.waitForLayout();
8989
+ fetchData();
8990
+ });
8823
8991
  return {
8824
8992
  page,
8825
8993
  pageSize,
@@ -8853,7 +9021,9 @@ var KCrudPage_default = /* @__PURE__ */ defineComponent({
8853
9021
  required: true
8854
9022
  } },
8855
9023
  setup(props, { slots }) {
8856
- const { page, pageSize, total, list, loading, filters, onPageChange, onSearch, onReset } = useCrudPage(props.config);
9024
+ const rootRef = ref();
9025
+ const tableBodyRef = computed(() => rootRef.value?.querySelector(".k-search-table-body"));
9026
+ const { page, pageSize, total, list, loading, filters, onPageChange, onSearch, onReset } = useCrudPage(props.config, tableBodyRef);
8857
9027
  /** 格式化日期 */
8858
9028
  const formatDate = (val) => {
8859
9029
  if (!val) return "";
@@ -8959,7 +9129,10 @@ var KCrudPage_default = /* @__PURE__ */ defineComponent({
8959
9129
  }
8960
9130
  }, null)]));
8961
9131
  };
8962
- return () => createVNode("div", { "class": "k-crud-page" }, [createVNode(KPageHeader_default, { "title": props.config.title }, { extra: slots.headerExtra }), createVNode(KSearchTable_default, {
9132
+ return () => createVNode("div", {
9133
+ "class": "k-crud-page",
9134
+ "ref": rootRef
9135
+ }, [createVNode(KPageHeader_default, { "title": props.config.title }, { extra: slots.headerExtra }), createVNode(KSearchTable_default, {
8963
9136
  "data": list.value,
8964
9137
  "loading": loading.value,
8965
9138
  "total": total.value,
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @description @kine-design/crud 单元测试配置
3
+ * @author 阿怪
4
+ * @date 2026/5/7
5
+ * @version v1.0.0
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+ declare const _default: import('vite').UserConfig;
10
+ export default _default;
package/package.json CHANGED
@@ -1,16 +1,19 @@
1
1
  {
2
2
  "name": "@kine-design/crud",
3
- "version": "0.0.1-beta.21",
3
+ "version": "0.0.1-beta.24",
4
4
  "type": "module",
5
5
  "main": "./dist/crud.js",
6
6
  "types": "./dist/index.d.ts",
7
+ "devDependencies": {
8
+ "vitest": "^4.1.0"
9
+ },
7
10
  "dependencies": {
8
11
  "@tanstack/vue-query": "^5.92.10",
9
12
  "pinia": "^3.0.3",
10
13
  "vue": "^3.5.30",
11
14
  "vue-router": "^5.0.3",
12
- "@kine-design/core": "0.0.1-beta.6",
13
- "kine-ui": "0.0.1-beta.12"
15
+ "@kine-design/core": "0.0.1-beta.8",
16
+ "kine-ui": "0.0.1-beta.17"
14
17
  },
15
18
  "publishConfig": {
16
19
  "access": "public",
@@ -19,7 +22,8 @@
19
22
  ]
20
23
  },
21
24
  "scripts": {
22
- "build": "vite build --config vite.config.build.ts"
25
+ "build": "vite build --config vite.config.build.ts",
26
+ "test": "vitest --config vitest.config.ts --run"
23
27
  },
24
28
  "module": "./dist/crud.js",
25
29
  "exports": {
package/setup.ts CHANGED
@@ -244,22 +244,15 @@ function createCrudAppWithOptions(
244
244
  }
245
245
  }
246
246
 
247
- // 2. Router(install 时触发初始导航,此时守卫已注册)
248
- if (options.router) {
249
- app.use(options.router as { install: (app: App) => void });
250
- }
251
-
252
- // 3. TanStack Query
247
+ // 2. TanStack Query
253
248
  setupCrud(app, options.query);
254
249
 
255
- // 4. Request
250
+ // 3. Request
256
251
  if (options.request) {
257
252
  const requestOptions: RequestOptions = { ...options.request };
258
- // 如果有 auth,自动注入 getToken
259
253
  if (auth && !requestOptions.getToken) {
260
254
  requestOptions.getToken = () => auth!.token.value;
261
255
  }
262
- // 如果有 auth,自动注入 onUnauthorized
263
256
  if (auth && !requestOptions.onUnauthorized) {
264
257
  requestOptions.onUnauthorized = options.auth?.onUnauthorized;
265
258
  }
@@ -267,15 +260,23 @@ function createCrudAppWithOptions(
267
260
  app.provide(REQUEST_CLIENT_KEY, client);
268
261
  }
269
262
 
270
- // 5. Error Handler
263
+ // 4. Error Handler
271
264
  if (options.error) {
272
265
  createErrorHandler(app, options.error);
273
266
  }
274
267
 
275
268
  // 返回增强的 app,mount 前先恢复 auth
269
+ // Router 安装延迟到 doMount:app.use(router) 会触发初始导航,
270
+ // 必须在 auth.restore() 完成(permissions 已加载)之后执行,否则
271
+ // authGuard 会在 permissions 为空时将受保护路由重定向到 /403。
276
272
  const enhancedApp: EnhancedApp = Object.create(app);
277
273
  enhancedApp.mount = (rootContainer: string | Element): App => {
278
- const doMount = () => app.mount(rootContainer);
274
+ const doMount = () => {
275
+ if (options.router) {
276
+ app.use(options.router as { install: (app: App) => void });
277
+ }
278
+ app.mount(rootContainer);
279
+ };
279
280
  if (auth) {
280
281
  auth.restore().finally(doMount);
281
282
  } else {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @description @kine-design/crud 单元测试配置
3
+ * @author 阿怪
4
+ * @date 2026/5/7
5
+ * @version v1.0.0
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+
10
+ import { defineConfig } from 'vitest/config';
11
+
12
+ export default defineConfig({
13
+ root: __dirname,
14
+ test: {
15
+ include: ['**/__tests__/*.test.ts'],
16
+ },
17
+ });