Element-Plus 二次封装 el-table LeTable组件

一.背景

在许多 Vue 项目中,我们经常需要使用表格来展示数据。Element Plus 提供了一个功能丰富的 el-table 组件,但在某些情况下,我们可能需要对其进行二次封装以满足特定需求。

本文将介绍基于 Element Plus 的 Table 组件的高阶封装LeTable,提供更便捷的配置方式和增强功能。

二.LeTable特性

  • 🚀 声明式配置表格列,表格自适应宽高
  • 🔧 默认支持 刷新,全屏
  • 🔧 支持列(Column)显/隐控制以及排序(配置columnsConfig 配置项remote存储)
  • 🔧 支持表头/单元格自定义渲染作用域插槽、tsx语法/h渲染
  • 📦 内置分页(Pagination)功能
  • 🔍 集成查询表单LeSearchForm

三.功能拆解

1.搜索表单

通过itemType类型动态创建需要的formItem生成表单

html 复制代码
// SearchForm.vue

<script lang="tsx">  
import { defineComponent, watch, ref, computed, PropType } from 'vue'  
import { Refresh, Search } from '@element-plus/icons-vue'  
import { LeFormItem, FormConfigOpts, FormItemSlots, SlotOption } from '@/components/FormConfig/formConfig.types'  
import InputNumber from './InputNumber.vue'  
import InputNumberRange from './InputNumberRange.vue'  
import CustomRender from './CustomRender.vue'  
import { useI18n } from 'vue-i18n'  
import { getOptions, renderOption, get_formSlots } from '@/components/FormConfig/utils'  
  
