UNPKG

vxe-table

Version:

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

588 lines (580 loc) 19.8 kB
import XEUtils from 'xe-utils/ctor' import GlobalConfig from '../../conf' import VXETable from '../../v-x-e-table' import { UtilTools, DomTools } from '../../tools' import { getOffsetSize, calcTreeLine, mergeBodyMethod } from './util' const cellType = 'body' // 滚动、拖动过程中不需要触发 function isOperateMouse ($xetable) { return $xetable._isResize || ($xetable.lastScrollTime && Date.now() < $xetable.lastScrollTime + $xetable.delayHover) } function renderLine (h, _vm, $xetable, rowLevel, items, params) { const column = params.column const { treeOpts, treeConfig } = $xetable const { slots, treeNode } = column if (slots && slots.line) { return slots.line.call($xetable, params, h) } if (treeConfig && treeNode && treeOpts.line) { return [ h('div', { class: 'vxe-tree--line-wrapper' }, [ h('div', { class: 'vxe-tree--line', style: { height: `${calcTreeLine(params, items)}px`, left: `${(rowLevel * treeOpts.indent) + (rowLevel ? 2 - getOffsetSize($xetable) : 0) + 16}px` } }) ]) ] } return [] } /** * 渲染列 */ function renderColumn (h, _vm, $xetable, $seq, seq, rowid, fixedType, rowLevel, row, rowIndex, $rowIndex, _rowIndex, column, $columnIndex, columns, items) { const { $listeners: tableListeners, afterFullData, tableData, height, columnKey, overflowX, scrollXLoad, scrollYLoad, highlightCurrentRow, showOverflow: allColumnOverflow, align: allAlign, currentColumn, cellClassName, cellStyle, mergeList, spanMethod, radioOpts, checkboxOpts, expandOpts, treeOpts, tooltipOpts, mouseConfig, editConfig, editOpts, editRules, validOpts, editStore, validStore } = $xetable const { type, cellRender, editRender, align, showOverflow, className, treeNode } = column const { actived } = editStore const { enabled } = tooltipOpts const columnIndex = $xetable.getColumnIndex(column) const _columnIndex = $xetable.getVTColumnIndex(column) let fixedHiddenColumn = fixedType ? column.fixed !== fixedType : column.fixed && overflowX const cellOverflow = (XEUtils.isUndefined(showOverflow) || XEUtils.isNull(showOverflow)) ? allColumnOverflow : showOverflow let showEllipsis = cellOverflow === 'ellipsis' const showTitle = cellOverflow === 'title' const showTooltip = cellOverflow === true || cellOverflow === 'tooltip' let hasEllipsis = showTitle || showTooltip || showEllipsis let isDirty const tdOns = {} const cellAlign = align || allAlign const hasValidError = validStore.row === row && validStore.column === column const hasDefaultTip = editRules && (validOpts.message === 'default' ? (height || tableData.length > 1) : validOpts.message === 'inline') const attrs = { 'data-colid': column.id } const bindMouseenter = tableListeners['cell-mouseenter'] const bindMouseleave = tableListeners['cell-mouseleave'] const triggerDblclick = (editRender && editConfig && editOpts.trigger === 'dblclick') const params = { $table: $xetable, $seq, seq, rowid, row, rowIndex, $rowIndex, _rowIndex, column, columnIndex, $columnIndex, _columnIndex, fixed: fixedType, type: cellType, isHidden: fixedHiddenColumn, level: rowLevel, visibleData: afterFullData, data: tableData, items } // 虚拟滚动不支持动态高度 if ((scrollXLoad || scrollYLoad) && !hasEllipsis) { showEllipsis = hasEllipsis = true } // hover 进入事件 if (showTitle || showTooltip || enabled || bindMouseenter) { tdOns.mouseenter = evnt => { if (isOperateMouse($xetable)) { return } if (showTitle) { DomTools.updateCellTitle(evnt.currentTarget, column) } else if (showTooltip || enabled) { // 如果配置了显示 tooltip $xetable.triggerBodyTooltipEvent(evnt, params) } if (bindMouseenter) { $xetable.emitEvent('cell-mouseenter', Object.assign({ cell: evnt.currentTarget }, params), evnt) } } } // hover 退出事件 if (showTooltip || enabled || bindMouseleave) { tdOns.mouseleave = evnt => { if (isOperateMouse($xetable)) { return } if (showTooltip || enabled) { $xetable.handleTargetLeaveEvent(evnt) } if (bindMouseleave) { $xetable.emitEvent('cell-mouseleave', Object.assign({ cell: evnt.currentTarget }, params), evnt) } } } // 按下事件处理 if (checkboxOpts.range || mouseConfig) { tdOns.mousedown = evnt => { $xetable.triggerCellMousedownEvent(evnt, params) } } // 点击事件处理 if (highlightCurrentRow || tableListeners['cell-click'] || (editRender && editConfig) || (expandOpts.trigger === 'row' || (expandOpts.trigger === 'cell')) || (radioOpts.trigger === 'row' || (column.type === 'radio' && radioOpts.trigger === 'cell')) || (checkboxOpts.trigger === 'row' || (column.type === 'checkbox' && checkboxOpts.trigger === 'cell')) || (treeOpts.trigger === 'row' || (column.treeNode && treeOpts.trigger === 'cell'))) { tdOns.click = evnt => { $xetable.triggerCellClickEvent(evnt, params) } } // 双击事件处理 if (triggerDblclick || tableListeners['cell-dblclick']) { tdOns.dblclick = evnt => { $xetable.triggerCellDBLClickEvent(evnt, params) } } // 合并行或列 if (mergeList.length) { const spanRest = mergeBodyMethod(mergeList, _rowIndex, _columnIndex) if (spanRest) { const { rowspan, colspan } = spanRest if (!rowspan || !colspan) { return null } if (rowspan > 1) { attrs.rowspan = rowspan } if (colspan > 1) { attrs.colspan = colspan } } } else if (spanMethod) { // 自定义合并行或列的方法 const { rowspan = 1, colspan = 1 } = spanMethod(params) || {} if (!rowspan || !colspan) { return null } if (rowspan > 1) { attrs.rowspan = rowspan } if (colspan > 1) { attrs.colspan = colspan } } // 如果被合并不可隐藏 if (fixedHiddenColumn && mergeList) { if (attrs.colspan > 1 || attrs.rowspan > 1) { fixedHiddenColumn = false } } // 如果编辑列开启显示状态 if (!fixedHiddenColumn && editConfig && (editRender || cellRender) && editOpts.showStatus) { isDirty = $xetable.isUpdateByRow(row, column.property) } const tdVNs = [] if (allColumnOverflow && fixedHiddenColumn) { tdVNs.push( h('div', { class: ['vxe-cell', { 'c--title': showTitle, 'c--tooltip': showTooltip, 'c--ellipsis': showEllipsis }] }) ) } else { // 渲染单元格 tdVNs.push( ...renderLine(h, _vm, $xetable, rowLevel, items, params), h('div', { class: ['vxe-cell', { 'c--title': showTitle, 'c--tooltip': showTooltip, 'c--ellipsis': showEllipsis }], attrs: { title: showTitle ? UtilTools.getCellLabel(row, column, params) : null } }, column.renderCell(h, params)) ) if (hasDefaultTip && hasValidError) { tdVNs.push( h('div', { class: 'vxe-cell--valid', style: validStore.rule && validStore.rule.maxWidth ? { width: `${validStore.rule.maxWidth}px` } : null }, [ h('span', { class: 'vxe-cell--valid-msg' }, validStore.content) ]) ) } } return h('td', { class: ['vxe-body--column', column.id, { [`col--${cellAlign}`]: cellAlign, [`col--${type}`]: type, 'col--last': $columnIndex === columns.length - 1, 'col--tree-node': treeNode, 'col--edit': !!editRender, 'col--ellipsis': hasEllipsis, 'fixed--hidden': fixedHiddenColumn, 'col--dirty': isDirty, 'col--actived': editConfig && editRender && (actived.row === row && (actived.column === column || editOpts.mode === 'row')), 'col--valid-error': hasValidError, 'col--current': currentColumn === column }, UtilTools.getClass(className, params), UtilTools.getClass(cellClassName, params)], key: columnKey ? column.id : $columnIndex, attrs, style: cellStyle ? (XEUtils.isFunction(cellStyle) ? cellStyle(params) : cellStyle) : null, on: tdOns }, tdVNs) } function renderRows (h, _vm, $xetable, $seq, rowLevel, fixedType, tableData, tableColumn) { const { stripe, rowKey, highlightHoverRow, rowClassName, rowStyle, showOverflow: allColumnOverflow, treeConfig, treeOpts, treeExpandeds, scrollYLoad, scrollYStore, editStore, rowExpandeds, radioOpts, checkboxOpts, expandColumn } = $xetable const rows = [] tableData.forEach((row, $rowIndex) => { const trOn = {} let rowIndex = $rowIndex let seq = rowIndex + 1 if (scrollYLoad) { seq += scrollYStore.startIndex } const _rowIndex = $xetable.getVTRowIndex(row) // 确保任何情况下 rowIndex 都精准指向真实 data 索引 rowIndex = $xetable.getRowIndex(row) // 事件绑定 if (highlightHoverRow) { trOn.mouseenter = evnt => { if (isOperateMouse($xetable)) { return } $xetable.triggerHoverEvent(evnt, { row, rowIndex }) } trOn.mouseleave = () => { if (isOperateMouse($xetable)) { return } $xetable.clearHoverRow() } } const rowid = UtilTools.getRowid($xetable, row) const params = { $table: $xetable, $seq, seq, rowid, fixed: fixedType, type: cellType, level: rowLevel, row, rowIndex, $rowIndex } rows.push( h('tr', { class: ['vxe-body--row', { 'row--stripe': stripe && ($xetable.getVTRowIndex(row) + 1) % 2 === 0, 'is--new': editStore.insertList.indexOf(row) > -1, 'row--radio': radioOpts.highlight && $xetable.selectRow === row, 'row--checked': checkboxOpts.highlight && $xetable.isCheckedByCheckboxRow(row) }, rowClassName ? XEUtils.isFunction(rowClassName) ? rowClassName(params) : rowClassName : ''], attrs: { 'data-rowid': rowid }, style: rowStyle ? (XEUtils.isFunction(rowStyle) ? rowStyle(params) : rowStyle) : null, key: rowKey || treeConfig ? rowid : $rowIndex, on: trOn }, tableColumn.map((column, $columnIndex) => { return renderColumn(h, _vm, $xetable, $seq, seq, rowid, fixedType, rowLevel, row, rowIndex, $rowIndex, _rowIndex, column, $columnIndex, tableColumn, tableData) })) ) // 如果行被展开了 if (expandColumn && rowExpandeds.length && rowExpandeds.indexOf(row) > -1) { let cellStyle if (treeConfig) { cellStyle = { paddingLeft: `${(rowLevel * treeOpts.indent) + 30}px` } } const { showOverflow } = expandColumn const hasEllipsis = (XEUtils.isUndefined(showOverflow) || XEUtils.isNull(showOverflow)) ? allColumnOverflow : showOverflow const expandParams = { $table: $xetable, $seq, seq, column: expandColumn, fixed: fixedType, type: cellType, level: rowLevel, row, rowIndex, $rowIndex } rows.push( h('tr', { class: 'vxe-body--expanded-row', key: `expand_${rowid}`, style: rowStyle ? (XEUtils.isFunction(rowStyle) ? rowStyle(expandParams) : rowStyle) : null, on: trOn }, [ h('td', { class: ['vxe-body--expanded-column', { 'fixed--hidden': fixedType, 'col--ellipsis': hasEllipsis }], attrs: { colspan: tableColumn.length } }, [ h('div', { class: 'vxe-body--expanded-cell', style: cellStyle }, [ expandColumn.renderData(h, expandParams) ]) ]) ]) ) } // 如果是树形表格 if (treeConfig && treeExpandeds.length) { const rowChildren = row[treeOpts.children] if (rowChildren && rowChildren.length && treeExpandeds.indexOf(row) > -1) { rows.push(...renderRows(h, _vm, $xetable, $seq ? `${$seq}.${seq}` : `${seq}`, rowLevel + 1, fixedType, rowChildren, tableColumn)) } } }) return rows } /** * 同步滚动条 */ let scrollProcessTimeout function syncBodyScroll (scrollTop, elem1, elem2) { if (elem1 || elem2) { if (elem1) { elem1.onscroll = null elem1.scrollTop = scrollTop } if (elem2) { elem2.onscroll = null elem2.scrollTop = scrollTop } clearTimeout(scrollProcessTimeout) scrollProcessTimeout = setTimeout(function () { if (elem1) { elem1.onscroll = elem1._onscroll } if (elem2) { elem2.onscroll = elem2._onscroll } }, 300) } } export default { name: 'VxeTableBody', props: { tableData: Array, tableColumn: Array, fixedColumn: Array, size: String, fixedType: String }, mounted () { const { $parent: $xetable, $el, $refs, fixedType } = this const { elemStore } = $xetable const prefix = `${fixedType || 'main'}-body-` elemStore[`${prefix}wrapper`] = $el elemStore[`${prefix}table`] = $refs.table elemStore[`${prefix}colgroup`] = $refs.colgroup elemStore[`${prefix}list`] = $refs.tbody elemStore[`${prefix}xSpace`] = $refs.xSpace elemStore[`${prefix}ySpace`] = $refs.ySpace elemStore[`${prefix}emptyBlock`] = $refs.emptyBlock this.$el.onscroll = this.scrollEvent this.$el._onscroll = this.scrollEvent }, beforeDestroy () { this.$el._onscroll = null this.$el.onscroll = null }, render (h) { const { _e, $parent: $xetable, fixedColumn, fixedType } = this let { $scopedSlots, tId, tableData, tableColumn, showOverflow: allColumnOverflow, keyboardConfig, keyboardOpts, mergeList, spanMethod, scrollXLoad, emptyRender, emptyOpts, mouseConfig, mouseOpts } = $xetable // 如果是固定列与设置了超出隐藏 if (!mergeList.length && !spanMethod && !(keyboardConfig && keyboardOpts.isMerge)) { if (fixedType && allColumnOverflow) { tableColumn = fixedColumn } else if (scrollXLoad) { if (fixedType) { tableColumn = fixedColumn } } } let emptyContent if ($scopedSlots.empty) { emptyContent = $scopedSlots.empty.call(this, { $table: $xetable }, h) } else { const compConf = emptyRender ? VXETable.renderer.get(emptyOpts.name) : null if (compConf && compConf.renderEmpty) { emptyContent = compConf.renderEmpty.call(this, h, emptyOpts, { $table: $xetable }) } else { emptyContent = $xetable.emptyText || GlobalConfig.i18n('vxe.table.emptyText') } } return h('div', { class: ['vxe-table--body-wrapper', fixedType ? `fixed-${fixedType}--wrapper` : 'body--wrapper'], attrs: { 'data-tid': tId } }, [ fixedType ? _e() : h('div', { class: 'vxe-body--x-space', ref: 'xSpace' }), h('div', { class: 'vxe-body--y-space', ref: 'ySpace' }), h('table', { class: 'vxe-table--body', attrs: { 'data-tid': tId, cellspacing: 0, cellpadding: 0, border: 0 }, ref: 'table' }, [ /** * 列宽 */ h('colgroup', { ref: 'colgroup' }, tableColumn.map((column, $columnIndex) => { return h('col', { attrs: { name: column.id }, key: $columnIndex }) })), /** * 内容 */ h('tbody', { ref: 'tbody' }, renderRows(h, this, $xetable, '', 0, fixedType, tableData, tableColumn)) ]), h('div', { class: 'vxe-table--checkbox-range' }), mouseConfig && mouseOpts.area ? h('div', { class: 'vxe-table--cell-area' }, [ h('span', { class: 'vxe-table--cell-main-area' }, mouseOpts.extension ? [ h('span', { class: 'vxe-table--cell-main-area-btn', on: { mousedown (evnt) { $xetable.triggerCellExtendMousedownEvent(evnt, { $table: $xetable, fixed: fixedType, type: cellType }) } } }) ] : null), h('span', { class: 'vxe-table--cell-copy-area' }), h('span', { class: 'vxe-table--cell-extend-area' }), h('span', { class: 'vxe-table--cell-multi-area' }), h('span', { class: 'vxe-table--cell-active-area' }) ]) : null, !fixedType ? h('div', { class: 'vxe-table--empty-block', ref: 'emptyBlock' }, [ h('div', { class: 'vxe-table--empty-content' }, emptyContent) ]) : null ]) }, methods: { /** * 滚动处理 * 如果存在列固定左侧,同步更新滚动状态 * 如果存在列固定右侧,同步更新滚动状态 */ scrollEvent (evnt) { const { $el, $parent: $xetable, fixedType } = this const { $refs, highlightHoverRow, scrollXLoad, scrollYLoad, lastScrollTop, lastScrollLeft } = $xetable const { tableHeader, tableBody, leftBody, rightBody, tableFooter, validTip } = $refs const headerElem = tableHeader ? tableHeader.$el : null const footerElem = tableFooter ? tableFooter.$el : null const bodyElem = tableBody.$el const leftElem = leftBody ? leftBody.$el : null const rightElem = rightBody ? rightBody.$el : null let scrollTop = $el.scrollTop const scrollLeft = bodyElem.scrollLeft const isX = scrollLeft !== lastScrollLeft const isY = scrollTop !== lastScrollTop $xetable.lastScrollTop = scrollTop $xetable.lastScrollLeft = scrollLeft $xetable.lastScrollTime = Date.now() if (highlightHoverRow) { $xetable.clearHoverRow() } if (leftElem && fixedType === 'left') { scrollTop = leftElem.scrollTop syncBodyScroll(scrollTop, bodyElem, rightElem) } else if (rightElem && fixedType === 'right') { scrollTop = rightElem.scrollTop syncBodyScroll(scrollTop, bodyElem, leftElem) } else { if (isX) { if (headerElem) { headerElem.scrollLeft = bodyElem.scrollLeft } if (footerElem) { footerElem.scrollLeft = bodyElem.scrollLeft } } if (leftElem || rightElem) { $xetable.checkScrolling() if (isY) { syncBodyScroll(scrollTop, leftElem, rightElem) } } } if (scrollXLoad && isX) { $xetable.triggerScrollXEvent(evnt) } if (scrollYLoad && isY) { $xetable.triggerScrollYEvent(evnt) } if (isX && validTip && validTip.visible) { validTip.updatePlacement() } $xetable.emitEvent('scroll', { type: cellType, fixed: fixedType, scrollTop, scrollLeft, isX, isY }, evnt) } } }