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