复杂表格示例
展示各种高级表格功能,包括多级表头、合并单元格、行内编辑、树形表格、拖拽排序等。
功能特性
- 🎯 多级表头 - 支持多级嵌套表头
- 🔀 合并单元格 - 支持行合并和列合并
- ✏️ 行内编辑 - 点击单元格直接编辑
- 🌳 树形表格 - 支持懒加载和展开收起
- 🎨 拖拽排序 - 拖拽行进行排序
完整代码
点击查看完整代码
vue
<template>
<div class="page-container">
<!-- 顶部工具栏 -->
<div class="toolbar">
<div class="toolbar-left">
<h2 class="page-title">
<el-icon><Grid /></el-icon>
复杂表格示例
</h2>
<span class="page-subtitle">支持多级表头、合并单元格、行内编辑等高级功能</span>
</div>
<div class="toolbar-right">
<el-button type="primary" @click="addRow">
<el-icon><Plus /></el-icon>新增行
</el-button>
<el-button @click="toggleEditMode">
<el-icon><Edit /></el-icon>{{ isEditMode ? '退出编辑' : '批量编辑' }}
</el-button>
<el-button @click="exportData">
<el-icon><Download /></el-icon>导出数据
</el-button>
</div>
</div>
<div class="content-wrapper">
<!-- 复杂表格 1:多级表头 + 合并单元格 -->
<el-card class="table-card">
<template #header>
<div class="card-header">
<span>多级表头与合并单元格</span>
<el-radio-group v-model="mergeType" size="small">
<el-radio-button value="none">不合并</el-radio-button>
<el-radio-button value="row">行合并</el-radio-button>
<el-radio-button value="col">列合并</el-radio-button>
</el-radio-group>
</div>
</template>
<el-table
:data="mergeTableData"
:span-method="objectSpanMethod"
border
style="width: 100%"
highlight-current-row
>
<!-- 一级表头:基本信息 -->
<el-table-column label="基本信息" align="center">
<el-table-column prop="department" label="部门" width="120" />
<el-table-column prop="name" label="姓名" width="100" />
<el-table-column prop="position" label="职位" width="120" />
</el-table-column>
<!-- 一级表头:薪资信息 -->
<el-table-column label="薪资信息" align="center">
<el-table-column prop="baseSalary" label="基本工资" width="120">
<template #default="{ row }">
<span>¥{{ row.baseSalary.toLocaleString() }}</span>
</template>
</el-table-column>
<el-table-column prop="bonus" label="奖金" width="120">
<template #default="{ row }">
<span>¥{{ row.bonus.toLocaleString() }}</span>
</template>
</el-table-column>
<el-table-column prop="total" label="总计" width="120">
<template #default="{ row }">
<strong>¥{{ (row.baseSalary + row.bonus).toLocaleString() }}</strong>
</template>
</el-table-column>
</el-table-column>
<!-- 一级表头:绩效信息 -->
<el-table-column label="绩效信息" align="center">
<el-table-column prop="performance" label="绩效等级" width="100">
<template #default="{ row }">
<el-tag :type="getPerformanceType(row.performance)">
{{ row.performance }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="attendance" label="出勤率" width="100">
<template #default="{ row }">
<el-progress :percentage="row.attendance" :status="row.attendance < 90 ? 'exception' : 'success'" />
</template>
</el-table-column>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row, $index }">
<el-button type="primary" link @click="editRow(row)">编辑</el-button>
<el-button type="danger" link @click="deleteRow($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 复杂表格 2:可编辑表格 -->
<el-card class="table-card">
<template #header>
<div class="card-header">
<span>行内编辑表格</span>
<div class="header-actions">
<el-input
v-model="searchQuery"
placeholder="搜索姓名或职位"
style="width: 200px"
size="small"
clearable
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button size="small" @click="saveAllChanges" type="success">
<el-icon><Check /></el-icon>保存所有
</el-button>
</div>
</div>
</template>
<el-table
:data="filteredEditTableData"
border
style="width: 100%"
@cell-click="handleCellClick"
>
<el-table-column type="index" width="50" />
<el-table-column prop="name" label="姓名" width="150">
<template #default="{ row }">
<el-input
v-if="row.editing?.name"
v-model="row.name"
size="small"
@blur="row.editing.name = false"
@keyup.enter="row.editing.name = false"
v-focus
/>
<span v-else class="editable-cell" @click="startEdit(row, 'name')">
{{ row.name }}
<el-icon class="edit-icon"><Edit /></el-icon>
</span>
</template>
</el-table-column>
<el-table-column prop="age" label="年龄" width="100">
<template #default="{ row }">
<el-input-number
v-if="row.editing?.age"
v-model="row.age"
:min="18"
:max="100"
size="small"
@blur="row.editing.age = false"
v-focus
/>
<span v-else class="editable-cell" @click="startEdit(row, 'age')">
{{ row.age }}
<el-icon class="edit-icon"><Edit /></el-icon>
</span>
</template>
</el-table-column>
<el-table-column prop="department" label="部门" width="150">
<template #default="{ row }">
<el-select
v-if="row.editing?.department"
v-model="row.department"
size="small"
@blur="row.editing.department = false"
@change="row.editing.department = false"
v-focus
>
<el-option label="技术部" value="技术部" />
<el-option label="产品部" value="产品部" />
<el-option label="运营部" value="运营部" />
<el-option label="人事部" value="人事部" />
</el-select>
<span v-else class="editable-cell" @click="startEdit(row, 'department')">
{{ row.department }}
<el-icon class="edit-icon"><Edit /></el-icon>
</span>
</template>
</el-table-column>
<el-table-column prop="salary" label="薪资" width="150">
<template #default="{ row }">
<el-input-number
v-if="row.editing?.salary"
v-model="row.salary"
:min="3000"
:max="50000"
:step="1000"
size="small"
@blur="row.editing.salary = false"
v-focus
/>
<span v-else class="editable-cell" @click="startEdit(row, 'salary')">
¥{{ row.salary.toLocaleString() }}
<el-icon class="edit-icon"><Edit /></el-icon>
</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="120">
<template #default="{ row }">
<el-switch
v-model="row.status"
:active-value="1"
:inactive-value="0"
active-text="在职"
inactive-text="离职"
/>
</template>
</el-table-column>
<el-table-column prop="joinDate" label="入职日期" width="150">
<template #default="{ row }">
<el-date-picker
v-if="row.editing?.joinDate"
v-model="row.joinDate"
type="date"
size="small"
style="width: 130px"
@blur="row.editing.joinDate = false"
@change="row.editing.joinDate = false"
v-focus
/>
<span v-else class="editable-cell" @click="startEdit(row, 'joinDate')">
{{ formatDate(row.joinDate) }}
<el-icon class="edit-icon"><Edit /></el-icon>
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row, $index }">
<el-button type="primary" link @click="saveRow(row)">保存</el-button>
<el-button type="danger" link @click="deleteEditRow($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 复杂表格 3:树形表格 -->
<el-card class="table-card">
<template #header>
<div class="card-header">
<span>树形表格(懒加载)</span>
<el-button size="small" @click="expandAll">
<el-icon><ArrowDown /></el-icon>展开全部
</el-button>
<el-button size="small" @click="collapseAll">
<el-icon><ArrowUp /></el-icon>收起全部
</el-button>
</div>
</template>
<el-table
ref="treeTableRef"
:data="treeTableData"
row-key="id"
border
default-expand-all
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:load="loadTreeData"
lazy
>
<el-table-column prop="name" label="名称" min-width="200">
<template #default="{ row }">
<el-icon v-if="row.type === 'folder'" class="folder-icon">
<Folder />
</el-icon>
<el-icon v-else class="file-icon">
<Document />
</el-icon>
<span class="tree-label">{{ row.name }}</span>
</template>
</el-table-column>
<el-table-column prop="type" label="类型" width="100">
<template #default="{ row }">
<el-tag :type="row.type === 'folder' ? 'warning' : 'info'">
{{ row.type === 'folder' ? '文件夹' : '文件' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="size" label="大小" width="120">
<template #default="{ row }">
{{ row.size ? formatFileSize(row.size) : '-' }}
</template>
</el-table-column>
<el-table-column prop="updateTime" label="修改时间" width="180">
<template #default="{ row }">
{{ formatDate(row.updateTime) }}
</template>
</el-table-column>
<el-table-column prop="creator" label="创建者" width="120" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="viewDetail(row)">查看</el-button>
<el-button type="primary" link @click="renameItem(row)">重命名</el-button>
<el-button type="danger" link @click="deleteItem(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 复杂表格 4:拖拽排序表格 -->
<el-card class="table-card">
<template #header>
<div class="card-header">
<span>拖拽排序表格</span>
<div>
<el-tag size="small" type="info" style="margin-right: 10px">拖拽行进行排序</el-tag>
<el-button size="small" type="primary" @click="saveSortOrder">
<el-icon><Check /></el-icon>保存排序
</el-button>
</div>
</div>
</template>
<el-table
ref="dragTableRef"
:data="dragTableData"
row-key="id"
border
class="drag-table"
>
<el-table-column type="index" width="50">
<template #header>
<el-icon><Rank /></el-icon>
</template>
</el-table-column>
<el-table-column prop="title" label="任务名称" min-width="200">
<template #default="{ row }">
<div class="task-title">
<el-icon class="drag-handle"><Rank /></el-icon>
<span>{{ row.title }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="priority" label="优先级" width="100">
<template #default="{ row }">
<el-tag :type="getPriorityType(row.priority)">
{{ row.priority }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="120">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="assignee" label="负责人" width="120">
<template #default="{ row }">
<div class="assignee-cell">
<el-avatar :size="24" :src="row.avatar" />
<span>{{ row.assignee }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="deadline" label="截止日期" width="150">
<template #default="{ row }">
{{ formatDate(row.deadline) }}
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</div>
</template>
<script setup>
import { ref, computed, nextTick, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import Sortable from 'sortablejs'
// 自定义指令:自动聚焦
const vFocus = {
mounted: (el) => {
const input = el.querySelector('input') || el
input?.focus()
}
}
// ==================== 表格 1:多级表头与合并单元格 ====================
const mergeType = ref('none')
const mergeTableData = ref([
{ department: '技术部', name: '张三', position: '高级工程师', baseSalary: 15000, bonus: 5000, performance: 'A', attendance: 98 },
{ department: '技术部', name: '李四', position: '前端开发', baseSalary: 12000, bonus: 3000, performance: 'B', attendance: 95 },
{ department: '技术部', name: '王五', position: '后端开发', baseSalary: 13000, bonus: 4000, performance: 'A', attendance: 96 },
{ department: '产品部', name: '赵六', position: '产品经理', baseSalary: 14000, bonus: 6000, performance: 'A', attendance: 99 },
{ department: '产品部', name: '钱七', position: '产品助理', baseSalary: 8000, bonus: 2000, performance: 'B', attendance: 94 },
{ department: '运营部', name: '孙八', position: '运营经理', baseSalary: 11000, bonus: 3500, performance: 'B', attendance: 92 },
])
// 合并单元格方法
const objectSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
if (mergeType.value === 'none') return { rowspan: 1, colspan: 1 }
if (mergeType.value === 'row') {
// 按部门行合并
if (columnIndex === 0) {
const department = row.department
const list = mergeTableData.value
// 找到该部门的第一行
const firstIndex = list.findIndex(item => item.department === department)
if (rowIndex === firstIndex) {
// 计算该部门的行数
const count = list.filter(item => item.department === department).length
return { rowspan: count, colspan: 1 }
} else if (list[rowIndex - 1]?.department === department) {
return { rowspan: 0, colspan: 0 }
}
}
}
return { rowspan: 1, colspan: 1 }
}
const getPerformanceType = (performance) => {
const types = { 'A': 'success', 'B': 'warning', 'C': 'danger' }
return types[performance] || 'info'
}
// ==================== 表格 2:可编辑表格 ====================
const isEditMode = ref(false)
const searchQuery = ref('')
const editTableData = ref([
{ id: 1, name: '张三', age: 28, department: '技术部', salary: 15000, status: 1, joinDate: new Date('2020-03-15'), editing: {} },
{ id: 2, name: '李四', age: 25, department: '产品部', salary: 12000, status: 1, joinDate: new Date('2021-06-20'), editing: {} },
{ id: 3, name: '王五', age: 32, department: '运营部', salary: 11000, status: 0, joinDate: new Date('2019-01-10'), editing: {} },
{ id: 4, name: '赵六', age: 29, department: '技术部', salary: 18000, status: 1, joinDate: new Date('2018-09-05'), editing: {} },
{ id: 5, name: '钱七', age: 26, department: '人事部', salary: 9000, status: 1, joinDate: new Date('2022-02-28'), editing: {} },
])
const filteredEditTableData = computed(() => {
if (!searchQuery.value) return editTableData.value
const query = searchQuery.value.toLowerCase()
return editTableData.value.filter(row =>
row.name.toLowerCase().includes(query) ||
row.department.toLowerCase().includes(query)
)
})
const startEdit = (row, field) => {
if (!row.editing) row.editing = {}
row.editing[field] = true
}
const saveRow = (row) => {
row.editing = {}
ElMessage.success('保存成功')
}
const saveAllChanges = () => {
editTableData.value.forEach(row => row.editing = {})
ElMessage.success('所有更改已保存')
}
// ==================== 表格 3:树形表格 ====================
const treeTableRef = ref()
const treeTableData = ref([
{
id: '1',
name: '公司文档',
type: 'folder',
size: null,
updateTime: new Date(),
creator: '管理员',
hasChildren: true,
children: [
{
id: '1-1',
name: '技术文档',
type: 'folder',
size: null,
updateTime: new Date(),
creator: '张三',
children: [
{ id: '1-1-1', name: 'API文档.pdf', type: 'file', size: 2048000, updateTime: new Date(), creator: '李四' },
{ id: '1-1-2', name: '架构设计.docx', type: 'file', size: 1024000, updateTime: new Date(), creator: '王五' },
]
},
]
},
])
const expandAll = () => {
treeTableData.value.forEach(row => {
treeTableRef.value?.toggleRowExpansion(row, true)
})
}
const collapseAll = () => {
treeTableData.value.forEach(row => {
treeTableRef.value?.toggleRowExpansion(row, false)
})
}
// ==================== 表格 4:拖拽排序表格 ====================
const dragTableRef = ref()
const dragTableData = ref([
{ id: '1', title: '完成用户管理模块开发', priority: '高', status: '进行中', assignee: '张三', avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png', deadline: new Date('2024-02-15') },
{ id: '2', title: '设计系统首页UI', priority: '中', status: '待处理', assignee: '李四', avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png', deadline: new Date('2024-02-20') },
])
// 初始化拖拽排序
const initSortable = () => {
const tbody = dragTableRef.value?.$el.querySelector('.el-table__body tbody')
if (!tbody) return
Sortable.create(tbody, {
handle: '.drag-handle',
animation: 150,
onEnd: (evt) => {
const { oldIndex, newIndex } = evt
if (oldIndex === newIndex) return
const item = dragTableData.value.splice(oldIndex, 1)[0]
dragTableData.value.splice(newIndex, 0, item)
ElMessage.success('排序已更新')
}
})
}
onMounted(() => {
nextTick(() => {
initSortable()
})
})
</script>核心要点
1. 多级表头
使用嵌套的 <el-table-column> 实现多级表头:
vue
<el-table-column label="基本信息" align="center">
<el-table-column prop="department" label="部门" />
<el-table-column prop="name" label="姓名" />
</el-table-column>2. 合并单元格
通过 span-method 属性自定义合并逻辑:
javascript
const objectSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
if (columnIndex === 0) {
// 返回 rowspan 和 colspan
return { rowspan: 2, colspan: 1 }
}
}3. 行内编辑
使用条件渲染实现点击编辑:
vue
<template #default="{ row }">
<el-input v-if="row.editing" v-model="row.name" />
<span v-else @click="startEdit(row)">{{ row.name }}</span>
</template>4. 树形表格
设置 row-key 和 tree-props 属性:
vue
<el-table
:data="treeData"
row-key="id"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
lazy
:load="loadTreeData"
>5. 拖拽排序
使用 SortableJS 实现拖拽排序:
javascript
import Sortable from 'sortablejs'
Sortable.create(tbody, {
handle: '.drag-handle',
onEnd: (evt) => {
// 处理排序逻辑
}
})