Content Table

VxeTable 数组结构

VxeTable 主要是支持虚拟滚动。

简单使用

表格的数据为对象的数组,手动固定列 (也可以使用循环)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<vxe-table border :data="tableData">
<vxe-column type="seq" width="60"/>

<!-- 没有使用插槽,列的值自动为行对象中属性为 name 的属性值 -->
<vxe-column field="name" title="Name"/>
<vxe-column field="gender" title="gender"/>
<vxe-column field="age" title="Age"/>
</vxe-table>
</template>

<script setup lang="ts">
const tableData = [
{ id: 10001, name: 'Test1', role: 'Develop', gender: 'Man', age: 28, address: 'test abc' },
{ id: 10002, name: 'Test2', role: 'Test', gender: 'Women', age: 22, address: 'Guangzhou' },
{ id: 10003, name: 'Test3', role: 'PM', gender: 'Man', age: 32, address: 'Shanghai' },
{ id: 10004, name: 'Test4', role: 'Designer', gender: 'Women', age: 24, address: 'Shanghai' }
];
</script>

普通数组

官方自定义插槽文档里给出的例子不一样,特殊场景下表头和数据均为普通数组而不是对象数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<vxe-table :data="data" border>
<vxe-column type="seq" width="60"/>
<vxe-column v-for="(header, idx) in headers" :key="idx">
<template #header>{{ header }}</template>
<template #default="{ row }">{{ row[idx] }}</template>
</vxe-column>
</vxe-table>
</template>

<script setup lang="ts">
// 提示: 表头和数据均使用普通数组。
const headers = ['name', 'id', 'age'];
const data = [
['Alice', 1, 21],
['Bob', 2, 22],
['Rose', 3, 23],
]
</script>

数据变化

1
2
3
4
5
6
7
<vxe-table :data="tableData">
const tableData = []; // 不是 ref()。

<vxe-table ref="tableRef"">
const tableData = []; // 不是 ref()。
const tableRef = ref(null);
tableRef.value.loadData(data);

表格绑定的数据 tableData 是非 ref 时:

  • 从界面上修改单元格的数据后 tableData 中的数据会被修改。
  • tableData.push() 增加新的数据后界面上的表格不会变化,需要调用 tableRef.value.loadData() 进行更新。

表格绑定的数据 tableData 是 ref 时:

  • 界面上变了 tableData 会被修改。
  • tableData 被修改了界面会同步更新。

表格编辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
<!--
为了方便理解,做如下约定:
- idx: 行数组中列的下标,从 0 开始。
- columnIndex: Vxe-Table 的事件函数参数中列的下标,从 1 开始,减 1 才是数组下标。

编辑逻辑:
- 表格有只读模式和编辑模式,调用函数 toggleEdit() 切换只读模式和编辑模式。
- 单元格是否可编辑:
- vxe-column 的 edit-render 决定了列是否可编辑。
- 列可编辑时,通过 vxe-table 的 edit-config 中的 beforeEditMethod 函数进行判断此列的单元格是否可编辑。
- 有些列可编辑,但此列中某些单元格的业务数据决定了单元格不可编辑。
- 编辑模式下,点击单元格激活激活单元格编辑框,此时会调用函数 onEditActive(),在其中创建单元格编辑记录,由于此时单元格的内容还没有变化,故其 edited 为 false,并备份单元格数据。
- 编辑记录使用 removedRows 和 editedCells 进行保存和恢复,不直接记录在表格的数据 data 中。
- 单元格内容变化后,调用函数 onCellValueChanged(),如果单元格当前的内容和最初备份的值不同则标记 edited 为 true,否则为 false。
- 单元格编辑框中按下回车或者失去焦点,调用函数 endEdit() 结束编辑。
- 单元格编辑框中按下 ESC,调用函数 redoCellEdit() 使用编辑前的值恢复单元格的值。
- 撤销编辑:
- 清空 Set removedRows。
- 使用 editedCells 中备份的数据恢复表格的数据 data。

