magiccube-vue3
Version:
vue3-js版组件库
499 lines (446 loc) • 19 kB
JavaScript
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 }