vue表格組件封裝舉例詳解(含完整代碼)
本文詳細(xì)解析 布局容器 + 動態(tài)搜索欄 + 智能表格 三組件的封裝邏輯、實現(xiàn)細(xì)節(jié)與標(biāo)準(zhǔn)用法,附帶完整可運行代碼。
一、整體架構(gòu)與協(xié)作關(guān)系
組件職責(zé)劃分
| 組件 | 職責(zé) | 關(guān)鍵能力 |
|---|---|---|
| LayoutContainer.vue | 布局骨架 | 統(tǒng)一結(jié)構(gòu)、操作按鈕(刷新/顯隱列/折疊搜索) |
| DynamicSearchBar.vue | 動態(tài)表單 | 根據(jù)配置生成 input/select/date 等控件 |
| SmartTable.vue | 數(shù)據(jù)展示 | 自動請求、分頁、字典轉(zhuǎn)換、時間格式化 |
數(shù)據(jù)流圖
[LayoutContainer]
│
├─ #search → [DynamicSearchBar] ←→ params (v-model)
│ │
│ └── tableRef.getList() ←──┐
│ │
└─ #default → [SmartTable] ←───────────────┘
↑
columns (響應(yīng)式數(shù)組)
tableRef (expose 方法)
? 關(guān)鍵設(shè)計:
columns是共享狀態(tài):LayoutContainer 修改.hide→ SmartTable 自動響應(yīng)tableRef是方法通道:SearchBar 和 LayoutContainer 通過它觸發(fā)表格刷新
二、智能表格組件(SmartTable.vue)
封裝目標(biāo)
- 自動處理分頁、排序、加載狀態(tài)
- 支持字典、時間、鏈接等常見字段類型
- 提供插槽覆蓋默認(rèn)渲染
完整代碼
<!-- SmartTable.vue -->
<template>
<div class="smart-table">
<el-table
ref="tableRef"
v-loading="loading"
:data="tableData"
v-bind="mergedConfig.table"
@sort-change="handleSortChange"
>
<!-- 遍歷 columns 渲染列 -->
<template v-for="column in visibleColumns" :key="column.prop">
<!-- selection / index 列 -->
<el-table-column
v-if="column.type === 'selection'"
type="selection"
:width="column.width || 55"
/>
<el-table-column
v-else-if="column.type === 'index'"
type="index"
:label="column.label"
:width="column.width || 60"
/>
<!-- 普通列 -->
<el-table-column
v-else
:prop="column.prop"
:label="column.label"
:width="column.width"
:sortable="column.sortable || false"
:show-overflow-tooltip="true"
>
<template #default="{ row }">
<!-- 插槽優(yōu)先 -->
<slot
:name="column.slot"
:row="row"
:column="column"
v-if="column.slot"
/>
<!-- 字典標(biāo)簽 -->
<dict-tag
v-else-if="column.dict"
:value="row[column.prop]"
:dict-key="column.dict"
/>
<!-- 時間格式化 -->
<span v-else-if="column.date">
{{ formatDate(row[column.prop], column.dateFormat) }}
</span>
<!-- 鏈接 -->
<el-link
v-else-if="column.link"
type="primary"
@click="handleLinkClick(column, row)"
>
{{ row[column.prop] }}
</el-link>
<!-- 默認(rèn)文本 -->
<span v-else>{{ row[column.prop] }}</span>
</template>
<!-- 表頭提示 -->
<template #header>
<span>{{ column.label }}</span>
<el-tooltip
v-if="column.tip"
:content="column.tip.content"
placement="top"
>
<i class="el-icon-question" style="margin-left: 4px; color: #999"></i>
</el-tooltip>
</template>
</el-table-column>
</template>
<!-- 空狀態(tài) -->
<template #empty>
<div class="no-data">
<img src="@/assets/images/no-data.png" alt="無數(shù)據(jù)" />
<p>暫無數(shù)據(jù)</p>
</div>
</template>
</el-table>
<!-- 分頁 -->
<el-pagination
v-if="!mergedConfig.notPagination && total > 0"
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="getList"
v-bind="mergedConfig.pagination"
/>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { parseTime } from '@/utils'
// Props
const props = defineProps({
columns: {
type: Array,
required: true
},
func: {
type: Function,
required: true
},
params: {
type: Object,
default: () => ({})
},
config: {
type: Object,
default: () => ({})
},
events: {
type: Object,
default: () => ({})
}
})
// Expose
const tableRef = ref(null)
defineExpose({
getList,
resetQuery,
reload
})
// 響應(yīng)式數(shù)據(jù)
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
...props.params
})
// 合并配置
const mergedConfig = computed(() => {
return {
table: {
border: true,
stripe: true,
...props.config.table
},
pagination: {
background: true,
pageSizes: [10, 20, 50, 100],
...props.config.pagination
},
sort: props.config.sort ?? false,
notPagination: props.config.notPagination ?? false,
autoPagination: props.config.autoPagination ?? false,
initResquest: props.config.initResquest ?? true
}
})
// 可見列(過濾 hide = true 的列)
const visibleColumns = computed(() => {
return props.columns.filter(col => !col.hide)
})
// 格式化時間
function formatDate(value, format = '{y}-{m}-wppm3vysvbp') {
if (!value) return ''
return parseTime(value, format)
}
// 獲取數(shù)據(jù)
async function getList() {
try {
loading.value = true
// 觸發(fā) formatParams 事件
let finalParams = { ...queryParams }
if (props.events?.formatParams) {
finalParams = props.events.formatParams(finalParams) || finalParams
}
const res = await props.func(finalParams)
// 觸發(fā) formatData 事件
let finalData = res
if (props.events?.formatData) {
finalData = props.events.formatData(res) || res
}
// 處理分頁數(shù)據(jù)
if (mergedConfig.value.autoPagination) {
// 前端分頁
tableData.value = finalData.data || []
total.value = tableData.value.length
} else {
// 后端分頁
tableData.value = finalData.data?.rows || []
total.value = finalData.data?.total || 0
}
} catch (error) {
console.error('表格請求失敗:', error)
tableData.value = []
total.value = 0
} finally {
loading.value = false
}
}
// 重置查詢
function resetQuery() {
queryParams.pageNum = 1
getList()
}
// 強制重繪
function reload() {
tableRef.value?.doLayout()
}
// 排序變更
function handleSortChange({ prop, order }) {
if (mergedConfig.value.sort) {
const sort = order ? { prop, order: order === 'ascending' ? 'asc' : 'desc' } : null
if (props.events?.onSortChange) {
props.events.onSortChange(queryParams, sort)
}
getList()
}
}
// 分頁大小變更
function handleSizeChange(val) {
queryParams.pageSize = val
getList()
}
// 鏈接點擊
function handleLinkClick(column, row) {
if (props.events?.onLinkClick) {
props.events.onLinkClick(column, row)
} else if (column.link?.name) {
// 路由跳轉(zhuǎn)
router.push({
name: column.link.name,
params: typeof column.link.params === 'function'
? column.link.params(row)
: column.link.params
})
}
}
// 初始化
onMounted(() => {
if (mergedConfig.value.initResquest) {
getList()
}
})
// 監(jiān)聽外部 params 變更
watch(() => props.params, (newVal) => {
Object.assign(queryParams, newVal)
}, { deep: true })
</script>
<style scoped>
.smart-table {
width: 100%;
}
.no-data {
text-align: center;
padding: 40px 0;
}
.no-data img {
width: 120px;
opacity: 0.6;
}
</style>
三、動態(tài)搜索欄組件(DynamicSearchBar.vue)
封裝目標(biāo)
- 根據(jù)配置動態(tài)生成不同類型的輸入控件
- 自動綁定參數(shù),支持回車查詢
- 超過3項自動折疊
完整代碼
<!-- DynamicSearchBar.vue -->
<template>
<el-form
ref="formRef"
:model="localParams"
:inline="true"
label-width="80px"
size="small"
>
<!-- 顯示項 -->
<el-form-item
v-for="(item, index) in displayedItems"
:key="item.prop"
:label="item.label"
v-has-permi="item.permi"
>
<!-- input -->
<el-input
v-if="item.component.is === 'input'"
v-model="localParams[item.prop]"
v-bind="item.component"
@keyup.enter="handleQuery"
clearable
/>
<!-- select -->
<el-select
v-else-if="item.component.is === 'select'"
v-model="localParams[item.prop]"
v-bind="item.component"
@change="handleQuery"
clearable
>
<el-option
v-for="opt in item.component.options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<!-- date-picker -->
<el-date-picker
v-else-if="item.component.is === 'date-picker'"
v-model="localParams[item.prop]"
v-bind="item.component"
@change="handleQuery"
/>
<!-- tree-select -->
<el-tree-select
v-else-if="item.component.is === 'tree-select'"
v-model="localParams[item.prop]"
v-bind="item.component"
@change="handleQuery"
/>
</el-form-item>
<!-- 操作按鈕 -->
<el-form-item>
<el-button type="primary" @click="handleQuery">查詢</el-button>
<el-button @click="handleReset">重置</el-button>
<el-button
v-if="items.length > 3"
type="text"
@click="toggleExpand"
>
{{ isExpanded ? '收起' : '展開' }}<i :class="`el-icon-arrow-${isExpanded ? 'up' : 'down'}`"></i>
</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { ref, reactive, computed, watch } from 'vue'
const props = defineProps({
items: {
type: Array,
required: true
},
params: {
type: Object,
required: true
},
config: {
type: Object,
default: () => ({})
},
tableRef: {
type: Object,
default: () => ({})
}
})
// 響應(yīng)式數(shù)據(jù)
const localParams = reactive({})
const isExpanded = ref(false)
// 計算顯示項(折疊邏輯)
const displayedItems = computed(() => {
if (isExpanded.value || props.items.length <= 3) {
return props.items
}
return props.items.slice(0, 3)
})
// 同步外部 params
watch(() => props.params, (newVal) => {
Object.assign(localParams, newVal)
}, { immediate: true, deep: true })
// 同步到外部
watch(localParams, (newVal) => {
Object.assign(props.params, newVal)
}, { deep: true })
// 查詢
function handleQuery() {
if (props.tableRef?.getList) {
props.tableRef.getList()
}
// 觸發(fā)事件
emit('query', { ...localParams })
}
// 重置
function handleReset() {
// 重置為初始值
for (const key in localParams) {
localParams[key] = ''
}
handleQuery()
emit('reset')
}
// 切換展開
function toggleExpand() {
isExpanded.value = !isExpanded.value
}
// 權(quán)限指令(示例)
const vHasPermi = {
mounted(el, binding) {
const { value } = binding
if (value && !checkPermission(value)) {
el.style.display = 'none'
}
}
}
// 模擬權(quán)限檢查
function checkPermission(permi) {
// 實際項目中從 store 或全局狀態(tài)獲取用戶權(quán)限
const userPermi = ['user:query', 'role:edit'] // 示例
if (Array.isArray(permi)) {
return permi.some(p => userPermi.includes(p))
}
return userPermi.includes(permi)
}
const emit = defineEmits(['query', 'reset'])
</script>
四、布局容器組件(LayoutContainer.vue)
封裝目標(biāo)
- 提供標(biāo)準(zhǔn)化布局結(jié)構(gòu)
- 集成常用操作(刷新/顯隱列/折疊搜索)
- 控制內(nèi)容區(qū)高度自適應(yīng)
完整代碼
<!-- LayoutContainer.vue -->
<template>
<div class="layout-container">
<!-- 搜索區(qū) -->
<div class="search-area" v-if="$slots.search" v-show="store.search">
<slot name="search"></slot>
</div>
<!-- 內(nèi)容區(qū) -->
<div :class="['content-area', config.fullContent ? 'full' : '']">
<!-- 操作欄 -->
<div class="action-bar" v-if="config.actions.show">
<div class="left-actions">
<slot name="actions-data"></slot>
</div>
<div class="right-actions" v-if="config.actions.table.show">
<!-- 折疊搜索 -->
<el-tooltip content="隱藏搜索" placement="top">
<el-button
circle
@click="store.search = !store.search"
v-show="config.actions.table.search"
>
<i class="el-icon-search"></i>
</el-button>
</el-tooltip>
<!-- 刷新 -->
<el-tooltip content="刷新" placement="top">
<el-button
circle
v-show="config.actions.table.refresh"
@click="handleRefresh"
>
<i class="el-icon-refresh"></i>
</el-button>
</el-tooltip>
<!-- 顯隱列 -->
<el-tooltip content="顯隱列" placement="top">
<el-dropdown
trigger="click"
:hide-on-click="false"
v-show="config.actions.table.columns"
popper-class="column-toggle-popper"
>
<el-button circle>
<i class="el-icon-menu"></i>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="col in props.columns"
:key="col.prop"
>
<el-checkbox
v-if="col.type !== 'selection'"
:model-value="!col.hide"
@update:model-value="(val) => toggleColumn(col, val)"
>
{{ col.label }}
</el-checkbox>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-tooltip>
</div>
</div>
<!-- 主體內(nèi)容 -->
<div class="main-content" v-if="$slots.default">
<slot></slot>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, computed } from 'vue'
import { merge } from 'lodash-es'
// 默認(rèn)配置
const DEFAULT_CONFIG = {
fullContent: true,
actions: {
show: true,
table: {
show: true,
search: true,
refresh: true,
columns: true
}
}
}
const props = defineProps({
columns: {
type: Array,
default: () => []
},
config: {
type: Object,
default: () => ({})
},
tableRef: {
type: Object,
default: () => ({})
}
})
const config = computed(() => {
return merge({}, DEFAULT_CONFIG, props.config)
})
const store = reactive({
search: true
})
// 刷新
function handleRefresh() {
if (props.tableRef?.getList) {
props.tableRef.getList()
}
}
// 切換列顯隱
function toggleColumn(column, visible) {
column.hide = !visible
}
</script>
<style scoped>
.layout-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.search-area {
padding: 15px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 16px;
}
.content-area {
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
flex: none;
}
.content-area.full {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px 0;
gap: 12px;
}
.left-actions,
.right-actions {
display: flex;
align-items: center;
gap: 12px;
}
.main-content {
flex: 1;
padding: 16px;
overflow: auto;
}
</style>
<style>
/* 全局樣式(非 scoped) */
.column-toggle-popper .el-dropdown-menu__item {
line-height: 32px;
padding: 0 16px;
}
</style>
五、標(biāo)準(zhǔn)使用示例
父組件(業(yè)務(wù)頁面)
<template>
<layout-container
:columns="columns"
:config="wrapConfig"
:table-ref="tableRef"
>
<!-- 搜索區(qū) -->
<template #search>
<dynamic-search-bar
:items="searchItems"
:params="queryParams"
:table-ref="tableRef"
/>
</template>
<!-- 左側(cè)操作 -->
<template #actions-data>
<el-button type="primary" @click="handleAdd">新增用戶</el-button>
</template>
<!-- 表格 -->
<smart-table
ref="tableRef"
:columns="columns"
:func="getUserList"
:params="queryParams"
:config="tableConfig"
:events="tableEvents"
>
<!-- 自定義操作列 -->
<template #operation="{ row }">
<el-button size="small" @click="handleEdit(row)">編輯</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">刪除</el-button>
</template>
</smart-table>
</layout-container>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { getUserListAPI } from '@/api/user'
// 查詢參數(shù)
const queryParams = reactive({
username: '',
status: '',
createTime: []
})
// 表格列
const columns = reactive([
{ label: '用戶名', prop: 'username' },
{ label: '狀態(tài)', prop: 'status', dict: 'sys_normal_disable' },
{ label: '創(chuàng)建時間', prop: 'createTime', date: true },
{ label: '操作', prop: 'operation', slot: 'operation', width: 180 }
])
// 配置
const wrapConfig = {
fullContent: true
}
const searchItems = [
{ label: '用戶名', prop: 'username', component: { is: 'input', placeholder: '請輸入' } },
{
label: '狀態(tài)',
prop: 'status',
component: {
is: 'select',
options: [
{ value: '1', label: '啟用' },
{ value: '0', label: '禁用' }
]
}
},
{
label: '創(chuàng)建時間',
prop: 'createTime',
component: { is: 'date-picker', type: 'daterange', rangeSeparator: '-' }
}
]
const tableConfig = {
sort: true
}
const tableEvents = {
formatParams(params) {
// 處理日期范圍
if (params.createTime?.length) {
params.beginTime = params.createTime[0]
params.endTime = params.createTime[1]
delete params.createTime
}
return params
}
}
const tableRef = ref(null)
// API 方法
async function getUserList(params) {
const res = await getUserListAPI(params)
return { data: { rows: res.list, total: res.total } }
}
// 操作方法
function handleAdd() { /* ... */ }
function handleEdit(row) { /* ... */ }
function handleDelete(row) { /* ... */ }
</script>
六、關(guān)鍵設(shè)計總結(jié)
為什么這樣設(shè)計?
| 問題 | 解決方案 | 優(yōu)勢 |
|---|---|---|
| 每頁重復(fù)寫表格結(jié)構(gòu) | SmartTable 封裝 | 減少 70% 模板代碼 |
| 搜索表單千奇百怪 | DynamicSearchBar 配置驅(qū)動 | 統(tǒng)一體驗,快速開發(fā) |
| 刷新/顯隱列位置不一 | LayoutContainer 標(biāo)準(zhǔn)化 | 全系統(tǒng)交互一致 |
| 列顯隱狀態(tài)難管理 | 直接修改 columns.hide | 無需 emit,天然響應(yīng)式 |
| 業(yè)務(wù)邏輯耦合 UI | events 解耦 + slot 覆蓋 | 高內(nèi)聚低耦合 |
使用注意事項
columns 必須是響應(yīng)式對象
// ? 正確 const columns = reactive([...]) // ? 錯誤 :columns="[{ label: 'ID', prop: 'id' }]"不要在組件內(nèi)部寫業(yè)務(wù) API
- 所有數(shù)據(jù)請求通過
funcprop 傳入 - 參數(shù)處理通過
events.formatParams
- 所有數(shù)據(jù)請求通過
復(fù)雜 UI 用插槽覆蓋
- 操作列 →
slot - 自定義單元格 →
slot
- 操作列 →
權(quán)限控制統(tǒng)一接入
- 搜索項權(quán)限 →
v-hasPermi - 按鈕權(quán)限 → 父組件控制
- 搜索項權(quán)限 →