提示:
- 更新表格样式: tableRef.value.refreshColumn()。
-->
<template>
<vxe-table
ref="tableRef"
:edit-config="{ trigger: 'click', mode: 'cell', beforeEditMethod: isCellEditable }"
:cell-class-name="cellClassName"
:row-class-name="rowClassName"
:scroll-y="{ enabled: true }"
border
@edit-actived="onEditActive"
>
<!-- 序号列 -->
<vxe-column type="seq" width="100" show-overflow>
<template #default="{ seq, row }">
{{ seq }} <button v-show="editable" @click="toggleRemoveRow(row)">删除</button>
</template>
</vxe-column>

<!-- 数据列 -->
<vxe-column v-for="(header, idx) in headers" :key="idx" :visible="idx !== 0" :edit-render="columnEditRender(header, idx)">
<template #header>{{ header }}</template>
<template #default="{ row }">{{ row[idx] }}</template>
<template #edit="{ row }">
<input v-model="row[idx]" type="text" @change="onCellValueChanged(row, idx)" @keyup.enter="endEdit" @keyup.esc="redoCellEdit(row, idx)">
</template>
</vxe-column>
</vxe-table>

<button @click="toggleEdit">Toggle Edit</button>
<button @click="outputEditedCells">显输出编辑过的单元格</button>
<button @click="appendData">增加数据</button>
<button @click="cancelEdit">取消编辑</button>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import type { VxeTablePropTypes } from 'vxe-table';

const tableRef = ref<any>();
const editable = ref<boolean>(true);

// 提示: 表头和数据均使用普通数组。
const headers = ['uid', 'id', 'name', 'age'];

// 第一列是行唯一的 ID,为了定位行使用,表格中修改数据后 data 的数据会同步。
const data = [
['uid-1', 1, 'Alice', 21],
['uid-2', 2, 'Bob', 22],
['uid-3', 3, 'Rose', 23],
];

// 修改过的单元格坐标 <row_uid => <idx => EditedCell>>。
const editedCells: Map<string, Map<number, EditedCell>> = new Map();

// 被删除的行坐标 (row_uid)。
const removedRows: Set<string> = new Set();

onMounted(() => {
// 加载数据。
tableRef.value.reloadData(data);
});

// 在最后面增加数据。
let sn = 4;
function appendData() {
// data.push(['uid-4', 4, 'Jack', 24]);
sn++;
data.push([`uid-${sn}`, sn, 'Other', 20 + sn]);
tableRef.value.loadData(data);
}

// 输出变更过的单元格。
function outputEditedCells() {
console.log(editedCells);
console.log(removedRows);
}

/**
* 切换只读模式和编辑模式。
*/
function toggleEdit(): void {
editable.value = !editable.value;
}

/**
* 根据行的数据和列的位置判断单元格是否可编辑。
*
* @param row 表格的行。
* @param columnIndex VxeTable 回调函数的列下标,从 1 开始。
*/
function isCellEditable({ row, columnIndex }: any): boolean {
// 被删除的列不可编辑。
const rowUuid = buildRowUuid(row);
if (removedRows.has(rowUuid)) {
return false;
}

return true;
}

/**
* 计算列的编辑 render,如果其 enabled 属性为 true 表示列可编辑,为 false 表示列不可编辑。
*
* @param header 列的表头。
* @param idx 列的下标。
* @returns 单元格的 render。
*/
function columnEditRender(header: string, idx: number) {
// 只读模式、最前面的 uuid 列不可编辑。
if (!editable.value || idx === 0 ) {
return { enabled: false };
}

// 主键列不可编辑。
if (idx === 1) {
return { enabled: false };
}

return { enabled: true };
}

/**
* 单元格内容变化后更新编辑状态。
*
* @param row 表格的行。
* @param idx 列的下标。
*/
function onCellValueChanged(row: any[], idx: number): void {
/*
逻辑:
查找到行被编辑过的单元格,设置单元格为是否被编辑过。
*/
const cell: EditedCell | null = findEditedCell(row, idx);
if (cell) {
// 数字编辑后得到的值是字符串,和原始的数字比永远不会相等,所以需要把原始值转为字符串后再进行比较。
cell.edited = (cell.originalValue+'') !== row[idx];
}
}

/**
* 结束编辑。
*/
function endEdit(): void {
tableRef.value.clearEdit();
}

