Vue3封装Table

在我们前端开发的过程中常见的需求有列表加分页加查询,尤其在后台管理系统中,在这里我封装了一套基于vue3 + element-plus + ts的组件,直接在组件中使用封装的组件和编写可配置文件即可,直接上代码,开箱即可用

先从简单的开始,分页组件 Pagination

  • 在components下新建文件夹Pagination,里面再建index.vue,代码如下
<script lang="ts" setup>
import { computed } from "vue";

defineOptions({
name: "MyPagination"
});

const props = defineProps({
// total 总条数
total: {
required: true,
type: Number,
default: 0
},
// page 当前页
page: {
type: Number,
default: 1
},
// 每页条数
limit: {
type: Number,
default: 20
},
// 每页条数选项
pageSizes: {
type: Array,
default() {
return [10, 20, 50, 100];
}
},
// 分页布局
layout: {
type: String,
default: "total, prev, pager, next, sizes, jumper"
},
// 是否显示背景色
background: {
type: Boolean,
default: true
},
// 是否自动滚动到顶部
autoScroll: {
type: Boolean,
default: true
}
});
// 定义emit
const emit = defineEmits(["pagination", "update:page", "update:limit"]);

// 定义当前页
const currentPage = computed({
get() {
return props.page;
},
set(newValue) {
// 更新当前页
emit("update:page", newValue);
console.log(newValue);
}
});
// 定义每页条数
const pageSize = computed({
get() {
return props.limit;
},
set(newValue) {
// 更新每页条数
emit("update:limit", newValue);
}
});

// 监听每页条数变化
const handleSizeChange = val => {
emit("pagination", { page: currentPage.value, limit: val });
};
// 监听当前页变化
const handleCurrentChange = val => {
emit("pagination", { page: val, limit: pageSize.value });
};
</script>

<template>
<div class="CardPagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="pageSizes"
:layout="layout"
:total="total"
:background="background"
v-bind="$attrs"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</template>

<style lang="scss" scoped>
.CardPagination {
display: flex;
justify-content: center;
}
</style>

其次是查询form组件 Form, 里面一些小的样式可自己修改,这里只是我的样式

  • 在components下新建文件夹Form,里面再建index.vue,代码如下
<template>
<div>
<el-row :gutter="18" style="display: block !important">
<el-form
ref="form"
class="form_flex"
:inline="true"
:label-width="`${labelWidth}px`"
>
<el-col v-for="(item, index) of items" :key="index" v-bind="colLayout">
<el-form-item :label="item.name">
<template v-if="item.slot">
<slot :name="item.slotName" />
</template>
<el-input
v-else-if="!item.type || item.type === 'input'"
:model-value="searchParams[item.key]"
v-bind="setAttrs(item)"
:placeholder="item.placeholder || '请输入'"
@update:model-value="handleValueChange($event, item.key)"
/>
<el-input
v-else-if="item.type === 'textarea'"
:model-value="searchParams[item.key]"
v-bind="setAttrs(item)"
:placeholder="item.placeholder || '请输入'"
@update:model-value="handleValueChange($event, item.key)"
/>
<el-select
v-else-if="item.type === 'select'"
v-bind="setAttrs(item)"
:model-value="searchParams[item.key]"
:placeholder="item.placeholder || '请选择'"
@update:model-value="handleValueChange($event, item.key)"
>
<el-option
v-for="(option, i) of item.options"
:key="i"
:label="
option[(item['props'] && item['props']['key']) || 'label']
"
:value="
option[(item['props'] && item['props']['value']) || 'value']
"
/>
</el-select>
<el-cascader
v-else-if="item.type === 'cascader'"
:model-value="searchParams[item.key]"
v-bind="setAttrs(item)"
:options="item.options"
:placeholder="item.placeholder || '请选择'"
:props="item.props"
@update:model-value="handleValueChange($event, item.key)"
/>
<el-date-picker
v-else-if="item.type === 'date-picker'"
v-bind="setAttrs(item)"
:default-time="defaultTime"
end-placeholder="结束日期"
:model-value="searchParams[item.key]"
:placeholder="item.placeholder || '请选择'"
prefix-icon=""
range-separator="-"
start-placeholder="开始日期"
:type="item.pickerType || 'daterange'"
@update:model-value="handleValueChange($event, item.key)"
/>
<template v-else-if="item.type === 'section'">
<el-input
class="section-input"
:model-value="searchParams[item.minKey]"
v-bind="setAttrs(item)"
:placeholder="item.placeholder || '请输入'"
/>
<span class="symbol">-</span>
<el-input
class="section-input"
:model-value="searchParams[item.maxKey]"
v-bind="setAttrs(item)"
:placeholder="item.placeholder || '请输入'"
/>
</template>
</el-form-item>
</el-col>
<el-col :span="btnWidth">
<slot name="prefix-button" />
<el-button-group class="search-wrap">
<el-button type="primary" @click="handleSearch">
<vab-icon icon="file-search-fill" />
查询
</el-button>
<el-button @click="handleReset">
<vab-icon icon="refresh-line" />
重置
</el-button>
</el-button-group>
<slot name="suffix-button" />
</el-col>
</el-form>
</el-row>
</div>
</template>
<script>
export default defineComponent({
name: 'MyForm',
props: {
labelWidth: {
type: Number,
default: 80,
},
// 表单配置
items: {
type: Array,
default: () => {
return []
},
},
// 表单参数
searchParams: {
type: Object,
default: () => {
return {}
},
},
btnWidth: {
type: Number,
default: 4,
},
colLayout: {
type: Object,
default: () => ({
xl: 5,
lg: 5,
md: 12,
sm: 24,
xs: 24,
}),
},
},
emits: ['update:searchParams', 'change', 'search', 'reset'],
setup(props, { emit }) {
const form = reactive({
initSearchParams: {},
})
const defaultTime = ref([
new Date(2000, 1, 1, 0, 0, 0),
new Date(2000, 2, 1, 23, 59, 59),
])

onMounted(() => {
// 保存初始值
form.initSearchParams = _.cloneDeep(props.searchParams)
})
const handleValueChange = (value, field) => {
emit('update:searchParams', { ...props.searchParams, [field]: value })
}

// 查询
const handleSearch = () => {
const cpValue = _.cloneDeep(props.searchParams)
let request = true
if (!cpValue.requestFlag) request = true
if (cpValue.requestFlag) request = false
emit('update:searchParams', { ...cpValue, request })
emit('change', cpValue)
emit('search', cpValue)
}
// 重置
const handleReset = () => {
const cpValue = _.cloneDeep(form.initSearchParams)
let request = true
if (!cpValue.requestFlag) request = true
if (cpValue.requestFlag) request = false
emit('update:searchParams', { ...cpValue, request })
emit('change', cpValue)
emit('reset', cpValue)
}
const setAttrs = (params) => {
const { slot, ...options } = params
return { ...options, slot }
}
return {
defaultTime,
handleValueChange,
setAttrs,
handleReset,
handleSearch,
}
},
})
</script>