const emits = ['update:searchData']  
export type SearchFormItem = LeFormItem  
export const SearchFormProps = {  
    forms: {  
    // SearchForm: 与FormConfig不同的是 change的第一个参数的params, 去掉了原来的value 选项  
    type: Array as PropType<SearchFormItem[]>,  
        required: true  
    },  
    // 后台传递的初始值 以及 双向绑定 对象  
    searchData: {  
        required: true,  
        type: Object as PropType<Recordable>  
    },  
    // item 修改后 自动触发搜索  
    triggerSearchAuto: {  
        type: Boolean,  
        default: false  
        // default: true  
    },  
    // 自动触发 Automatic trigger  
    // formModal的配置项对象  
    formConfig: {  
        type: Object as PropType<FormConfigOpts>,  
        default: () => ({})  
    },  
    loading: {  
        type: Boolean,  
        default: false  
    },  
    reset: {  
        type: Function as PropType<(initSearchData: Record<string, any>) => any>  
    }  
}  
export const SearchForm = defineComponent({  
    name: 'LeSearchForm',  
    props: SearchFormProps,  
    emits,  
    setup(props, ctx) {  
        const { t } = useI18n()  
        const formRef = ref(/*formRef*/)  
        let initSearchData = undefined  
        watch(  
        () => props.searchData,  
        (newValue, oldValue) => {  
            if (!initSearchData && newValue) {  
                initSearchData = { ...newValue }  
            }  
        },  
        {  
            immediate: true  
        }  
        )  

        // 重置搜索  
        const local_resetHandler = () => {  
            // 若有reset 将不会触发默认重置的操作  
            const emitReset = props.reset  
            const _initSearchData = { ...(initSearchData || {}) }  
            if (emitReset) {  
                emitReset(_initSearchData)  
            } else {  
                // formRef.value!.resetFields()  
                // 撤回为初始化状态  
                ctx.emit('update:searchData', _initSearchData)  
            }  
        }  
        // 搜索  
        const searchHandler = () => {  
            ctx.emit('update:searchData', { ...props.searchData })  
        }  
        // 强行修改初始化数据(用于 重置方法进行数据重置)  
        const forceUpdateInitParams = (searchData = props.searchData) => {  
            initSearchData = { ...searchData }  
        }  
        ctx.expose({  
            formRef,  
            forceUpdateInitParams  
        })  
        const vSlots = ctx.slots  
        const realForms = computed(() => {  
            return (props.forms || []).map(form => {  
                return {  
                    ...form,  
                    le_slots: get_formSlots(vSlots, form.slots)  
                }  
            })  
        })  

        // render渲染  
        return () => {  
            const { searchData, formConfig = {}, triggerSearchAuto } = props  
            let warpClass = 'le-search-form-container labelStyle'  
            const getItemStyle = (itemStyle, defaultWidth) => {  
            return itemStyle + (/width\:/g.test(itemStyle) ? '' : `;width:${defaultWidth}`)  
            }  
            const itemRender = (form, _label) => {  
            // 申明: onChange 会导致(类input) change后触发两次(组件定义一次,原生change一次) 对组件定义进行过滤,仅留原生触发,组件触发onChange 用change 替代  
            const { prop, itemType, itemWidth, options, change, onChange, itemStyle = '', placeholder, t_placeholder, le_slots, ...formOthers } = form  
            const _options = options || []  
            const _itemStyle = itemStyle + (itemWidth ? `;width: ${itemWidth}` : '')  
            let disabled = form.disabled  
            const _placeholder: string = (t_placeholder ? t(t_placeholder) : placeholder) || _label  
            // 优化后的 change事件  
            let formatterChange = async () => {  
                if (typeof change === 'function') {  
                return change(searchData[prop], _options, searchData)  
                }  
            }  
            let bindInputEvents = {}  
            let changeAndSearch = formatterChange  
            if (triggerSearchAuto) {  
                changeAndSearch = () => formatterChange().then(searchHandler)  
                bindInputEvents = {  
                    onBlur: searchHandler,  
                    // 回车触发搜索  
                    onKeydown: (e: KeyboardEvent) => {  
                        // console.error(e, 'onKeyDown', e.key)  
                        if (e.key === 'Enter') {  
                            searchHandler()  
                        }  
                    }  
                }  
            }  
            switch (itemType) {  
                // 自定义render  
                case 'render':  
                return <CustomRender form={form} params={searchData} />  
                // 下拉框  
                case 'select':  
                return (  
                    <el-select  
                        {...formOthers}  
                        v-model={searchData[prop]}  
                        onChange={changeAndSearch}  
                        style={getItemStyle(_itemStyle, '200px')}  
                        disabled={disabled}  
                        clearable={form.clearable ?? true}  
                        placeholder={_placeholder}  
                        >  
                        {getOptions(_options, form).map((option, i) => {  
                            const { label, value, disabled } = option  
                                return (  
                                <el-option key={`${value}_${i}`} value={value} label={label} disabled={disabled} title={label}>
                                {renderOption(le_slots.option, option)}  
                                </el-option>  
                                )  
                        })}  
                    </el-select>  
                )  
                // 单选框  
                case 'radio':  
                return (  
                    <el-radio-group  
                        {...formOthers}  
                        v-model={searchData[prop]}  
                        disabled={disabled}  
                        onChange={changeAndSearch}  
                        style={getItemStyle(_itemStyle, 'auto')}  
                    >  
                    {getOptions(_options, form).map((option, i) => {  
                    const { label, value, disabled } = option  
                    return (  
                        <el-radio-button key={`${value}_${i}`} label={value} disabled={disabled} title={label}>  
                        {renderOption(le_slots.option, option)}  
                        </el-radio-button>  
                        )  
                    })}  
                    </el-radio-group>  
                )  
                // 级联  
                case 'cascader':  
                const slots_cascader = {  
                    default: le_slots.option as SlotOption<{ data: any; node: any }>  
                }  
                return (  
                    <el-cascader  
                        {...formOthers}  
                        v-model={searchData[prop]}  
                        onChange={changeAndSearch}  
                        style={getItemStyle(_itemStyle, '200px')}  
                        disabled={disabled}  
                        clearable={form.clearable ?? true}  
                        filterable={form.filterable ?? true}  
                        options={_options}  
                        placeholder={_placeholder}  
                        v-slots={slots_cascader}  
                    />  
                )  
                // 数字  
                case 'inputNumber':  
                return (  
                    <InputNumber  
                        class="rate100"  
                        {...bindInputEvents}  
                        {...formOthers}  
                        slots={le_slots}  
                        v-model={searchData[prop]}  
                        onChange={formatterChange}  
                        style={getItemStyle(_itemStyle, '130px')}  
                        disabled={disabled}  
                        placeholder={_placeholder}  
                        precision={form.precision || 0}  
                    />  
                )  
                // 数字区间  
                case 'inputNumberRange':  
                const numberChange = (e, propKey) => {  
                    change && change(searchData[propKey], _options, searchData, propKey)  
                }  
                return (  
                    <InputNumberRange  
                        {...bindInputEvents}  
                        prop={prop}  
                        {...formOthers}  
                        v-model={searchData[prop]}  
                        isValueArray  
                        // modelValue={searchData}  
                        onChange={numberChange}  
                        itemStyle={getItemStyle(_itemStyle, '230px')}  
                        disabled={disabled}  
                        placeholder={_placeholder}  
                        precision={form.precision || 0}  
                        v-slots={le_slots}  
                    />  
                )  
                // 日期选择 (单日期 || 日期区间 ...) year/month/date/datetime/ week/datetimerange/daterange  
                case 'datePicker':  
                let dateWidthDefault = '160px'  
                let dateOpts: any = {}  
                dateOpts.valueFormat = form.valueFormat || 'MM/DD/YYYY'  
                dateOpts.format = form.format || dateOpts.valueFormat  
                // 区间类型  
                if (/range$/.test(form.type || '')) {  
                    dateWidthDefault = '220px'  
                    const startPlaceholder = form.t_startPlaceholder ? t(form.t_startPlaceholder) : form.startPlaceholder  
                    const endPlaceholder = form.t_endPlaceholder ? t(form.t_endPlaceholder) : form.endPlaceholder  
                    dateOpts = Object.assign(dateOpts, {  
                    startPlaceholder: startPlaceholder ?? t('le.filter.startDate'),  
                    endPlaceholder: endPlaceholder ?? t('le.filter.endDate'),  
                    unlinkPanels: form.unlinkPanels ?? true // 解除联动  
                    })  
                } else {  
                    dateOpts.placeholder = _placeholder || t('le.filter.selectDate')  
                }  
                return (  
                    <el-date-picker  
                        {...formOthers}  
                        {...dateOpts}  
                        v-model={searchData[prop]}  
                        onChange={changeAndSearch}  
                        style={getItemStyle(_itemStyle, dateWidthDefault)}  
                        disabled={disabled}  
                    />  
                )  
                // switch  
                case 'switch':  
                return <el-switch {...formOthers} v-model={searchData[prop]} onChange={changeAndSearch} style={_itemStyle} disabled={disabled} />  
                case 'input':  
                default:  
                return (  
                    <el-input  
                        {...bindInputEvents}  
                        {...formOthers}  
                        v-model={searchData[prop]}  
                        onChange={formatterChange}  
                        disabled={disabled}  
                        placeholder={_placeholder}  
                        style={getItemStyle(_itemStyle, '160px')}  
                    />  
                    )  
                }  
            }  
            return (  
            <div class={warpClass}>  
                <div class="le-search-form-flex">  
                    <el-form ref={formRef} inline={true} size="default" class="le-search-form-flex-wrap" model={searchData} {...formConfig}>  
                        <el-row class="form_wrap" gutter={8}>  
                        {realForms.value.map((form: SearchFormItem & { le_slots: Recordable }, idx) => {  
                        // 通过 form.visible 控制 是否展示  
                        const _label = form.t_label ? t(form.t_label) : form.label  
                        const slots = {  
                            label: form.le_slots.label  
                        }  
                        return (  
                        <el-col v-show={form.visible !== false} key={idx} span={form.span ?? 1024}>  
                            <el-form-item  
                                class={form.showLabel === false ? 'hideLabel' : ''}  
                                {...form}  
                                label={_label}  
                                v-slots={slots}  
                            >  
                            {itemRender(form, _label)}  
                            </el-form-item>  
                        </el-col>  
                        )  
                        })}  
                        </el-row>  
                    </el-form>  
                    <div class="action-wrap">  
                        <el-button size="default" plain icon={Refresh} disabled={props.loading} onClick={local_resetHandler}>  
                        {t('le.btn.reset')}  
                        </el-button>  
                        <el-button size="default" type="primary" loading={props.loading} icon={Search} onClick={searchHandler}>  
                        {t('le.btn.search')}  
                        </el-button>  
                    </div>  
                </div>  
            </div>  
            )  
        }  
    }  
})  
export default SearchForm  
</script>

