stk-table-vue 0.2.2 → 0.2.5
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/README.md +30 -8
- package/lib/src/StkTable/StkTable.vue.d.ts +29 -20
- package/lib/src/StkTable/index.d.ts +1 -0
- package/lib/src/StkTable/types/index.d.ts +17 -3
- package/lib/src/StkTable/useFixedStyle.d.ts +3 -3
- package/lib/src/StkTable/useHighlight.d.ts +10 -3
- package/lib/src/StkTable/useVirtualScroll.d.ts +3 -2
- package/lib/src/StkTable/utils.d.ts +6 -2
- package/lib/stk-table-vue.js +218 -122
- package/package.json +60 -60
- package/src/StkTable/StkTable.vue +78 -37
- package/src/StkTable/types/index.ts +17 -3
- package/src/StkTable/useFixedStyle.ts +49 -28
- package/src/StkTable/useHighlight.ts +50 -33
- package/src/StkTable/useKeyboardArrowScroll.ts +8 -4
- package/src/StkTable/useVirtualScroll.ts +42 -11
- package/src/StkTable/utils.ts +85 -47
|
@@ -1,22 +1,41 @@
|
|
|
1
1
|
import { interpolateRgb } from 'd3-interpolate';
|
|
2
|
-
import { Ref, computed } from 'vue';
|
|
2
|
+
import { Ref, computed, ref } from 'vue';
|
|
3
3
|
import { Highlight_Color, Highlight_Color_Change_Freq, Highlight_Duration } from './const';
|
|
4
|
+
import { UniqKey } from './types';
|
|
4
5
|
|
|
5
6
|
type Params = {
|
|
6
7
|
props: { theme: 'light' | 'dark'; virtual: boolean; dataSource: any[] };
|
|
7
8
|
tableContainer: Ref<HTMLElement | undefined>;
|
|
8
|
-
rowKeyGen: (p: any) => string;
|
|
9
9
|
};
|
|
10
|
+
|
|
11
|
+
/** 高亮行保存的东西 */
|
|
12
|
+
type HighlightRowStore = {
|
|
13
|
+
bgc: string;
|
|
14
|
+
bgc_progress_ms: number;
|
|
15
|
+
bgc_progress: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** 高亮行class */
|
|
19
|
+
const HIGHLIGHT_ROW_CLASS = 'highlight-row';
|
|
20
|
+
/** 高连单元格class */
|
|
21
|
+
const HIGHLIGHT_CELL_CLASS = 'highlight-cell';
|
|
22
|
+
|
|
10
23
|
/**
|
|
11
24
|
* 高亮单元格,行
|
|
12
25
|
* row中新增_bgc_progress_ms 属性控制高亮状态,_bgc控制颜色
|
|
13
26
|
*/
|
|
14
|
-
export function useHighlight({ props, tableContainer
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
export function useHighlight({ props, tableContainer }: Params) {
|
|
28
|
+
/**
|
|
29
|
+
* 高亮行记录 key-rowKey, value-obj
|
|
30
|
+
*/
|
|
31
|
+
const highlightRowStore = ref<Record<UniqKey, HighlightRowStore>>({});
|
|
32
|
+
|
|
33
|
+
const highlightFrom = computed(() => Highlight_Color[props.theme].from);
|
|
34
|
+
const highlightTo = computed(() => Highlight_Color[props.theme].to);
|
|
35
|
+
const highlightInter = computed(() => interpolateRgb(highlightFrom.value, highlightTo.value));
|
|
36
|
+
|
|
37
|
+
/** 存放高亮行的key*/
|
|
38
|
+
const highlightDimRowKeys = new Set<UniqKey>();
|
|
20
39
|
/** 高亮后渐暗的行定时器 */
|
|
21
40
|
const highlightDimRowsTimeout = new Map();
|
|
22
41
|
/** 高亮后渐暗的单元格定时器 */
|
|
@@ -36,9 +55,8 @@ export function useHighlight({ props, tableContainer, rowKeyGen }: Params) {
|
|
|
36
55
|
const recursion = () => {
|
|
37
56
|
window.setTimeout(() => {
|
|
38
57
|
const nowTs = Date.now();
|
|
39
|
-
const needDeleteRows: any = [];
|
|
40
58
|
|
|
41
|
-
|
|
59
|
+
highlightDimRowKeys.forEach(rowKeyValue => {
|
|
42
60
|
// const rowKeyValue = rowKeyGen(row);
|
|
43
61
|
// const rowEl = tableContainer.value?.querySelector<HTMLElement>(`[data-row-key="${rowKeyValue}"]`);
|
|
44
62
|
// if (rowEl && row._bgc_progress === 0) {
|
|
@@ -47,21 +65,18 @@ export function useHighlight({ props, tableContainer, rowKeyGen }: Params) {
|
|
|
47
65
|
// void rowEl.offsetHeight; // reflow
|
|
48
66
|
// rowEl.classList.add('highlight-row-transition');
|
|
49
67
|
// }
|
|
50
|
-
|
|
68
|
+
const highlightItem = highlightRowStore.value[rowKeyValue];
|
|
51
69
|
/** 经过的时间 ÷ 高亮持续时间 计算出 颜色过渡进度 (0-1) */
|
|
52
|
-
const progress = (nowTs -
|
|
53
|
-
// row._bgc_progress = progress;
|
|
70
|
+
const progress = (nowTs - highlightItem.bgc_progress_ms) / Highlight_Duration;
|
|
54
71
|
if (0 < progress && progress < 1) {
|
|
55
|
-
|
|
72
|
+
highlightItem.bgc = highlightInter.value(progress);
|
|
56
73
|
} else {
|
|
57
|
-
|
|
58
|
-
|
|
74
|
+
highlightItem.bgc = ''; // 清空颜色
|
|
75
|
+
highlightDimRowKeys.delete(rowKeyValue);
|
|
59
76
|
}
|
|
60
77
|
});
|
|
61
|
-
needDeleteRows.forEach((row: any) => highlightDimRows.delete(row));
|
|
62
|
-
// TODO: shallowRef 时,需要手动更新
|
|
63
78
|
|
|
64
|
-
if (
|
|
79
|
+
if (highlightDimRowKeys.size > 0) {
|
|
65
80
|
// 还有高亮的行,则下一次循环
|
|
66
81
|
recursion();
|
|
67
82
|
} else {
|
|
@@ -78,16 +93,16 @@ export function useHighlight({ props, tableContainer, rowKeyGen }: Params) {
|
|
|
78
93
|
// TODO: 支持动态计算高亮颜色。不易实现。需记录每一个单元格的颜色情况。
|
|
79
94
|
const cellEl = tableContainer.value?.querySelector<HTMLElement>(`[data-row-key="${rowKeyValue}"]>[data-index="${dataIndex}"]`);
|
|
80
95
|
if (!cellEl) return;
|
|
81
|
-
if (cellEl.classList.contains(
|
|
82
|
-
cellEl.classList.remove(
|
|
96
|
+
if (cellEl.classList.contains(HIGHLIGHT_CELL_CLASS)) {
|
|
97
|
+
cellEl.classList.remove(HIGHLIGHT_CELL_CLASS);
|
|
83
98
|
void cellEl.offsetHeight; // 通知浏览器重绘
|
|
84
99
|
}
|
|
85
|
-
cellEl.classList.add(
|
|
100
|
+
cellEl.classList.add(HIGHLIGHT_CELL_CLASS);
|
|
86
101
|
window.clearTimeout(highlightDimCellsTimeout.get(rowKeyValue));
|
|
87
102
|
highlightDimCellsTimeout.set(
|
|
88
103
|
rowKeyValue,
|
|
89
104
|
window.setTimeout(() => {
|
|
90
|
-
cellEl.classList.remove(
|
|
105
|
+
cellEl.classList.remove(HIGHLIGHT_CELL_CLASS);
|
|
91
106
|
highlightDimCellsTimeout.delete(rowKeyValue);
|
|
92
107
|
}, Highlight_Duration),
|
|
93
108
|
);
|
|
@@ -97,18 +112,19 @@ export function useHighlight({ props, tableContainer, rowKeyGen }: Params) {
|
|
|
97
112
|
* 高亮一行
|
|
98
113
|
* @param rowKeyValues
|
|
99
114
|
*/
|
|
100
|
-
function setHighlightDimRow(rowKeyValues:
|
|
115
|
+
function setHighlightDimRow(rowKeyValues: UniqKey[]) {
|
|
101
116
|
if (!Array.isArray(rowKeyValues)) rowKeyValues = [rowKeyValues];
|
|
102
117
|
if (props.virtual) {
|
|
103
118
|
// --------虚拟滚动用js计算颜色渐变的高亮方案
|
|
104
119
|
const nowTs = Date.now(); // 重置渐变进度
|
|
105
120
|
for (let i = 0; i < rowKeyValues.length; i++) {
|
|
106
121
|
const rowKeyValue = rowKeyValues[i];
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
highlightRowStore.value[rowKeyValue] = {
|
|
123
|
+
bgc: '',
|
|
124
|
+
bgc_progress: 0,
|
|
125
|
+
bgc_progress_ms: nowTs,
|
|
126
|
+
};
|
|
127
|
+
highlightDimRowKeys.add(rowKeyValue);
|
|
112
128
|
}
|
|
113
129
|
calcHighlightLoop();
|
|
114
130
|
} else {
|
|
@@ -121,8 +137,8 @@ export function useHighlight({ props, tableContainer, rowKeyGen }: Params) {
|
|
|
121
137
|
const rowKeyValue = rowKeyValues[i];
|
|
122
138
|
const rowEl = tableContainer.value?.querySelector<HTMLTableRowElement>(`[data-row-key="${rowKeyValue}"]`);
|
|
123
139
|
if (!rowEl) continue;
|
|
124
|
-
if (rowEl.classList.contains(
|
|
125
|
-
rowEl.classList.remove(
|
|
140
|
+
if (rowEl.classList.contains(HIGHLIGHT_ROW_CLASS)) {
|
|
141
|
+
rowEl.classList.remove(HIGHLIGHT_ROW_CLASS);
|
|
126
142
|
needRepaint = true;
|
|
127
143
|
}
|
|
128
144
|
rowElTemp.push(rowEl);
|
|
@@ -131,7 +147,7 @@ export function useHighlight({ props, tableContainer, rowKeyGen }: Params) {
|
|
|
131
147
|
highlightDimRowsTimeout.set(
|
|
132
148
|
rowKeyValue,
|
|
133
149
|
window.setTimeout(() => {
|
|
134
|
-
rowEl.classList.remove(
|
|
150
|
+
rowEl.classList.remove(HIGHLIGHT_ROW_CLASS);
|
|
135
151
|
highlightDimRowsTimeout.delete(rowKeyValue); // 回收内存
|
|
136
152
|
}, Highlight_Duration),
|
|
137
153
|
);
|
|
@@ -139,11 +155,12 @@ export function useHighlight({ props, tableContainer, rowKeyGen }: Params) {
|
|
|
139
155
|
if (needRepaint) {
|
|
140
156
|
void tableContainer.value?.offsetWidth; //强制浏览器重绘
|
|
141
157
|
}
|
|
142
|
-
rowElTemp.forEach(el => el.classList.add(
|
|
158
|
+
rowElTemp.forEach(el => el.classList.add(HIGHLIGHT_ROW_CLASS)); // 统一添加动画
|
|
143
159
|
}
|
|
144
160
|
}
|
|
145
161
|
|
|
146
162
|
return {
|
|
163
|
+
highlightRowStore,
|
|
147
164
|
setHighlightDimRow,
|
|
148
165
|
setHighlightDimCell,
|
|
149
166
|
};
|
|
@@ -44,11 +44,15 @@ export function useKeyboardArrowScroll<DT extends Record<string, any>>(
|
|
|
44
44
|
if (!isMouseOver) return; // 不悬浮还是要触发键盘事件的
|
|
45
45
|
e.preventDefault(); // 不触发键盘默认的箭头事件
|
|
46
46
|
|
|
47
|
-
const { scrollTop, rowHeight,
|
|
47
|
+
const { scrollTop, rowHeight, containerHeight } = virtualScroll.value;
|
|
48
48
|
const { scrollLeft } = virtualScrollX.value;
|
|
49
49
|
const { headless, headerRowHeight } = props;
|
|
50
|
-
|
|
50
|
+
|
|
51
|
+
// 这里不用virtualScroll 中的pageSize,因为我需要上一页的最后一条放在下一页的第一条
|
|
51
52
|
const headerHeight = headless ? 0 : tableHeaders.value.length * (headerRowHeight || rowHeight);
|
|
53
|
+
/** 表体的page */
|
|
54
|
+
const bodyPageSize = Math.floor((containerHeight - headerHeight) / rowHeight);
|
|
55
|
+
|
|
52
56
|
if (e.code === SCROLL_CODES[0]) {
|
|
53
57
|
scrollTo(scrollTop - rowHeight, null);
|
|
54
58
|
} else if (e.code === SCROLL_CODES[1]) {
|
|
@@ -58,9 +62,9 @@ export function useKeyboardArrowScroll<DT extends Record<string, any>>(
|
|
|
58
62
|
} else if (e.code === SCROLL_CODES[3]) {
|
|
59
63
|
scrollTo(null, scrollLeft - rowHeight);
|
|
60
64
|
} else if (e.code === SCROLL_CODES[4]) {
|
|
61
|
-
scrollTo(scrollTop - rowHeight *
|
|
65
|
+
scrollTo(scrollTop - rowHeight * bodyPageSize + headerHeight, null);
|
|
62
66
|
} else if (e.code === SCROLL_CODES[5]) {
|
|
63
|
-
scrollTo(scrollTop + rowHeight *
|
|
67
|
+
scrollTo(scrollTop + rowHeight * bodyPageSize - headerHeight, null);
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
70
|
|
|
@@ -4,10 +4,11 @@ import { StkTableColumn } from './types';
|
|
|
4
4
|
import { getColWidth } from './utils';
|
|
5
5
|
|
|
6
6
|
type Option<DT extends Record<string, any>> = {
|
|
7
|
-
tableContainer: Ref<HTMLElement | undefined>;
|
|
8
7
|
props: any;
|
|
8
|
+
tableContainer: Ref<HTMLElement | undefined>;
|
|
9
9
|
dataSourceCopy: ShallowRef<DT[]>;
|
|
10
10
|
tableHeaderLast: Ref<StkTableColumn<DT>[]>;
|
|
11
|
+
tableHeaders: Ref<StkTableColumn<DT>[][]>;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
/** 暂存纵向虚拟滚动的数据 */
|
|
@@ -49,7 +50,13 @@ const VUE2_SCROLL_TIMEOUT_MS = 200;
|
|
|
49
50
|
* @param param0
|
|
50
51
|
* @returns
|
|
51
52
|
*/
|
|
52
|
-
export function useVirtualScroll<DT extends Record<string, any>>({
|
|
53
|
+
export function useVirtualScroll<DT extends Record<string, any>>({
|
|
54
|
+
props,
|
|
55
|
+
tableContainer,
|
|
56
|
+
dataSourceCopy,
|
|
57
|
+
tableHeaderLast,
|
|
58
|
+
tableHeaders,
|
|
59
|
+
}: Option<DT>) {
|
|
53
60
|
const virtualScroll = ref<VirtualScrollStore>({
|
|
54
61
|
containerHeight: 0,
|
|
55
62
|
rowHeight: props.rowHeight,
|
|
@@ -140,10 +147,14 @@ export function useVirtualScroll<DT extends Record<string, any>>({ props, tableC
|
|
|
140
147
|
} else {
|
|
141
148
|
containerHeight = offsetHeight || Default_Table_Height;
|
|
142
149
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
150
|
+
const { headless, headerRowHeight } = props;
|
|
151
|
+
let pageSize = Math.ceil(containerHeight / rowHeight);
|
|
152
|
+
if (!headless) {
|
|
153
|
+
/** 表头高度占几行表体高度数 */
|
|
154
|
+
const headerToBodyRowHeightCount = Math.floor((tableHeaders.value.length * (headerRowHeight || rowHeight)) / rowHeight);
|
|
155
|
+
pageSize -= headerToBodyRowHeightCount; //减去表头行数
|
|
156
|
+
}
|
|
157
|
+
Object.assign(virtualScroll.value, { containerHeight, pageSize });
|
|
147
158
|
updateVirtualScrollY(scrollTop);
|
|
148
159
|
}
|
|
149
160
|
|
|
@@ -168,9 +179,30 @@ export function useVirtualScroll<DT extends Record<string, any>>({ props, tableC
|
|
|
168
179
|
/** 通过滚动条位置,计算虚拟滚动的参数 */
|
|
169
180
|
function updateVirtualScrollY(sTop = 0) {
|
|
170
181
|
const { rowHeight, pageSize, scrollTop, startIndex: oldStartIndex } = virtualScroll.value;
|
|
171
|
-
|
|
172
|
-
|
|
182
|
+
// 先更新滚动条位置记录,其他地方可能有依赖。(stripe 时ArrowUp/Down滚动依赖)
|
|
183
|
+
virtualScroll.value.scrollTop = sTop;
|
|
184
|
+
let startIndex = Math.floor(sTop / rowHeight);
|
|
185
|
+
if (props.stripe) {
|
|
186
|
+
startIndex -= 1; //预渲染1行
|
|
187
|
+
}
|
|
188
|
+
if (startIndex < 0) {
|
|
189
|
+
startIndex = 0;
|
|
190
|
+
}
|
|
191
|
+
if (props.stripe && startIndex !== 0) {
|
|
192
|
+
const scrollRows = Math.abs(oldStartIndex - startIndex);
|
|
193
|
+
// 斑马纹情况下,每滚动偶数行才加载。防止斑马纹错位。
|
|
194
|
+
if (scrollRows < 2) {
|
|
195
|
+
return;
|
|
196
|
+
} else if (scrollRows % 2) {
|
|
197
|
+
startIndex -= 1; // 奇数-1变成偶数
|
|
198
|
+
}
|
|
199
|
+
}
|
|
173
200
|
let endIndex = startIndex + pageSize;
|
|
201
|
+
if (props.stripe) {
|
|
202
|
+
// 由于上方预渲染一行,这里也要预渲染1+1行
|
|
203
|
+
endIndex += 2;
|
|
204
|
+
}
|
|
205
|
+
const offsetTop = startIndex * rowHeight; // startIndex之前的高度
|
|
174
206
|
if (endIndex > dataSourceCopy.value.length) {
|
|
175
207
|
endIndex = dataSourceCopy.value.length; // 溢出index修正
|
|
176
208
|
}
|
|
@@ -184,13 +216,12 @@ export function useVirtualScroll<DT extends Record<string, any>>({ props, tableC
|
|
|
184
216
|
// 向上滚动
|
|
185
217
|
Object.assign(virtualScroll.value, {
|
|
186
218
|
startIndex,
|
|
187
|
-
offsetTop,
|
|
188
219
|
endIndex,
|
|
189
|
-
|
|
220
|
+
offsetTop,
|
|
190
221
|
});
|
|
191
222
|
} else {
|
|
192
223
|
// vue2向下滚动优化
|
|
193
|
-
|
|
224
|
+
virtualScroll.value.endIndex = endIndex;
|
|
194
225
|
vue2ScrollYTimeout = window.setTimeout(() => {
|
|
195
226
|
Object.assign(virtualScroll.value, { startIndex, offsetTop });
|
|
196
227
|
}, VUE2_SCROLL_TIMEOUT_MS);
|
package/src/StkTable/utils.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { Default_Col_Width } from './const';
|
|
2
2
|
import { Order, SortConfig, SortOption, SortState, StkTableColumn } from './types';
|
|
3
3
|
|
|
4
|
+
/** 是否空值 */
|
|
5
|
+
function isEmptyValue(val: any, isNumber?: boolean) {
|
|
6
|
+
let isEmpty = val === null || val === '';
|
|
7
|
+
if (isNumber) {
|
|
8
|
+
isEmpty ||= typeof val === 'boolean' || Number.isNaN(+val);
|
|
9
|
+
}
|
|
10
|
+
return isEmpty;
|
|
11
|
+
}
|
|
12
|
+
|
|
4
13
|
/**
|
|
5
14
|
* 对有序数组插入新数据
|
|
6
15
|
* @param sortState
|
|
@@ -10,15 +19,30 @@ import { Order, SortConfig, SortOption, SortState, StkTableColumn } from './type
|
|
|
10
19
|
* @param newItem 要插入的数据
|
|
11
20
|
* @param targetArray 表格数据
|
|
12
21
|
*/
|
|
13
|
-
export function insertToOrderedArray<T extends object>(
|
|
22
|
+
export function insertToOrderedArray<T extends object>(
|
|
23
|
+
sortState: SortState<keyof T>,
|
|
24
|
+
newItem: any,
|
|
25
|
+
targetArray: T[],
|
|
26
|
+
sortConfig: SortConfig<T> = {},
|
|
27
|
+
) {
|
|
14
28
|
const { dataIndex, order } = sortState;
|
|
29
|
+
sortConfig = { emptyToBottom: false, ...sortConfig };
|
|
15
30
|
let { sortType } = sortState;
|
|
16
31
|
if (!sortType) sortType = typeof newItem[dataIndex] as 'number' | 'string';
|
|
32
|
+
const isNumber = sortType === 'number';
|
|
17
33
|
const data = [...targetArray];
|
|
34
|
+
|
|
18
35
|
if (!order) {
|
|
36
|
+
// 没有排序的情况,插入在最上方
|
|
19
37
|
data.unshift(newItem);
|
|
20
38
|
return data;
|
|
21
39
|
}
|
|
40
|
+
|
|
41
|
+
if (sortConfig.emptyToBottom && isEmptyValue(data)) {
|
|
42
|
+
// 空值排在最下方
|
|
43
|
+
data.push(newItem);
|
|
44
|
+
}
|
|
45
|
+
|
|
22
46
|
// 二分插入
|
|
23
47
|
let sIndex = 0;
|
|
24
48
|
let eIndex = data.length - 1;
|
|
@@ -27,7 +51,7 @@ export function insertToOrderedArray<T extends object>(sortState: SortState<keyo
|
|
|
27
51
|
// console.log(sIndex, eIndex);
|
|
28
52
|
const midIndex = Math.floor((sIndex + eIndex) / 2);
|
|
29
53
|
const midVal: any = data[midIndex][dataIndex];
|
|
30
|
-
const compareRes = strCompare(midVal, targetVal,
|
|
54
|
+
const compareRes = strCompare(midVal, targetVal, isNumber, sortConfig.stringLocaleCompare);
|
|
31
55
|
if (compareRes === 0) {
|
|
32
56
|
//midVal == targetVal
|
|
33
57
|
sIndex = midIndex;
|
|
@@ -47,41 +71,44 @@ export function insertToOrderedArray<T extends object>(sortState: SortState<keyo
|
|
|
47
71
|
}
|
|
48
72
|
/**
|
|
49
73
|
* 字符串比较
|
|
50
|
-
* @param
|
|
51
|
-
* @param
|
|
52
|
-
* @param
|
|
74
|
+
* @param a
|
|
75
|
+
* @param b
|
|
76
|
+
* @param type 类型
|
|
77
|
+
* @param isNumber 是否是数字类型
|
|
78
|
+
* @param localeCompare 是否 使用Array.prototype.localeCompare
|
|
53
79
|
* @return {-1|0|1}
|
|
54
80
|
*/
|
|
55
|
-
function strCompare(a: string, b: string,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
function strCompare(a: string, b: string, isNumber: boolean, localeCompare = false): number {
|
|
82
|
+
let _a: number | string = a;
|
|
83
|
+
let _b: number | string = b;
|
|
84
|
+
if (isNumber) {
|
|
85
|
+
// 是数字就转数字
|
|
86
|
+
_a = +a;
|
|
87
|
+
_b = +b;
|
|
88
|
+
} else if (localeCompare) {
|
|
89
|
+
// 字符串才可以localeCompare
|
|
62
90
|
return String(a).localeCompare(b);
|
|
63
91
|
}
|
|
92
|
+
if (_a > _b) return 1;
|
|
93
|
+
else if (_a === _b) return 0;
|
|
94
|
+
else return -1;
|
|
64
95
|
}
|
|
65
96
|
|
|
66
97
|
/**
|
|
67
|
-
*
|
|
98
|
+
* 分离出空数据和非空数据成两个数组。NaN视为空数据。
|
|
68
99
|
* @param sortOption
|
|
69
100
|
* @param targetDataSource
|
|
70
|
-
* @param isNumber
|
|
101
|
+
* @param isNumber 是否数字
|
|
71
102
|
* @return [值数组,空数组]
|
|
72
103
|
*/
|
|
73
|
-
function separatedData(sortOption: SortOption
|
|
74
|
-
const emptyArr:
|
|
75
|
-
const valueArr:
|
|
104
|
+
function separatedData<T extends Record<string, any>>(sortOption: SortOption<T>, targetDataSource: T[], isNumber?: boolean) {
|
|
105
|
+
const emptyArr: T[] = [];
|
|
106
|
+
const valueArr: T[] = [];
|
|
76
107
|
|
|
77
108
|
for (let i = 0; i < targetDataSource.length; i++) {
|
|
78
109
|
const row = targetDataSource[i];
|
|
79
110
|
const sortField = sortOption.sortField || sortOption.dataIndex;
|
|
80
|
-
|
|
81
|
-
if (isNumber) {
|
|
82
|
-
isEmpty ||= typeof row[sortField] === 'boolean' || Number.isNaN(+row[sortField]);
|
|
83
|
-
}
|
|
84
|
-
|
|
111
|
+
const isEmpty = isEmptyValue(row[sortField], isNumber);
|
|
85
112
|
if (isEmpty) {
|
|
86
113
|
emptyArr.push(row);
|
|
87
114
|
} else {
|
|
@@ -96,48 +123,49 @@ function separatedData(sortOption: SortOption, targetDataSource: any[], isNumber
|
|
|
96
123
|
* 可以在组件外部自己实现表格排序,组件配置remote,使表格不排序。
|
|
97
124
|
* 使用者在@sort-change事件中自行更改table props 'dataSource'完成排序。
|
|
98
125
|
* TODO: key 唯一值,排序字段相同时,根据唯一值排序。
|
|
126
|
+
*
|
|
127
|
+
* sortConfig.defaultSort 会在order为null时生效
|
|
99
128
|
* @param sortOption 列配置
|
|
100
129
|
* @param order 排序方式
|
|
101
130
|
* @param dataSource 排序的数组
|
|
102
131
|
*/
|
|
103
|
-
export function tableSort
|
|
132
|
+
export function tableSort<T extends Record<string, any>>(
|
|
133
|
+
sortOption: SortOption<T>,
|
|
134
|
+
order: Order,
|
|
135
|
+
dataSource: T[],
|
|
136
|
+
sortConfig: SortConfig<T> = {},
|
|
137
|
+
): T[] {
|
|
104
138
|
if (!dataSource?.length) return dataSource || [];
|
|
105
|
-
sortConfig =
|
|
139
|
+
sortConfig = { emptyToBottom: false, ...sortConfig };
|
|
106
140
|
let targetDataSource = [...dataSource];
|
|
141
|
+
let sortField = sortOption.sortField || sortOption.dataIndex;
|
|
142
|
+
|
|
143
|
+
if (!order && sortConfig.defaultSort) {
|
|
144
|
+
// 默认排序
|
|
145
|
+
order = sortConfig.defaultSort.order;
|
|
146
|
+
sortField = sortConfig.defaultSort.dataIndex;
|
|
147
|
+
}
|
|
107
148
|
|
|
108
149
|
if (typeof sortOption.sorter === 'function') {
|
|
109
150
|
const customSorterData = sortOption.sorter(targetDataSource, { order, column: sortOption });
|
|
110
151
|
if (customSorterData) targetDataSource = customSorterData;
|
|
111
152
|
} else if (order) {
|
|
112
|
-
const sortField = sortOption.sortField || sortOption.dataIndex;
|
|
113
153
|
let { sortType } = sortOption;
|
|
114
154
|
if (!sortType) sortType = typeof dataSource[0][sortField] as 'number' | 'string';
|
|
115
155
|
|
|
116
156
|
const [valueArr, emptyArr] = separatedData(sortOption, targetDataSource, sortType === 'number');
|
|
157
|
+
const isNumber = sortType === 'number';
|
|
117
158
|
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
// 非数字当作最小值处理
|
|
121
|
-
if (order === 'asc') {
|
|
122
|
-
valueArr.sort((a, b) => +a[sortField] - +b[sortField]);
|
|
123
|
-
targetDataSource = [...emptyArr, ...valueArr];
|
|
124
|
-
} else {
|
|
125
|
-
valueArr.sort((a, b) => +b[sortField] - +a[sortField]);
|
|
126
|
-
targetDataSource = [...valueArr, ...emptyArr];
|
|
127
|
-
}
|
|
159
|
+
if (order === 'asc') {
|
|
160
|
+
valueArr.sort((a, b) => strCompare(a[sortField], b[sortField], isNumber, sortConfig.stringLocaleCompare));
|
|
128
161
|
} else {
|
|
129
|
-
|
|
130
|
-
if (order === 'asc') {
|
|
131
|
-
valueArr.sort((a, b) => String(a[sortField]).localeCompare(b[sortField]));
|
|
132
|
-
targetDataSource = [...emptyArr, ...valueArr];
|
|
133
|
-
} else {
|
|
134
|
-
valueArr.sort((a, b) => String(a[sortField]).localeCompare(b[sortField]) * -1);
|
|
135
|
-
targetDataSource = [...valueArr, ...emptyArr];
|
|
136
|
-
}
|
|
162
|
+
valueArr.sort((a, b) => strCompare(b[sortField], a[sortField], isNumber, sortConfig.stringLocaleCompare));
|
|
137
163
|
}
|
|
138
164
|
|
|
139
|
-
if (sortConfig.emptyToBottom) {
|
|
165
|
+
if (order === 'desc' || sortConfig.emptyToBottom) {
|
|
140
166
|
targetDataSource = [...valueArr, ...emptyArr];
|
|
167
|
+
} else {
|
|
168
|
+
targetDataSource = [...emptyArr, ...valueArr];
|
|
141
169
|
}
|
|
142
170
|
}
|
|
143
171
|
return targetDataSource;
|
|
@@ -156,8 +184,18 @@ export function howDeepTheHeader(arr: StkTableColumn<any>[], level = 1) {
|
|
|
156
184
|
|
|
157
185
|
/** 获取列宽 */
|
|
158
186
|
export function getColWidth(col: StkTableColumn<any> | null): number {
|
|
159
|
-
|
|
160
|
-
|
|
187
|
+
const val = col?.width ?? Default_Col_Width;
|
|
188
|
+
if (typeof val === 'number') {
|
|
189
|
+
return Math.floor(val);
|
|
190
|
+
}
|
|
191
|
+
return parseInt(val);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** 获取列宽配置。用于支持列宽配置数字 */
|
|
195
|
+
export function getColWidthStr(col: StkTableColumn<any> | null | undefined, key: 'width' | 'minWidth' | 'maxWidth' = 'width') {
|
|
196
|
+
const val = col?.[key];
|
|
197
|
+
if (typeof val === 'number') {
|
|
198
|
+
return val + 'px';
|
|
161
199
|
}
|
|
162
|
-
return
|
|
200
|
+
return val;
|
|
163
201
|
}
|