UNPKG

vxe-table

Version:

一个基于 vue 的 PC 端表格组件,支持增删改查、虚拟滚动、懒加载、快捷菜单、数据校验、树形结构、打印导出、表单渲染、数据分页、虚拟列表、模态窗口、自定义模板、渲染器、贼灵活的配置项、扩展接口等...

1,549 lines (1,532 loc) 143 kB
import XEUtils from 'xe-utils/ctor' import GlobalConfig from '../../conf' import Cell from './cell' import VXETable from '../../v-x-e-table' import { UtilTools, DomTools } from '../../tools' import { clearTableAllStatus } from './util' const { getRowid, getRowkey, setCellValue, getCellLabel, hasChildrenList, getColumnList } = UtilTools const { browse, calcHeight, hasClass, addClass, removeClass, getEventTargetNode } = DomTools const isWebkit = browse['-webkit'] && !browse.edge const debounceScrollYDuration = browse.msie ? 40 : 20 const resizableStorageKey = 'VXE_TABLE_CUSTOM_COLUMN_WIDTH' const visibleStorageKey = 'VXE_TABLE_CUSTOM_COLUMN_VISIBLE' /** * 生成行的唯一主键 */ function getRowUniqueId () { return XEUtils.uniqueId('row_') } /** * 单元格的值为:'' | null | undefined 时都属于空值 */ function eqCellNull (cellValue) { return cellValue === '' || XEUtils.eqNull(cellValue) } function eqCellValue (row1, row2, field) { const val1 = XEUtils.get(row1, field) const val2 = XEUtils.get(row2, field) if (eqCellNull(val1) && eqCellNull(val2)) { return true } if (XEUtils.isString(val1) || XEUtils.isNumber(val1)) { /* eslint-disable eqeqeq */ return val1 == val2 } return XEUtils.isEqual(val1, val2) } function getNextSortOrder (_vm, column) { const orders = _vm.sortOpts.orders const currOrder = column.order || null const oIndex = orders.indexOf(currOrder) + 1 return orders[oIndex < orders.length ? oIndex : 0] } function getCustomStorageMap (key) { const version = GlobalConfig.version const rest = XEUtils.toStringJSON(localStorage.getItem(key)) return rest && rest._v === version ? rest : { _v: version } } function getRecoverRow (_vm, list) { const { fullAllDataRowMap } = _vm return list.filter(row => fullAllDataRowMap.has(row)) } function handleReserveRow (_vm, reserveRowMap) { const { fullDataRowIdData } = _vm const reserveList = [] XEUtils.each(reserveRowMap, (item, rowid) => { if (fullDataRowIdData[rowid] && reserveList.indexOf(fullDataRowIdData[rowid].row) === -1) { reserveList.push(fullDataRowIdData[rowid].row) } }) return reserveList } function computeVirtualX (_vm) { const { $refs, visibleColumn } = _vm const { tableBody } = $refs const tableBodyElem = tableBody ? tableBody.$el : null if (tableBodyElem) { const { scrollLeft, clientWidth } = tableBodyElem const endWidth = scrollLeft + clientWidth let toVisibleIndex = -1 let cWidth = 0 let visibleSize = 0 for (let colIndex = 0, colLen = visibleColumn.length; colIndex < colLen; colIndex++) { cWidth += visibleColumn[colIndex].renderWidth if (toVisibleIndex === -1 && scrollLeft < cWidth) { toVisibleIndex = colIndex } if (toVisibleIndex >= 0) { visibleSize++ if (cWidth > endWidth) { break } } } return { toVisibleIndex: Math.max(0, toVisibleIndex), visibleSize: Math.max(8, visibleSize) } } return { toVisibleIndex: 0, visibleSize: 8 } } function computeVirtualY (_vm) { const { $refs, vSize, rowHeightMaps } = _vm const { tableHeader, tableBody } = $refs const tableBodyElem = tableBody ? tableBody.$el : null if (tableBodyElem) { const tableHeaderElem = tableHeader ? tableHeader.$el : null let rowHeight = 0 let firstTrElem firstTrElem = tableBodyElem.querySelector('tr') if (!firstTrElem && tableHeaderElem) { firstTrElem = tableHeaderElem.querySelector('tr') } if (firstTrElem) { rowHeight = firstTrElem.clientHeight } if (!rowHeight) { rowHeight = rowHeightMaps[vSize || 'default'] } const visibleSize = Math.max(8, Math.ceil(tableBodyElem.clientHeight / rowHeight) + 2) return { rowHeight, visibleSize } } return { rowHeight: 0, visibleSize: 8 } } function calculateMergerOffserIndex (list, offsetItem, type) { for (let mcIndex = 0, len = list.length; mcIndex < len; mcIndex++) { const mergeItem = list[mcIndex] const { startIndex, endIndex } = offsetItem const mergeStartIndex = mergeItem[type] const mergeSpanNumber = mergeItem[type + 'span'] const mergeEndIndex = mergeStartIndex + mergeSpanNumber if (mergeStartIndex < startIndex && startIndex < mergeEndIndex) { offsetItem.startIndex = mergeStartIndex } if (mergeStartIndex < endIndex && endIndex < mergeEndIndex) { offsetItem.endIndex = mergeEndIndex } if (offsetItem.startIndex !== startIndex || offsetItem.endIndex !== endIndex) { mcIndex = -1 } } } function setMerges (_vm, merges, mList, rowList) { if (merges) { const { treeConfig, visibleColumn } = _vm if (treeConfig) { throw new Error(UtilTools.getLog('vxe.error.noTree', ['merge-footer-items'])) } if (!XEUtils.isArray(merges)) { merges = [merges] } merges.forEach(item => { let { row, col, rowspan, colspan } = item if (rowList && XEUtils.isNumber(row)) { row = rowList[row] } if (XEUtils.isNumber(col)) { col = visibleColumn[col] } if ((rowList ? row : XEUtils.isNumber(row)) && col && (rowspan || colspan)) { rowspan = XEUtils.toNumber(rowspan) || 1 colspan = XEUtils.toNumber(colspan) || 1 if (rowspan > 1 || colspan > 1) { const mcIndex = XEUtils.findIndexOf(mList, item => item._row === row && item._col === col) const mergeItem = mList[mcIndex] if (mergeItem) { mergeItem.rowspan = rowspan mergeItem.colspan = colspan mergeItem._rowspan = rowspan mergeItem._colspan = colspan } else { const mergeRowIndex = rowList ? rowList.indexOf(row) : row const mergeColIndex = visibleColumn.indexOf(col) mList.push({ row: mergeRowIndex, col: mergeColIndex, rowspan, colspan, _row: row, _col: col, _rowspan: rowspan, _colspan: colspan }) } } } }) } } function removeMerges (_vm, merges, mList, rowList) { const rest = [] if (merges) { const { treeConfig, visibleColumn } = _vm if (treeConfig) { throw new Error(UtilTools.getLog('vxe.error.noTree', ['merge-cells'])) } if (!XEUtils.isArray(merges)) { merges = [merges] } merges.forEach(item => { let { row, col } = item if (rowList && XEUtils.isNumber(row)) { row = rowList[row] } if (XEUtils.isNumber(col)) { col = visibleColumn[col] } const mcIndex = XEUtils.findIndexOf(mList, item => item._row === row && item._col === col) if (mcIndex > -1) { const rItems = mList.splice(mcIndex, 1) rest.push(rItems[0]) } }) } return rest } function clearAllSort (_vm) { _vm.tableFullColumn.forEach((column) => { column.order = null }) } const Methods = { /** * 获取父容器元素 */ getParentElem () { return this.$xegrid ? this.$xegrid.$el.parentNode : this.$el.parentNode }, /** * 获取父容器的高度 */ getParentHeight () { return this.$xegrid ? this.$xegrid.getParentHeight() : this.getParentElem().clientHeight }, /** * 获取需要排除的高度 * 但渲染表格高度时,需要排除工具栏或分页等相关组件的高度 * 如果存在表尾合计滚动条,则需要排除滚动条高度 */ getExcludeHeight () { return this.$xegrid ? this.$xegrid.getExcludeHeight() : 0 }, /** * 重置表格的一切数据状态 */ clearAll () { return clearTableAllStatus(this) }, /** * 同步 data 数据 * 如果用了该方法,那么组件将不再记录增删改的状态,只能自行实现对应逻辑 * 对于某些特殊的场景,比如深层树节点元素发生变动时可能会用到 */ syncData () { return this.$nextTick().then(() => { this.tableData = [] return this.$nextTick().then(() => this.loadTableData(this.tableFullData)) }) }, /** * 手动处理数据 * 对于手动更改了排序、筛选...等条件后需要重新处理数据时可能会用到 */ updateData () { return this.handleTableData(true).then(this.updateFooter).then(this.recalculate) }, handleTableData (force) { const { scrollYLoad, scrollYStore } = this const fullData = force ? this.updateAfterFullData() : this.afterFullData this.tableData = scrollYLoad ? fullData.slice(scrollYStore.startIndex, scrollYStore.endIndex) : fullData.slice(0) return this.$nextTick() }, /** * 加载表格数据 * @param {Array} datas 数据 */ loadTableData (datas) { const { keepSource, treeConfig, editStore, sYOpts, scrollYStore, scrollXStore } = this const tableFullData = datas ? datas.slice(0) : [] const scrollYLoad = !treeConfig && sYOpts.gt > -1 && sYOpts.gt < tableFullData.length scrollYStore.startIndex = 0 scrollYStore.endIndex = 1 scrollXStore.startIndex = 0 scrollXStore.endIndex = 1 editStore.insertList = [] editStore.removeList = [] // 全量数据 this.tableFullData = tableFullData // 缓存数据 this.updateCache(true) // 原始数据 this.tableSynchData = datas if (keepSource) { this.tableSourceData = XEUtils.clone(tableFullData, true) } this.scrollYLoad = scrollYLoad if (scrollYLoad) { if (!(this.height || this.maxHeight)) { UtilTools.error('vxe.error.reqProp', ['height | max-height']) } if (!this.showOverflow) { UtilTools.warn('vxe.error.reqProp', ['show-overflow']) } if (this.spanMethod) { UtilTools.warn('vxe.error.scrollErrProp', ['span-method']) } } this.clearMergeCells() this.clearMergeFooterItems() this.handleTableData(true) this.updateFooter() return this.computeScrollLoad().then(() => { // 是否加载了数据 if (scrollYLoad) { scrollYStore.endIndex = scrollYStore.visibleSize } this.handleReserveStatus() this.checkSelectionStatus() return this.$nextTick().then(() => this.recalculate()).then(() => this.refreshScroll()) }) }, /** * 重新加载数据,不会清空表格状态 * @param {Array} datas 数据 */ loadData (datas) { return this.loadTableData(datas).then(() => { if (!this.inited) { this.inited = true this.handleDefaults() } this.recalculate() }) }, /** * 重新加载数据,会清空表格状态 * @param {Array} datas 数据 */ reloadData (datas) { return this.clearAll() .then(() => { this.inited = true return this.loadTableData(datas) }) .then(this.handleDefaults) }, /** * 局部加载行数据并恢复到初始状态 * 对于行数据需要局部更改的场景中可能会用到 * @param {Row} row 行对象 * @param {Object} record 新数据 * @param {String} field 字段名 */ reloadRow (row, record, field) { const { keepSource, tableSourceData, tableData } = this if (keepSource) { const rowIndex = this.getRowIndex(row) const oRow = tableSourceData[rowIndex] if (oRow && row) { if (field) { XEUtils.set(oRow, field, XEUtils.get(record || row, field)) } else { if (record) { tableSourceData[rowIndex] = record XEUtils.clear(row, undefined) Object.assign(row, this.defineField(Object.assign({}, record))) this.updateCache(true) } else { XEUtils.destructuring(oRow, XEUtils.clone(row, true)) } } } this.tableData = tableData.slice(0) } else { UtilTools.warn('vxe.error.reqProp', ['keep-source']) } return this.$nextTick() }, /** * 加载列配置 * 对于表格列需要重载、局部递增场景下可能会用到 * @param {ColumnInfo} columns 列配置 */ loadColumn (columns) { const collectColumn = XEUtils.mapTree(columns, column => Cell.createColumn(this, column)) this.handleColumn(collectColumn) return this.$nextTick() }, /** * 加载列配置并恢复到初始状态 * 对于表格列需要重载、局部递增场景下可能会用到 * @param {ColumnInfo} columns 列配置 */ reloadColumn (columns) { this.clearAll() return this.loadColumn(columns) }, handleColumn (collectColumn) { this.collectColumn = collectColumn const tableFullColumn = getColumnList(collectColumn) this.tableFullColumn = tableFullColumn this.cacheColumnMap() this.restoreCustomStorage() this.refreshColumn().then(() => { if (this.scrollXLoad) { this.loadScrollXData(true) } }) this.clearMergeCells() this.clearMergeFooterItems() this.handleTableData(true) if ((this.scrollXLoad || this.scrollYLoad) && this.expandColumn) { UtilTools.warn('vxe.error.scrollErrProp', ['column.type=expand']) } this.$nextTick(() => { if (this.$toolbar) { this.$toolbar.syncUpdate({ collectColumn, $table: this }) } }) }, /** * 更新数据行的 Map * 牺牲数据组装的耗时,用来换取使用过程中的流畅 */ updateCache (source) { const { treeConfig, treeOpts, tableFullData, fullDataRowMap, fullAllDataRowMap } = this let { fullDataRowIdData, fullAllDataRowIdData } = this const rowkey = getRowkey(this) const isLazy = treeConfig && treeOpts.lazy const handleCache = (row, index, items, path, parent) => { let rowid = getRowid(this, row) if (!rowid) { rowid = getRowUniqueId() XEUtils.set(row, rowkey, rowid) } if (isLazy && row[treeOpts.hasChild] && XEUtils.isUndefined(row[treeOpts.children])) { row[treeOpts.children] = null } const rest = { row, rowid, index: treeConfig && parent ? -1 : index, items, parent } if (source) { fullDataRowIdData[rowid] = rest fullDataRowMap.set(row, rest) } fullAllDataRowIdData[rowid] = rest fullAllDataRowMap.set(row, rest) } if (source) { fullDataRowIdData = this.fullDataRowIdData = {} fullDataRowMap.clear() } fullAllDataRowIdData = this.fullAllDataRowIdData = {} fullAllDataRowMap.clear() if (treeConfig) { XEUtils.eachTree(tableFullData, handleCache, treeOpts) } else { tableFullData.forEach(handleCache) } }, appendTreeCache (row, childs) { const { keepSource, tableSourceData, treeOpts, fullDataRowIdData, fullDataRowMap, fullAllDataRowMap, fullAllDataRowIdData } = this const { children, hasChild } = treeOpts const rowkey = getRowkey(this) const rowid = getRowid(this, row) let matchObj if (keepSource) { matchObj = XEUtils.findTree(tableSourceData, item => rowid === getRowid(this, item), treeOpts) } XEUtils.eachTree(childs, (row, index, items, path, parent) => { let rowid = getRowid(this, row) if (!rowid) { rowid = getRowUniqueId() XEUtils.set(row, rowkey, rowid) } if (row[hasChild] && XEUtils.isUndefined(row[children])) { row[children] = null } const rest = { row, rowid, index: -1, items, parent } fullDataRowIdData[rowid] = rest fullDataRowMap.set(row, rest) fullAllDataRowIdData[rowid] = rest fullAllDataRowMap.set(row, rest) }, treeOpts) if (matchObj) { matchObj.item[children] = XEUtils.clone(childs, true) } }, /** * 更新数据列的 Map * 牺牲数据组装的耗时,用来换取使用过程中的流畅 */ cacheColumnMap () { const { tableFullColumn, collectColumn, fullColumnMap } = this const fullColumnIdData = this.fullColumnIdData = {} const fullColumnFieldData = this.fullColumnFieldData = {} const isGroup = collectColumn.some(hasChildrenList) let expandColumn let treeNodeColumn let checkboxColumn let radioColumn let hasFixed const handleFunc = (column, index, items, path, parent) => { const { id: colid, property, fixed, type, treeNode } = column const rest = { column, colid, index, items, parent } if (property) { if (fullColumnFieldData[property]) { UtilTools.error('vxe.error.colRepet', ['field', property]) } fullColumnFieldData[property] = rest } if (!hasFixed && fixed) { hasFixed = fixed } if (treeNode) { if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') { if (treeNodeColumn) { UtilTools.warn('vxe.error.colRepet', ['tree-node', treeNode]) } } if (!treeNodeColumn) { treeNodeColumn = column } } else if (type === 'expand') { if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') { if (expandColumn) { UtilTools.warn('vxe.error.colRepet', ['type', type]) } } if (!expandColumn) { expandColumn = column } } if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') { if (type === 'checkbox') { if (checkboxColumn) { UtilTools.warn('vxe.error.colRepet', ['type', type]) } if (!checkboxColumn) { checkboxColumn = column } } else if (type === 'radio') { if (radioColumn) { UtilTools.warn('vxe.error.colRepet', ['type', type]) } if (!radioColumn) { radioColumn = column } } } if (fullColumnIdData[colid]) { UtilTools.error('vxe.error.colRepet', ['colId', colid]) } fullColumnIdData[colid] = rest fullColumnMap.set(column, rest) } fullColumnMap.clear() if (isGroup) { XEUtils.eachTree(collectColumn, (column, index, items, path, parent, nodes) => { column.level = nodes.length handleFunc(column, index, items, path, parent) }) } else { tableFullColumn.forEach(handleFunc) } if (expandColumn && hasFixed) { UtilTools.warn('vxe.error.errConflicts', ['column.fixed', 'column.type=expand']) } if (expandColumn && this.mouseOpts.area) { UtilTools.error('vxe.error.errConflicts', ['mouse-config.area', 'column.type=expand']) } this.isGroup = isGroup this.treeNodeColumn = treeNodeColumn this.expandColumn = expandColumn }, /** * 根据 tr 元素获取对应的 row 信息 * @param {Element} tr 元素 */ getRowNode (tr) { if (tr) { const { fullAllDataRowIdData } = this const rowid = tr.getAttribute('data-rowid') const rest = fullAllDataRowIdData[rowid] if (rest) { return { rowid: rest.rowid, item: rest.row, index: rest.index, items: rest.items, parent: rest.parent } } } return null }, /** * 根据 th/td 元素获取对应的 column 信息 * @param {Element} cell 元素 */ getColumnNode (cell) { if (cell) { const { fullColumnIdData } = this const colid = cell.getAttribute('data-colid') const rest = fullColumnIdData[colid] if (rest) { return { colid: rest.colid, item: rest.column, index: rest.index, items: rest.items, parent: rest.parent } } } return null }, /** * 根据 row 获取相对于 data 中的索引 * @param {Row} row 行对象 */ getRowIndex (row) { return this.fullDataRowMap.has(row) ? this.fullDataRowMap.get(row).index : -1 }, /** * 根据 row 获取相对于当前数据中的索引 * @param {Row} row 行对象 */ getVTRowIndex (row) { return this.afterFullData.indexOf(row) }, // 在 v3 中废弃 _getRowIndex (row) { if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') { UtilTools.warn('vxe.error.delFunc', ['_getRowIndex', 'getVTRowIndex']) } return this.getVTRowIndex(row) }, /** * 根据 row 获取渲染中的虚拟索引 * @param {Row} row 行对象 */ getVMRowIndex (row) { return this.tableData.indexOf(row) }, // 在 v3 中废弃 $getRowIndex (row) { if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') { UtilTools.warn('vxe.error.delFunc', ['$getRowIndex', 'getVMRowIndex']) } return this.getVMRowIndex(row) }, /** * 根据 column 获取相对于 columns 中的索引 * @param {ColumnInfo} column 列配置 */ getColumnIndex (column) { return this.fullColumnMap.has(column) ? this.fullColumnMap.get(column).index : -1 }, /** * 根据 column 获取相对于当前表格列中的索引 * @param {ColumnInfo} column 列配置 */ getVTColumnIndex (column) { return this.visibleColumn.indexOf(column) }, // 在 v3 中废弃 _getColumnIndex (column) { if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') { UtilTools.warn('vxe.error.delFunc', ['_getColumnIndex', 'getVTColumnIndex']) } return this.getVTColumnIndex(column) }, /** * 根据 column 获取渲染中的虚拟索引 * @param {ColumnInfo} column 列配置 */ getVMColumnIndex (column) { return this.tableColumn.indexOf(column) }, // 在 v3 中废弃 $getColumnIndex (column) { if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') { UtilTools.warn('vxe.error.delFunc', ['$getColumnIndex', 'getVMColumnIndex']) } return this.getVMColumnIndex(column) }, /** * 判断是否为索引列 * @param {ColumnInfo} column 列配置 */ isSeqColumn (column) { return column && column.type === 'seq' }, /** * 定义行数据中的列属性,如果不存在则定义 * @param {Row} record 行数据 */ defineField (record) { const { radioOpts, checkboxOpts, treeConfig, treeOpts, expandOpts } = this const rowkey = getRowkey(this) this.visibleColumn.forEach(({ property, editRender }) => { if (property && !XEUtils.has(record, property)) { XEUtils.set(record, property, editRender && !XEUtils.isUndefined(editRender.defaultValue) ? editRender.defaultValue : null) } }) const ohterFields = [radioOpts.labelField, checkboxOpts.checkField, checkboxOpts.labelField, expandOpts.labelField] ohterFields.forEach((key) => { if (key && !XEUtils.get(record, key)) { XEUtils.set(record, key, null) } }) if (treeConfig && treeOpts.lazy && XEUtils.isUndefined(record[treeOpts.children])) { record[treeOpts.children] = null } // 必须有行数据的唯一主键,可以自行设置;也可以默认生成一个随机数 if (!XEUtils.get(record, rowkey)) { XEUtils.set(record, rowkey, getRowUniqueId()) } return record }, /** * 创建 data 对象 * 对于某些特殊场景可能会用到,会自动对数据的字段名进行检测,如果不存在就自动定义 * @param {Array} records 新数据 */ createData (records) { const rowkey = getRowkey(this) const rows = records.map(record => this.defineField(Object.assign({}, record, { [rowkey]: null }))) return this.$nextTick().then(() => rows) }, /** * 创建 Row|Rows 对象 * 对于某些特殊场景需要对数据进行手动插入时可能会用到 * @param {Array/Object} records 新数据 */ createRow (records) { const isArr = XEUtils.isArray(records) if (!isArr) { records = [records] } return this.$nextTick().then(() => this.createData(records).then(rows => isArr ? rows : rows[0])) }, /** * 还原数据 * 如果不传任何参数,则还原整个表格 * 如果传 row 则还原一行 * 如果传 rows 则还原多行 * 如果还额外传了 field 则还原指定的单元格数据 */ revertData (rows, field) { const { keepSource, tableSourceData, treeConfig } = this if (keepSource) { if (arguments.length) { if (rows && !XEUtils.isArray(rows)) { rows = [rows] } rows.forEach(row => { if (!this.isInsertByRow(row)) { const rowIndex = this.getRowIndex(row) if (treeConfig && rowIndex === -1) { throw new Error(UtilTools.getLog('vxe.error.noTree', ['revertData'])) } const oRow = tableSourceData[rowIndex] if (oRow && row) { if (field) { XEUtils.set(row, field, XEUtils.clone(XEUtils.get(oRow, field), true)) } else { XEUtils.destructuring(row, XEUtils.clone(oRow, true)) } } } }) return this.$nextTick() } return this.reloadData(tableSourceData) } else { UtilTools.warn('vxe.error.reqProp', ['keep-source']) } return this.$nextTick() }, /** * 清空单元格内容 * 如果不创参数,则清空整个表格内容 * 如果传 row 则清空一行内容 * 如果传 rows 则清空多行内容 * 如果还额外传了 field 则清空指定单元格内容 * @param {Array/Row} rows 行数据 * @param {String} field 字段名 */ clearData (rows, field) { const { tableFullData, visibleColumn } = this if (!arguments.length) { rows = tableFullData } else if (rows && !XEUtils.isArray(rows)) { rows = [rows] } if (field) { rows.forEach(row => XEUtils.set(row, field, null)) } else { rows.forEach(row => { visibleColumn.forEach(column => { if (column.property) { setCellValue(row, column, null) } }) }) } return this.$nextTick() }, /** * 检查是否为临时行数据 * @param {Row} row 行对象 */ isInsertByRow (row) { return this.editStore.insertList.indexOf(row) > -1 }, /** * 检查行或列数据是否发生改变 * @param {Row} row 行对象 * @param {String} field 字段名 */ isUpdateByRow (row, field) { const { visibleColumn, keepSource, treeConfig, treeOpts, tableSourceData, fullDataRowIdData } = this if (keepSource) { let oRow, property const rowid = getRowid(this, row) // 新增的数据不需要检测 if (!fullDataRowIdData[rowid]) { return false } if (treeConfig) { const children = treeOpts.children const matchObj = XEUtils.findTree(tableSourceData, item => rowid === getRowid(this, item), treeOpts) row = Object.assign({}, row, { [children]: null }) if (matchObj) { oRow = Object.assign({}, matchObj.item, { [children]: null }) } } else { const oRowIndex = fullDataRowIdData[rowid].index oRow = tableSourceData[oRowIndex] } if (oRow) { if (arguments.length > 1) { return !eqCellValue(oRow, row, field) } for (let index = 0, len = visibleColumn.length; index < len; index++) { property = visibleColumn[index].property if (property && !eqCellValue(oRow, row, property)) { return true } } } } return false }, /** * 获取表格的可视列,也可以指定索引获取列 * @param {Number} columnIndex 索引 */ getColumns (columnIndex) { const columns = this.visibleColumn return arguments.length ? columns[columnIndex] : columns.slice(0) }, /** * 根据列的唯一主键获取列 * @param {String} colid 列主键 */ getColumnById (colid) { const fullColumnIdData = this.fullColumnIdData return fullColumnIdData[colid] ? fullColumnIdData[colid].column : null }, /** * 根据列的字段名获取列 * @param {String} field 字段名 */ getColumnByField (field) { const fullColumnFieldData = this.fullColumnFieldData return fullColumnFieldData[field] ? fullColumnFieldData[field].column : null }, /** * 获取当前表格的列 * 收集到的全量列、全量表头列、处理条件之后的全量表头列、当前渲染中的表头列 */ getTableColumn () { return { collectColumn: this.collectColumn.slice(0), fullColumn: this.tableFullColumn.slice(0), visibleColumn: this.visibleColumn.slice(0), tableColumn: this.tableColumn.slice(0) } }, /** * 获取数据,和 data 的行为一致,也可以指定索引获取数据 */ getData (rowIndex) { const tableSynchData = this.data || this.tableSynchData return arguments.length ? tableSynchData[rowIndex] : tableSynchData.slice(0) }, /** * 用于多选行,获取已选中的数据 */ getCheckboxRecords () { const { tableFullData, treeConfig, treeOpts, checkboxOpts } = this const { checkField: property } = checkboxOpts let rowList = [] if (property) { if (treeConfig) { rowList = XEUtils.filterTree(tableFullData, row => XEUtils.get(row, property), treeOpts) } else { rowList = tableFullData.filter(row => XEUtils.get(row, property)) } } else { const { selection } = this if (treeConfig) { rowList = XEUtils.filterTree(tableFullData, row => selection.indexOf(row) > -1, treeOpts) } else { rowList = tableFullData.filter(row => selection.indexOf(row) > -1) } } return rowList }, /** * 获取处理后全量的表格数据 * 如果存在筛选条件,继续处理 */ updateAfterFullData () { const { visibleColumn, tableFullData, filterOpts, sortOpts } = this const { remote: allRemoteFilter, filterMethod: allFilterMethod } = filterOpts const { remote: allRemoteSort, sortMethod: allSortMethod, multiple: sortMultiple } = sortOpts let tableData = tableFullData.slice(0) const filterColumns = [] const orderColumns = [] visibleColumn.forEach(column => { const { sortable, order, filters } = column if (!allRemoteFilter && filters && filters.length) { const valueList = [] const itemList = [] filters.forEach((item) => { if (item.checked) { itemList.push(item) valueList.push(item.value) } }) if (itemList.length) { filterColumns.push({ column, valueList, itemList }) } } if (!allRemoteSort && sortable && order) { orderColumns.push({ column, sortBy: column.sortBy, property: column.property, order }) } }) if (filterColumns.length) { tableData = tableData.filter(row => { return filterColumns.every(({ column, valueList, itemList }) => { if (valueList.length && !allRemoteFilter) { const { filterRender, property } = column let { filterMethod } = column const compConf = filterRender ? VXETable.renderer.get(filterRender.name) : null if (!filterMethod && compConf && compConf.renderFilter) { filterMethod = compConf.filterMethod } if (allFilterMethod && !filterMethod) { return allFilterMethod({ options: itemList, values: valueList, row, column }) } return filterMethod ? itemList.some(item => filterMethod({ value: item.value, option: item, row, column })) : valueList.indexOf(XEUtils.get(row, property)) > -1 } return true }) }) } const firstOrderColumn = orderColumns[0] if (!allRemoteSort && firstOrderColumn) { if (allSortMethod) { const sortRests = allSortMethod({ data: tableData, column: firstOrderColumn.column, property: firstOrderColumn.property, order: firstOrderColumn.order, sortList: orderColumns, $table: this }) tableData = XEUtils.isArray(sortRests) ? sortRests : tableData } else { const params = { $table: this } // 兼容 v4 if (sortMultiple) { tableData = XEUtils.orderBy(tableData, orderColumns.map(({ column, property, order }) => { return { field: (column.sortBy ? (XEUtils.isArray(column.sortBy) ? column.sortBy[0] : column.sortBy) : null) || (column.formatter ? (row) => getCellLabel(row, column, params) : property), order } })) } else { // 兼容 v2,在 v4 中废弃, sortBy 不能为数组 let sortByConfs if (firstOrderColumn.sortBy) { sortByConfs = (XEUtils.isArray(firstOrderColumn.sortBy) ? firstOrderColumn.sortBy : [firstOrderColumn.sortBy]).map(item => { return { field: item, order: firstOrderColumn.order } }) } tableData = XEUtils.orderBy(tableData, sortByConfs || [firstOrderColumn].map(({ column, property, order }) => { return { field: column.formatter ? (row) => getCellLabel(row, column, params) : property, order } })) } } } this.afterFullData = tableData return tableData }, /** * 根据行的唯一主键获取行 * @param {String/Number} rowid 行主键 */ getRowById (rowid) { const fullDataRowIdData = this.fullDataRowIdData return fullDataRowIdData[rowid] ? fullDataRowIdData[rowid].row : null }, /** * 根据行获取行的唯一主键 * @param {Row} row 行对象 */ getRowid (row) { const fullAllDataRowMap = this.fullAllDataRowMap return fullAllDataRowMap.has(row) ? fullAllDataRowMap.get(row).rowid : null }, /** * 获取处理后的表格数据 * 如果存在筛选条件,继续处理 * 如果存在排序,继续处理 */ getTableData () { const { tableFullData, afterFullData, tableData, footerData } = this return { fullData: tableFullData.slice(0), visibleData: afterFullData.slice(0), tableData: tableData.slice(0), footerData: footerData.slice(0) } }, /** * 默认行为只允许执行一次,除非被重置 */ handleDefaults () { if (this.checkboxConfig) { this.handleDefaultSelectionChecked() } if (this.radioConfig) { this.handleDefaultRadioChecked() } if (this.sortConfig) { this.handleDefaultSort() } if (this.expandConfig) { this.handleDefaultRowExpand() } if (this.treeConfig) { this.handleDefaultTreeExpand() } if (this.mergeCells) { this.handleDefaultMergeCells() } if (this.mergeFooterItems) { this.handleDefaultMergeFooterItems() } this.$nextTick(() => requestAnimationFrame(this.recalculate)) }, /** * 隐藏指定列 * @param {ColumnInfo} column 列配置 */ hideColumn (column) { column.visible = false return this.handleCustom() }, /** * 显示指定列 * @param {ColumnInfo} column 列配置 */ showColumn (column) { column.visible = true return this.handleCustom() }, /** * 手动重置列的显示隐藏、列宽拖动的状态; * 如果为 true 则重置所有状态 * 如果已关联工具栏,则会同步更新 */ resetColumn (options) { const { customOpts } = this const { checkMethod } = customOpts const opts = Object.assign({ visible: true, resizable: options === true }, options) this.tableFullColumn.forEach(column => { if (opts.resizable) { column.resizeWidth = 0 } if (!checkMethod || checkMethod({ column })) { column.visible = column.defaultVisible } }) if (opts.resizable) { this.saveCustomResizable(true) } return this.handleCustom() }, handleCustom () { this.saveCustomVisible() this.analyColumnWidth() return this.refreshColumn() }, /** * 还原自定义列操作状态 */ restoreCustomStorage () { const { id, collectColumn, customConfig, customOpts } = this const { storage } = customOpts const isAllStorage = customOpts.storage === true const isResizable = isAllStorage || (storage && storage.resizable) const isVisible = isAllStorage || (storage && storage.visible) if (customConfig && (isResizable || isVisible)) { const customMap = {} if (!id) { UtilTools.error('vxe.error.reqProp', ['id']) return } if (isResizable) { const columnWidthStorage = getCustomStorageMap(resizableStorageKey)[id] if (columnWidthStorage) { XEUtils.each(columnWidthStorage, (resizeWidth, field) => { customMap[field] = { field, resizeWidth } }) } } if (isVisible) { const columnVisibleStorage = getCustomStorageMap(visibleStorageKey)[id] if (columnVisibleStorage) { const colVisibles = columnVisibleStorage.split('|') const colHides = colVisibles[0] ? colVisibles[0].split(',') : [] const colShows = colVisibles[1] ? colVisibles[1].split(',') : [] colHides.forEach(field => { if (customMap[field]) { customMap[field].visible = false } else { customMap[field] = { field, visible: false } } }) colShows.forEach(field => { if (customMap[field]) { customMap[field].visible = true } else { customMap[field] = { field, visible: true } } }) } } const keyMap = {} XEUtils.eachTree(collectColumn, column => { const colKey = column.getKey() if (colKey) { keyMap[colKey] = column } }) XEUtils.each(customMap, ({ visible, resizeWidth }, field) => { const column = keyMap[field] if (column) { if (XEUtils.isNumber(resizeWidth)) { column.resizeWidth = resizeWidth } if (XEUtils.isBoolean(visible)) { column.visible = visible } } }) } }, saveCustomVisible () { const { id, collectColumn, customConfig, customOpts } = this const { checkMethod, storage } = customOpts const isAllStorage = customOpts.storage === true const isVisible = isAllStorage || (storage && storage.visible) if (customConfig && isVisible) { const columnVisibleStorageMap = getCustomStorageMap(visibleStorageKey) const colHides = [] const colShows = [] if (!id) { UtilTools.error('vxe.error.reqProp', ['id']) return } XEUtils.eachTree(collectColumn, column => { if (!checkMethod || checkMethod({ column })) { if (!column.visible && column.defaultVisible) { const colKey = column.getKey() if (colKey) { colHides.push(colKey) } } else if (column.visible && !column.defaultVisible) { const colKey = column.getKey() if (colKey) { colShows.push(colKey) } } } }) columnVisibleStorageMap[id] = [colHides.join(',')].concat(colShows.length ? [colShows.join(',')] : []).join('|') || undefined localStorage.setItem(visibleStorageKey, XEUtils.toJSONString(columnVisibleStorageMap)) } }, saveCustomResizable (isReset) { const { id, collectColumn, customConfig, customOpts } = this const { storage } = customOpts const isAllStorage = customOpts.storage === true const isResizable = isAllStorage || (storage && storage.resizable) if (customConfig && isResizable) { const columnWidthStorageMap = getCustomStorageMap(resizableStorageKey) let columnWidthStorage if (!id) { UtilTools.error('vxe.error.reqProp', ['id']) return } if (!isReset) { columnWidthStorage = XEUtils.isPlainObject(columnWidthStorageMap[id]) ? columnWidthStorageMap[id] : {} XEUtils.eachTree(collectColumn, column => { if (column.resizeWidth) { const colKey = column.getKey() if (colKey) { columnWidthStorage[colKey] = column.renderWidth } } }) } columnWidthStorageMap[id] = XEUtils.isEmpty(columnWidthStorage) ? undefined : columnWidthStorage localStorage.setItem(resizableStorageKey, XEUtils.toJSONString(columnWidthStorageMap)) } }, /** * 刷新列信息 * 将固定的列左边、右边分别靠边 */ refreshColumn () { const leftList = [] const centerList = [] const rightList = [] const { collectColumn, tableFullColumn, isGroup, columnStore, sXOpts, scrollXStore } = this // 如果是分组表头,如果子列全部被隐藏,则根列也隐藏 if (isGroup) { const leftGroupList = [] const centerGroupList = [] const rightGroupList = [] XEUtils.eachTree(collectColumn, (column, index, items, path, parent) => { const isColGroup = hasChildrenList(column) // 如果是分组,必须按组设置固定列,不允许给子列设置固定 if (parent && parent.fixed) { column.fixed = parent.fixed } if (parent && column.fixed !== parent.fixed) { UtilTools.error('vxe.error.groupFixed') } if (isColGroup) { column.visible = !!XEUtils.findTree(column.children, subColumn => hasChildrenList(subColumn) ? null : subColumn.visible) } else if (column.visible) { if (column.fixed === 'left') { leftList.push(column) } else if (column.fixed === 'right') { rightList.push(column) } else { centerList.push(column) } } }) collectColumn.forEach((column) => { if (column.visible) { if (column.fixed === 'left') { leftGroupList.push(column) } else if (column.fixed === 'right') { rightGroupList.push(column) } else { centerGroupList.push(column) } } }) this.tableGroupColumn = leftGroupList.concat(centerGroupList).concat(rightGroupList) } else { // 重新分配列 tableFullColumn.forEach((column) => { if (column.visible) { if (column.fixed === 'left') { leftList.push(column) } else if (column.fixed === 'right') { rightList.push(column) } else { centerList.push(column) } } }) } const visibleColumn = leftList.concat(centerList).concat(rightList) let scrollXLoad = sXOpts.gt > -1 && sXOpts.gt < tableFullColumn.length Object.assign(columnStore, { leftList, centerList, rightList }) if (scrollXLoad && isGroup) { scrollXLoad = false UtilTools.warn('vxe.error.scrollXNotGroup') } if (scrollXLoad) { if (this.showHeader && !this.showHeaderOverflow) { UtilTools.warn('vxe.error.reqProp', ['show-header-overflow']) } if (this.showFooter && !this.showFooterOverflow) { UtilTools.warn('vxe.error.reqProp', ['show-footer-overflow']) } if (this.spanMethod) { UtilTools.warn('vxe.error.scrollErrProp', ['span-method']) } if (this.footerSpanMethod) { UtilTools.warn('vxe.error.scrollErrProp', ['footer-span-method']) } const { visibleSize } = computeVirtualX(this) scrollXStore.startIndex = 0 scrollXStore.endIndex = visibleSize scrollXStore.visibleSize = visibleSize } // 如果列被显示/隐藏,则清除合并状态 // 如果列被设置为固定,则清除合并状态 if (visibleColumn.length !== this.visibleColumn.length || !this.visibleColumn.every((column, index) => column === visibleColumn[index])) { this.clearMergeCells() this.clearMergeFooterItems() } this.scrollXLoad = scrollXLoad this.visibleColumn = visibleColumn this.handleTableColumn() return this.$nextTick().then(() => { this.updateFooter() return this.recalculate(true) }).then(() => { this.updateCellAreas() return this.$nextTick().then(() => this.recalculate()) }) }, /** * 指定列宽的列进行拆分 */ analyColumnWidth () { const { columnOpts } = this const { width: defaultWidth, minWidth: defaultMinWidth } = columnOpts const resizeList = [] const pxList = [] const pxMinList = [] const scaleList = [] const scaleMinList = [] const autoList = [] this.tableFullColumn.forEach(column => { if (defaultWidth && !column.width) { column.width = defaultWidth } if (defaultMinWidth && !column.minWidth) { column.minWidth = defaultMinWidth } if (column.visible) { if (column.resizeWidth) { resizeList.push(column) } else if (DomTools.isPx(column.width)) { pxList.push(column) } else if (DomTools.isScale(column.width)) { scaleList.push(column) } else if (DomTools.isPx(column.minWidth)) { pxMinList.push(column) } else if (DomTools.isScale(column.minWidth)) { scaleMinList.push(column) } else { autoList.push(column) } } }) Object.assign(this.columnStore, { resizeList, pxList, pxMinList, scaleList, scaleMinList, autoList }) }, /** * 刷新滚动操作,手动同步滚动相关位置(对于某些特殊的操作,比如滚动条错位、固定列不同步) */ refreshScroll () { const { lastScrollLeft, lastScrollTop } = this return this.clearScroll().then(() => { if (lastScrollLeft || lastScrollTop) { // 重置最后滚动状态 this.lastScrollLeft = 0 this.lastScrollTop = 0 // 还原滚动状态 return this.scrollTo(lastScrollLeft, lastScrollTop) } }) }, /** * 计算单元格列宽,动态分配可用剩余空间 * 支持 width=? width=?px width=?% min-width=? min-width=?px min-width=?% */ recalculate (refull) { const { $refs } = this const { tableBody, tableHeader, tableFooter } = $refs const bodyElem = tableBody ? tableBody.$el : null const headerElem = tableHeader ? tableHeader.$el : null const footerElem = tableFooter ? tableFooter.$el : null if (bodyElem) { this.autoCellWidth(headerElem, bodyElem, footerElem) if (refull === true) { // 初始化时需要在列计算之后再执行优化运算,达到最优显示效果 return this.computeScrollLoad().then(() => { this.autoCellWidth(headerElem, bodyElem, footerElem) this.computeScrollLoad() }) } } return this.computeScrollLoad() }, /** * 列宽算法 * 支持 px、%、固定 混合分配 * 支持动态列表调整分配 * 支持自动分配偏移量 * @param {Element} headerElem * @param {Element} bodyElem * @param {Element} footerElem * @param {Number} bodyWidth */ autoCellWidth (headerElem, bodyElem, footerElem) { let tableWidth = 0 const minCellWidth = 40 // 列宽最少限制 40px const bodyWidth = bodyElem.clientWidth let remainWidth = bodyWidth let meanWidth = remainWidth / 100 const { fit, columnStore } = this const { resizeList, pxMinList, pxList, scaleList, scaleMinList, autoList } = columnStore // 最小宽 pxMinList.forEach(column => { const minWidth = parseInt(column.minWidth) tableWidth += minWidth column.renderWidth = minWidth }) // 最小百分比 scaleMinList.forEach(column => { const scaleWidth = Math.floor(parseInt(column.minWidth) * meanWidth) tableWidth += scaleWidth column.renderWidth = scaleWidth }) // 固定百分比 scaleList.forEach(column => { const scaleWidth = Math.floor(parseInt(column.width) * meanWidth) tableWidth += scaleWidth column.renderWidth = scaleWidth }) // 固定宽 pxList.forEach(column => { const width = parseInt(column.width) tableWidth += width column.renderWidth = width }) // 调整了列宽 resizeList.forEach(column => { const width = parseInt(column.resizeWidth) tableWidth += width column.renderWidth = width }) remainWidth -= tableWidth meanWidth = remainWidth > 0 ? Math.floor(remainWidth / (scaleMinList.length + pxMinList.length + autoList.length)) : 0 if (fit) { if (remainWidth > 0) { scaleMinList.concat(pxMinList).forEach(column => { tableWidth += meanWidth column.renderWidth += meanWidth }) } } else { meanWidth = minCellWidth } // 自适应 autoList.forEach(column => { const width = Math.max(meanWidth, minCellWidth) column.renderWidth = width tableWidth += width }) if (fit) { /** * 偏移量算法 * 如果所有列足够放的情况下,从最后动态列开始分配 */ const dynamicList = scaleList.concat(scaleMinList).concat(pxMinList).concat(autoList) let dynamicSize = dynamicList.length - 1 if (dynamicSize > 0) { let odiffer = bodyWidth - tableWidth if (odiffer > 0) { while (odiffer > 0 && dynamicSize >= 0) { odiffer-- dynamicList[dynamicSize--].renderWidth++ } tableWidth = bodyWidth } } } const tableHeight = bodyElem.offsetHeight const overflowY = bodyElem.scrollHeight > bodyElem.clientHeight this.scrollbarWidth = overflowY ? bodyElem.offsetWidth - bodyWidth : 0 this.overflowY = overflowY this.tableWidth = tableWidth this.tableHeight = tableHeight if (headerElem) { this.headerHeight = headerElem.clientHeight // 检测是否同步滚动 if (headerElem.scrollLeft !== bodyElem.scrollLeft) { headerElem.scrollLeft = bodyElem.scrollLeft } } else { this.headerHeight = 0 } if (footerElem) { const footerHeight = footerElem.offsetHeight this.scrollbarHeight = Math.max(footerHeight - footerElem.clientHeight, 0) this.overflowX = tableWidth > footerElem.clientWidth this.footerHeight = footerHeight } else { this.footerHeight = 0 this.scrollbarHeight = Math.max(tableHeight - bodyElem.clientHeight, 0) this.overflowX = tableWidth > bodyWidth } this.customHeight = calcHeight(this, 'height') this.customMaxHeight = calcHeight(this, 'maxHeight') this.parentHeight = Math.max(this.headerHeight + this.footerHeight + 20, this.getParentHeight()) if (this.overflowX) { this.checkScrolling() } }, updateStyle () { let { $refs, isGroup, fullColumnIdData, tableColumn, customHeight, customMaxHeight, border, headerHeight, showFooter, showOverflow: allColumnOverflow, showHeaderOverflow: allColumnHeaderOverflow, showFooterOverflow: allColumnFooterOverflow, footerHeight, tableHeight, tableWidth, scrollbarHeight, scrollbarWidth, scrollXLoad, scrollYLoad, cellOffsetWidth, columnStore, elemStore, editStore, currentRow, mouseConfig } = this const containerList = ['main', 'left', 'right'] const emptyPlaceholderElem = $refs.emptyPlaceholder const bodyWrapperElem = elemStore['main-body-wrapper'] if (emptyPlaceholderElem) { emptyPlaceholderElem.style.top = `${headerHeight}px` em