搜索配置示例:

tsx 复制代码
//支持的 itemType 有
//    element-plus 组件:
//        select(el-select)
//        radio(el-radio-group)
//        cascader(el-cascader)
//        datePicker(el-date-picker)
//        switch(el-switch)
//        input(el-input)
//    自定义 组件:
//        render(自定义渲染)
//        inputNumber(基于el-input-number二次封装)
//        InputNumberRange(基于el-input-number二次封装)
import { LeFormItem } from '@/components/FormConfig/formConfig.types'
const search_forms: LeFormItem[] = [  
    {  
        prop: 'search_word',  
        label: '邮箱',  
        itemType: 'input',  
        itemWidth: '230px'  
    },  
    {  
        prop: 'search_status',  
        label: '账号状态',  
        itemType: 'select',  
        options: [  
            { value: 0, label: '禁用' },  
            { value: 1, label: '正常' }  
        ]  
    }  
]

2.左侧功能按钮插槽

html 复制代码
<LeTable v-bind="tableOpts" :columns="activeColumns">  
    <template #toolLeft>  
        <el-button type="primary" size="default" @click="addItem">  
        新增<el-icon><Plus /></el-icon>  
        </el-button>  
    </template>  
</LeTable>

3.右侧插槽 以及 table自带扩展功能

html 复制代码
<LeTable v-bind="tableOpts" v-model:checked-options="activeColumns" :columns="activeColumns">
    <template #toolRight>  
        <el-button type="primary"> toolRight </el-button>  
    </template>  
</LeTable>

