@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.
- 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 +195 -22
- package/dist/vitest.config.d.ts +10 -0
- package/package.json +8 -4
- package/setup.ts +12 -11
- 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
|
@@ -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 ../
|
|
286
|
+
//#region ../core/components/template/table/index.ts
|
|
287
287
|
/**
|
|
288
|
-
* @description
|
|
288
|
+
* @description table core 导出
|
|
289
289
|
* @author 阿怪
|
|
290
|
-
* @date 2026/2/
|
|
290
|
+
* @date 2026/2/25
|
|
291
291
|
* @version v1.0.0
|
|
292
292
|
*
|
|
293
293
|
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
294
294
|
*/
|
|
295
|
-
var
|
|
295
|
+
var TableCore = {
|
|
296
296
|
props: props$10,
|
|
297
297
|
useTable
|
|
298
298
|
};
|
|
299
|
-
|
|
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.
|
|
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 =
|
|
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.
|
|
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 =
|
|
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
|
|
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 =
|
|
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 = () =>
|
|
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(
|
|
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
|
|
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", {
|
|
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,
|
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/setup.ts
CHANGED
|
@@ -244,22 +244,15 @@ function createCrudAppWithOptions(
|
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
// 2.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 = () =>
|
|
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 {
|
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
|
+
});
|