<style lang="scss" scoped>
.form_flex {
display: flex;
flex-wrap: wrap;
.search-wrap {
margin-bottom: 16px;
}
:deep() {
.el-select {
width: 100%;
}
.el-cascader {
width: 100%;
}
.el-form-item {
display: flex;
width: 100%;
margin-bottom: 18px;
.el-form-item__label {
font-size: 14px;
color: #606060d9;
}
.el-form-item__content {
flex: 1;
}
}
.el-date-editor.el-input,
.el-date-editor.el-input__inner {
width: 100% !important;
}
.el-range-editor.el-input__inner {
padding: 3px 1px 3px 5px;
}
}
}
</style>

最后就是table组件的封装了,需要引入分页组件

  • 在components下新建文件夹Table,里面再建index.vue和render.ts,代码如下
  • render.ts代码如下
export default {
props: {
row: Object,
render: Function,
index: Number,
column: {
type: Object,
default: null,
},
},
setup: function (props: any, context: any) {
return () =>
props.render({
...props,
...context.attrs,
})
},
}
  • index.vue代码如下
<template>
<div>
<el-table
v-loading="loading"
:border="true"
v-bind="setAttrs(tableAttr)"
:data="tableData"
stripe
@selection-change="selectionChange"
>
<!-- :height="state.height" -->
<el-table-column
v-if="selectVisible"
align="left"
:selectable="selectable"
type="selection"
width="45"
/>
<template v-for="(column, index) in columns" :key="index">
<el-table-column
v-if="column.render"
v-bind="setAttrs(column)"
show-overflow-tooltip
>
<template #default="scope">
<Render
:column="column"
:index="scope.$index"
:render="column.render"
:row="scope.row"
v-bind="$attrs"
/>
</template>
</el-table-column>
<el-table-column
v-else-if="column.type === 'index'"
type="index"
v-bind="setAttrs(column)"
width="70"
/>
<el-table-column
v-else
:key="column.prop"
show-overflow-tooltip
v-bind="setAttrs(column)"
/>
</template>
</el-table>
<!-- 分页 -->
<Pagination
v-if="!hidden"
v-model:limit="pageOpt.pageSize"
v-model:page="pageOpt.pageNum"
:page-sizes="pageSizes"
:total="total"
@pagination="pagination"
/>
</div>
</template>

