Ctrl+鼠标左键(不连续选择):
Shift+鼠标左键(连续选择):
批量勾选:
效果:
监听row-click,判断点击行是否为当前行,若是调用setCurrentRow(),反之调用setCurrentRow(row)
思路:获取el-table实例的columns属性,里面包含了每一列的列标题(label)
效果:
<template>
<el-table class="uiot-table"
:class="{'has-rowcheckbox':showRowCheckbox}"
ref="table"
v-loading="loading"
:row-key="rowKey"
:border="border"
:default-sort="defaultSort"
:highlight-current-row="highlightCurrentRow"
:data="tableRows"
:header-row-style="customHeadRowStyle"
:header-cell-style="customHeadCellStyle"
:cell-style="customCellStyle"
:row-style="customRowStyle"
:height="height"
:max-height="maxHeight"
:cell-class-name="customCellClassName"
:row-class-name="customRowClassName"
:current-row-key.sync="currentRowKey"
v-bind="$attrs"
v-on="$listeners"
@sort-change="handleSortChange"
@header-click="handleHeaderClick"
@row-click="handleRowClick"
@row-dblclick="handleRowDBlClick"
@current-change="handleCurrentRowChange"
@select="handleRowCheck"
@selection-change="handleSelectionChange">
<template v-if="$scopedSlots.default||(columns&&columns.length>0)">
<el-table-column v-if="showRowCheckbox"
type="selection"
width="50px"
align="center">
</el-table-column>
<el-table-column v-if="showLinenum"
type="index"
label="No."
header-align="center"
align="center"
:width="$idxNoWidth"
:index="getRowIndex">
</el-table-column>
</template>
<!-- 如果有slot则渲染slot -->
<template v-if="!$scopedSlots.default&&columns">
<el-table-column v-for="(col,index) of columns"
:key="index"
v-bind="col" />
</template>
<slot v-else></slot>
</el-table>
</template>
<script>
import { exportTo } from '@/utils/exportTo'
function parseOrder(order) {
return typeof order === 'boolean' ? (order ? 'ascending' : 'descending') : order
}
const computed = {
/** @returns {Vue} */
table() {
return this.$refs.table
},
border() {
return this.tableStyle == 'normal'
},
/** @this {number} */
outsideHeight() {
return this.$el.clientHeight
},
clearSelection() {
return this.table?.clearSelection
},
toggleRowSelection() {
return this.table?.toggleRowSelection
},
toggleAllSelection() {
return this.table?.toggleAllSelection
},
toggleRowExpansion() {
return this.table?.toggleRowExpansion
},
clearSort() {
return this.table?.clearSort
},
clearFilter() {
return this.table?.clearFilter
},
doLayout() {
return this.table?.doLayout
},
store() {
return this.table?.store
},
// el-table内部的currentRow
_currentRow() {
return this.store?.states.currentRow
},
/** @returns {any[]} */
_rowSelection() {
return this.store?.states.selection
},
}
export default {
inheritAttrs: false,
name: 'UiotTable',
props: {
/** 是否显示loading */
loading: {
type: Boolean,
default: false,
},
/** 自定义列,优先级小于slot.default */
columns: Array,
/** 数据源 */
tableRows: {
type: Array,
default: () => []
},
/** 表格排序参数 */
tableSort: Object,
/** 行数据主键属性 */
rowKey: {
type: String,
default: 'id'
},
/** 当前页码 */
pageIndex: Number,
/** 页大小 */
pageSize: Number,
/** 当前选中行,支持双向绑定*/
currentRow: Object,
/** 是否可以反选当前行,即点击当前行时取消选中状态 */
reverseCurrentRow: Boolean,
/** 是否显示行号 */
showLinenum: {
type: Boolean,
default: true,
},
/** 是否在每行前面显示复选框 */
showRowCheckbox: {
type: Boolean,
default: false,
},
/** 表格风格,用于控制样式 */
tableStyle: {
type: String,
validator: value => ['normal', 'concise'].indexOf(value) > -1
},
/** 原生属性 */
size: {
type: String,
default: 'mini',
},
/** 原生属性 */
height: {
type: [String, Number],
default: '100%',
},
/** 原生属性 */
maxHeight: {
type: [String, Number],
default: '100%',
},
/** 原生属性,高亮当前行 */
highlightCurrentRow: {
type: Boolean,
default: false,
},
/** 原生属性[只写] -> ps: 设置此属性只会在外观上改变当前行 */
currentRowKey: [String, Number],
/** 当前勾选行集合,支持双向绑定 */
rowSelection: Array,
/** 原生属性 */
cellStyle: Function,
/** 原生属性 */
rowStyle: Function,
/** 原生属性 */
rowClassName: Function,
/** 原生属性 */
cellClassName: Function,
},
created() {
// debounce请自己实现
this.rowClickDeb = this.$utils.debounce((row, column, event) => {
this.$emit('_row-click', row, column, event)
}, 300)
},
mounted() {
if (this.tableSort) {
this.setDefaultSort(this.tableSort.field || '', this.tableSort.order)
}
console.log('uiot-table -> mounted', this.table, this.store)
},
data() {
return {
rowClickDeb: null,
defaultSort: {
prop: '',
order: 'ascending',
},
lastTableRows: null,
cacheData: {
// 此变量的作用是在handleRowClick事件后才更新当前行,目前仅供reverseCurrentRow功能使用
// 因为el-table在rowclick事件触发时,内部先处理了当前行,然后再抛出此事件,这样的话参数中的row永远都是当前行
currentRow: null,
lastRow: null,
pageIndex: 1,
pageSize: 1
},
selectedRows: [],
selectedRange: [-1, -1],
}
},
computed: computed,
watch: {
'tableSort.default'(nval) {
// 备份当前sort
const { field, order } = this.tableSort
// 获取到默认sort
this.tableSort.default()
// 设置默认sort
this.setDefaultSort(this.tableSort.field, this.tableSort.order)
// 还原sort
this.tableSort.field = field
this.tableSort.field = order
},
showLinenum(val) {
if (val) {
if (typeof this.pageIndex != 'number') throw '如果显示行号,必须传入"pageIndex"属性'
if (typeof this.pageSize != 'number') throw '如果显示行号,必须传入"pageSize"属性'
}
},
tableRows(nval, oval) {
// 如果有当前行,则清除
if (this.cacheData.currentRow) {
this.setCurrentRow()
}
this.$nextTick(() => {
if (this.doLayout) this.doLayout()
// 只有数据源改变了才去更新页码和页大小,否则还在loading状态,行号就已经变了,视觉效果不好
this.cacheData.pageIndex = this.pageIndex || 1
this.cacheData.pageSize = this.pageSize || 1
})
},
currentRow(nval, oval) {
if (nval != this.cacheData.currentRow) {
// 这里一定要nextTick,因为在watch表格数据源变化时,会清除当前行
this.$nextTick(() => this.setCurrentRow(nval))
}
}
},
methods: {
// #region 方法
scrollTo() {
this.table?.$refs.bodyWrapper?.scrollTo(...arguments)
},
sort(prop, order) {
return this.table?.sort(prop, parseOrder(order))
},
setDefaultSort(prop, order) {
this.defaultSort.prop = prop ?? ''
this.defaultSort.order = parseOrder(order)
},
/**
* 重写setCurrentRow方法,请勿调用el-table提供的setCurrentRow
*/
setCurrentRow(row = null) {
if (this.cacheData.currentRow != row) {
this.cacheData.lastRow = this.cacheData.currentRow
this.cacheData.currentRow = row
if (this.currentRow != this.cacheData.currentRow) {
this.$emit('update:currentRow', this.cacheData.currentRow)
}
this.$emit('_current-change', this.cacheData.currentRow, this.cacheData.lastRow)
console.log('表格当前行改变 -> ', 'currentRow', this.cacheData.currentRow, 'lastRow', this.cacheData.lastRow)
}
if (this._currentRow != row) {
// 如果el-table内部的当前行不是传入的row,则更新一下
this.table.setCurrentRow(row)
}
},
// #endregion
// #region 事件处理
handleRowCheck(selection, row) {
if (this.showRowCheckbox && this.selectedRows.length > 0 && this.selectedRows.includes(row)) {
const check = selection.includes(row)
for (const row of this.selectedRows) {
this.toggleRowSelection(row, check)
}
}
},
/**
* @param {MouseEvent} event
*/
handleRowClick(row, column, event) {
event.preventDefault()
console.log('row-click -> ', row, column, event)
this.rowClickDeb(row, column, event)
if (this.showRowCheckbox) {
if (!this.tableRows && this.selectedRows.length > 0) {
this.selectedRows = []
return
}
if (event.shiftKey && this.cacheData.currentRow) {
// 找出上一次点击行与当前点击行的序号
this.selectedRows = []
window.getSelection().removeAllRanges()
let startNo = this.tableRows.findIndex((item) =>
this.cacheData.currentRow && item === this.cacheData.currentRow
)
let endNo = this.tableRows.findIndex((item) => item === row)
if (startNo != -1 && endNo != -1) {
// 正序一下
if (startNo > endNo) {
let temp = endNo
endNo = startNo
startNo = temp
}
for (let i = startNo; i <= endNo; i++) {
this.selectedRows.push(this.tableRows[i])
}
console.log('多选', startNo, endNo, this.selectedRows)
// 多选不需要设置当前行
return
}
}
else if (event.ctrlKey) {
if (this.selectedRows.includes(row)) {
this.selectedRows.$remove(row)
}
else {
this.selectedRows.push(row)
}
this.table.setCurrentRow()
return
// if()
// this.toggleRowSelection(row)
}
}
if (this.selectedRows.length > 0) this.selectedRows = []
if (this.reverseCurrentRow && row === this.cacheData.currentRow) {
this.setCurrentRow()
}
else {
this.setCurrentRow(row)
}
},
handleRowDBlClick() {
this.rowClickDeb.cancel()
console.log('row-dblclick -> ')
},
handleHeaderClick() {
},
handleCurrentRowChange(newRow, oldRow) {
// this.cacheData.lastRow = oldRow
// if (this.currentRow != newRow) {
// this.$emit('update:currentRow', newRow)
// }
// console.log('表格当前行改变 -> ', newRow, oldRow)
},
handleSortChange({ column, prop, order }) {
console.log(column, prop, order)
if (typeof this.tableSort == 'object') {
if (order) {
this.tableSort.field = prop
this.tableSort.order = order === 'descending' ? false : true
}
else if (typeof this.tableSort.default == 'function') {
this.tableSort.default()
}
else {
this.tableSort.field = this.defaultSort.prop
this.tableSort.order = this.defaultSort.order === 'descending' ? false : true
}
}
// this.$emit('sort-change', ...arguments)
},
handleSelectionChange(val) {
if (typeof this.rowSelection != 'object') throw '如果开启了"showRowCheckbox", 必须传入"rowSelection"'
this.$emit('update:rowSelection', val)
},
// #endregion
getRowIndex(index) {
return index + 1 + (this.cacheData.pageIndex - 1) * this.cacheData.pageSize //this.loading ? index + 1 + (this.lastPageIndex - 1) * this.lastPageSize : index + 1 + (this.pageIndex - 1) * this.pageSize
},
/**
* 获取table的列头文本(获取到的是i18n翻译后的)
*/
getColumnLabelMap() {
const columnLabelMap = {}
for (const col of this.table.columns) {
if ((!this.showLinenum && col.type == 'index') || col.type === 'selection') continue
if (col.property && col.label) {
columnLabelMap[col.property] = col.label
}
}
return columnLabelMap
},
exportTo({ showLinenum = false, columnMap, name }) {
if (this.tableRows && this.tableRows.length > 0 && this.table.columns.length > 0) {
columnMap = columnMap ? columnMap : this.getColumnLabelMap()
// 替换列标题
const data = this.tableRows.map((row) => {
const temp = {}
for (const key in columnMap) {
// label : value
temp[columnMap[key]] = row[key]
}
return temp
})
exportTo({
data: data,
dataType: 'json',
fileName: name
})
}
},
// #region 样式、类重载
customCellClassName({ row, column, rowIndex, columnIndex }) {
let clsname = ''
if (this.cellClassName) clsname += this.cellClassName(arguments[0]) || ''
return clsname.trim()
},
customRowClassName({ row, rowIndex }) {
let clsname = ''
if (this.showRowCheckbox) {
// if (this._rowSelection && this._rowSelection.indexOf(row) != -1) {
// clsname += ' is-checked'
// }
// if (rowIndex >= this.selectedRange[0] && rowIndex <= this.selectedRange[1]) {
// 如果是选中行,添加css类
if (this.selectedRows.includes(row)) {
clsname += ' is-selected'
}
}
if (this.rowClassName) clsname += this.rowClassName(arguments[0]) || ''
return clsname.trim()
},
// #region 表格样式
customHeadCellStyle({ row, column, rowIndex, columnIndex }) {
switch (this.tableStyle) {
case 'concise':
return 'padding:0px;height:30px;'
case 'normal':
default:
return {
background: '#f0f0f0',
color: '#505050',
padding: 0,
height: '34px'
}
}
},
customHeadRowStyle({ row, rowIndex }) {
switch (this.tableStyle) {
case 'concise':
case 'normal':
default:
return ''
}
},
customRowStyle({ row, rowIndex }) {
switch (this.tableStyle) {
case 'concise':
return ''
case 'normal':
default:
return ''
}
},
customCellStyle({ row, column, rowIndex, columnIndex }) {
switch (this.tableStyle) {
case 'concise':
return { padding: '2px 0 2px 0' }
case 'normal':
default:
return { padding: '3px 0 3px 0' }
}
},
// #endregion
}
}
</script>
<style lang='scss'>
.uiot-table {
}
</style>