Skip to content

Latest commit

 

History

History
679 lines (665 loc) · 26.7 KB

vue3table.md

File metadata and controls

679 lines (665 loc) · 26.7 KB

vue3.0 基础表格算法

基础数据算法

import { buildProps } from "@wisdom-plus/utils/props"
import {defineComponent, ExtractPropTypes, PropType} from "vue"

export const tableProps = buildProps({
    columns: {
        type: [Array] as PropType<Array<any>>,
        default: ()=>[]
    },
    data: {
        type: [Array] as PropType<Array<any>>,
        default: ()=>[]
    },
    spanCell: {
        type: [Function] as PropType<Function>,
        default: ()=>[]
    },
})

export type TableProps = ExtractPropTypes<typeof tableProps>

export default defineComponent({
    name: 'WpTable',
    props: tableProps,
    setup(props) {
        /**
         * 获取合并单元格栏目数据
         * @param columns
         */
        const getColumnsMergedCell = (columns)=>{
            let rowspanMax = 0;
            let colspanMax = 0;
            let columns_col:any = [];
            let columnsMap = {};
            /**
             * 平铺单元格栏目
             * @param itemColumns 单元格栏目集合
             * @param result 返回平铺数据
             */
            const flatDry:any = (itemColumns,result:any = []) =>{
                itemColumns.forEach(it=>{
                    if(it.columns.length > 1){
                        flatDry(it.columns,result)
                    }else {
                        result.push(it);
                    }
                })
                return result;
            }
            /**
             * 获取单元格栏目信息
             * @param columns 栏目
             * @param level 栏目层深
             */
            const getItemColumns = (columns, level = 0)=>{
                return columns.map(it=>{
                    const levelIndex = level+1;
                    rowspanMax = levelIndex > rowspanMax ? levelIndex : rowspanMax;
                    colspanMax = it.columns ? colspanMax : colspanMax + 1;
                    columnsMap[levelIndex] = columnsMap[levelIndex] || [];
                    const itemColumns = getItemColumns(it.columns || [],levelIndex);
                    let colspanArr = flatDry(itemColumns);
                    const item = {
                        ...it,
                        level:levelIndex,
                        columns:itemColumns,
                        colspanArr,
                        colspan:colspanArr.length || 1,
                    }
                    if(!it.columns){
                        columns_col.push(item);
                    }
                    columnsMap[levelIndex].push(item);
                    return item;
                });
            }
            getItemColumns(columns);
            return {
                // 栏目映射
                columnsMap,
                // 总行数
                rowspanMax,
                // 总列数
                colspanMax,
                // 栏目列集合
                columns_col,
            }
        }
        /**
         * 合并body单元格
         */
        const getTbodyMergedCells:any = ()=>{
            const result:any = [];
            const spanCellFilters:any = []
            props.data.forEach((row,rowIndex)=>{
                const item:any = [];
                theadColumns.columns_col.forEach((column, columnIndex)=>{
                    const it:any = {
                        column,
                        row,
                        rowIndex,
                        columnIndex,
                    }
                    const spanCell = props.spanCell(it) || []
                    it.spanCell = [spanCell[0] || 1,spanCell[1] || 1];
                    if(it.spanCell.reduce((a,b)=>a+b) > 2){
                        for(let x = 0; x < it.spanCell[1]; x++){
                            for(let y = 0; y < it.spanCell[0]; y++){
                                const str = [rowIndex + y, columnIndex + x].join("-");
                                const startStr = [rowIndex, columnIndex].join("-");
                                if(str != startStr){
                                    spanCellFilters.push(str)
                                }
                            }
                        }
                    }
                    item.push(it);
                });
                result.push(item)
            });
            return result.map((it,rk)=>{
                return it.filter((ee,ck)=>{
                    return !spanCellFilters.includes([rk,ck].join("-"))
                })
            })
        }

        const theadColumns = getColumnsMergedCell(props.columns)
        const tbodyCells = getTbodyMergedCells();
        return {
            theadColumns,
            tbodyCells
        }
    },
    render() {
        const theadRender = ()=>(<thead>
            {Object.values(this.theadColumns.columnsMap).map((item:any,key:number)=>(
                <tr>
                    {item.map((column)=>(
                        <th class={{
                                "wp-table__cell":true,
                            }}
                            colspan={column.colspan}
                            rowspan={column.colspan === 1 ? this.theadColumns.rowspanMax-key:1}>
                            <div class={{
                                "cell":true
                            }}>{column.label}</div>
                        </th>
                    ))}
                </tr>
            ))}
        </thead>)
        const tbodyRender = ()=>(<tbody>
            {this.tbodyCells.map(item=>(
                <tr>
                    {item.map(({column, row, spanCell, rowIndex}:any)=>(
                        <td class={{
                                "wp-table__cell":true,
                            }}
                            rowspan={spanCell[0]}
                            colspan={spanCell[1]}
                        >
                            <div class={{
                                "cell":true
                            }}>{rowIndex}/{row[column.prop]}</div>
                        </td>
                    ))}
                </tr>
            ))}
        </tbody>)
        return (
            <div class={'wp-table'}>
                <table class={{
                    'wp-table--body': true,
                }} border={0} cellpadding={0} cellspacing={0}>
                    {theadRender()}
                    {tbodyRender()}
                </table>
            </div>
        )
    }
})