<script>
import Render from './render'
import { resetParams } from '@/utils'
import request from '@/utils/request'
import Pagination from '@/plugins/Pagination/index.vue'
export default defineComponent({
name: 'SrmTable',
components: { Render, Pagination },
props: {
url: {
type: String,
default: '',
},
// 筛选条件
params: {
type: Object,
default: () => ({}),
},
// 分页查询数据
pageRequest: {
type: Object,
default: () => ({
pageNum: 1,
pageSize: 20,
}),
},
// 外部传入的table数据源 不走组件内接口
sourceData: {
type: Array,
default: () => [],
},
contentType: {
type: String,
default: 'application/json',
},
isLoad: {
type: Boolean,
default: true,
},
columns: {
type: Array,
default: () => [],
},
// 每页条数选择器
pageSizes: {
type: Array,
default: () => [10, 20, 50, 100],
},
// 控制分页组件的显示隐藏
hidden: {
type: Boolean,
default: false,
},
// table组件的属性设置
tableAttr: {
type: Object,
default: () => ({
border: true,
}),
},
// 是否可选择行
selectVisible: {
type: Boolean,
default: false,
},
selectable: {
type: Function,
default: () => {},
},
},
emits: [
'getTotal',
'update:params',
'update:pageRequest',
'pagination',
'selectionChange',
],

setup(props, { emit }) {
const $baseTableHeight = inject('$baseTableHeight')

const state = reactive({
height: $baseTableHeight() - 30,
loading: false,
downloadLoading: false,
selections: [], // 当前选择项的集合
tableData: [],
total: 0,
pageOpt: props.pageRequest,
selfLoading: true,
})
// 判断是否有外部数据源传入
const isExternal = computed(() => {
const { sourceData = [] } = props
return sourceData.length
})

onMounted(() => {
if (!isExternal.value && props.isLoad) {
initTable()
}
})
// c初始化数据
const initTable = async () => {
state.loading = true
const realParams = resetParams(props.params)
const reqOpt = props.url.split('|')
const [method, url] = reqOpt[1] ? reqOpt : ['post', reqOpt[0]]
const reqKey = reqOpt[2]
? reqOpt[2]
: method.toLowerCase() === 'post'
? 'data'
: 'params'
try {
const { data } = await request({
url,
method,
[reqKey]: {
...state.pageOpt,
...realParams,
},
headers: {
'Content-Type': props.contentType,
},
})
setTimeout(() => {
state.loading = false
state.tableData = data.records || data.list
state.total = data.total
emit('getTotal', state.total)
}, 500)
} catch (e) {
state.loading = false
}
}
// 分页
const pagination = (ev) => {
const { page, limit } = ev
state.pageOpt.pageNum = page
state.pageOpt.pageSize = limit
// 有外部数据源注入时不走内部分页
if (isExternal.value) {
emit('update:pageRequest', state.pageOpt)
emit('pagination', ev)
} else {
initTable()
}
}
const selectionChange = (selections) => {
emit('selectionChange', selections)
}
// 给元素设置属性
const setAttrs = (params) => {
const { ...options } = params
if (!options.align) {
options.align = 'left'
}
return { ...options }
}

watchEffect(() => {
if (props.sourceData.length) {
state.tableData = props.sourceData
}
})
watch(
() => props.params,
(params) => {
const { request } = params
if (request) {
state.pageOpt.pageNum = 1
initTable()
}
params['request'] = false
emit('update:params', params)
}
)
return {
...toRefs(state),
isExternal,
pagination,
selectionChange,
setAttrs,
initTable,
}
},
})
</script>
<style lang="scss">
.el-popper {
max-width: 50%;
}
</style>

最后说下怎么使用,举例:

image-20230330134632530

  • index.vue组件写法如下:
<form-cpn v-model:search-params="tableParams" :items="formConfig" />
<srm-table
ref="tableRef"
:columns="contentTableConfig"
:handel-delete="handelDelete"
:handle-edit="handleEdit"
:params="tableParams"
url="post|/user/page"
/>
import contentTableConfig from './config/content.config'
import formConfig from './config/form.config'
  • content.config.js代码如下:
import { ElLink, ElDivider, ElTag } from 'element-plus'
export default [
{
label: '编号',
width: 80,
type: 'index',
},
{ label: '员工姓名', prop: 'staffName' },
{
label: '角色',
render(props) {
const { roleVOList } = props.row
return [
roleVOList.map((item) => {
return <ElTag type="success">{item.name}</ElTag>
}),
]
},
},
{ label: '手机号', prop: 'phone' },
{ label: '邮箱', prop: 'email' },
{ label: '创建时间', prop: 'createTime' },
{
label: '状态',
width: 100,
align: 'center',
render(props) {
const { row } = props
return [
<ElTag effect="dark" type={['success', 'warning'][row.state]}>
{['启用', '停用'][row.state]}
</ElTag>,
]
},
},
{
label: '操作',
align: 'center',
render(props) {
const { row } = props
return [
<ElLink type="primary" onClick={() => props['handle-edit'](row)}>
编辑
</ElLink>,
<ElDivider direction="vertical" />,
<ElLink type="danger" onClick={() => props['handel-delete'](row)}>
删除
</ElLink>,
]
},
},
]
  • form.config.js代码如下:
export default [
{ name: '姓名', key: 'userName', clearable: true },
{ name: '手机号', key: 'mobile', clearable: true },
{
name: '创建时间',
key: 'picker:beginTime,endTime',
type: 'date-picker',
span: 6,
},
]

以上就是一套简单的封装,如有不足,可以提出问题,欢迎大家补充