在前端开发中,表格操作按钮的loading状态处理是一个常见的需求。本文将深入探讨Vue3中实现表格按钮loading的四种方案,并分析各自的适用场景。
引言
在现代Web应用中,数据表格是最常见的组件之一。表格中的操作按钮(如编辑、删除、审核等)通常需要与后端进行异步交互。为了提供良好的用户体验,我们需要在按钮点击后显示loading状态,防止用户重复点击并提示操作正在进行。
在Vue3中,我们有多种方式来实现这一功能。本文将详细介绍四种实现方案,并通过对比分析帮助你选择最适合的方案。
方案一:基于行数据的Loading状态
这是最简单直观的方案,直接在行数据对象中添加loading字段。
<template>
<el-table :data="tableData">
<el-table-column label="操作">
<template #default="{ row }">
<el-button
:loading="row.loading || false"
@click="handleAction(row)"
>
操作
</el-button>
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts">
const handleAction = async (row) => {
row.loading = true
try {
await apiCall()
} finally {
row.loading = false
}
}
</script>
优点:
- 实现简单,代码直观
- 状态与数据绑定紧密
- 无需额外的状态管理
缺点:
- 污染了原始数据模型
- 对于复杂操作(多个按钮)需要添加多个字段
- 数据序列化时需要注意排除这些临时状态
方案二:使用Map存储Loading状态
使用Map或对象来存储每个按钮的loading状态,键通常由行ID和操作类型组合而成。
<template>
<el-table :data="tableData">
<el-table-column label="操作">
<template #default="{ row }">
<el-button
:loading="loadingMap.get(`${row.id}-approve`) || false"
@click="approveRow(row)"
>
审核
</el-button>
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts">
const loadingMap = ref(new Map<string, boolean>())
const approveRow = async (row) => {
const key = `${row.id}-approve`
loadingMap.value.set(key, true)
try {
await apiCall()
} finally {
loadingMap.value.set(key, false)
}
}
</script>
优点:
- 不污染原始数据
- 状态管理集中,易于调试
- 支持复杂的键组合
缺点:
- 需要手动管理键的生成
- 模板中需要计算键名
- Map在模板中的使用稍显复杂
方案三:响应式对象集中管理
使用一个响应式对象来统一管理所有按钮的loading状态,结构更加清晰。
<template>
<el-table :data="tableData">
<el-table-column label="操作">
<template #default="{ row }">
<el-button
:loading="getButtonLoading(row.id, 'edit')"
@click="editRow(row)"
>
编辑
</el-button>
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts">
const buttonLoading = reactive<Record<string, Record<string, boolean>>>({})
const setButtonLoading = (rowId, action, loading) => {
if (!buttonLoading[rowId]) buttonLoading[rowId] = {}
buttonLoading[rowId][action] = loading
}
const getButtonLoading = (rowId, action) => {
return buttonLoading[rowId]?.[action] || false
}
const editRow = async (row) => {
setButtonLoading(row.id, 'edit', true)
try {
await apiCall()
} finally {
setButtonLoading(row.id, 'edit', false)
}
}
</script>
优点:
- 状态管理集中且结构清晰
- 易于扩展和复用
- 支持多种操作类型
- 调试方便
缺点:
- 代码量相对较多
- 需要额外的工具函数
方案四:自定义Hook封装
将loading逻辑封装成自定义Hook,实现逻辑复用和关注点分离。
// useTableLoading.ts
export function useTableLoading() {
const loadingState = reactive<Record<string, Record<string, boolean>>>({})
const setLoading = (rowId, action, isLoading) => {
if (!loadingState[rowId]) loadingState[rowId] = {}
loadingState[rowId][action] = isLoading
}
const getLoading = (rowId, action) => {
return loadingState[rowId]?.[action] || false
}
const withLoading = async (rowId, action, asyncFn) => {
setLoading(rowId, action, true)
try {
return await asyncFn()
} finally {
setLoading(rowId, action, false)
}
}
return { getLoading, setLoading, withLoading }
}
优点:
- 高度复用,可跨组件使用
- 关注点分离,业务逻辑更清晰
- 易于测试
- 提供统一的错误处理和状态管理
缺点:
- 学习成本稍高
- 对于简单场景可能显得臃肿
详细对比分析
1. 代码复杂度对比
| 方案 | 模板复杂度 | 逻辑复杂度 | 整体代码量 |
|---|---|---|---|
| 方案一 | 低 | 低 | 少 |
| 方案二 | 中 | 中 | 中 |
| 方案三 | 中 | 中 | 中 |
| 方案四 | 低 | 高 | 多(但可复用) |
2. 性能影响对比
// 性能考虑点:
// 1. 响应式依赖数量
// 2. 内存占用
// 3. 渲染性能
// 方案一:每个行对象都添加了响应式属性
// 方案二:使用Map,响应式依赖较少
// 方案三:集中管理,响应式对象嵌套
// 方案四:与方案三类似,但可优化
3. 适用场景对比
| 方案 | 小规模表格 | 大规模表格 | 多操作类型 | 跨组件复用 |
|---|---|---|---|---|
| 方案一 | ✅ 推荐 | ❌ 不推荐 | ⚠️ 有限支持 | ❌ 不支持 |
| 方案二 | ⚠️ 可用 | ✅ 推荐 | ✅ 支持 | ⚠️ 需要调整 |
| 方案三 | ✅ 推荐 | ✅ 推荐 | ✅ 支持 | ⚠️ 需要调整 |
| 方案四 | ❌ 过重 | ✅ 推荐 | ✅ 支持 | ✅ 推荐 |
实战案例:完整实现
下面是一个结合了方案三和方案四优点的综合实现:
<template>
<div class="table-container">
<!-- 搜索和筛选 -->
<div class="table-header">
<el-input v-model="searchText" placeholder="搜索..." />
<el-button type="primary" @click="loadData">刷新</el-button>
</div>
<!-- 数据表格 -->
<el-table :data="filteredData" v-loading="tableLoading">
<el-table-column prop="name" label="姓名" />
<el-table-column prop="status" label="状态">
<template #default="{ row }">
<el-tag :type="statusMap[row.status]">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="300">
<template #default="{ row }">
<div class="action-buttons">
<!-- 使用统一的loading管理 -->
<el-button
size="small"
type="primary"
:loading="getButtonLoading(row.id, 'edit')"
@click="editItem(row)"
:disabled="row.status === 'disabled'"
>
编辑
</el-button>
<el-button
size="small"
type="success"
:loading="getButtonLoading(row.id, 'enable')"
@click="toggleStatus(row, 'enabled')"
v-if="row.status === 'disabled'"
>
启用
</el-button>
<el-button
size="small"
type="warning"
:loading="getButtonLoading(row.id, 'disable')"
@click="toggleStatus(row, 'disabled')"
v-else
>
禁用
</el-button>
<el-button
size="small"
type="danger"
:loading="getButtonLoading(row.id, 'delete')"
@click="deleteItem(row)"
>
删除
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="table-footer">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
@current-change="loadData"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useTableLoading } from '@/composables/useTableLoading'
// 使用自定义Hook
const { getButtonLoading, withLoading } = useTableLoading()
// 表格数据
const tableData = ref([])
const tableLoading = ref(false)
const searchText = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
// 状态映射
const statusMap = {
enabled: 'success',
disabled: 'info',
pending: 'warning'
}
// 过滤后的数据
const filteredData = computed(() => {
if (!searchText.value) return tableData.value
return tableData.value.filter(item =>
item.name.includes(searchText.value) ||
item.email.includes(searchText.value)
)
})
// 加载数据
const loadData = async () => {
tableLoading.value = true
try {
// 模拟API调用
const response = await fetchData(currentPage.value, pageSize.value)
tableData.value = response.data
total.value = response.total
} catch (error) {
ElMessage.error('加载数据失败')
} finally {
tableLoading.value = false
}
}
// 编辑项目
const editItem = async (row) => {
await withLoading(row.id, 'edit', async () => {
// 这里调用编辑API
await new Promise(resolve => setTimeout(resolve, 1000))
ElMessage.success('编辑成功')
})
}
// 切换状态
const toggleStatus = async (row, newStatus) => {
const action = newStatus === 'enabled' ? 'enable' : 'disable'
await withLoading(row.id, action, async () => {
await new Promise(resolve => setTimeout(resolve, 800))
row.status = newStatus
ElMessage.success(`已${newStatus === 'enabled' ? '启用' : '禁用'}`)
})
}
// 删除项目(带确认)
const deleteItem = async (row) => {
try {
await ElMessageBox.confirm(
`确定删除 "${row.name}" 吗?`,
'警告',
{ type: 'warning' }
)
await withLoading(row.id, 'delete', async () => {
await new Promise(resolve => setTimeout(resolve, 800))
// 从列表中移除
const index = tableData.value.findIndex(item => item.id === row.id)
if (index > -1) {
tableData.value.splice(index, 1)
}
ElMessage.success('删除成功')
})
} catch (error) {
if (error === 'cancel') {
ElMessage.info('已取消删除')
}
}
}
// 模拟API
const fetchData = async (page, size) => {
return new Promise(resolve => {
setTimeout(() => {
resolve({
data: Array.from({ length: size }, (_, i) => ({
id: (page - 1) * size + i + 1,
name: `用户${(page - 1) * size + i + 1}`,
email: `user${(page - 1) * size + i + 1}@example.com`,
status: ['enabled', 'disabled', 'pending'][Math.floor(Math.random() * 3)]
})),
total: 100
})
}, 500)
})
}
onMounted(() => {
loadData()
})
</script>
<style scoped>
.table-container {
padding: 20px;
background: #fff;
border-radius: 8px;
}
.table-header {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.action-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.table-footer {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
</style>
最佳实践建议
1. 根据场景选择方案
- 简单场景(小表格、简单操作):方案一或方案二
- 复杂场景(大表格、多操作):方案三
- 需要复用(多个表格组件):方案四
2. 性能优化技巧
// 1. 使用唯一键,避免重复创建响应式对象
const getLoadingKey = (rowId, action) => `${rowId}_${action}`
// 2. 批量操作时,使用防抖节流
import { debounce } from 'lodash-es'
const handleClick = debounce(async (row) => {
// ...
}, 300)
// 3. 虚拟滚动配合懒加载
总结
在Vue3中实现表格按钮的loading状态有多种方案,每种方案都有其适用场景:
- 方案一适合快速开发和简单场景,但要小心数据污染问题
- 方案二提供了更好的关注点分离,适合中等复杂度应用
- 方案三通过集中管理提供了最大的灵活性,适合大型应用
- 方案四通过自定义Hook实现了最佳的可复用性和可测试性