一.背景
在许多 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-table
的 Table-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/...