?? 結(jié)語:這套組件體系已在多個大型后臺項目中驗證,顯著提升開發(fā)效率與代碼質(zhì)量。核心思想是 “配置驅(qū)動 UI,事件解耦邏輯,插槽覆蓋特例”,在規(guī)范性與靈活性之間取得平衡。
到此這篇關(guān)于vue表格組件封裝的文章就介紹到這了,更多相關(guān)vue表格組件封裝內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue3基于elementplus 簡單實現(xiàn)表格二次封裝過程
公司渲染表格數(shù)據(jù)時需要將空數(shù)據(jù)顯示‘-’,并且對于每一列數(shù)據(jù)的顯示也有一定的要求,基于這個需求對element-plus簡單進(jìn)行了二次封裝,這篇文章主要介紹了vue3基于elementplus 簡單實現(xiàn)表格二次封裝過程,需要的朋友可以參考下2024-05-05
使用Vue與WebSocket創(chuàng)建實時通知系統(tǒng)
在現(xiàn)代應(yīng)用開發(fā)中,實時性已成為用戶體驗的一個重要組成部分,ue 作為一款流行的前端框架,配合 WebSocket,可以輕松構(gòu)建實時通知系統(tǒng),在本文中,我們將通過一個簡單的示例,使用 Vue 3 的 Composition API(setup 語法糖)來創(chuàng)建一個實時通知系統(tǒng)2024-11-11
解決vue2中使用axios http請求出現(xiàn)的問題
下面小編就為大家分享一篇解決vue2中使用axios http請求出現(xiàn)的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-03-03
laravel5.4+vue+element簡單搭建的示例代碼
本篇文章主要介紹了laravel5.4+vue+element簡單搭建的示例代碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-08-08

