UNPKG

vxe-table-demonic

Version:

一个基于 vue 的 PC 端表单/表格组件,支持增删改查、虚拟列表、虚拟树、懒加载、快捷菜单、数据校验、树形结构、打印导出、表单渲染、数据分页、弹窗、自定义模板、渲染器、JSON 配置式...

1,492 lines (1,404 loc) 273 kB
import { ComponentOptions, ComponentPublicInstance, computed, ComputedRef, createCommentVNode, defineComponent, getCurrentInstance, h, inject, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, onUnmounted, provide, reactive, ref, Ref, resolveComponent, watch } from 'vue' import XEUtils, { isFunction } from 'xe-utils' import { addClass, browse, getEventTargetNode, getPaddingTopBottomSize, hasClass, isNodeElement, isPx, isScale, removeClass, setScrollLeft, setScrollTop } from '../../tools/dom' import { eqEmptyValue, formatText, getFuncText, getLastZIndex, hasChildrenList, isEnableConf, nextZIndex } from '../../tools/utils' import { errLog, warnLog } from '../../tools/log' import { createResizeEvent, XEResizeObserver } from '../../tools/resize' import { EVENT_KEYS, GlobalEvent, hasEventKey } from '../../tools/event' import { useSize } from '../../hooks/size' import { VXETable } from '../../v-x-e-table' import GlobalConfig from '../../v-x-e-table/src/conf' import Cell from './cell' import TableBodyComponent from './body' import TableHeaderComponent from '../../header' import TableFooterComponent from '../../footer' import tableProps from './props' import tableEmits from './emits' import VxeLoading from '../../loading/index' import { clearTableAllStatus, colToVisible, getCellValue, getRootColumn, getRowid, getRowkey, getRowUniqueId, handleFieldOrColumn, restoreScrollListener, restoreScrollLocation, rowToVisible, setCellValue, toTreePathSeq, XEBodyScrollElement } from './util' import { getSlotVNs } from '../../tools/vn' import { TableInternalData, TableMethods, TablePrivateMethods, TableReactData, VxeColumnPropTypes, VxeGridConstructor, VxeGridPrivateMethods, VxeMenuPanelInstance, VxeTableConstructor, VxeTableDataRow, VxeTableDefines, VxeTableMethods, VxeTablePrivateComputed, VxeTablePrivateMethods, VxeTablePrivateRef, VxeTableProps, VxeTablePropTypes, VxeToolbarConstructor, VxeTooltipInstance } from '../../../types/all' const isWebkit = browse['-webkit'] && !browse.edge const resizableStorageKey = 'VXE_TABLE_CUSTOM_COLUMN_WIDTH' const visibleStorageKey = 'VXE_TABLE_CUSTOM_COLUMN_VISIBLE' const fixedStorageKey = 'VXE_TABLE_CUSTOM_COLUMN_FIXED' const orderStorageKey = 'VXE_TABLE_CUSTOM_COLUMN_ORDER' export default defineComponent({ name: 'VxeTable', props: tableProps, emits: tableEmits, setup (props, context) { const { slots, emit } = context const hasUseTooltip = VXETable.tooltip const xID = XEUtils.uniqueId() const computeSize = useSize(props) const instance = getCurrentInstance() const reactData = reactive<TableReactData>({ // 低性能的静态列 staticColumns: [], // 渲染的列分组 tableGroupColumn: [], // 可视区渲染的列 tableColumn: [], // 渲染中的数据 tableData: [], // 是否启用了横向 X 可视渲染方式加载 scrollXLoad: false, // 是否启用了纵向 Y 可视渲染方式加载 scrollYLoad: false, // 是否存在纵向滚动条 overflowY: true, // 是否存在横向滚动条 overflowX: false, // 纵向滚动条的宽度 scrollbarWidth: 0, // 横向滚动条的高度 scrollbarHeight: 0, // 最后滚动时间戳 lastScrollTime: 0, // 行高 rowHeight: 0, // 表格父容器的高度 parentHeight: 0, // 是否使用分组表头 isGroup: false, isAllOverflow: false, // 复选框属性,是否全选 isAllSelected: false, // 复选框属性,有选中且非全选状态 isIndeterminate: false, // 复选框属性,已选中的行集合 selectCheckboxMaps: {}, // 当前行 currentRow: null, // 单选框属性,选中列 currentColumn: null, // 单选框属性,选中行 selectRadioRow: null, // 表尾合计数据 footerTableData: [], // 展开列信息 expandColumn: null, // 树节点列信息 treeNodeColumn: null, hasFixedColumn: false, // 已展开的行集合 rowExpandedMaps: {}, // 懒加载中的展开行的集合 rowExpandLazyLoadedMaps: {}, // 已展开树节点集合 treeExpandedMaps: {}, // 懒加载中的树节点的集合 treeExpandLazyLoadedMaps: {}, // 树节点不确定状态的集合 treeIndeterminateMaps: {}, // 合并单元格的对象集 mergeList: [], // 合并表尾数据的对象集 mergeFooterList: [], // 刷新列标识,当列筛选被改变时,触发表格刷新数据 upDataFlag: 0, // 刷新列标识,当列的特定属性被改变时,触发表格刷新列 reColumnFlag: 0, // 已标记的对象集 pendingRowMaps: {}, // 已标记的行 pendingRowList: [], // 初始化标识 initStore: { filter: false, import: false, export: false }, // 当前选中的筛选列 filterStore: { isAllSelected: false, isIndeterminate: false, style: null, options: [], column: null, multiple: false, visible: false, maxHeight: null }, // 存放列相关的信息 columnStore: { leftList: [], centerList: [], rightList: [], resizeList: [], pxList: [], pxMinList: [], scaleList: [], scaleMinList: [], autoList: [] }, // 存放快捷菜单的信息 ctxMenuStore: { selected: null, visible: false, showChild: false, selectChild: null, list: [], style: null }, // 存放可编辑相关信息 editStore: { indexs: { columns: [] }, titles: { columns: [] }, // 选中源 selected: { row: null, column: null }, // 已复制源 copyed: { cut: false, rows: [], columns: [] }, // 激活 actived: { row: null, column: null }, insertMaps: {}, removeMaps: {} }, // 存放 tooltip 相关信息 tooltipStore: { row: null, column: null, content: null, visible: false, currOpts: null }, // 存放数据校验相关信息 validStore: { visible: false }, validErrorMaps: {}, // 导入相关信息 importStore: { inited: false, file: null, type: '', modeList: [], typeList: [], filename: '', visible: false }, importParams: { mode: '', types: null, message: true }, // 导出相关信息 exportStore: { inited: false, name: '', modeList: [], typeList: [], columns: [], isPrint: false, hasFooter: false, hasMerge: false, hasTree: false, hasColgroup: false, visible: false }, exportParams: { filename: '', sheetName: '', mode: '', type: '', isColgroup: false, isMerge: false, isAllExpand: false, useStyle: false, original: false, message: true, isHeader: false, isFooter: false }, scrollVMLoading: false, _isResize: false }) const internalData: TableInternalData = { tZindex: 0, elemStore: {}, // 存放横向 X 虚拟滚动相关的信息 scrollXStore: { offsetSize: 0, visibleSize: 0, startIndex: 0, endIndex: 0 }, // 存放纵向 Y 虚拟滚动相关信息 scrollYStore: { rowHeight: 0, offsetSize: 0, visibleSize: 0, startIndex: 0, endIndex: 0 }, // 表格宽度 tableWidth: 0, // 表格高度 tableHeight: 0, // 表头高度 headerHeight: 0, // 表尾高度 footerHeight: 0, customHeight: 0, customMinHeight: 0, customMaxHeight: 0, // 当前 hover 行 hoverRow: null, // 最后滚动位置 lastScrollLeft: 0, lastScrollTop: 0, // 单选框属性,已选中保留的行 radioReserveRow: null, // 复选框属性,已选中保留的行集合 checkboxReserveRowMap: {}, // 行数据,已展开保留的行集合 rowExpandedReserveRowMap: {}, // 树结构数据,已展开保留的行集合 treeExpandedReserveRowMap: {}, // 树结构数据,不确定状态的集合 treeIndeterminateRowMaps: {}, // 列表完整数据、条件处理后 tableFullData: [], afterFullData: [], afterTreeFullData: [], // 列表条件处理后数据集合 afterFullRowMaps: {}, // 树结构完整数据、条件处理后 tableFullTreeData: [], tableSynchData: [], tableSourceData: [], // 收集的列配置(带分组) collectColumn: [], // 完整所有列(不带分组) tableFullColumn: [], // 渲染所有列 visibleColumn: [], // 总的缓存数据集 fullAllDataRowIdData: {}, // 渲染中缓存数据 sourceDataRowIdData: {}, fullDataRowIdData: {}, fullColumnIdData: {}, fullColumnFieldData: {}, inited: false, tooltipTimeout: null, initStatus: false, isActivated: false } let tableMethods = {} as TableMethods let tablePrivateMethods = {} as TablePrivateMethods const refElem = ref() as Ref<HTMLDivElement> const refTooltip = ref() as Ref<VxeTooltipInstance> const refCommTooltip = ref() as Ref<VxeTooltipInstance> const refValidTooltip = ref() as Ref<VxeTooltipInstance> const refTableFilter = ref() as Ref<ComponentPublicInstance> const refTableMenu = ref() as Ref<VxeMenuPanelInstance> const refTableHeader = ref() as Ref<ComponentPublicInstance> const refTableBody = ref() as Ref<ComponentPublicInstance> const refTableFooter = ref() as Ref<ComponentPublicInstance> const refTableLeftHeader = ref() as Ref<ComponentPublicInstance> const refTableLeftBody = ref() as Ref<ComponentPublicInstance> const refTableLeftFooter = ref() as Ref<ComponentPublicInstance> const refTableRightHeader = ref() as Ref<ComponentPublicInstance> const refTableRightBody = ref() as Ref<ComponentPublicInstance> const refTableRightFooter = ref() as Ref<ComponentPublicInstance> const refLeftContainer = ref() as Ref<HTMLDivElement> const refRightContainer = ref() as Ref<HTMLDivElement> const refCellResizeBar = ref() as Ref<HTMLDivElement> const refEmptyPlaceholder = ref() as Ref<HTMLDivElement> const $xegrid = inject<(VxeGridConstructor & VxeGridPrivateMethods) | null>('$xegrid', null) let $xetoolbar: VxeToolbarConstructor const computeValidOpts = computed(() => { return Object.assign({}, GlobalConfig.table.validConfig, props.validConfig) as VxeTablePropTypes.ValidOpts }) const computeSXOpts = computed(() => { return Object.assign({}, GlobalConfig.table.scrollX, props.scrollX) as VxeTablePropTypes.SXOpts }) const computeSYOpts = computed(() => { return Object.assign({}, GlobalConfig.table.scrollY, props.scrollY) as VxeTablePropTypes.SYOpts }) const computeRowHeightMaps = computed(() => { return { default: 48, medium: 44, small: 40, mini: 36 } }) const computeColumnOpts = computed(() => { return Object.assign({}, GlobalConfig.table.columnConfig, props.columnConfig) as VxeTablePropTypes.ColumnOpts }) const computeRowOpts = computed(() => { return Object.assign({}, GlobalConfig.table.rowConfig, props.rowConfig) as VxeTablePropTypes.RowOpts }) const computeResizeleOpts = computed(() => { return Object.assign({}, GlobalConfig.table.resizeConfig, props.resizeConfig) as VxeTablePropTypes.ResizeOpts }) const computeResizableOpts = computed(() => { return Object.assign({}, GlobalConfig.table.resizableConfig, props.resizableConfig) as VxeTablePropTypes.ResizableOpts }) const computeSeqOpts = computed(() => { return Object.assign({ startIndex: 0 }, GlobalConfig.table.seqConfig, props.seqConfig) as VxeTablePropTypes.SeqOpts }) const computeRadioOpts = computed(() => { return Object.assign({}, GlobalConfig.table.radioConfig, props.radioConfig) as VxeTablePropTypes.RadioOpts }) const computeCheckboxOpts = computed(() => { return Object.assign({}, GlobalConfig.table.checkboxConfig, props.checkboxConfig) as VxeTablePropTypes.CheckboxOpts }) let computeTooltipOpts = ref() as ComputedRef<VxeTablePropTypes.TooltipOpts> computeTooltipOpts = computed(() => { return Object.assign({}, GlobalConfig.tooltip, GlobalConfig.table.tooltipConfig, props.tooltipConfig) }) const computeTipConfig = computed(() => { const { tooltipStore } = reactData const tooltipOpts = computeTooltipOpts.value return { ...tooltipOpts, ...tooltipStore.currOpts } }) const computeValidTipOpts = computed(() => { const tooltipOpts = computeTooltipOpts.value return Object.assign({ isArrow: false }, tooltipOpts) }) const computeEditOpts = computed(() => { return Object.assign({}, GlobalConfig.table.editConfig, props.editConfig) as VxeTablePropTypes.EditOpts }) const computeSortOpts = computed(() => { return Object.assign({ orders: ['asc', 'desc', null] }, GlobalConfig.table.sortConfig, props.sortConfig) as VxeTablePropTypes.SortOpts }) const computeFilterOpts = computed(() => { return Object.assign({}, GlobalConfig.table.filterConfig, props.filterConfig) as VxeTablePropTypes.FilterOpts }) const computeMouseOpts = computed(() => { return Object.assign({}, GlobalConfig.table.mouseConfig, props.mouseConfig) as VxeTablePropTypes.MouseOpts }) const computeAreaOpts = computed(() => { return Object.assign({}, GlobalConfig.table.areaConfig, props.areaConfig) as VxeTablePropTypes.AreaOpts }) const computeKeyboardOpts = computed(() => { return Object.assign({}, GlobalConfig.table.keyboardConfig, props.keyboardConfig) as VxeTablePropTypes.KeyboardOpts }) const computeClipOpts = computed(() => { return Object.assign({}, GlobalConfig.table.clipConfig, props.clipConfig) }) const computeFNROpts = computed(() => { return Object.assign({}, GlobalConfig.table.fnrConfig, props.fnrConfig) as VxeTablePropTypes.FNROpts }) const computeMenuOpts = computed(() => { return Object.assign({}, GlobalConfig.table.menuConfig, props.menuConfig) as VxeTablePropTypes.MenuOpts }) const computeHeaderMenu = computed(() => { const menuOpts = computeMenuOpts.value const headerOpts = menuOpts.header return headerOpts && headerOpts.options ? headerOpts.options : [] }) const computeBodyMenu = computed(() => { const menuOpts = computeMenuOpts.value const bodyOpts = menuOpts.body return bodyOpts && bodyOpts.options ? bodyOpts.options : [] }) const computeFooterMenu = computed(() => { const menuOpts = computeMenuOpts.value const footerOpts = menuOpts.footer return footerOpts && footerOpts.options ? footerOpts.options : [] }) const computeIsMenu = computed(() => { const menuOpts = computeMenuOpts.value const headerMenu = computeHeaderMenu.value const bodyMenu = computeBodyMenu.value const footerMenu = computeFooterMenu.value return !!(props.menuConfig && isEnableConf(menuOpts) && (headerMenu.length || bodyMenu.length || footerMenu.length)) }) const computeMenuList = computed(() => { const { ctxMenuStore } = reactData const rest: any[] = [] ctxMenuStore.list.forEach((list) => { list.forEach((item) => { rest.push(item) }) }) return rest }) const computeExportOpts = computed(() => { return Object.assign({}, GlobalConfig.table.exportConfig, props.exportConfig) as VxeTablePropTypes.ExportOpts }) const computeImportOpts = computed(() => { return Object.assign({}, GlobalConfig.table.importConfig, props.importConfig) as VxeTablePropTypes.ImportOpts }) const computePrintOpts = computed(() => { return Object.assign({}, GlobalConfig.table.printConfig, props.printConfig) as VxeTablePropTypes.PrintOpts }) const computeExpandOpts = computed(() => { return Object.assign({}, GlobalConfig.table.expandConfig, props.expandConfig) as VxeTablePropTypes.ExpandOpts }) const computeTreeOpts = computed(() => { return Object.assign({}, GlobalConfig.table.treeConfig, props.treeConfig) as VxeTablePropTypes.TreeOpts }) const computeEmptyOpts = computed(() => { return Object.assign({}, GlobalConfig.table.emptyRender, props.emptyRender) as VxeTablePropTypes.EmptyOpts }) const computeLoadingOpts = computed(() => { return Object.assign({}, GlobalConfig.table.loadingConfig, props.loadingConfig) as VxeTablePropTypes.LoadingOpts }) const computeCellOffsetWidth = computed(() => { return props.border ? Math.max(2, Math.ceil(reactData.scrollbarWidth / reactData.tableColumn.length)) : 1 }) const computeCustomOpts = computed(() => { return Object.assign({}, GlobalConfig.table.customConfig, props.customConfig) }) const computeFixedColumnSize = computed(() => { const { tableFullColumn } = internalData let fixedSize = 0 tableFullColumn.forEach((column) => { if (column.fixed) { fixedSize++ } }) return fixedSize }) const computeIsMaxFixedColumn = computed(() => { const fixedColumnSize = computeFixedColumnSize.value const columnOpts = computeColumnOpts.value const { maxFixedSize } = columnOpts if (maxFixedSize) { return fixedColumnSize >= maxFixedSize } return false }) const computeTableBorder = computed(() => { const { border } = props if (border === true) { return 'full' } if (border) { return border } return 'default' }) const computeIsAllCheckboxDisabled = computed(() => { const { treeConfig } = props const { tableData } = reactData const { tableFullData } = internalData const checkboxOpts = computeCheckboxOpts.value const { strict, checkMethod } = checkboxOpts if (strict) { if (tableData.length || tableFullData.length) { if (checkMethod) { if (treeConfig) { // 暂时不支持树形结构 } // 如果所有行都被禁用 return tableFullData.every((row) => !checkMethod({ row })) } return false } return true } return false }) const refMaps: VxeTablePrivateRef = { refElem, refTooltip, refValidTooltip, refTableFilter, refTableMenu, refTableHeader, refTableBody, refTableFooter, refTableLeftHeader, refTableLeftBody, refTableLeftFooter, refTableRightHeader, refTableRightBody, refTableRightFooter, refLeftContainer, refRightContainer, refCellResizeBar } const computeMaps: VxeTablePrivateComputed = { computeSize, computeValidOpts, computeSXOpts, computeSYOpts, computeColumnOpts, computeRowOpts, computeResizeleOpts, computeResizableOpts, computeSeqOpts, computeRadioOpts, computeCheckboxOpts, computeTooltipOpts, computeEditOpts, computeSortOpts, computeFilterOpts, computeMouseOpts, computeAreaOpts, computeKeyboardOpts, computeClipOpts, computeFNROpts, computeHeaderMenu, computeBodyMenu, computeFooterMenu, computeIsMenu, computeMenuOpts, computeExportOpts, computeImportOpts, computePrintOpts, computeExpandOpts, computeTreeOpts, computeEmptyOpts, computeLoadingOpts, computeCustomOpts, computeFixedColumnSize, computeIsMaxFixedColumn, computeIsAllCheckboxDisabled } const $xetable = { xID, props: props as VxeTableProps, context, instance, reactData, internalData, getRefMaps: () => refMaps, getComputeMaps: () => computeMaps, xegrid: $xegrid } as unknown as VxeTableConstructor & VxeTableMethods & VxeTablePrivateMethods const eqCellValue = (row1: any, row2: any, field: string) => { const val1 = XEUtils.get(row1, field) const val2 = XEUtils.get(row2, field) if (eqEmptyValue(val1) && eqEmptyValue(val2)) { return true } if (XEUtils.isString(val1) || XEUtils.isNumber(val1)) { return ('' + val1) === ('' + val2) } return XEUtils.isEqual(val1, val2) } const getNextSortOrder = (column: VxeTableDefines.ColumnInfo) => { const sortOpts = computeSortOpts.value const { orders } = sortOpts const currOrder = column.order || null const oIndex = orders.indexOf(currOrder) + 1 return orders[oIndex < orders.length ? oIndex : 0] } const getCustomStorageMap = (key: string) => { const version = GlobalConfig.version const rest = XEUtils.toStringJSON(localStorage.getItem(key) || '') return rest && rest._v === version ? rest : { _v: version } } const getRecoverRowMaps = (keyMaps: Record<string, any>) => { const { fullAllDataRowIdData } = internalData const restKeys: Record<string, any> = {} XEUtils.each(keyMaps, (row, rowid) => { if (fullAllDataRowIdData[rowid]) { restKeys[rowid] = row } }) return restKeys } const handleReserveRow = (reserveRowMap: any) => { const { fullDataRowIdData } = internalData const reserveList: any[] = [] XEUtils.each(reserveRowMap, (item, rowid) => { if (fullDataRowIdData[rowid] && $xetable.findRowIndexOf(reserveList, fullDataRowIdData[rowid].row) === -1) { reserveList.push(fullDataRowIdData[rowid].row) } }) return reserveList } const computeVirtualX = () => { const { visibleColumn } = internalData const tableBody = refTableBody.value const tableBodyElem = tableBody ? tableBody.$el as HTMLDivElement : 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 } } const computeVirtualY = () => { const tableHeader = refTableHeader.value const tableBody = refTableBody.value const tableBodyElem = tableBody ? tableBody.$el as HTMLDivElement : null const vSize = computeSize.value const rowHeightMaps = computeRowHeightMaps.value if (tableBodyElem) { const tableHeaderElem = tableHeader ? tableHeader.$el as HTMLDivElement : 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 } } const calculateMergerOffserIndex = (list: any[], offsetItem: any, type: 'row' | 'col') => { 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 } } } const setMerges = (merges: VxeTableDefines.MergeOptions | VxeTableDefines.MergeOptions[], mList: VxeTableDefines.MergeItem[], rowList?: any[]) => { if (merges) { const { treeConfig } = props const { visibleColumn } = internalData if (!XEUtils.isArray(merges)) { merges = [merges] } if (treeConfig && merges.length) { errLog('vxe.error.noTree', ['merge-cells | merge-footer-items']) } 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 || getRowid($xetable, item._row) === getRowid($xetable, row)) && ((item as any)._col.id === col || item._col.id === (col as VxeTableDefines.ColumnInfo).id)) const mergeItem = mList[mcIndex] if (mergeItem) { mergeItem.rowspan = rowspan mergeItem.colspan = colspan mergeItem._rowspan = rowspan mergeItem._colspan = colspan } else { const mergeRowIndex = rowList ? $xetable.findRowIndexOf(rowList, row) : row const mergeColIndex = tableMethods.getVTColumnIndex(col) mList.push({ row: mergeRowIndex, col: mergeColIndex, rowspan, colspan, _row: row, _col: col, _rowspan: rowspan, _colspan: colspan }) } } } }) } } const removeMerges = (merges: VxeTableDefines.MergeOptions | VxeTableDefines.MergeOptions[], mList: VxeTableDefines.MergeItem[], rowList?: any) => { const rest: VxeTableDefines.MergeItem[] = [] if (merges) { const { treeConfig } = props const { visibleColumn } = internalData if (!XEUtils.isArray(merges)) { merges = [merges] } if (treeConfig && merges.length) { errLog('vxe.error.noTree', ['merge-cells | merge-footer-items']) } 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 || getRowid($xetable, item._row) === getRowid($xetable, row)) && ((item as any)._col.id === col || item._col.id === (col as VxeTableDefines.ColumnInfo).id)) if (mcIndex > -1) { const rItems = mList.splice(mcIndex, 1) rest.push(rItems[0]) } }) } return rest } const clearAllSort = () => { const { tableFullColumn } = internalData tableFullColumn.forEach((column) => { column.order = null }) } const calcHeight = (key: 'height' | 'minHeight' | 'maxHeight') => { const { parentHeight } = reactData const val = props[key] let num = 0 if (val) { if (val === 'auto') { num = parentHeight } else { const excludeHeight = $xetable.getExcludeHeight() if (isScale(val)) { num = Math.floor((XEUtils.toInteger(val) || 1) / 100 * parentHeight) } else { num = XEUtils.toNumber(val) } num = Math.max(40, num - excludeHeight) } } return num } /** * 还原自定义列操作状态 */ const restoreCustomStorage = () => { const { id, customConfig } = props const { collectColumn } = internalData const customOpts = computeCustomOpts.value const { storage } = customOpts const isCustomResizable = storage === true || (storage && storage.resizable) const isCustomVisible = storage === true || (storage && storage.visible) const isCustomFixed = storage === true || (storage && storage.fixed) const isCustomOrder = storage === true || (storage && storage.order) if (customConfig && (isCustomResizable || isCustomVisible || isCustomFixed || isCustomOrder)) { const customMap: { [key: string]: { field?: VxeColumnPropTypes.Field resizeWidth?: number visible?: boolean fixed?: string order?: number } } = {} if (!id) { errLog('vxe.error.reqProp', ['id']) return } // 自定义列宽 if (isCustomResizable) { const columnWidthStorage = getCustomStorageMap(resizableStorageKey)[id] if (columnWidthStorage) { XEUtils.each(columnWidthStorage, (resizeWidth: number, colKey) => { customMap[colKey] = { resizeWidth } }) } } // 自定义固定列 if (isCustomFixed) { const columnFixedStorage = getCustomStorageMap(fixedStorageKey)[id] if (columnFixedStorage) { const colFixeds = columnFixedStorage.split(',') colFixeds.forEach((fixConf: any) => { const [colKey, fixed] = fixConf.split('|') if (customMap[colKey]) { customMap[colKey].fixed = fixed } else { customMap[colKey] = { fixed } } }) } } // 自定义顺序 if (isCustomOrder) { const columnOrderStorage = getCustomStorageMap(orderStorageKey)[id] if (columnOrderStorage) { // const colOrderSeqs = columnOrderStorage.split(',') // colOrderSeqs.forEach((orderConf: any) => { // const [colKey, order] = orderConf.split('|') // if (customMap[colKey]) { // customMap[colKey].order = order // } else { // customMap[colKey] = { order } // } // }) } } // 自定义隐藏列 if (isCustomVisible) { const columnVisibleStorage = getCustomStorageMap(visibleStorageKey)[id] if (columnVisibleStorage) { const colVisibles = columnVisibleStorage.split('|') const colHides: string[] = colVisibles[0] ? colVisibles[0].split(',') : [] const colShows: string[] = colVisibles[1] ? colVisibles[1].split(',') : [] colHides.forEach((colKey) => { if (customMap[colKey]) { customMap[colKey].visible = false } else { customMap[colKey] = { visible: false } } }) colShows.forEach((colKey) => { if (customMap[colKey]) { customMap[colKey].visible = true } else { customMap[colKey] = { visible: true } } }) } } const keyMap: { [key: string]: VxeTableDefines.ColumnInfo } = {} XEUtils.eachTree(collectColumn, (column) => { const colKey = column.getKey() if (colKey) { keyMap[colKey] = column } }) XEUtils.each(customMap, ({ visible, resizeWidth, fixed, order }, colKey) => { const column = keyMap[colKey] if (column) { if (XEUtils.isNumber(resizeWidth)) { column.resizeWidth = resizeWidth } if (XEUtils.isBoolean(visible)) { column.visible = visible } if (fixed) { column.fixed = fixed } if (order) { column.customOrder = order } } }) } } /** * 更新数据列的 Map * 牺牲数据组装的耗时,用来换取使用过程中的流畅 */ const cacheColumnMap = () => { const { tableFullColumn, collectColumn } = internalData const fullColumnIdData: any = internalData.fullColumnIdData = {} const fullColumnFieldData: any = internalData.fullColumnFieldData = {} const mouseOpts = computeMouseOpts.value const columnOpts = computeColumnOpts.value const rowOpts = computeRowOpts.value const isGroup = collectColumn.some(hasChildrenList) let isAllOverflow = !!props.showOverflow let expandColumn: VxeTableDefines.ColumnInfo | undefined let treeNodeColumn: VxeTableDefines.ColumnInfo | undefined let checkboxColumn: VxeTableDefines.ColumnInfo | undefined let radioColumn: VxeTableDefines.ColumnInfo | undefined let htmlColumn: VxeTableDefines.ColumnInfo | undefined let hasFixed: VxeColumnPropTypes.Fixed | undefined const handleFunc = (column: VxeTableDefines.ColumnInfo, index: number, items: VxeTableDefines.ColumnInfo[], path?: string[], parent?: VxeTableDefines.ColumnInfo) => { const { id: colid, field, fixed, type, treeNode } = column const rest = { column, colid, index, items, parent } if (field) { if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') { if (fullColumnFieldData[field]) { warnLog('vxe.error.colRepet', ['field', field]) } } fullColumnFieldData[field] = rest } if (!hasFixed && fixed) { hasFixed = fixed } if (!htmlColumn && type === 'html') { htmlColumn = column } if (treeNode) { if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') { if (treeNodeColumn) { warnLog('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) { warnLog('vxe.error.colRepet', ['type', type]) } } if (!expandColumn) { expandColumn = column } } if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') { if (type === 'checkbox') { if (checkboxColumn) { warnLog('vxe.error.colRepet', ['type', type]) } if (!checkboxColumn) { checkboxColumn = column } } else if (type === 'radio') { if (radioColumn) { warnLog('vxe.error.colRepet', ['type', type]) } if (!radioColumn) { radioColumn = column } } } if (isAllOverflow && column.showOverflow === false) { isAllOverflow = false } if (fullColumnIdData[colid]) { errLog('vxe.error.colRepet', ['colId', colid]) } fullColumnIdData[colid] = rest } 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 (process.env.VUE_APP_VXE_TABLE_ENV === 'development') { if (expandColumn && mouseOpts.area) { errLog('vxe.error.errConflicts', ['mouse-config.area', 'column.type=expand']) } } if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') { if (htmlColumn) { if (!columnOpts.useKey) { errLog('vxe.error.reqProp', ['column-config.useKey', 'column.type=html']) } if (!rowOpts.useKey) { errLog('vxe.error.reqProp', ['row-config.useKey', 'column.type=html']) } } } reactData.isGroup = isGroup reactData.treeNodeColumn = treeNodeColumn reactData.expandColumn = expandColumn reactData.isAllOverflow = isAllOverflow } const updateHeight = () => { internalData.customHeight = calcHeight('height') internalData.customMinHeight = calcHeight('minHeight') internalData.customMaxHeight = calcHeight('maxHeight') } /** * 列宽算法 * 支持 px、%、固定 混合分配 * 支持动态列表调整分配 * 支持自动分配偏移量 */ const autoCellWidth = () => { const tableHeader = refTableHeader.value const tableBody = refTableBody.value const tableFooter = refTableFooter.value const bodyElem = tableBody ? tableBody.$el as HTMLDivElement : null const headerElem = tableHeader ? tableHeader.$el as HTMLDivElement : null const footerElem = tableFooter ? tableFooter.$el as HTMLDivElement : null if (!bodyElem) { return } let tableWidth = 0 const minCellWidth = 40 // 列宽最少限制 40px const bodyWidth = bodyElem.clientWidth - 1 let remainWidth = bodyWidth let meanWidth = remainWidth / 100 const { fit } = props const { columnStore } = reactData const { resizeList, pxMinList, pxList, scaleList, scaleMinList, autoList } = columnStore // 最小宽 pxMinList.forEach((column) => { const minWidth = XEUtils.toInteger(column.minWidth) tableWidth += minWidth column.renderWidth = minWidth }) // 最小百分比 scaleMinList.forEach((column) => { const scaleWidth = Math.floor(XEUtils.toInteger(column.minWidth) * meanWidth) tableWidth += scaleWidth column.renderWidth = scaleWidth }) // 固定百分比 scaleList.forEach((column) => { const scaleWidth = Math.floor(XEUtils.toInteger(column.width) * meanWidth) tableWidth += scaleWidth column.renderWidth = scaleWidth }) // 固定宽 pxList.forEach((column) => { const width = XEUtils.toInteger(column.width) tableWidth += width column.renderWidth = width }) // 调整了列宽 resizeList.forEach((column) => { const width = XEUtils.toInteger(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 let scrollbarWidth = 0 if (overflowY) { scrollbarWidth = Math.max(bodyElem.offsetWidth - bodyElem.clientWidth, 0) } reactData.scrollbarWidth = scrollbarWidth reactData.overflowY = overflowY internalData.tableWidth = tableWidth internalData.tableHeight = tableHeight let headerHeight = 0 if (headerElem) { headerHeight = headerElem.clientHeight nextTick(() => { // 检测是否同步滚动 if (headerElem && bodyElem && headerElem.scrollLeft !== bodyElem.scrollLeft) { headerElem.scrollLeft = bodyElem.scrollLeft } }) } internalData.headerHeight = headerHeight let overflowX = false let footerHeight = 0 let scrollbarHeight = 0 if (footerElem) { footerHeight = footerElem.offsetHeight overflowX = tableWidth > footerElem.clientWidth if (overflowX) { scrollbarHeight = Math.max(footerHeight - footerElem.clientHeight, 0) } } else { overflowX = tableWidth > bodyWidth if (overflowX) { scrollbarHeight = Math.max(tableHeight - bodyElem.clientHeight, 0) } } internalData.footerHeight = footerHeight reactData.overflowX = overflowX reactData.scrollbarHeight = scrollbarHeight updateHeight() reactData.parentHeight = Math.max(internalData.headerHeight + footerHeight + 20, tablePrivateMethods.getParentHeight()) if (overflowX) { tablePrivateMethods.checkScrolling() } } const getOrderField = (column: VxeTableDefines.ColumnInfo) => { const { sortBy, sortType } = column return (row: any) => { let cellValue if (sortBy) { cellValue = XEUtils.isFunction(sortBy) ? sortBy({ row, column }) : XEUtils.get(row, sortBy) } else { cellValue = tablePrivateMethods.getCellLabel(row, column) } if (!sortType || sortType === 'auto') { return isNaN(cellValue) ? cellValue : XEUtils.toNumber(cellValue) } else if (sortType === 'number') { return XEUtils.toNumber(cellValue) } else if (sortType === 'string') { return XEUtils.toValueString(cellValue) } return cellValue } } /** * 预编译 * 对渲染中的数据提前解析序号及索引。牺牲提前编译耗时换取渲染中额外损耗,使运行时更加流畅 */ const updateAfterDataIndex = () => { const { treeConfig } = props const { afterFullData, fullDataRowIdData, fullAllDataRowIdData } = internalData const { afterTreeFullData } = internalData const treeOpts = computeTreeOpts.value const childrenField = treeOpts.children || treeOpts.childrenField const fullMaps: Record<string, any> = {} if (treeConfig) { XEUtils.eachTree(afterTreeFullData, (row, index, items, path) => { const rowid = getRowid($xetable, row) const allrest = fullAllDataRowIdData[rowid] const seq = path.map((num, i) => i % 2 === 0 ? (Number(num) + 1) : '.').join('') if (allrest) { allrest.seq = seq allrest._index = index } else { const rest = { row, rowid, seq, index: -1, $index: -1, _index: index, items: [], parent: null, level: 0 } fullAllDataRowIdData[rowid] = rest fullDataRowIdData[rowid] = rest } fullMaps[rowid] = row }, { children: treeOpts.transform ? treeOpts.mapChildrenField : childrenField }) } else { afterFullData.forEach((row, index) => { const rowid = getRowid($xetable, row) const allrest = fullAllDataRowIdData[rowid] const seq = index + 1 if (allrest) { allrest.seq = seq allrest._index = index } else { const rest = { row, rowid, seq, index: -1, $index: -1, _index: index, items: [], parent: null, level: 0 } fullAllDataRowIdData[rowid] = rest fullDataRowIdData[rowid] = rest } fullMaps[rowid] = row }) } internalData.afterFullRowMaps = fullMaps } /** * 如果为虚拟树,将树结构拍平 * @returns */ const handleVirtualTreeToList = () => { const { treeConfig } = props const { treeExpandedMaps } = reactData const treeOpts = computeTreeOpts.value if (treeConfig && treeOpts.transform) { const fullData: any[] = [] const expandMaps: { [key: string]: number } = {} XEUtils.eachTree(internalData.afterTreeFullData, (row, index, items, path, parent) => { const rowid = getRowid($xetable, row) const parentRowid = getRowid($xetable, parent) if (!parent || (expandMaps[parentRowid] && treeExpandedMaps[parentRowid])) { expandMaps[rowid] = 1 fullData.push(row) } }, { children: treeOpts.mapChildrenField }) internalData.afterFullData = fullData updateScrollYStatus(fullData) return fullData } return internalData.afterFullData } /** * 获取处理后全量的表格数据 * 如果存在筛选条件,继续处理 */ const updateAfterFullData = () => { const { treeConfig } = props const { tableFullColumn, tableFullData, tableFullTreeData } = internalData const filterOpts = computeFilterOpts.value const sortOpts = computeSortOpts.value const treeOpts = computeTreeOpts.value const { transform } = treeOpts const { remote: allRemoteFilter, filterMethod: allFilterMethod } = filterOpts const { remote: allRemoteSort, sortMethod: allSortMethod, multiple: sortMultiple, chronological } = sortOpts let tableData: any[] = [] let tableTree: any[] = [] // 处理列 if (!allRemoteFilter || !allRemoteSort) { const filterColumns: { column: VxeTableDefines.ColumnInfo valueList: any[] itemList: VxeTableDefines.FilterOption[] }[] = [] let orderColumns: VxeTableDefines.SortCheckedParams[] = [] tableFullColumn.forEach((column) => { const { field, sortable, order, filters } = column if (!allRemoteFilter && filters && filters.length) { const valueList: any[] = [] const itemList: VxeTableDefines.FilterOption[] = [] filters.forEach((item) => { if (item.checked) { itemList.push(item as VxeTableDefines.FilterOption) valueList.push(item.value) } }) if (itemList.length) { filterColumns.push({ column, valueList, itemList }) } } if (!allRemoteSort && sortable && order) { orderColumns.push({ column, field, property: field, order, sortTime: column.sortTime }) } }) if (sortMultiple && chronological && orderColumns.length > 1) { orderColumns = XEUtils.orderBy(orderColumns, 'sortTime') } // 处理筛选 // 支持单列、多列、组合筛选 if (!allRemoteFilter && filterColumns.length) { const handleFilter = (row: any) => { return filterColumns.every(({ column, valueList, itemList }) => { const { filterMethod, filterRender } = column const compConf = filterRender ? VXETable.renderer.get(filterRender.name) : null const compFilterMethod = compConf ? compConf.filterMethod : null const defaultFilterMethod = compConf ? compConf.defaultFilterMethod : null const cellValue = getCellValue(row, column) if (filterMethod) { return itemList.some((item) => filterMethod({ value: item.value, option: item, cellValue, row, column, $table: $xetable })) } else if (compFilterMethod) { return itemList.some((item) => compFilterMethod({ value: item.value, option: item, cellValue, row, column, $table: $xetable })) } else if (allFilterMethod) { return allFilterMethod({ options: itemList, values: valueList, cellValue, row, column }) } else if (defaultFilterMethod) { return itemList.some((item) => defa