Skip to content

复杂表格示例

展示各种高级表格功能,包括多级表头、合并单元格、行内编辑、树形表格、拖拽排序等。

功能特性

  • 🎯 多级表头 - 支持多级嵌套表头
  • 🔀 合并单元格 - 支持行合并和列合并
  • ✏️ 行内编辑 - 点击单元格直接编辑
  • 🌳 树形表格 - 支持懒加载和展开收起
  • 🎨 拖拽排序 - 拖拽行进行排序

完整代码

点击查看完整代码
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-keytree-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) => {
    // 处理排序逻辑
  }
})

基于 MIT 许可发布