UNPKG

magiccube-vue3

Version:

vue3-js版组件库

499 lines (446 loc) 19 kB
import THead from './head' import TBody from './body' import { ref, provide, computed, getCurrentInstance, nextTick, watch, Transition } from 'vue' import Scrollbar from './scrollbar' import '../../style/table.less' import * as utils from '../../utils/common' import ICON_SETTING from '../../img/icon_setting.svg' const getAllColumnWidthSum = (arr = []) => { const _a = arr.map(n => n.cellWidth) return sum(_a) } const sum = (arr = []) => arr?.length ? arr.reduce((a, b) => a + b) : 0 const Table = { name: 'McTable', props: { data: [Array, null], checkbox: Boolean, emptyText: { type: String, default: '暂无相应信息' }, emptyImg: String, height: { type: [Number, String], default: 200 }, fixedLeft: { type: [String, Number], default: 0 }, fixedRight: { type: [String, Number], default: 0 }, checkboxWidth: { type: [String, Number], default: 35 }, tableName: { type: String, default: 'mc-table-component' }, /** * 列表类型:平铺、流体 * 平铺:default * 流体:fixed */ type: { type: String, default: 'default' }, /* 表头吸顶定位 */ stickyTop: { type: [Number, String], default: 0 }, // 滚动条触点尺寸 scrollThumbSize: { type: Number, default: 6 }, // 底部滚动条是否吸底 bottomScrollSticky: Boolean, bottomScrollStickySite: { type: Number, default: 40 }, enableTableSetting: Boolean, }, emits: ['update:data', 'select', 'setting'], setup(props, { emit, slots, attrs }) { const mainEl = ref(null) const headerWrapEl = ref(null) const bodyWrapEl = ref(null) const scrollTop = ref(0) const scrollLeft = ref(0) const uuid = utils._uuid() // 是否显示滚动条 const displayScrollbarX = ref(false) const displayScrollbarY = ref(false) // checkbox列公共样式 const checkboxStyle= { position: 'absolute', top: '50%', left: '15px', transform: 'translate(0, -50%)' } const model = computed({ get() { return props.data || [] }, set(list) { emit('update:data', list) } }) // 是否显示冻结列及表头阴影 const showLeftShadow = ref(false) const showRightShadow = ref(false) // 鼠标悬停行标 const isHoverRow = ref(false) // 多级表头行数 const rowCount = ref(0) // 全选状态 const selectAll = ref(false) // 基础列宽 const BASE_COLUMN_WIDTH = 145 // 父级页面包裹列表部分宽度 const wrapWidth = computed(() => mainEl.value?.offsetWidth || 0) // 表身整体高度 不包含表头 const bodyHeight = computed(() => props.height === 'auto' ? 'auto' : props.height - (headerWrapEl?.value?.offsetHeight || 0)) // 表头checkbox的include显示状态 const subCheckboxSomeSelected = computed(() => !selectAll.value && model.value.some(n => n.isChecked)) /* 处理可用列 */ const column = computed(() => { const instance = getCurrentInstance() const _default = instance.vnode.children.default()[0] /** * !注意: * 如果空间使用JSX方法引用,该处default的值是数组 * 如果是在template中直接饮用,该处将是一个对象 * 兼容两种引用方式 */ const _arr = Array.isArray(_default)? collectSlots(_default) : _default.children.map((n, i) => ({ ...n, rowspan: 0, enableCell: true })) /* 处理多级表头 */ return _arr }) /* 处理多级表头函数 */ let multiTitleRowGroupNumber = 0 // 多级表头分组编号、区分子级表头颜色 const collectSlots = (el, i = 0, group) => { let _arr = [] rowCount.value = rowCount.value < i ? i + 1 : rowCount.value Array.isArray(el) && el.forEach(n => { let _slot let headerSlot if (n.children?.header && n.children.header({})) { _slot = n.children.header({}) headerSlot = _slot } else { _slot = n.children.default({}) headerSlot = null } n.rowspan = i if(group) n.groupNumber = group if (Array.isArray(_slot) && _slot.length) { multiTitleRowGroupNumber++ n.colspan = _slot.length _arr.push(n) _arr = [..._arr, ...collectSlots(_slot, i + 1, multiTitleRowGroupNumber)] } else { n.enableCell = true n.headerSlot = headerSlot _arr.push(n) } }) return _arr } /* 计算列表宽度,如果是流体列表,进行冻结列定位属性设置 */ const totalWidth = computed(() => { const enableCell = column.value.filter(n => n.enableCell) if(props.type === 'default') { /* 平铺列表 给每一列设置宽度 如果在调用处设置了列宽就用设置的参数 如果未设置就设置为0(0为自动列宽) */ enableCell.forEach(n => { const { width } = n.props n.cellWidth = width || 0 }) return '100%' } else { /* 流体列表设置 如果没有设置column宽度 就取用默认宽度 */ const existWidthAttr = enableCell.reduce((a, b) => a + Number(b?.props?.width || 0), 0) const freeWidthColLength = (enableCell.filter(n => n.props && !n.props.width).length) || 1 const freeWidthSize = wrapWidth?.value? wrapWidth?.value - existWidthAttr : 0 const every = freeWidthSize / freeWidthColLength const _w = wrapWidth?.value === 0? Math.max(document.body.offsetWidth / enableCell.length, BASE_COLUMN_WIDTH) : Math.max(every, BASE_COLUMN_WIDTH) const columnsWidth = [] enableCell.forEach(n => { const { width } = n.props n.cellWidth = width || _w columnsWidth.push(n.cellWidth) }) const _arr = handleColumnFixedAttr(enableCell, columnsWidth) return getAllColumnWidthSum(_arr) + (props.checkbox ? props.checkboxWidth : 0) + 'px' } }) /* 设置固定列属性 */ const handleColumnFixedAttr = (arr, allWidth) => { let left = props.checkbox? props.checkboxWidth : 0 let right = sum(allWidth.slice(allWidth.length - props.fixedRight)) arr.forEach((n, i) => { // 左边固定列 if(i < props.fixedLeft){ n.fixedLeft = true n.fixedLeftPosition = left left += allWidth[i] } // 右边固定列 if(arr.length - i <= props.fixedRight){ right -= allWidth[i] n.fixedRight = true n.fixedRightPosition = right } // 左边固定列最后一列 n.fixedLeftLast = i === props.fixedLeft - 1 // 右边固定列第一列 n.fixedRightFirst = arr.length - i === props.fixedRight }) return arr } const handleSelectAll = () => { model.value = model.value.map(n => n.isChecked = selectAll.value) if (selectAll.value) { emit('select', model.value) } else { emit('select', []) } } const handleSelectChange = (item) => { if (!item.isChecked) { selectAll.value = false } emit('select', model.value.filter(n => n.isChecked)) } /* 向子集传递数据 */ provide('isHoverRow', isHoverRow) provide('selectAll', selectAll) provide('handleSelectAll', handleSelectAll) provide('handleSelectChange', handleSelectChange) provide('checkboxStyle', checkboxStyle) provide('showLeftShadow', showLeftShadow) provide('showRightShadow', showRightShadow) /* tbody滚动事件 */ const handleScroll = (e) => { scrollTop.value = e.target.scrollTop scrollLeft.value = e.target.scrollLeft /* 同步表身的滚动定位 表身与表头同步滚动 */ headerWrapEl.value.scrollLeft = e.target.scrollLeft /* 冻结列的阴影显示控制 */ showLeftShadow.value = e.target.scrollLeft > 0 showRightShadow.value = e.target.scrollLeft < bodyWrapEl.value.scrollWidth - bodyWrapEl.value.offsetWidth } /** * 滚动条相关方法 */ const scrollThumbWidth = ref(0) const scrollThumbHeight = ref(0) // 倍率 const multiplierX = ref(0) const multiplierY = ref(0) const mouseDraggerState = ref(false) let mouseInArea = false const handleSetScrollbar = () => { try{ // 滚动倍率计算 multiplierX.value = bodyWrapEl.value.scrollWidth / bodyWrapEl.value.offsetWidth multiplierY.value = bodyWrapEl.value.scrollHeight / bodyWrapEl.value.offsetHeight // 滚动条尺寸计算 scrollThumbWidth.value = bodyWrapEl.value.offsetWidth * (bodyWrapEl.value.offsetWidth / bodyWrapEl.value.scrollWidth) scrollThumbHeight.value = bodyWrapEl.value.offsetHeight * (bodyWrapEl.value.offsetHeight / bodyWrapEl.value.scrollHeight) } catch (e) { // error } } // 滚动条拖动事件 const handleScrollbarMove = ({type, x, y}) => { switch(type){ case 'horizontal': { const _x = x * multiplierX.value headerWrapEl.value.scrollLeft = _x bodyWrapEl.value.scrollLeft = _x break } case 'vertical': { const _y = y * multiplierY.value bodyWrapEl.value.scrollTop = _y break } } } // 鼠标进入列表区域显示滚动条 const handleMouseenterTableArea = () => { try{ mouseInArea = true handleSetScrollbar() displayScrollbarX.value = bodyWrapEl.value.scrollWidth !== bodyWrapEl.value.offsetWidth && model.value?.length displayScrollbarY.value = bodyWrapEl.value.scrollHeight !== bodyWrapEl.value.offsetHeight && model.value?.length } catch (e) { // error } } const handleMouseleaveTableArea = () => { mouseInArea = false if(!mouseDraggerState.value){ displayScrollbarX.value = false displayScrollbarY.value = false } } const handleMouseDraggerState = state => { mouseDraggerState.value = state if(!mouseInArea) handleMouseleaveTableArea() } // 监听列表数据变化 改变全选状态 watch(() => props.data, () => selectAll.value = false) /* 初始化右边冻结列阴影状态 */ nextTick(() => { showRightShadow.value = props.type === 'fixed' }) return () => ( <div ref={mainEl} id={uuid} class="mc-table" onMouseenter={handleMouseenterTableArea} onMouseleave={handleMouseleaveTableArea}> {/* table header */} <div style={props.height === 'auto'? { position: 'sticky', top: props.stickyTop, zIndex: 100 } : {}}> <div ref={headerWrapEl} class={[ 'mc-table__panel', 'mc-table__panel--header', { 'table-header-shadow': scrollTop.value > 0 } ]}> <THead column={column.value} width={totalWidth.value} rowCount={rowCount.value || 1} checkbox={props.checkbox} checkboxWidth={props.checkboxWidth} fixedLeft={props.fixedLeft} type={props.type} subCheckboxSomeSelected={subCheckboxSomeSelected.value} /> </div> { props.enableTableSetting? ( <span class="mc-table__panel__setting" onClick={() => emit('setting')}> <img src={ICON_SETTING} /> </span> ) : '' } </div> {/* table body */} <div class={[ 'mc-table__panel', 'mc-table__panel--body', ]} style={{ height: bodyHeight.value === 'auto'? '' : Number(bodyHeight.value) + 'px' }}> {/* 滚动包裹区域 方便scrollbar定位 */} <div ref={bodyWrapEl} class={[ 'mc-table__panel--body--scroll' ]} onScroll={handleScroll}> { model.value?.length ? ( <TBody column={column.value} width={totalWidth.value} data={model.value} checkbox={props.checkbox} checkboxWidth={props.checkboxWidth} fixedLeft={props.fixedLeft} type={props.type} onRenderDone={handleSetScrollbar} /> ) : ( <div class="mc-table__panel--empty"> { slots.emptyList? slots.emptyList() : <div>{props.emptyText}</div> } </div> ) } </div> {/* 滚动条 */} { !props.bottomScrollSticky && !attrs.bottomScrollSticky? ( <Transition name="mc-fade"> <Scrollbar v-show={displayScrollbarX.value} type="horizontal" scroll-height={props.scrollThumbSize} thumb-width={scrollThumbWidth.value} x={scrollLeft.value / multiplierX.value} horizontalStyle={attrs['horizontal-style']} onMouseDragger={handleMouseDraggerState} onMove={handleScrollbarMove} /> </Transition> ) : '' } { props.height !== 'auto'? ( <Transition name="mc-fade"> <Scrollbar v-show={displayScrollbarY.value} type="vertical" scroll-width={props.scrollThumbSize} thumb-height={scrollThumbHeight.value} y={scrollTop.value / multiplierY.value} vertical-style={attrs['vertical-style']} onMouseDragger={handleMouseDraggerState} onMove={handleScrollbarMove} /> </Transition> ) : '' } </div> {/* 底部横向滚动条吸底 */} { props.bottomScrollSticky || attrs.bottomScrollSticky? ( <div style={{ position: 'sticky', bottom: props.bottomScrollStickySite + 'px', zIndex: 10 }}> <div style={{ position: 'relative', overflow: 'hidden', height: props.scrollThumbSize + 'px' }}> <Transition name="mc-fade"> <Scrollbar v-show={displayScrollbarX.value} type="horizontal" scroll-height={props.scrollThumbSize} thumb-width={scrollThumbWidth.value} x={scrollLeft.value / multiplierX.value} horizontalStyle={attrs['horizontal-style']} onMouseDragger={handleMouseDraggerState} onMove={handleScrollbarMove} /> </Transition> </div> </div> ) : '' } </div> ) } } Table.install = (app) => app.component(Table.name, Table) const McTable = Table export { McTable, McTable as default }