@kine-design/crud 0.0.1-beta.23 → 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.
- package/components/crudPage/KCrudPage.tsx +8 -3
- package/composables/page/__tests__/useAutoPageSize.test.ts +194 -0
- package/composables/page/index.ts +2 -0
- package/composables/page/types.ts +7 -0
- package/composables/page/useAutoPageSize.ts +55 -0
- package/composables/page/useCrudPage.ts +18 -3
- package/dist/composables/page/__tests__/useAutoPageSize.test.d.ts +1 -0
- package/dist/composables/page/index.d.ts +2 -0
- package/dist/composables/page/types.d.ts +10 -0
- package/dist/composables/page/useAutoPageSize.d.ts +10 -0
- package/dist/composables/page/useCrudPage.d.ts +7 -6
- package/dist/crud.js +54 -4
- package/dist/vitest.config.d.ts +10 -0
- package/package.json +8 -4
- package/vitest.config.ts +17 -0
|
@@ -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(
|
|
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,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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:
|
|
4
|
-
pageSize:
|
|
5
|
-
total:
|
|
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:
|
|
8
|
-
loading:
|
|
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
|
@@ -8894,6 +8894,46 @@ function createCrudAppWithOptions(rootComponent, options) {
|
|
|
8894
8894
|
return enhancedApp;
|
|
8895
8895
|
}
|
|
8896
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
|
|
8897
8937
|
//#region composables/page/useCrudPage.ts
|
|
8898
8938
|
/**
|
|
8899
8939
|
* @description CrudPage composable — 配置驱动的列表页数据层
|
|
@@ -8903,13 +8943,15 @@ function createCrudAppWithOptions(rootComponent, options) {
|
|
|
8903
8943
|
*
|
|
8904
8944
|
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8905
8945
|
*/
|
|
8906
|
-
function useCrudPage(config) {
|
|
8946
|
+
function useCrudPage(config, tableBodyRef) {
|
|
8907
8947
|
const client = useRequestClient();
|
|
8908
8948
|
const page = ref(1);
|
|
8909
8949
|
const pageSize = ref(config.pageSize ?? 20);
|
|
8910
8950
|
const total = ref(0);
|
|
8911
8951
|
const list = ref([]);
|
|
8912
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;
|
|
8913
8955
|
const filters = reactive({});
|
|
8914
8956
|
if (config.filters) for (const f of config.filters) filters[f.param] = void 0;
|
|
8915
8957
|
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)));
|
|
@@ -8942,7 +8984,10 @@ function useCrudPage(config) {
|
|
|
8942
8984
|
page.value = 1;
|
|
8943
8985
|
fetchData();
|
|
8944
8986
|
}
|
|
8945
|
-
onMounted(
|
|
8987
|
+
onMounted(async () => {
|
|
8988
|
+
if (auto) pageSize.value = await auto.waitForLayout();
|
|
8989
|
+
fetchData();
|
|
8990
|
+
});
|
|
8946
8991
|
return {
|
|
8947
8992
|
page,
|
|
8948
8993
|
pageSize,
|
|
@@ -8976,7 +9021,9 @@ var KCrudPage_default = /* @__PURE__ */ defineComponent({
|
|
|
8976
9021
|
required: true
|
|
8977
9022
|
} },
|
|
8978
9023
|
setup(props, { slots }) {
|
|
8979
|
-
const
|
|
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);
|
|
8980
9027
|
/** 格式化日期 */
|
|
8981
9028
|
const formatDate = (val) => {
|
|
8982
9029
|
if (!val) return "";
|
|
@@ -9082,7 +9129,10 @@ var KCrudPage_default = /* @__PURE__ */ defineComponent({
|
|
|
9082
9129
|
}
|
|
9083
9130
|
}, null)]));
|
|
9084
9131
|
};
|
|
9085
|
-
return () => createVNode("div", {
|
|
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, {
|
|
9086
9136
|
"data": list.value,
|
|
9087
9137
|
"loading": loading.value,
|
|
9088
9138
|
"total": total.value,
|
package/package.json
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kine-design/crud",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
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.
|
|
13
|
-
"kine-ui": "0.0.1-beta.
|
|
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/vitest.config.ts
ADDED
|
@@ -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
|
+
});
|