完整封装代码

import { buildProps } from "@wisdom-plus/utils/props"
import {defineComponent, ExtractPropTypes, PropType, computed, ref, watch} from "vue"
//  虚拟滚动条
import  simpleScroll from "./simpleScroll.js"
export const tableProps = buildProps({
    columns: {
        type: [Array] as PropType<Array<any>>,
        default: ()=>[]
    },
    data: {
        type: [Array] as PropType<Array<any>>,
        default: ()=>[]
    },
    spanCell: {
        type: Function as PropType<(CellItem:{
            column:object;
            row:object;
            rowIndex:number;
            columnIndex:number;
        }) => number[]>,
        default: ()=>()=>void (0)
    },
    stripe: {
        type: [Boolean] as PropType<boolean>,
        default: false
    },
    border: {
        type: [Boolean] as PropType<boolean>,
        default: false
    },
    height: {
        type: [String, Number] as PropType<string|number>,
        default: null
    },
    tree: {
        type: [Boolean, String] as PropType<boolean|string>,
        default: false
    },
    treeLevelDeep: {
        type: [Number] as PropType<number>,
        default: 15
    },
    treeChildrenFieldName: {
        type: [String] as PropType<string>,
        default: "children"
    },
    draggable: {
        type: [Boolean] as PropType<boolean>,
        default: false
    },
})

export type TableProps = ExtractPropTypes<typeof tableProps>