<script setup lang="tsx">
import { LeTableColumnProps } from '@/components/Table'
const columns: LeTableColumnProps[] = [
    {  
        prop: 'username',  
        label: '用户名',  
        minWidth: 120,  
        fixed: 'left'
    },  
    {  
        prop: 'phone',  
        label: '手机号',  
        minWidth: 140  
    },  
    {  
        prop: 'email',  
        label: '邮箱',  
        minWidth: 200  
    }
]
const tableOpts = reactive({
    // 控制列配置 有该配置便支持column 排序/显隐
    columnsConfig: {
        // 排序配置列
        columns,
        // 默认配置列
        defaultCheckedOptions: columns.slice(0, 2)
    }
})
const activeColumns = ref(columns.slice(0, 2))
</script>

4.自定义top插槽

html 复制代码
<LeTable v-bind="tableOpts" :columns="activeColumns">  
    <template #top>  
        <div>顶部自定义插槽#top</div>  
    </template>
</LeTable>

5.table区内容

html 复制代码
<LeTable v-bind="tableOpts" v-model:checked-options="activeColumns" :columns="activeColumns">
    <template #toolRight>  
        <el-button type="primary"> toolRight </el-button>  
    </template>  
    <template #usernameTitle>  
        <el-button> 用户名 </el-button>  
    </template>  
    <template #username="{row}">  
        <le-text value={row.username} copy />
    </template>  
</LeTable>

<script setup lang="tsx">
import { LeTableColumnProps } from '@/components/Table'
const columns: LeTableColumnProps[] = [
    {  
        prop: 'username',  
        label: '用户名',  
        minWidth: 120,  
        fixed: 'left',
        // ++++++
        slots: {
            // 表头 插槽写法
             header: 'usernameTitle',
            // 单元格内容 插槽写法
            default: 'username'
        }
        // ++++++
    },  
    {  
        prop: 'phone',  
        label: '手机号',  
        minWidth: 140,
        // ++++++
        slots: {
            // 表头 h函数
            header() {  
                return h('div', { class: 'bar', innerHTML: '自定义header' })  
            },
            // 单元格内容 tsx
            default({ row }) {  
                return <le-text value={row.phone} copy />  
            }
        }
        // ++++++
    },  
    {  
        prop: 'email',  
        label: '邮箱',  
        minWidth: 200  
    }
]
const tableOpts = reactive({
    // 控制列配置 有该配置便支持column 排序/显隐
    columnsConfig: {
        // 排序配置列
        columns,
        // 默认配置列
        defaultCheckedOptions: columns.slice(0, 2)
    }
})
const activeColumns = ref(columns.slice(0, 2))
</script>

6.分页区

分页区 主要还是el-pagination集成进来 通过 showPagination控制是否分页控制 如下图

四.使用文档说明

1.LeTable Api

Letable 属性

属性名 说明 类型 默认值
list 数据列表 any[] []
columns 基于 el-column 配置 的额外扩展,详见 LeTable-column LeTableColumnProps[] []
checkedOptions 勾选的columns LeTableColumnProps[] []
columnsConfig 自定义列配置相关 { columns: LeTableColumnProps[], defaultCheckedOptions?: LeTableColumnProps[] } []
searchParams 列表搜索参数 页数/页面条目/搜索条件 { page: number; size: number, [key: string]: any } {page: 1, size: 20}
total 列表总数 number 0
options 表格的控制参数 继承el-table的props & 额外扩展 具体参数参考下面LeTableOptions声明 LeTableOptions {}
curRow 当前el-table高亮(选中)的行数据, 需要配置options.rowKey 或 currentRowKey Object null

LeTableOptions声明

ts 复制代码
type Options = {  
    // el-table参数  
    height?: string  
    maxHeight?: string  
    size?: string  

    // 表格分页器 pageSizes (默认:[10, 20, 50, 100])  
    pageSizes?: number[]  
    // 表格分页器 layout (默认:'total, sizes, prev, pager, next, jumper')  
    layout?: string  
    // 表格分页器 背景 (默认:true)  
    background?: boolean  

    // 额外table参数  
    // 表格loading (默认:false)  
    loading?: boolean  
    // 是否多选类型 (默认:false)  
    multipleSelect?: boolean  
    // 多选类型|(currentRowKey当前行选中key)选中标记唯一key (默认:'id') 【table $prop属性之一】  
    rowKey?: (row) => any | string  
    // 根据 该值 查找当前页面数据是否包含当前数据 添加 高亮状态 (默认:'id')  
    currentRowKey?: string  
    // table 默认 column 对齐方式 (默认:'center')  
    align?: string  
    // table 默认 column 是否允许拖动 (默认:true)  
    resizable?: boolean  
    // columnItem 超出内容 省略号 同时添加 tooltip (默认:false)  
    showOverflowTooltip?: boolean  
    // 是否展示数据序列号 (默认:true)  
    showIndex?: boolean  
    // 展示数据序列号_label (默认:'序号')  
    indexLabel?: string  
    // 是否加载table 分页栏 (默认:true)  
    showPagination?: boolean  

    // 以及其他  继承 el-table 的配置
    [key: string]: any  
}
type LeTableOptions = InstanceType<typeof ElTable>['$props'] & Options

