vxe-table-demonic
Version:
一个基于 vue 的 PC 端表单/表格组件,支持增删改查、虚拟列表、虚拟树、懒加载、快捷菜单、数据校验、树形结构、打印导出、表单渲染、数据分页、弹窗、自定义模板、渲染器、JSON 配置式...
1,492 lines (1,404 loc) • 273 kB
text/typescript
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