export default defineComponent({
    name: 'WpTable',
    props: tableProps,
    setup(props) {
        // 当前表格数据
        const tableDatas:any = ref([]);
        /**
         * 获取合并单元格栏目数据
         * @param columns
         */
        const getColumnsMergedCell = (columns)=>{
            let rowspanMax = 0;
            let colspanMax = 0;
            let columns_col:any = [];
            let columnsMap = {};
            let columnsIndex = 0;
            /**
             * 平铺单元格栏目
             * @param itemColumns 单元格栏目集合
             * @param result 返回平铺数据
             */
            const flatDry:any = (itemColumns,result:any = []) =>{
                itemColumns.forEach(it=>{
                    if(it.columns.length > 1){
                        flatDry(it.columns,result)
                    }else {
                        result.push(it);
                    }
                })
                return result;
            }
            /**
             * 获取单元格栏目信息
             * @param columns 栏目
             * @param level 栏目层深
             */
            const getItemColumns = (columns, level = 0)=>{
                return columns.map(it=>{
                    const levelIndex = level+1;
                    rowspanMax = levelIndex > rowspanMax ? levelIndex : rowspanMax;
                    colspanMax = it.columns ? colspanMax : colspanMax + 1;
                    columnsMap[levelIndex] = columnsMap[levelIndex] || [];
                    const itemColumns = getItemColumns(it.columns || [],levelIndex);
                    let colspanArr = flatDry(itemColumns);
                    let fixedConfig:any = {};
                    if(it.fixed){
                        // 固定列配置
                        fixedConfig.position =  "sticky";
                        fixedConfig[it.fixed === true ? 'left' : (it.fixed || 'left')] = `${it.offset || 0}px`;
                    }
                    const item = {
                        ...it,
                        level:levelIndex,
                        columns:itemColumns,
                        colspanArr,
                        colspan:colspanArr.length || 1,
                        index:it.columns ? -1:columnsIndex += 1,
                        fixedConfig
                    }
                    if(!it.columns){
                        columns_col.push(item);
                    }
                    columnsMap[levelIndex].push(item);
                    return item;
                });
            }
            getItemColumns(columns);
            return {
                // 栏目映射
                columnsMap,
                // 总行数
                rowspanMax,
                // 总列数
                colspanMax,
                // 栏目列集合
                columns_col,
            }
        }
        /**
         * 扁平化数据
         * @param bodyCellData
         * @param treeChildrenFieldName
         * @param callback
         * @param result
         * @param parent
         * @param level
         */
        const flattenDeep = (bodyCellData,treeChildrenFieldName:string,callback:any = ()=>{}, result:any = [], parent:any = null,level:number = 0)=>{
            bodyCellData.forEach(it=>{
                const item = ref(it);
                callback({item, parent,level, bodyCellData, result});
                result.push(item.value);
                if(Object.prototype.toString.call(item.value[treeChildrenFieldName]) === '[object Array]'){
                    flattenDeep(item.value[treeChildrenFieldName], treeChildrenFieldName, callback,  result, item.value,level+1);
                }
            })
            return result;
        }
        /**
         * 合并body单元格
         */
        const getTbodyMergedCells:any = (bodyCellData, notResetShow = false)=>{
            if(props.tree){
                bodyCellData = flattenDeep(bodyCellData, props.treeChildrenFieldName, ({item, parent,level})=>{
                    if(!notResetShow){
                        item.value.$$treeShow = false;
                    }
                    item.value.$$parent = parent;
                    item.value.$$parentDeep = parent ? parent.$$parentDeep.concat([parent]) : [];
                    item.value.$$level = level;
                })
            }
            const result:any = [];
            const spanCellFilters:any = []
            bodyCellData.forEach((row,rowIndex)=>{
                const item:any = [];
                theadColumns.value.columns_col.forEach((column, columnIndex)=>{
                    const it:any = {
                        column,
                        row,
                        rowIndex,
                        columnIndex,
                    }
                    row.$$rowIndex = rowIndex;
                    const spanCell = props.spanCell(it) || []
                    it.spanCell = [spanCell[0] || 1,spanCell[1] || 1];
                    if(it.spanCell.reduce((a,b)=>a+b) > 2){
                        for(let x = 0; x < it.spanCell[1]; x++){
                            for(let y = 0; y < it.spanCell[0]; y++){
                                const str = [rowIndex + y, columnIndex + x].join("-");
                                const startStr = [rowIndex, columnIndex].join("-");
                                if(str != startStr){
                                    spanCellFilters.push(str)
                                }
                            }
                        }
                    }
                    item.push(it);
                });
                result.push(item)
            });
            return result.map((it,rk)=>{
                return it.filter((ee,ck)=>{
                    return !spanCellFilters.includes([rk,ck].join("-"))
                })
            })
        }

        let theadColumns:any = ref({});// 表头数据
        let tbodyCells:any = ref([]);// 表单元格数据
        let colgroupArr:any = ref([]);// 表限制关联数据
        let tableWidth:any = ref(null);// 表格宽度
        // 重置表渲染
        const resetTbale = (newdata, bool)=>{
            tableDatas.value = newdata;
            theadColumns.value = getColumnsMergedCell(props.columns)
            tbodyCells.value = getTbodyMergedCells(tableDatas.value, bool);
            colgroupArr = computed(()=>{
                return theadColumns.value.columns_col.filter((e)=>props.height || !!e.width);
            })
            tableWidth = computed(()=>{
                const sum = colgroupArr.value.reduce((a,b)=>a+(b.width),0)
                return sum ? ((sum+50) + 'px') : null;
            })
        }
        // 数据监听,响应式
        watch([
            computed(()=>props.columns),
            computed(()=>props.data)
        ],()=>{
            resetTbale(props.data, false);
        },{ immediate:true})

        let isDragstart = false;// 是否拖拽
        let draggableObjData = ref(null);// 拖拽目标对象数据
        let draggableObjDataIndex:any = ref(-1);// 拖拽目标对象索引
        let draggableObjDataIndexstart:any = ref(-1);// 拖拽对象开始索引
        let draggableInset:any = ref(false);// 是否同级拖拽,代表内部追加
        let draggableForbid:any = ref(false);// 是否禁止存放
        let draggableForbidIndex:any = ref(-1);// 是否禁止存放索引
        // 开始拖拽
        const onDragstart = (ev)=>{
            isDragstart = true;
            draggableObjData.value = null;
            draggableObjDataIndex.value = -1;
            draggableObjDataIndexstart.value = ev.target.attributes.getNamedItem("index").value;
            draggableInset.value = false;
            draggableForbid.value = false;
            draggableForbidIndex.value = -1;
        }
        // 结束拖拽
        const onDragend = ()=>{
            if(!draggableForbid.value && draggableObjDataIndexstart.value !==  draggableObjDataIndex.value){
                const start = Number(draggableObjDataIndexstart.value);
                const end = Number(draggableObjDataIndex.value);
                const child_start = tbodyCells.value[start][0].row;
                const child_end = tbodyCells.value[end][0].row;
                const deleteStart = (data)=>{
                    return data.filter(it=>{
                        if(Object.prototype.toString.call(it[props.treeChildrenFieldName]) === '[object Array]'){
                            it[props.treeChildrenFieldName] = deleteStart(it[props.treeChildrenFieldName])
                        }
                        delete it.$$level;
                        delete it.$$parent;
                        delete it.$$parentDeep;
                        return it.$$rowIndex !== child_start.$$rowIndex
                    })
                }
                const addStart = (data)=>{
                    let result:any = [];
                    data.forEach((it,k)=>{
                        if(Object.prototype.toString.call(it[props.treeChildrenFieldName]) === '[object Array]'){
                            it[props.treeChildrenFieldName] = addStart(it[props.treeChildrenFieldName])
                        }
                        if(it.$$rowIndex === child_end.$$rowIndex){
                            if(draggableInset.value){
                                // 内部追加
                                data.forEach((dataIt,kk)=>{
                                    if(k === kk){
                                        dataIt[props.treeChildrenFieldName] = dataIt[props.treeChildrenFieldName] || [];
                                        dataIt[props.treeChildrenFieldName].push(child_start)
                                    }
                                    dataIt.$$treeShow =  flattenDeep([child_start],props.treeChildrenFieldName).filter(fit=>fit.$$treeShow).length > 0;
                                    result.push(dataIt);
                                });
                            }else {
                                // 同级追加
                                data.forEach((dataIt,kk)=>{
                                    result.push(dataIt);
                                    if(k === kk){
                                        result.push(child_start)
                                    }
                                });
                            }
                        }
                    })
                    if(result.length > 0){
                        return  result;
                    }
                    return data
                }
                resetTbale(addStart(deleteStart(tableDatas.value)), true);
            }
            isDragstart = false;
            draggableObjData.value = null;
            draggableObjDataIndex.value = -1;
            draggableObjDataIndexstart.value = -1;
            draggableInset.value = false;
            draggableForbid.value = false;
            draggableForbidIndex.value = -1;
        }
        // 拖拽过程
        const onDragover = (ev)=>{
            draggableForbid.value = false;
            try {
                if(isDragstart){
                    let el = ev.path.find(e=>(e.tagName || "").toLowerCase().indexOf("tr") > -1);
                    if(el){
                        draggableForbidIndex.value = Number(el.attributes.getNamedItem("index").value);
                        const srart_row = tbodyCells.value[draggableObjDataIndexstart.value][0].row
                        const end_row = tbodyCells.value[draggableForbidIndex.value][0].row
                        if(srart_row.index !== end_row.index && end_row.$$parentDeep.map(e=>e.index).indexOf(srart_row.index) === -1){
                            draggableInset.value = ev.offsetY < el.clientHeight*0.5;
                            draggableObjDataIndex.value = draggableForbidIndex.value;
                            draggableObjData.value = tbodyCells.value[draggableObjDataIndex.value];
                        }else {
                            draggableObjData.value = null;
                            draggableObjDataIndex.value = -1;
                            draggableInset.value = false;
                            draggableForbid.value = true;
                        }
                    }
                }
            }catch (e){}
        }
        return {
            onDragstart,
            onDragend,
            onDragover,
            draggableObjData,
            draggableObjDataIndex,
            draggableForbidIndex,
            draggableObjDataIndexstart,
            draggableInset,
            draggableForbid,
            theadColumns,
            tbodyCells,
            colgroupArr,
            tableWidth,
            flattenDeep,
            tableDatas,
        }
    },
    mounted() {
        this.$nextTick(()=>{
            const el = this.$el.querySelector('.wp-table--fixed-header--wrapper')
            if(el){
                simpleScroll(el, this.$el).init()
            }
        })
    },
    render() {
        const getNameIndex =  (index)=>`wp-table_${this._.uid}_column_${index || 0}`
        const theadRender = ()=>(<thead>
            {Object.values(this.theadColumns.columnsMap).map((item:any,key:number)=>(
                <tr>
                    {item.map((column)=>(
                        <th class={{
                                "wp-table__cell":true,
                                [getNameIndex(column.index)]:true,
                            }}
                            style={{
                                ...column.fixedConfig
                            }}
                            align={column.align}
                            colspan={column.colspan}
                            rowspan={column.colspan === 1 ? this.theadColumns.rowspanMax-key:1}>
                            <div class={{
                                "cell":true
                            }}>{ this.$slots.header?.(column) || column.label}</div>
                        </th>
                    ))}
                </tr>
            ))}
        </thead>)
        const cellClick = ({row})=>{
            if(this.tree && Object.prototype.toString.call(row[this.treeChildrenFieldName]) === '[object Array]'){
                this.flattenDeep(row[this.treeChildrenFieldName] || [],this.treeChildrenFieldName).forEach(_row=>_row.$$treeShow = false);
                row.$$treeShow = !row.$$treeShow;
            }
        }
        const treeArrowRender = (bool, row)=>(
            <i class={{
                "cell-tree-item-arrow":true,
                "cell-tree-item-arrow-parent":bool,
                "cell-tree-item-arrow-parent-open":row.$$treeShow,
            }} style={{
                marginLeft:`${this.treeLevelDeep*row.$$level}px`
            }}></i>
        )

        const tbodyRender = ()=>(<tbody onDragover={this.onDragover}>
            {this.tbodyCells.map((item,key)=>!item[0].row.$$parent || item[0].row.$$parent.$$treeShow  ?(
                <tr
                    draggable={this.draggable}
                    onDragstart={this.onDragstart}
                    onDragend={this.onDragend}
                    index={key}
                    class={{
                    "stripe":this.stripe,
                    "draggable-is-active":key === Number(this.draggableForbidIndex),
                    "draggable-is-active-inset":key === Number(this.draggableObjDataIndex) && this.draggableInset,
                    "draggable-is-active-forbid":key === Number(this.draggableForbidIndex) && this.draggableForbid,
                    "draggable-is-active-start":key === Number(this.draggableObjDataIndexstart),
                }}>
                    {item.map(({column, row, spanCell, rowIndex, columnIndex}:any)=>(
                        <td onClick={(ev)=>cellClick({column, row, spanCell, rowIndex, columnIndex, ev})} class={{
                                "wp-table__cell":true,
                                [getNameIndex(column.index)]:true,
                            }}
                            align={column.align}
                            rowspan={spanCell[0]}
                            colspan={spanCell[1]}
                            style={{
                                minWidth:column.minWidth,
                                maxWidth:column.maxWidth,
                                ...column.fixedConfig
                            }}
                        >
                            <div class={{
                                "cell":true,
                                "cell-tree-item":this.tree && (this.tree === column.prop || column.index === 1)
                            }}>
                                {this.tree && (this.tree === column.prop || column.index === 1) ?
                                    ((row[this.treeChildrenFieldName] || []).length > 0 ? (
                                        treeArrowRender(true,row)
                                    ) : treeArrowRender(false,row))
                                    : null}
                                {this.$slots.default?.({
                                column, row, spanCell, rowIndex, columnIndex
                            }) || row[column.prop]}
                            </div>
                        </td>
                    ))}
                </tr>
            ) : null)}
        </tbody>)
        const colgroupRender = ()=>this.colgroupArr ? (
            <colgroup>
                {this.colgroupArr.map((it)=>(
                    <col name={getNameIndex(it.index)} width={it.width}></col>
                ))}
            </colgroup>
        ) : null;
        const tableRender = (isFixedHeader = false)=>(<div
            class={{
            'wp-table--body': true,
            'wp-table--body--fixed-header': isFixedHeader,
        }}>
            <div class={'wp-table--body--content'}>
                <table border={0} cellPadding={0} cellSpacing={0} style={{width:this.height ? this.tableWidth : '100%'}}>
                    { this.height ? [
                        isFixedHeader ? [colgroupRender(),theadRender()] : [colgroupRender(),tbodyRender()]
                    ] : [this.colgroupArr.length === 0 ? null: colgroupRender(),theadRender(),tbodyRender()]}
                </table>
                {!isFixedHeader && this.tbodyCells.length === 0 ? (<div class={{
                    'wp-table--empty':true,
                }}>暂无数据!</div>) : null}
            </div>
        </div>)
        return (
            <div class={{
                'wp-table':true,
                'wp-table--border':this.border,
                'wp-table--fixed-header':this.height,
            }}>
                <div class={{
                    'wp-table--fixed-header--wrapper--box':true
                }}>
                    {this.height ? tableRender(true) : null}
                    <div class={{
                        'wp-table--fixed-header--wrapper': this.height,
                    }} style={{
                        height:typeof this.height === 'number' ? `${this.height}px` :this.height
                    }}>
                        {tableRender()}
                    </div>
                </div>
            </div>
        )
    }
})