Letable 事件

事件名 说明 类型
update:searchParams 更新搜索数据 ({page: number; size: number; [key: string]: any }) => void
update:checkedOptions 更新列配置 (LeTableColumnProps[]) => void
sortChange 排序条件发生变化的时候会触发该事件 ({prop: string, order: any }) => void
refresh 点击刷新页面 () => void
row-click el-table的行点击 (row: any, column: any) => void
update:curRow 更新当前选中行数据 (row: any) => void

Letable 插槽

插槽名 说明
toolLeft 工具栏左边插槽
toolRight 工具栏右边插槽
top 顶部插槽

Letable Exposes

属性名 说明 类型
tableRef el-table 实例 -
setCurrentRow 手动选中行 `(rowOrIndex: { [key: string]: any }

2.LeTable-column API

LeTable-column 属性(LeTableColumnProps)

其他配置请参考 el-tableTable-columnAPI 配置

属性名 说明 类型 默认值
t_label 多语言label转义字符 string -
slots 插槽配置 `{ default?:((scope: { row; column; $index }) => JSX.Element string
children 多语言label转义字符 LeTableColumnProps[] -
titleHelp 标题提示 `{ message: string JSX.Element; icon: string }`

五.更多参考

LeTable组件源码

html 复制代码
<script lang="tsx">  
import { defineComponent, PropType, computed, unref, watch, ref, nextTick } from 'vue'  
import { LeTableColumnProps, LeTableOptions, SearchParams, LeTableProps } from './index.d'  
import NoData from '@/components/NoData.vue'  
import Icon from '@/components/Icon.vue'  
import TableColumnsPopover from './components/TableColumnsPopover.vue'  
import { createTableContext } from './hooks/useTableContext'  
import { useColumns, useColumnsOpts } from './hooks/useColumns'  
import { useI18n } from 'vue-i18n'

export const tableProps = {  
    // 数据列表  
    list: {  
    type: Array as PropType<Record<string, any>[]>,  
    default: () => []  
    },  
    /**  
    * [{  
    * prop, // 属性名  
    * label, // 列名  
    * align, // 对齐方式  
    * width, // 列宽  
    * minWidth, // 最小列宽  
    * sortable, // 是否允许排列顺序  
    * formatter: function(row, column, cellValue, index){}, // 返回需要展示的数据  
    * slots: { header: fn || slotName, default: fn({row, column, $index...}) || slotName }(slots.default > formatter)  
    * }]  
    */  
    columns: {  
        type: Array as PropType<LeTableColumnProps[]>,  
        default: () => []  
    },  
    // 选中column的配置参数  
    checkedOptions: {  
        type: Array as PropType<LeTableColumnProps[]>,  
        default: () => []  
    },  
    // 自定义列配置相关  
    columnsConfig: {  
        type: Object as PropType<Pick<LeTableProps, 'columnsConfig'>>,  
        default: () => ({  
            // defaultCheckedOptions: [], // { t_label: string; label: string; prop: string; fixed: boolean|string }[]// Array 没有存储数据时 系统给予的默认配置  
            columns: []  
        })  
    },  
    // 列表搜索参数  
    searchParams: {  
        type: Object as PropType<SearchParams>,  
        default: () => ({  
        page: 1, // 页数  
        size: 20 // 页面条目  
        })  
    },  
    total: {  
        type: Number,  
        default: 0  
    }, // 总数  
    /**  
    * table 表格的控制参数  
    * 具体配置参考 computedOptions 默认参  
    */  
    options: {  
        type: Object as PropType<LeTableOptions>,  
        default: () => {  
            return {}  
        }  
    },  
    // 当前行(高亮)  
    curRow: {  
        type: Object as PropType<{ [key: string]: any } | null>,  
        default: null  
    }  
}
const TableComponent = defineComponent({  
    name: 'LeTable',  
    props: tableProps,  
    // 更新搜索条件, 更新列配置, table Sort 排序, table 刷新  
    emits: ['update:searchParams', 'update:checkedOptions', 'sortChange', 'refresh', 'row-click', 'update:curRow'],
    setup(props, { slots, emit, expose }) {
        const { t } = useI18n()  
        const tableRef = ref(/*tableRef*/)  
        const isFullscreen = ref(false)  
        // 切换全屏  
        const toggleFullscreen = () => {  
            isFullscreen.value = !isFullscreen.value  
        }  

        // 切换页码  
        const handleIndexChange = (page: number) => {  
            // console.error(' handleIndexChange index', index)  
            emit('update:searchParams', {  
                ...props.searchParams,  
                page  
            })  
        }  
        // 刷新列表  
        const refreshHandler = () => {  
            // const index = props.searchParams.page  
            // handleIndexChange(1)  
            handleIndexChange(props.searchParams.page)  
            // 额外相关操作  
            emit('refresh')  
        }  
        // 切换每页显示的数量  
        const handleSizeChange = size => {  
            // console.error(' handleSizeChange size', size)  
            emit('update:searchParams', {  
                ...props.searchParams,  
                size  
            })  
        }  
        // 排序  
        const tableSortChange = ({ column, prop, order }) => {  
            const sortParams = {  
                prop,  
                order  
            }  
            emit('update:searchParams', {  
                ...props.searchParams,  
                sortParams  
            })  
            emit('sortChange', sortParams)  
        }  
        // table 相关配置  
        const computedOptions = computed(() => {  
            const res = {  
            ...default_tableConfig,  
            ...props.options  
            } as LeTableOptions  
            // 高亮当前  
            // eslint-disable-next-line  
            // @ts-ignore  
            res.currentRowKey = res.currentRowKey ?? res.rowKey  
            return res  
        })  
        // 更新选中列配置  
        const checkedOptionsChange = checkedOptions => {  
            emit('update:checkedOptions', checkedOptions)  
        }  
        // 点击当前行  
        const onRowClick = (row, column) => {  
            emit('row-click', row, column)  
            emit('update:curRow', row)  
        }  
        // 设置当前行  
        const setCurrentRow = (rowOrIndex: { [key: string]: any } | number | null = null, update = true) => {  
            let curRowIndex = -1  
            const list = props.list  
            if (typeof rowOrIndex === 'number') {  
                curRowIndex = rowOrIndex  
            } else if (Object.keys(rowOrIndex || {}).length) {  
                const currentRowKey = computedOptions.value.currentRowKey  
                curRowIndex = list.findIndex(_item => {  
                    return _item[currentRowKey] === props.curRow?.[currentRowKey]  
                })  
            }  
            update && emit('update:curRow', list[curRowIndex] ?? null)  
            // console.error(curRowIndex, 'curRowIndex')  
            nextTick(() => {  
                tableRef.value.setCurrentRow(list[curRowIndex]) // 高亮原本被选中的数据 
            })  
        }  
        watch(  
            () => props.list,  
            () => {  
                // 高亮数据判断  
                if (Object.keys(props.curRow || {}).length) {  
                    setCurrentRow(props.curRow, false)  
                }  
            }  
        )  

        const table_slots = {  
            empty: () => <NoData size={unref(computedOptions).size}></NoData>  
        }  
        createTableContext({ tableRef })  
        const { localColumns, renderColumn } = useColumns({  
            propsRef: props,  
            computedOptions,  
            slots,  
            tableRef  
        } as useColumnsOpts)  
        expose({  
            tableRef,  
            setCurrentRow  
        })
        
        
        return () => {  
            const { list, total, searchParams, columnsConfig, checkedOptions } = props  
            return (  
                <div class={`le-table-warp ${unref(isFullscreen) ? 'le-table-warp-maximize' : ''}`}>  
                <div class="tableBody">  
                    {/* 工具栏 */}  
                    <div class="toolBarWrap">  
                        <div class="toolLeft">  
                            {/* 工具栏左边插槽 */}  
                            {slots.toolLeft?.()}  
                        </div>  
                        <div class="toolRight">  
                            {/* 工具栏右边插槽 */}  
                            {slots.toolRight?.()}  
                            {/* 刷新 */}  
                            <el-tooltip placement="top" content={t('le.refresh')}>  
                                <el-button class="icon-button button-refresh" onClick={refreshHandler}>  
                                    <Icon iconClass="le-refresh" />  
                                </el-button>  
                            </el-tooltip>  
                            {/* 全屏 */}  
                            <el-tooltip placement="top" content={t(isFullscreen.value ? 'le.exitFullscreen' : 'le.fullscreen')}>  
                                <el-button class="icon-button button-screen" onClick={toggleFullscreen}>  
                                    <Icon iconClass={isFullscreen.value ? 'le-suoxiao' : 'le-fangda'} />  
                                </el-button>  
                            </el-tooltip>  
                            {/* columns过滤 */}  
                            {columnsConfig?.columns?.length ? (  
                            <TableColumnsPopover value={checkedOptions} onChange={checkedOptionsChange} {...columnsConfig} />  
                            ) : (  
                            ''  
                            )}  
                        </div>  
                    </div>  
                    {/* 顶部插槽 */}  
                    {slots.top?.()}  
                    {/* ElTable组件 */}  
                    <div class="tableParentEl">  
                        <el-table  
                            class="le-table"  
                            ref={tableRef}  
                            v-loading={unref(computedOptions).loading}  
                            border  
                            element-loading-text="加载中..."  
                            element-loading-background="rgba(0, 0, 0, 0.1)"  
                            {...unref(computedOptions)}  
                            data={list}  
                            // 组件内单独封装 事件  
                            onSortChange={tableSortChange}  
                            onRowClick={onRowClick}  
                            // onSelectionChange={this.handleSelectionChange}  
                            v-slots={table_slots}  
                        >  
                            {localColumns.value.map(renderColumn)}  
                        </el-table>  
                    </div>  
                </div>  
                {/*--分页--*/}  
                {unref(computedOptions).showPagination && (  
                    <el-pagination  
                        total={total}  
                        currentPage={searchParams.page}  
                        pageSize={searchParams.size}  
                        pageSizes={unref(computedOptions).pageSizes}  
                        layout={unref(computedOptions).layout}  
                        background={unref(computedOptions).background}  
                        onSizeChange={handleSizeChange}  
                        onCurrentChange={handleIndexChange}  
                    />  
                )}  
                </div>  
            )  
        }

    }
})
export default TableComponent

/**  
TableComponent的 组件配置  
// 需要展示的列配置  
columns = [{  
    prop: 'principalName',  
    label: '项目负责人',  
    align: 'center',  
    width: 100  
}]  
*/  
  
/* <TableComponent  
    :list="list" // 后台请求的列表数据  
    :total="total" // 该列表总共有多少数据  
    :options="options" // table相关的 配置对象 // 配置参考 defaultOptions  
    :columns="columns" // 需要展示的列配置 // 参考上面的 columns  
    :searchParams="searchParams"  
    v-model:curRow="testCurRow" // 当前高亮的 数据 【需要高亮上次数据必传】  
/>*/
</script>

为LeTable组件创建的Hooks(useTablePage)

ts 复制代码
import { reactive, ref, computed, unref, watch, nextTick } from 'vue'  
import { $log } from '@/utils'  
import { LeTableColumnProps, LeTableProps, SearchParams } from '@/components/Table'  
type SearchData = { [prop: string]: any }  
export type UseTableConfig = {  
    // 搜索表单数据  
    searchData: SearchData  
    // 更新searchParams参数数据  
    updateParams: () => void  
    // 请求表格数据方法  
    queryList: () => void  
    // 是否初始化请求  
    fetchImmediate?: boolean  
}

export const useTablePage = (tableProps: Partial<LeTableProps> = {}, config: Partial<UseTableConfig> = {}) => {
    const localConfig: UseTableConfig = Object.assign(  
    {  
        searchData: {},  
        fetchImmediate: true,  
        queryList: () => $log('请添加queryList', JSON.stringify(tableOpts.searchParams)),  
        updateParams: () => {  
            tableOpts.searchParams = {  
                ...(tableOpts.searchParams as SearchParams),  
                ...searchData.value,  
                // ...若有更多操作 searchData 进行请求,请覆盖updateParams 方法  
                page: 1  
            }  
        }  
    },  
    config  
    )  
    const tableOpts = reactive<LeTableProps>({  
        total: 0,  
        list: [],  
        columns: [],  
        ...tableProps,  
        searchParams: {  
        page: 1,  
        size: 20,  
        ...(tableProps.searchParams || {})  
        },  
        options: {  
        loading: false,  
        showOverflowTooltip: false,  
        multipleSelect: true, // 是否支持列表项选中功能  
        // stripe: true, // 是否为斑马纹 table  
        // highlightCurrentRow: true, // 是否支持当前行高亮显示  
        ...(tableProps.options || {})  
        }  
    })  
    const searchData = ref<SearchData>({  
        ...config.searchData  
    })
    
    const checkedColumns = ref([])  
    const activeColumns = computed(() => {  
        const _checkedColumns = unref(checkedColumns) as LeTableColumnProps[]  
        if (!_checkedColumns.length) return tableOpts.columns  
        return _checkedColumns  
        .map(v => {  
            const cur = tableOpts.columns.find(column => column.prop === v.prop)  
            if (cur) {  
                const fixedFlag = cur.fixed  
                if (fixedFlag) {  
                    // eslint-disable-next-line  
                    // @ts-ignore  
                    v.fixed = fixedFlag  
                }  
                return cur  
            }  
        })  
        .filter(Boolean)  
    })
    watch(  
        () => searchData.value,  
        () => {  
            nextTick().then(localConfig.updateParams)  
        },  
        {  
            immediate: localConfig.fetchImmediate  
        }  
    )
    watch(() => tableOpts.searchParams, localConfig.queryList)  
    return {  
        tableOpts,  
        checkedColumns,  
        activeColumns,  
        // 搜索表单数据  
        searchData,  
        // 刷新数据方法  
        updateParams: localConfig.updateParams  
        }
}

LeTable组件使用示例

html 复制代码
<template>  
<div class="column-page-wrap">  
    <!-- 公用搜索组件 -->  
    <LeSearchForm ref="searchForm" v-model:search-data="searchData" :forms="forms" :loading="tableOpts.options.loading"> </LeSearchForm>  
    <LeTable v-model:search-params="tableOpts.searchParams" v-bind="tableOpts" v-model:checked-options="checkedColumns" :columns="activeColumns">  
        <template #toolLeft>  
        <el-button type="primary" @click="addHandler"> 新增 </el-button>  
        </template>  
        <template #操作="{ row, column, $index }">  
            <div>  
            <el-button icon="delete" />  
            </div>  
        </template>  
    </LeTable>  
</div>  
</template>  
  
<script lang="tsx" setup>  
    import { nextTick, ref, watch } from 'vue'  
    import { getAdminList } from '@/api/demo'  
    import { useTablePage } from '@/hooks/useTablePage'  
    import { LeFormItem } from '@/components/FormConfig/formConfig.types'  
    const span = 23  
    const searchForm = ref()  
    const forms: LeFormItem[] = [  
        // select 单选  
        {  
            prop: 'type',  
            label: '类型',  
            itemType: 'select',  
            options: [  
                { value: '1', label: '类型一' },  
                { value: '2', label: '类型二' }  
            ]  
        },  
        {  
            prop: 'custName',  
            label: '客户名称',  
            itemType: 'input'  
        }  
    ]  

    // table列表数据请求  
    const queryList = () => {  
        const { options } = tableOpts  
        options.loading = true  
        getAdminList(tableOpts.searchParams)  
        .then((data: any) => {  
            const { total, data: list } = data  
            tableOpts.total = total  
            tableOpts.list = list  
        })  
        .finally(() => {  
            options.loading = false // 更改加载中的 loading值  
        })  
    }  
    // table 参数  
    const columns = [  
    {  
        prop: 'username',  
        label: '角色',  
        minWidth: 100  
    },  
    {  
        prop: 'add_time',  
        label: '创建日期',  
        sortable: true,  
        minWidth: 100,  
        titleHelp: {  
        message: (  
            <span style="background: var(--el-color-danger)">  
                我是自定义 <br />  
                提示提示  
            </span>  
            )  
        },  
        formatter: (row: any, _column: any) => {  
            return <div style="background: #f0f;">${row.name || '- 66666 -'}</div>  
        }  
    },  
    {  
        prop: 'describe',  
        label: '备注信息',  
        minWidth: 100  
    },  
    {  
        prop: 'email',  
        label: '邮箱',  
        minWidth: 200  
    },  
    {  
        prop: 'action',  
        label: '操作',  
        width: 100,  
        fixed: 'right',  
        slots: {  
            default: '操作'  
        }  
    }  
    ]  
    const { searchData, tableOpts, checkedColumns, activeColumns, updateParams } = useTablePage(  
    {  
        searchParams: {  
            page: 1,  
            size: 10  
        },  
        // 需要展示的列  
        columns,  
        // 控制列配置  
        columnsConfig: {  
            columns,  
            defaultCheckedOptions: columns.slice(0, 2)  
        }  
    },  
    {  
        searchData: {  
            inputNumberRange: [1, 5]  
        },  
        queryList,  
        fetchImmediate: false  
    }  
    )  
    nextTick(() => {  
        // 模拟特殊情况初始化搜索数据  
        searchData.value = {
            type: '1'  
        }  
        searchForm.value?.forceUpdateInitParams(tableOpts.searchParams)  
    })  
    // 模拟从api获取到选中的columns  
    checkedColumns.value = columns.slice(-3)  

    const addHandler = () => {  
        // 添加  
    }  
</script>  
  
<style scoped lang="scss"></style>

附上示例效果

六.预览/源码

预览 lancejiang.github.io/Lance-Eleme...
源码 github.com/LanceJiang/...

相关推荐
大怪v2 分钟前
前端佬们!塌房了!用过Element-Plus的进来~
前端·javascript·element
拉不动的猪13 分钟前
electron的主进程与渲染进程之间的通信
前端·javascript·面试
生产队队长29 分钟前
Vue+SpringBoot:整合JasperReport作PDF报表,并解决中文不显示问题
vue.js·spring boot·pdf
软件技术NINI37 分钟前
html css 网页制作成品——HTML+CSS非遗文化扎染网页设计(5页)附源码
前端·css·html
fangcaojushi38 分钟前
npm常用的命令
前端·npm·node.js
阿丽塔~1 小时前
新手小白 react-useEffect 使用场景
前端·react.js·前端框架
程序猿大波1 小时前
基于Java,SpringBoot和Vue高考志愿填报辅助系统设计
java·vue.js·spring boot
鱼樱前端1 小时前
Rollup 在前端工程化中的核心应用解析-重新认识下Rollup
前端·javascript
m0_740154671 小时前
SpringMVC 请求和响应
java·服务器·前端
加减法原则1 小时前
探索 RAG(检索增强生成)
前端