/**
* 回退单元格的编辑。
*
* @param row 表格的行。
* @param idx 列的下标。
*/
function redoCellEdit(row: any[], idx: number): void {
// 使用 previousValue 进行恢复。
const cell: EditedCell | null = findEditedCell(row, idx);
if (cell) {
row[idx] = cell.previousValue;
}

tableRef.value.loadData(data);
}

/**
* 取消编辑,恢复数据到编辑前。
*/
function cancelEdit(): void {
/*
逻辑:
1. 清空删除列。
2. 遍历所有编辑过的单元格,使用 originalValue 进行恢复其数据,并且清空编辑的单元格。
3. 重新加载表格的数据。
*/

// [1] 清空删除列。
removedRows.clear();

// [2] 遍历所有编辑过的单元格,使用 originalValue 进行恢复其数据,并且清空编辑的单元格。
for (const [rowUuid, rowCellsMap] of editedCells.entries()) {
const row = data.find(row => row[0] === rowUuid);
if (row) {
for (const [idx, cell] of rowCellsMap.entries()) {
row[idx] = cell.originalValue;
}
}
}
editedCells.clear();

// [3] 重新加载表格的数据。
tableRef.value.loadData(data);
}

/**
* 删除行。
*
* @param row 表格的行。
*/
function toggleRemoveRow(row: any[]): void {
const rowUuid = buildRowUuid(row);

// 没有则添加,有则删除。
if (removedRows.has(rowUuid)) {
removedRows.delete(rowUuid);
} else {
removedRows.add(rowUuid);
}

tableRef.value.refreshColumn();
}

/**
* 编辑框激活,备份数据。
*/
function onEditActive({ row, columnIndex }: any) {
/*
逻辑:
1. 查找单元格。
2. 找到则备份编辑前数据。
3. 找不到则创建单元格并且备份原始值和备份编辑器前的数据。
*/

// [1] 查找单元格。
const idx = columnIndex - 1;
const cell = findEditedCell(row, idx);

// [2] 找到则备份编辑前数据。
if (cell) {
cell.previousValue = row[idx];
return;
}

// [3] 找不到则创建单元格并且备份原始值和备份编辑器前的数据。
const rowUuid = buildRowUuid(row);

// 所在行数据不存在则创建。
if (!editedCells.get(rowUuid)) {
editedCells.set(rowUuid, new Map());
}

// 创建单元格,备份原始值和备份编辑器前的数据。
const rowMap: Map<number, EditedCell> = editedCells.get(rowUuid)!;
rowMap.set(idx, {
edited: false,
originalValue: row[idx],
previousValue: row[idx]
});
}

/**
* 查找行中下标为传入的 idx 被编辑过的单元格。
*
* @param row 表格的行。
* @param idx 列的下标。
*/
function findEditedCell(row: any[], idx: number): EditedCell | null {
const rowUuid = buildRowUuid(row);
return editedCells.get(rowUuid)?.get(idx) || null;
}

/**
* 构建行的 Uuid。
*
* @param row 表格的行。
* @returns 返回行的 Uuid。
*/
function buildRowUuid(row: any[]): string {
return row[0];
}

/**
* 单元格的样式,编辑过的单元格返回 class 'col--dirty'。
*/
const cellClassName: VxeTablePropTypes.CellClassName<any> = ({ row, columnIndex }) => {
const idx = columnIndex - 1;
const cell: EditedCell | null = findEditedCell(row, idx);

if (cell?.edited) {
return 'col--dirty';
}

return null;
}

/**
* 行的样式,被删除的行进行高亮。
*/
const rowClassName: VxeTablePropTypes.RowClassName<any> = ({ row }) => {
const rowUuid = buildRowUuid(row);
if (removedRows.has(rowUuid)) {
return 'row--removed';
}

return null;
}

/**
* 被编辑过的单元格类型。
*/
interface EditedCell {
edited : boolean; // 是否被编辑过。
originalValue: any; // 最初的值 (点击撤销编辑时使用这个值进行恢复)。
previousValue: any; // 本次编辑前的值 (一个单元格可能被编辑多次,按下 ESC 时使用这个值进行恢复)。
}
</script>

<style>
.row--removed {
background-color: rgba(255, 0, 0, 0.1);
}

button {
margin-top: 10px;
margin-right: 10px;
height: 25px;
display: inline-flex;
align-items: center;
}
</style>