为了确保功能的完整性和健壮性,这部分代码的体量确实有点多🤦♂️。重点讲解的是导出功能,导出功能为前端导出。
el-table表格全屏/管理显示字段/导出功能封装
- 单行表头导出
-
- 先看效果
- 完整代码和封装的组件
-
- 完整示例:src\views\TestExport\headerExport.vue
- 管理显示字段的抽屉弹窗:src\components\TableColumnManager\index.vue
- [封装全屏/管理显示字段/导出功能的 hooks:src\composables\useTableCommon.ts](#封装全屏/管理显示字段/导出功能的 hooks:src\composables\useTableCommon.ts)
- [excel 导出工具类:src\utils\TableExportUtil.ts](#excel 导出工具类:src\utils\TableExportUtil.ts)
- 时间格式化函数:src\utils\TimeUtil.ts
- [useTableCommon 使用指南](#useTableCommon 使用指南)
-
- [📋 功能概述](#📋 功能概述)
- [🎯 适用场景](#🎯 适用场景)
- [📦 API 参数](#📦 API 参数)
- [🔧 ColumnConfig 类型定义](#🔧 ColumnConfig 类型定义)
- [💡 使用示例](#💡 使用示例)
- [1. 基础配置](#1. 基础配置)
- [2. 完整配置(含导出)](#2. 完整配置(含导出))
- [3. 模板中使用](#3. 模板中使用)
- [⚠️ 注意事项](#⚠️ 注意事项)
- [1. 变量声明顺序](#1. 变量声明顺序)
- [2. 导出 API 规范](#2. 导出 API 规范)
- [3. 条件样式颜色格式](#3. 条件样式颜色格式)
- [4. 事件监听清理](#4. 事件监听清理)
- [🎨 样式建议](#🎨 样式建议)
- 表格容器样式
- 条件样式示例
- [🔗 相关组件](#🔗 相关组件)
- [📚 完整示例](#📚 完整示例)
- 多行表头导出
-
- 先看效果
- 完整代码和封装的组件
- [useTableExportComplex 使用指南](#useTableExportComplex 使用指南)
-
- [📋 功能概述](#📋 功能概述)
- [🎯 适用场景](#🎯 适用场景)
- [📦 API 参数](#📦 API 参数)
- [🔧 ExportColumnDef 类型定义](#🔧 ExportColumnDef 类型定义)
- [💡 使用示例](#💡 使用示例)
-
- [1. 基础配置(分组列 + 平铺列)](#1. 基础配置(分组列 + 平铺列))
- [2. 组合字段示例(公差范围)](#2. 组合字段示例(公差范围))
- [3. 嵌套数据提取示例](#3. 嵌套数据提取示例)
- [4. 完整配置(与 useTableCommon 配合使用)](#4. 完整配置(与 useTableCommon 配合使用))
- [5. 模板中使用](#5. 模板中使用)
- [⚠️ 注意事项](#⚠️ 注意事项)
-
- [1. 与 useTableCommon 配合使用](#1. 与 useTableCommon 配合使用)
- [2. prop 字段必须匹配](#2. prop 字段必须匹配)
- [3. 异常判断函数必须返回布尔值](#3. 异常判断函数必须返回布尔值)
- [4. 导出 API 规范](#4. 导出 API 规范)
- [5. 数字格式化](#5. 数字格式化)
- [6. 变量声明顺序](#6. 变量声明顺序)
- [🎨 样式建议](#🎨 样式建议)
- [🔗 与 useTableCommon 的区别](#🔗 与 useTableCommon 的区别)
- [📚 完整示例](#📚 完整示例)
单行表头导出
先看效果
tip:表格展示的数据和导出的 Excel 数据是 mock 数据,数据是随机的,所以数据对不上。
| 页面渲染 | 导出 Excel |
|---|---|
![]() |
![]() |
完整代码和封装的组件
完整示例:src\views\TestExport\headerExport.vue
javascript
<template>
<!-- 表格外层容器,用于全屏操作 -->
<div
ref="tableInfoRef"
class="table-container"
:class="{ fullscreen: isFullscreen }"
>
<!-- 工具栏 -->
<div class="toolbar">
<div class="toolbar-left">
<h2>单表头导出示例</h2>
<p class="description">
演示 useTableCommon 的完整功能:全屏、列管理、单表头导出、条件样式
</p>
</div>
<div class="toolbar-right">
<!-- 管理显示字段按钮 -->
<el-button
type="primary"
plain
@click="handleTableSetting"
:icon="Setting"
>
管理显示字段
</el-button>
<!-- 全屏切换按钮 -->
<el-button
type="primary"
plain
@click="handleFullScreen"
:icon="isFullscreen ? Minus : FullScreen"
>
{{ isFullscreen ? "退出全屏" : "全屏展示" }}
</el-button>
<!-- 导出按钮 -->
<el-button type="primary" @click="handleExport" :icon="Download">
导出 Excel
</el-button>
</div>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<el-table
:key="refreshKey"
:data="tableData"
border
:max-height="tableMaxHeight"
v-loading="loading"
ref="tableRef"
>
<!-- 序号列(固定) -->
<el-table-column
prop="index"
label="序号"
width="60"
fixed="left"
align="center"
/>
<!-- 姓名列 -->
<el-table-column
prop="name"
label="姓名"
:visible="isColVisible('name')"
:fixed="isColFixed('name')"
min-width="120"
align="center"
/>
<!-- 部门列 -->
<el-table-column
prop="department"
label="部门"
:visible="isColVisible('department')"
:fixed="isColFixed('department')"
min-width="120"
align="center"
/>
<!-- 年龄列 -->
<el-table-column
prop="age"
label="年龄"
:visible="isColVisible('age')"
:fixed="isColFixed('age')"
width="80"
align="center"
/>
<!-- 入职日期列 -->
<el-table-column
prop="hireDate"
label="入职日期"
:visible="isColVisible('hireDate')"
:fixed="isColFixed('hireDate')"
min-width="120"
align="center"
/>
<!-- 薪资列(带条件样式,与导出逻辑一致:salary > 20000 显示红色) -->
<el-table-column
prop="salary"
label="薪资(元)"
:visible="isColVisible('salary')"
:fixed="isColFixed('salary')"
min-width="120"
align="center"
>
<template #default="{ row }">
<span :class="{ 'salary-highlight': row.salary > 20000 }">
{{ formatNumber(row.salary) }}
</span>
</template>
</el-table-column>
<!-- 绩效评分列(带条件样式,与导出逻辑一致:performance < 60 显示红色背景) -->
<el-table-column
prop="performance"
label="绩效评分"
:visible="isColVisible('performance')"
:fixed="isColFixed('performance')"
width="100"
align="center"
>
<template #default="{ row }">
<span :class="{ 'performance-abnormal': row.performance < 60 }">
{{ row.performance }}分
</span>
</template>
</el-table-column>
<!-- 状态列(与导出逻辑一致:在职显示绿色,离职显示橙色) -->
<el-table-column
prop="status"
label="状态"
:visible="isColVisible('status')"
:fixed="isColFixed('status')"
width="100"
align="center"
>
<template #default="{ row }">
<span
:class="{
'status-active': row.status === '在职',
'status-inactive': row.status === '离职',
}"
>
{{ row.status }}
</span>
</template>
</el-table-column>
</el-table>
</div>
<!-- 列管理弹窗 -->
<TableColumnManager
v-model:visible="columnManagerVisible"
:columns="columnConfig"
:default-columns="defaultColumnConfig"
@confirm="handleColumnConfigConfirm"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from "vue";
import type { ColumnConfig } from "@/components/TableColumnManager/index.vue";
import TableColumnManager from "@/components/TableColumnManager/index.vue";
import { useTableCommon } from "@/composables/useTableCommon";
import { Setting, FullScreen, Minus, Download } from "@element-plus/icons-vue";
/**
* 模拟表格数据
*/
const tableData = ref<Record<string, any>[]>([]);
const loading = ref(false);
/**
* 生成模拟数据
*/
const generateMockData = () => {
const departments = ["研发部", "产品部", "市场部", "财务部", "人事部"];
const statuses = ["在职", "离职"];
const data: Record<string, any>[] = [];
for (let i = 1; i <= 50; i++) {
const salary = Math.floor(Math.random() * 20000) + 5000;
const performance = Math.floor(Math.random() * 50) + 50;
data.push({
index: i,
name: `员工${i}`,
department: departments[Math.floor(Math.random() * departments.length)],
age: Math.floor(Math.random() * 30) + 22,
hireDate: `20${Math.floor(Math.random() * 20) + 10}-${String(
Math.floor(Math.random() * 12) + 1,
).padStart(2, "0")}-${String(Math.floor(Math.random() * 28) + 1).padStart(
2,
"0",
)}`,
salary,
performance,
status: statuses[Math.floor(Math.random() * statuses.length)],
});
}
return data;
};
/**
* 格式化数字(保留千分位)
*/
const formatNumber = (val: number): string => {
if (val == null) return "";
return val.toLocaleString("zh-CN");
};
/**
* 模拟导出 API
* 模拟从后端获取全部数据
*/
const mockExportApi = async (params: { pageNo: number; pageSize: number }) => {
// 模拟网络延迟
await new Promise((resolve) => setTimeout(resolve, 800));
const data = generateMockData();
return {
data: {
records: data,
total: data.length,
},
};
};
/**
* 表格列定义
* 定义所有表格列的配置信息,包括:
* - prop: 字段名,对应数据对象的key
* - label: 表头显示文字
* - show: 是否显示(默认true)
* - fixed: 是否固定(默认false)
* - sortable: 是否可排序(默认true,由列管理控制)
* - required: 是否必填(必填列不能隐藏)
* - order: 显示顺序
* 注意:sortable 字段保留是为了兼容列管理功能(用户仍可控制排序)
*/
const topLevelColumns: ColumnConfig[] = [
{
prop: "index",
label: "序号",
show: true,
fixed: true,
sortable: false,
required: true,
order: 0,
width: 60,
},
{
prop: "name",
label: "姓名",
show: true,
fixed: false,
sortable: true,
required: false,
order: 1,
minWidth: 120,
},
{
prop: "department",
label: "部门",
show: true,
fixed: false,
sortable: true,
required: false,
order: 2,
minWidth: 120,
},
{
prop: "age",
label: "年龄",
show: true,
fixed: false,
sortable: true,
required: false,
order: 3,
width: 80,
},
{
prop: "hireDate",
label: "入职日期",
show: true,
fixed: false,
sortable: true,
required: false,
order: 4,
minWidth: 120,
},
{
prop: "salary",
label: "薪资(元)",
show: true,
fixed: false,
sortable: true,
required: false,
order: 5,
minWidth: 120,
},
{
prop: "performance",
label: "绩效评分",
show: true,
fixed: false,
sortable: true,
required: false,
order: 6,
width: 100,
},
{
prop: "status",
label: "状态",
show: true,
fixed: false,
sortable: true,
required: false,
order: 7,
width: 100,
},
];
/**
* 使用 useTableCommon 组合式函数
* 传入必要的配置参数:
* - configKey: 配置存储的key,用于后端保存/加载列配置(这里使用mock)
* - topLevelColumns: 表格所有的列定义
* - exportApi: 导出数据的API函数
* - exportFileName: 导出文件名
* - exportFieldFormatters: 自定义字段格式化函数
* - exportFieldStyles: 自定义字段样式函数(用于Excel条件样式)
*/
const {
isFullscreen,
tableInfoRef,
tableRef,
columnManagerVisible,
refreshKey,
windowHeight,
columnConfig,
defaultColumnConfig,
isColVisible,
isColFixed,
handleFullScreen,
handleTableSetting,
handleColumnConfigConfirm,
handleExport,
setupFullscreenListeners,
removeFullscreenListeners,
loadFieldConfig,
} = useTableCommon({
configKey: "header-export-demo",
topLevelColumns,
// 模拟获取列配置(实际项目中调用后端API)
getFieldConfig: async () => {
await new Promise((resolve) => setTimeout(resolve, 300));
return {
data: {
config: {
columns: {}, // 初始为空,使用默认配置
},
},
};
},
// 模拟保存列配置(实际项目中调用后端API)
saveFieldConfig: async (params) => {
await new Promise((resolve) => setTimeout(resolve, 300));
console.log("保存列配置:", params);
return { success: true };
},
// 导出API
exportApi: mockExportApi,
// 导出文件名
exportFileName: "员工信息表",
// 自定义字段格式化函数
exportFieldFormatters: {
// 薪资字段添加千分位
salary: (value: any) => {
if (value == null) return "";
return Number(value).toLocaleString("zh-CN");
},
// 绩效评分添加百分号
performance: (value: any) => {
if (value == null) return "";
return `${value}分`;
},
},
// 自定义字段样式函数(用于Excel条件样式)
exportFieldStyles: {
// 薪资大于20000显示红色
salary: (value: any) => {
if (Number(value) > 20000) {
return { color: "#FF0000", bgColor: "#FFF2F0" };
}
return null;
},
// 绩效评分低于60显示红色背景
performance: (value: any) => {
if (Number(value) < 60) {
return { color: "#9C0006", bgColor: "#FFC7CE" };
}
return null;
},
},
});
/**
* 表格最大高度计算
* 全屏模式下减去工具栏高度,非全屏模式下使用固定高度
* 必须在 useTableCommon 解构后定义,因为依赖 isFullscreen 和 windowHeight
*/
const tableMaxHeight = computed(() => {
return isFullscreen.value ? `${windowHeight.value - 120}px` : "500px";
});
/**
* 组件挂载时初始化
*/
onMounted(() => {
// 注册全屏和窗口resize事件监听
setupFullscreenListeners();
// 加载列配置
loadFieldConfig();
// 加载表格数据
loading.value = true;
setTimeout(() => {
tableData.value = generateMockData();
loading.value = false;
}, 500);
});
/**
* 组件卸载时清理
*/
onUnmounted(() => {
// 移除事件监听
removeFullscreenListeners();
});
</script>
<style lang="scss" scoped>
.table-container {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
&.fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
border-radius: 0;
margin: 0;
}
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #eee;
.toolbar-left {
h2 {
margin: 0 0 4px 0;
font-size: 18px;
font-weight: 600;
color: #303133;
}
.description {
margin: 0;
font-size: 13px;
color: #909399;
}
}
.toolbar-right {
display: flex;
gap: 12px;
}
}
.table-wrapper {
flex: 1;
padding: 16px 20px;
overflow: auto;
}
:deep(.el-table) {
width: 100%;
}
/* 薪资高亮样式:薪资 > 20000 显示红色(与导出逻辑一致) */
.salary-highlight {
color: #ff0000;
background-color: #fff2f0;
padding: 2px 6px;
border-radius: 4px;
}
/* 绩效异常样式:绩效评分 < 60 显示红色背景(与导出逻辑一致) */
.performance-abnormal {
color: #9c0006;
background-color: #ffc7ce;
padding: 2px 6px;
border-radius: 4px;
}
/* 状态样式(与导出逻辑一致) */
.status-active {
color: #67c23a;
font-weight: 500;
}
.status-inactive {
color: #e6a23c;
font-weight: 500;
}
</style>
管理显示字段的抽屉弹窗:src\components\TableColumnManager\index.vue
javascript
<template>
<el-drawer
title="管理显示字段"
v-model="dialogVisible"
direction="rtl"
size="600px"
:before-close="handleClose"
custom-class="column-manager-drawer"
>
<div class="drawer-content">
<!-- 固定的头部区域 -->
<div class="drawer-header-fixed">
<div class="description-wrapper">
<div class="description">
直接拖动字段名称可调整显示字段排序,勾选是否展示可选择需要展示的字段
</div>
</div>
<el-button style="margin-bottom: 10px; float: right;" type="primary" plain @click="handleResetDefault">恢复默认</el-button>
</div>
<!-- 可滚动的表格区域 -->
<div class="table-container">
<el-table
:key="refreshKey"
:data="tableColumns"
border
row-key="prop"
ref="columnTable"
height="100%"
:header-cell-style="{ position: 'sticky', top: 0, zIndex: 10 }"
>
<el-table-column label="列表字段名称" prop="label" min-width="120" />
<el-table-column label="展示" min-width="80" align="center">
<template #header>
<div class="header-content">
<el-checkbox
v-model="allShowChecked"
:indeterminate="isShowIndeterminate"
@change="handleShowAllChange"
/>
<span class="header-text">展示</span>
<el-tooltip content="选择后的字段会在表格中展示" placement="top">
<el-icon class="header-help"><QuestionFilled /></el-icon>
</el-tooltip>
</div>
</template>
<template #default="{row}">
<el-checkbox
v-model="row.show"
:disabled="row.required || (isOnlyOneVisible && row.show)"
@change="handleShowChange"
/>
</template>
</el-table-column>
<el-table-column label="冻结" min-width="80" align="center">
<template #header>
<div class="header-content">
<el-checkbox
v-model="allFixedChecked"
:indeterminate="isFixedIndeterminate"
@change="handleFixedAllChange"
/>
<span class="header-text">冻结</span>
<el-tooltip content="选择后的字段会按照正序固定排在表格前列" placement="top">
<el-icon class="header-help"><QuestionFilled /></el-icon>
</el-tooltip>
</div>
</template>
<template #default="{row}">
<el-checkbox
v-model="row.fixed"
:disabled="(reachedMaxFixedColumns && !row.fixed)"
@change="handleFixedChange"
/>
</template>
</el-table-column>
<el-table-column label="排序" min-width="80" align="center">
<template #header>
<div class="header-content">
<el-checkbox
v-model="allSortableChecked"
:indeterminate="isSortableIndeterminate"
@change="handleSortableAllChange"
/>
<span class="header-text">排序</span>
<el-tooltip content="选择后,数据列表可以对该字段进行顺序或倒序排列" placement="top">
<el-icon class="header-help"><QuestionFilled /></el-icon>
</el-tooltip>
</div>
</template>
<template #default="{row}">
<el-checkbox
v-model="row.sortable"
:disabled="false"
@change="handleSortableChange"
/>
</template>
</el-table-column>
<el-table-column label="顺序" min-width="80" align="center">
<template #header>
<div class="header-content">
<span class="header-text">顺序</span>
<el-tooltip content="拖拽调整顺序保存后,列表表头显示顺序会随之改变" placement="top">
<el-icon class="header-help"><QuestionFilled /></el-icon>
</el-tooltip>
</div>
</template>
<template #default="{row}">
<el-icon
class="drag-handle"
style="color: #697dff; font-size: 18px; cursor: move"
>
<Rank />
</el-icon>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 底部按钮区域 -->
<div class="drawer-footer">
<el-button plain @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleConfirm">确定</el-button>
</div>
<!-- 二次确认对话框 -->
<el-dialog
title="提示"
v-model="confirmDialogVisible"
width="400px"
append-to-body
:close-on-click-modal="false"
>
<div style="text-align: center;">{{ confirmMessage }}</div>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" plain @click="confirmDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmAction">确定</el-button>
</span>
</template>
</el-dialog>
</el-drawer>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import { QuestionFilled, Rank } from '@element-plus/icons-vue'
import Sortable from 'sortablejs'
export interface ColumnConfig {
prop: string
label: string
show: boolean
fixed: boolean
sortable: boolean
required: boolean
order: number
width?: string | number
minWidth?: string | number
[key: string]: any
}
interface Props {
visible: boolean
columns: ColumnConfig[]
defaultColumns?: ColumnConfig[]
}
interface Emits {
(e: 'update:visible', value: boolean): void
(e: 'confirm', columns: ColumnConfig[]): void
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
defaultColumns: () => []
})
const emit = defineEmits<Emits>()
// 响应式数据
const tableColumns = ref<ColumnConfig[]>([])
const originalColumns = ref<ColumnConfig[]>([])
const allShowChecked = ref(true)
const allFixedChecked = ref(false)
const allSortableChecked = ref(false)
const isShowIndeterminate = ref(false)
const isFixedIndeterminate = ref(false)
const isSortableIndeterminate = ref(false)
const refreshKey = ref(0)
const confirmDialogVisible = ref(false)
const confirmMessage = ref('')
const confirmActionType = ref('')
// 引用
const columnTable = ref()
let sortable: any = null
// 计算属性
const dialogVisible = computed({
get() {
return props.visible
},
set(value: boolean) {
emit('update:visible', value)
}
})
const isOnlyOneVisible = computed(() => {
return tableColumns.value.filter(item => item.show).length <= 1
})
const fixedColumnsCount = computed(() => {
return tableColumns.value.filter(item => item.fixed).length
})
const reachedMaxFixedColumns = computed(() => {
return fixedColumnsCount.value >= 6
})
// 监听器
watch(() => props.visible, (val: boolean) => {
if (val) {
initColumns()
nextTick(() => {
initSortable()
})
}
})
// 初始化列配置
const initColumns = () => {
// 深拷贝列配置,避免直接修改原始数据
tableColumns.value = JSON.parse(JSON.stringify(props.columns))
// 过滤掉序号列,序号列不参与管理
.filter((col: ColumnConfig) => col.prop !== 'index')
// 排序
tableColumns.value.sort((a, b) => a.order - b.order)
// 保存原始配置,用于取消操作
originalColumns.value = JSON.parse(JSON.stringify(tableColumns.value))
// 更新全选状态
updateCheckStatus()
}
// 初始化拖拽功能
const initSortable = () => {
if (!columnTable.value) return
const el = columnTable.value.$el.querySelector('.el-table__body-wrapper tbody')
if (!el) return
// 销毁之前的实例
if (sortable) {
sortable.destroy()
}
sortable = Sortable.create(el, {
handle: '.drag-handle',
animation: 150,
ghostClass: 'sortable-ghost',
onEnd: handleDragEnd
})
}
// 拖拽结束处理
const handleDragEnd = (evt: any) => {
const { oldIndex, newIndex } = evt
if (oldIndex === newIndex) return
// 获取被拖动的项
const draggedItem = tableColumns.value[oldIndex]
// 从数组中移除该项
tableColumns.value.splice(oldIndex, 1)
// 在新位置插入该项
tableColumns.value.splice(newIndex, 0, draggedItem)
// 更新所有项的顺序
updateOrderAfterDrag()
}
// 更新拖拽后的顺序
const updateOrderAfterDrag = () => {
// 只在拖拽后调用,重新分配order值
// 直接按照当前列表的顺序设置order值,确保拖拽后的顺序能正确保存
tableColumns.value.forEach((column, index) => {
column.order = index
})
}
// 更新选择状态
const updateCheckStatus = () => {
// 更新"展示"的全选状态
const showTotal = tableColumns.value.length
const showChecked = tableColumns.value.filter(col => col.show).length
allShowChecked.value = showChecked === showTotal
isShowIndeterminate.value = showChecked > 0 && showChecked < showTotal
// 更新"冻结"的全选状态
const fixedTotal = tableColumns.value.length
const fixedChecked = tableColumns.value.filter(col => col.fixed).length
allFixedChecked.value = fixedChecked === fixedTotal
isFixedIndeterminate.value = fixedChecked > 0 && fixedChecked < fixedTotal
// 更新"排序"的全选状态
const sortableTotal = tableColumns.value.filter(col => col.show).length
const sortableChecked = tableColumns.value.filter(col => col.show && col.sortable).length
allSortableChecked.value = sortableChecked > 0 && sortableChecked === sortableTotal
isSortableIndeterminate.value = sortableChecked > 0 && sortableChecked < sortableTotal
}
// 处理展示状态变化
const handleShowChange = () => {
// 确保至少有一列是选中的
const visibleColumns = tableColumns.value.filter(col => col.show)
if (visibleColumns.length === 0) {
// 如果没有任何一个选中,则选中第一个非必选项
const firstColumn = tableColumns.value.find(col => !col.required)
if (firstColumn) {
firstColumn.show = true
ElMessage.warning('至少需要保留一个显示字段')
}
}
updateCheckStatus()
}
// 处理展示全选变化
const handleShowAllChange = (val: boolean) => {
// 全选或取消全选"展示"
tableColumns.value.forEach(column => {
if (!column.required) {
column.show = val
}
})
// 如果全部取消选择,确保至少有一个选中
if (!val && tableColumns.value.filter(col => col.show).length === 0) {
// 如果没有任何一个选中,则选中第一个非必选项
const firstColumn = tableColumns.value.find(col => !col.required)
if (firstColumn) {
firstColumn.show = true
}
}
updateCheckStatus()
}
// 处理冻结全选变化
const handleFixedAllChange = () => {
// 获取当前实际选中的数量
const currentFixedCount = tableColumns.value.filter(col => col.fixed).length
// 如果当前有选中的项目,则执行取消全选操作
if (currentFixedCount > 0) {
// 取消全选时,将所有列的fixed设为false
tableColumns.value.forEach(column => {
column.fixed = false
})
} else {
// 如果当前没有选中项,则执行全选操作,需要限制最多选择6个
let count = 0
// 按顺序选择前6个列
for (const column of tableColumns.value) {
if (count < 6) {
column.fixed = true
count++
} else {
column.fixed = false
}
}
// 如果超过6个,显示提示
if (tableColumns.value.length > 6) {
ElMessage.warning('最多只能冻结6列')
}
}
updateCheckStatus()
}
// 处理排序全选变化
const handleSortableAllChange = (val: boolean) => {
// 全选或取消全选"排序"
tableColumns.value.forEach(column => {
column.sortable = val
})
updateCheckStatus()
}
// 处理冻结变化
const handleFixedChange = () => {
// 处理冻结列的单项checkbox的@change事件
const fixedCount = tableColumns.value.filter(col => col.fixed).length
if (fixedCount >= 6) {
ElMessage.warning('最多只能冻结6列')
}
updateCheckStatus()
}
// 处理排序变化
const handleSortableChange = () => {
// 处理排序列的单项checkbox的@change事件
updateCheckStatus()
}
// 处理取消
const handleCancel = () => {
// 恢复原始配置
tableColumns.value = JSON.parse(JSON.stringify(originalColumns.value))
dialogVisible.value = false
}
// 处理恢复默认
const handleResetDefault = () => {
// 显示恢复默认的确认对话框
confirmMessage.value = '确定要恢复默认设置吗?'
confirmActionType.value = 'reset'
confirmDialogVisible.value = true
}
// 处理确认
const handleConfirm = () => {
// 显示保存的确认对话框
confirmMessage.value = '只对本用户生效且记忆该配置,确定保存吗?'
confirmActionType.value = 'save'
confirmDialogVisible.value = true
}
// 确认操作
const confirmAction = () => {
// 关闭确认对话框
confirmDialogVisible.value = false
if (confirmActionType.value === 'save') {
// 保存配置
saveConfig()
} else if (confirmActionType.value === 'reset') {
// 恢复默认配置
resetToDefault()
}
}
// 保存配置
const saveConfig = () => {
// 检查是否至少有一列是选中的
const visibleColumns = tableColumns.value.filter(col => col.show)
if (visibleColumns.length === 0) {
// 如果没有任何一个选中,则选中第一个非必选项
const firstColumn = tableColumns.value.find(col => !col.required)
if (firstColumn) {
firstColumn.show = true
ElMessage.warning('至少需要保留一个显示字段')
}
}
// 发送更新后的配置,包括添加回序号列
const indexColumn = props.columns.find(col => col.prop === 'index')
const finalColumns = indexColumn ? [indexColumn, ...tableColumns.value] : tableColumns.value
// 保存成功后关闭抽屉
emit('confirm', finalColumns)
dialogVisible.value = false
}
// 恢复默认配置
const resetToDefault = () => {
// 恢复为默认配置
if (props.defaultColumns && props.defaultColumns.length > 0) {
tableColumns.value = JSON.parse(JSON.stringify(props.defaultColumns))
.filter((col: ColumnConfig) => col.prop !== 'index')
updateCheckStatus()
} else {
// 如果没有提供默认配置,则提示错误
ElMessage.warning('没有可用的默认配置')
}
}
// 处理关闭
const handleClose = () => {
handleCancel()
}
</script>
<style lang="scss" scoped>
.column-manager-drawer {
:deep(.el-drawer__header) {
padding: 20px;
margin-bottom: 0;
font-size: 18px;
font-weight: bold;
}
:deep(.el-drawer__body) {
padding: 0 20px;
}
}
.description-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.description {
color: #606266;
font-size: 14px;
}
.drawer-content {
height: calc(100% - 30px); /* 减去底部按钮区域的高度 */
display: flex;
flex-direction: column;
}
.drawer-header-fixed {
flex-shrink: 0; /* 不允许压缩 */
}
.table-container {
flex: 1; /* 占据剩余空间 */
overflow: hidden; /* 隐藏容器的滚动条,让表格自己处理滚动 */
}
.header-content {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
.header-text {
font-size: 14px;
white-space: nowrap;
}
.header-help {
font-size: 14px;
}
}
.sortable-ghost {
opacity: 0.5;
background: #c8ebfb !important;
}
.drag-handle {
cursor: move;
}
.drawer-footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 15px;
border-top: 1px solid #e4e7ed;
background: #fff;
text-align: center;
}
// 只隐藏本组件表格内的复选框标签,不影响其他地方
.column-manager-drawer :deep(.el-table .el-checkbox__label) {
display: none;
}
</style>
封装全屏/管理显示字段/导出功能的 hooks:src\composables\useTableCommon.ts
javascript
/**
* 表格通用功能组合式函数
*
* 封装了三个所有表格都需要的通用功能:
* 1. 全屏展示 / 退出全屏
* 2. 管理显示字段(列的显示/隐藏/冻结/排序)
* 3. 导出 Excel(按当前可见列配置导出全部数据)
*
* @param options.configKey - 配置存储的 key,用于后端保存/加载列配置
* @param options.topLevelColumns - 表格所有的列定义(ColumnConfig[])
* @param options.getFieldConfig - (可选)获取列配置的 API 函数
* @param options.saveFieldConfig - (可选)保存列配置的 API 函数
* @param options.exportApi - (可选)导出数据的 API 函数,调用时传入 { pageNo: 1, pageSize: -1 } 获取全部数据
* @param options.exportFileName - (可选)导出 Excel 的文件名,默认为 configKey
*/
import { ref, computed, nextTick } from "vue";
import { ElMessage } from "element-plus";
import { TableExportUtil } from "@/utils/TableExportUtil";
import type { ColumnConfig } from "@/components/TableColumnManager/index.vue";
/**
* 导出 API 的参数类型
* pageNo: 页码
* pageSize: 每页条数(传 -1 表示查询全部)
*/
export interface ExportApiParams {
pageNo: number;
pageSize: number;
[key: string]: any;
}
/**
* 将列索引转为 Excel 列字母(0→A, 1→B, 25→Z, 26→AA ...)
*/
function encodeCol(col: number): string {
let result = "";
let n = col;
while (n >= 0) {
result = String.fromCharCode(65 + (n % 26)) + result;
n = Math.floor(n / 26) - 1;
}
return result;
}
export function useTableCommon(options: {
configKey: string;
topLevelColumns: ColumnConfig[];
getFieldConfig?: (key: string) => Promise<any>;
saveFieldConfig?: (params: { page: string; config: any }) => Promise<any>;
exportApi?: (params: ExportApiParams) => Promise<any>;
exportFileName?: string;
exportFieldFormatters?: Record<string, (value: any, record: any) => any>;
exportFieldStyles?: Record<
string,
(value: any, record: any) => { color?: string; bgColor?: string } | null
>;
}) {
// ==================== 响应式状态 ====================
// 是否处于全屏模式
const isFullscreen = ref(false);
// 表格外层容器的 DOM 引用(用于全屏操作)
const tableInfoRef = ref<HTMLElement | null>(null);
// el-table 组件引用
const tableRef = ref();
// 是否显示"管理显示字段"弹窗
const columnManagerVisible = ref(false);
// 强制刷新 key,用于列配置变化后重新渲染表格
const refreshKey = ref(0);
// 当前窗口高度(用于计算表格最大高度)
const windowHeight = ref(window.innerHeight);
// 当前列配置(包含用户自定义的显示/隐藏/冻结状态)
const columnConfig = ref<ColumnConfig[]>(
options.topLevelColumns.map((col) => ({ ...col })),
);
// 默认列配置(用于"恢复默认"功能)
const defaultColumnConfig = computed(() =>
options.topLevelColumns.map((col) => ({ ...col })),
);
// ==================== 列可见性与固定判断 ====================
/** 判断指定 prop 的列是否可见 */
function isColVisible(prop: string): boolean {
return columnConfig.value.find((c) => c.prop === prop)?.show ?? true;
}
/** 判断指定 prop 的列是否冻结(固定在左侧) */
function isColFixed(prop: string): boolean {
return columnConfig.value.find((c) => c.prop === prop)?.fixed ?? false;
}
// ==================== 全屏功能 ====================
/** 切换全屏/退出全屏 */
function handleFullScreen() {
const element = tableInfoRef.value;
if (!element) return;
if (!isFullscreen.value) {
// 进入全屏(兼容不同浏览器)
if (element.requestFullscreen) {
element.requestFullscreen();
} else if ((element as any).webkitRequestFullscreen) {
(element as any).webkitRequestFullscreen();
} else if ((element as any).mozRequestFullScreen) {
(element as any).mozRequestFullScreen();
} else if ((element as any).msRequestFullscreen) {
(element as any).msRequestFullscreen();
}
} else {
// 退出全屏(兼容不同浏览器)
if (document.exitFullscreen) {
document.exitFullscreen();
} else if ((document as any).webkitExitFullscreen) {
(document as any).webkitExitFullscreen();
} else if ((document as any).mozCancelFullScreen) {
(document as any).mozCancelFullScreen();
} else if ((document as any).msExitFullscreen) {
(document as any).msExitFullscreen();
}
}
}
/** 全屏状态变化时的处理:更新状态、刷新表格、调整尺寸 */
function fullscreenChangeHandler() {
isFullscreen.value = !!(
document.fullscreenElement ||
(document as any).webkitFullscreenElement ||
(document as any).mozFullScreenElement ||
(document as any).msFullscreenElement
);
// 全屏切换后强制刷新表格
nextTick(() => {
refreshKey.value++;
});
// 更新窗口高度,重新计算表格高度
windowHeight.value = window.innerHeight;
// 通知 el-table 重新计算自身尺寸
nextTick(() => {
tableInfoRef.value
?.querySelector?.(".el-table")
?.dispatchEvent?.(new Event("resize"));
});
}
/** 窗口大小变化时的处理:更新 windowHeight 并通知 el-table 重算尺寸 */
function windowResizeHandler() {
windowHeight.value = window.innerHeight;
nextTick(() => {
tableInfoRef.value
?.querySelector?.(".el-table")
?.dispatchEvent?.(new Event("resize"));
});
}
// ==================== 列配置管理 ====================
/**
* 将后端保存的列配置与默认列配置合并
* 后端只保存了 show/fixed/sortable/order/minWidth 等变更字段,
* 需要与完整的默认列配置合并,防止新增列时缺少配置
*/
function mergeColumnConfig(
savedColumns: Record<string, any>,
): ColumnConfig[] {
const defaults = options.topLevelColumns.map((col) => ({ ...col }));
return defaults.map((col) => {
if (savedColumns[col.prop]) {
return { ...col, ...savedColumns[col.prop] };
}
return col;
});
}
/** 从后端加载列配置 */
async function loadFieldConfig() {
if (!options.configKey || !options.getFieldConfig) return;
try {
const res = await options.getFieldConfig(options.configKey);
if (res.data && res.data.config && res.data.config.columns) {
const columnsObj = res.data.config.columns || {};
columnConfig.value = mergeColumnConfig(columnsObj);
columnConfig.value.sort((a, b) => a.order - b.order);
} else {
// 后端没有配置时,使用默认列配置
columnConfig.value = options.topLevelColumns.map((col) => ({ ...col }));
}
} catch (error) {
console.error("获取列配置失败:", error);
}
}
/** 点击"管理显示字段"按钮:先刷新配置再打开弹窗 */
async function handleTableSetting() {
if (!options.configKey || !options.getFieldConfig) {
columnManagerVisible.value = true;
return;
}
try {
const res = await options.getFieldConfig(options.configKey);
if (res.data && res.data.config && res.data.config.columns) {
const columnsObj = res.data.config.columns || {};
columnConfig.value = mergeColumnConfig(columnsObj);
columnConfig.value.sort((a, b) => a.order - b.order);
} else {
columnConfig.value = options.topLevelColumns.map((col) => ({ ...col }));
}
} catch (error) {
console.error("获取最新列配置失败:", error);
ElMessage.warning("获取最新列配置失败,将使用当前配置");
}
columnManagerVisible.value = true;
}
/** 弹窗确认后的处理:保存列配置到后端 */
async function handleColumnConfigConfirm(config: ColumnConfig[]) {
columnConfig.value = config;
if (!options.saveFieldConfig) {
columnManagerVisible.value = false;
return;
}
try {
// 构建保存的配置对象(只保存需要持久化的字段)
const configObject = {
columns: config.reduce((obj, column) => {
obj[column.prop] = {
show: column.show,
fixed: column.fixed,
sortable: column.sortable,
order: column.order,
minWidth: column.minWidth,
};
return obj;
}, {} as Record<string, any>),
version: "1.0",
};
await options.saveFieldConfig({
page: options.configKey,
config: configObject,
});
ElMessage.success("配置成功");
await loadFieldConfig();
refreshKey.value++;
} catch (error) {
console.error("保存配置失败:", error);
ElMessage.error("保存失败");
refreshKey.value++;
}
columnManagerVisible.value = false;
}
// ==================== 导出 Excel 功能 ====================
/**
* 导出 Excel
*
* 流程:
* 1. 先刷新列配置,确保导出列与"管理显示字段"的配置一致
* 2. 调用 exportApi 获取全部数据(pageNo=1, pageSize=-1)
* 3. 从返回数据中提取可见列的字段值,按表格展示顺序排列
* 4. 使用 TableExportUtil 生成并下载 Excel 文件
*/
async function handleExport() {
if (!options.exportApi) {
ElMessage.warning("未配置导出功能");
return;
}
try {
ElMessage.info("正在导出数据...");
// 先刷新列配置,确保导出的列与"管理显示字段"中的配置一致
await loadFieldConfig();
// 查询全部数据:pageNo=1, pageSize=-1
const exportParams: ExportApiParams = {
pageNo: 1,
pageSize: -1,
};
const res = await options.exportApi(exportParams);
// 兼容后端返回格式:res.data.records 或 res.data
const records: Record<string, any>[] =
res.data?.records || res.data || [];
if (!records || records.length === 0) {
ElMessage.warning("没有可导出的数据");
return;
}
// 只导出当前可见的列,按配置顺序排列(与表格展示顺序一致)
const visibleColumns = columnConfig.value
.filter((col) => col.show)
.sort((a, b) => a.order - b.order);
// 表头字段名(与数据对象 key 对应)
const jsonHeader = visibleColumns.map((col) => col.prop);
// 表头中文标签
const headerLabels = visibleColumns.map((col) => col.label);
// 构建第一行(中文表头),格式:{ prop: "标签" }
const headerRow = headerLabels.reduce((obj, label, index) => {
obj[jsonHeader[index]] = label;
return obj;
}, {} as Record<string, string>);
// 从原始数据中只提取可见列的字段,确保导出的数据不包含隐藏列
const exportData = records.map((record) => {
const row: Record<string, any> = {};
jsonHeader.forEach((prop) => {
const value = record[prop];
// 如果有自定义格式化函数,则使用格式化函数处理
if (
options.exportFieldFormatters &&
options.exportFieldFormatters[prop]
) {
row[prop] = options.exportFieldFormatters[prop](value, record);
} else {
row[prop] = value;
}
});
return row;
});
// 使用 TableExportUtil 导出
const exportUtil = new TableExportUtil();
// 添加数据:第一行为中文表头,后面为只含可见列的数据
exportUtil.addJson([headerRow, ...exportData], jsonHeader);
// 处理条件样式
if (options.exportFieldStyles) {
const styledPositions: {
pos: string;
style: { color?: string; bgColor?: string };
}[] = [];
for (let rowIdx = 0; rowIdx < records.length; rowIdx++) {
const record = records[rowIdx];
for (let colIdx = 0; colIdx < jsonHeader.length; colIdx++) {
const prop = jsonHeader[colIdx];
if (options.exportFieldStyles[prop]) {
const value = record[prop];
const style = options.exportFieldStyles[prop](value, record);
if (style && (style.color || style.bgColor)) {
const colLetter = encodeCol(colIdx);
styledPositions.push({
pos: `${colLetter}${rowIdx + 2}`, // 第1行为表头,数据从第2行开始
style,
});
}
}
}
}
if (styledPositions.length > 0) {
exportUtil.setStyle((ws: any) => {
styledPositions.forEach((item) => {
if (ws[item.pos]) {
ws[item.pos].s = {
...(ws[item.pos].s || {}),
...(item.style.bgColor
? {
fill: {
patternType: "solid",
fgColor: { rgb: item.style.bgColor.replace("#", "") },
},
}
: {}),
...(item.style.color
? {
font: {
...(ws[item.pos].s?.font || {}),
color: { rgb: item.style.color.replace("#", "") },
},
}
: {}),
};
}
});
});
}
}
exportUtil.export({
name: options.exportFileName || options.configKey,
autoWidth: true,
border: true,
skipRow: 1,
});
ElMessage.success("导出成功");
} catch (error) {
console.error("导出失败:", error);
ElMessage.error("导出失败");
}
}
// ==================== 事件监听管理 ====================
/** 注册全屏状态变化的事件监听(兼容各浏览器)及窗口 resize 监听 */
function setupFullscreenListeners() {
document.addEventListener("fullscreenchange", fullscreenChangeHandler);
document.addEventListener(
"webkitfullscreenchange",
fullscreenChangeHandler,
);
document.addEventListener("mozfullscreenchange", fullscreenChangeHandler);
document.addEventListener("MSFullscreenChange", fullscreenChangeHandler);
window.addEventListener("resize", windowResizeHandler);
}
/** 移除全屏状态变化的事件监听及窗口 resize 监听 */
function removeFullscreenListeners() {
document.removeEventListener("fullscreenchange", fullscreenChangeHandler);
document.removeEventListener(
"webkitfullscreenchange",
fullscreenChangeHandler,
);
document.removeEventListener(
"mozfullscreenchange",
fullscreenChangeHandler,
);
document.removeEventListener("MSFullscreenChange", fullscreenChangeHandler);
window.removeEventListener("resize", windowResizeHandler);
}
// ==================== 导出给组件使用的属性和方法 ====================
return {
// 状态
isFullscreen,
tableInfoRef,
tableRef,
columnManagerVisible,
refreshKey,
windowHeight,
columnConfig,
defaultColumnConfig,
// 方法
isColVisible,
isColFixed,
handleFullScreen,
handleTableSetting,
handleColumnConfigConfirm,
handleExport,
loadFieldConfig,
setupFullscreenListeners,
removeFullscreenListeners,
mergeColumnConfig,
};
}
excel 导出工具类:src\utils\TableExportUtil.ts
使用的插件版本:
bash
"xlsx-js-style": "^1.2.0"
"moment": "^2.30.1",
javascript
import XLSX from "xlsx-js-style";
import TimeUtil from "./TimeUtil";
/**
* 基本通用Xlsx导出
* @param elDom DOM表格id,class
* @param name 导出xlsx名字
* @param jsonData json数据
* @param jsonHeader json表头
* @param merges 合并表格
* @param styleCB 表格样式
* @param autoWidth 是否动态调整宽度
* @param border 是否添加边框
*/
export class TableExportUtil {
public FONT_TITLE_DEFAULT = {
font: {
sz: 18,
},
alignment: {
horizontal: "center",
vertical: "center",
},
};
public FONT_DEFAULT = {
alignment: {
horizontal: "center",
vertical: "center",
},
};
public FONT_RIGHT = {
alignment: {
horizontal: "right",
vertical: "center",
},
};
public FONT_LEFT = {
alignment: {
horizontal: "right",
vertical: "center",
},
};
private workbook = XLSX.utils.book_new();
private worksheet: any;
private mysheets: any = {};
private m_lastCol = -1;
public getObjectKeys = (obj: any) => {
const temp: any = [];
for (const key in obj) {
temp.push(key);
}
return temp;
};
// 添加DOM表格
public addTable = (elDom: string) => {
let el: any = document.querySelector(elDom);
if (el.querySelector(".el-table__fixed")) {
el = el.querySelector(".el-table__fixed");
}
if (el.querySelector(".el-table__fixed-right")) {
el = el.querySelector(".el-table__fixed-right");
}
if (this.worksheet == undefined) {
this.worksheet = XLSX.utils.table_to_sheet(el, { raw: true });
} else {
// 确定续接位置
XLSX.utils.sheet_add_dom(this.worksheet, el, { raw: true, origin: -1 });
}
};
// 添加Json数据
public addJson = (jsonData: any[], jsonHeader: string[] = []) => {
if (jsonData.length > 0) {
if (jsonHeader) {
if (this.worksheet == undefined) {
this.worksheet = XLSX.utils.json_to_sheet(jsonData, {
header: jsonHeader,
skipHeader: true,
});
} else {
// 确定续接位置
XLSX.utils.sheet_add_json(this.worksheet, jsonData, {
header: jsonHeader,
skipHeader: true,
origin: -1,
});
}
} else {
const header = Object.keys(jsonData[0]);
if (this.worksheet == undefined) {
this.worksheet = XLSX.utils.json_to_sheet(jsonData, {
header: header,
skipHeader: true,
});
} else {
// 确定续接位置
XLSX.utils.sheet_add_json(this.worksheet, jsonData, {
header: header,
skipHeader: true,
origin: -1,
});
}
}
}
};
//删除指定列数据 0为第一列
public deleteCol = (col: number) => {
const letter = XLSX.utils.encode_col(col);
const wsArray = Object.keys(this.worksheet);
wsArray.forEach((v) => {
const v1 = v.replace(/[0-9]+/g, "");
if (v1 == letter) {
this.worksheet[v] = { t: "s", v: "" };
}
});
};
//手动指定多少列为最后一列 0为第一列
public setLastCol(col: number) {
this.m_lastCol = col;
const wsArray = Object.keys(this.worksheet);
wsArray.forEach((v) => {
const v1 = v.replace(/[0-9]+/g, "");
const tempCol = XLSX.utils.decode_col(v1);
if (tempCol > col) {
this.worksheet[v] = { t: "s", v: "" };
}
});
}
// 合并
public setMerges = (merges: string[]) => {
if (!this.worksheet["!merges"]) this.worksheet["!merges"] = [];
merges.forEach((item) => {
this.worksheet["!merges"].push(XLSX.utils.decode_range(item));
});
};
// 样式
public setStyle = (styleCB: Function) => {
styleCB(this.worksheet);
};
public getCellWidth(value: any) {
// 判断是否为null或undefined
if (value == null || value == "" || value == undefined) {
return 9;
} else if (/.*[\u4e00-\u9fa5]+.*$/.test(value)) {
// 中文的长度
const chineseLength = value.match(/[\u4e00-\u9fa5]/g).length;
// 其他不是中文的长度
const otherLength = value.length - chineseLength;
return chineseLength * 2.4 + otherLength * 2;
} else {
return value.toString().length * 2;
/* 另一种方案
value = value.toString()
return value.replace(/[\u0391-\uFFE5]/g, 'aa').length
*/
}
}
public s2ab(s: any) {
const buf = new ArrayBuffer(s.length);
const view = new Uint8Array(buf);
for (let i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xff;
return buf;
}
public openDownloadDialog(url: Blob | string, saveName: string) {
if (typeof url == "object" && url instanceof Blob) {
url = URL.createObjectURL(url); // 创建blob地址
}
const aLink = document.createElement("a");
aLink.href = url;
aLink.download = saveName || ""; // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
let event;
if (window.MouseEvent) event = new MouseEvent("click");
else {
event = document.createEvent("MouseEvents");
event.initMouseEvent(
"click",
true,
false,
window,
0,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null,
);
}
aLink.dispatchEvent(event);
}
public addSheet(SheetName: any) {
this.mysheets[SheetName] = this.worksheet;
this.worksheet = null;
}
//导出
public export = ({
autoWidth = true,
skipRow = 0, //默认跳过计算的宽度行数,1代表从第2行开始计算列的宽度
border = true,
name = "",
}) => {
const wsArray = Object.keys(this.worksheet);
const wObj: any = {};
wsArray.forEach((v) => {
if (v[0] == "!") return false;
const w = this.getCellWidth(this.worksheet[v].v);
const v1 = v.replace(/[0-9]+/g, "");
const v2 = Number(v.replace(/[A-Z]+/g, "")); //行数
if ((!wObj[v1] || wObj[v1] < w) && v2 > skipRow) {
wObj[v1] = w;
}
});
// 自动宽度设置
if (autoWidth) {
this.worksheet["!cols"] = Object.keys(wObj).map((v, index) => {
const width = wObj[XLSX.utils.encode_col(index)];
// 设置最小宽度15,最大宽度50
const clampedWidth = Math.max(15, Math.min(50, width));
return { wch: clampedWidth };
});
} else {
this.worksheet["!cols"] = Object.keys(wObj).map((v, index) => {
return { wch: 20 };
});
}
console.log("cols = ", this.worksheet["!cols"]);
// 设置边框
if (border) {
const ref = this.worksheet["!ref"].split(":");
const v1 = ref[1].replace(/[0-9]+/g, "");
const lastRow = Number(ref[1].replace(/[A-Z]+/g, ""));
// 判断最右侧空格
const wsArray = Object.keys(this.worksheet);
const isValue = wsArray.some((v) => {
if (v.indexOf(v1) != -1) {
if (this.worksheet[v].v) {
return true;
}
}
return false;
});
let lastCol = isValue
? XLSX.utils.decode_col(v1)
: XLSX.utils.decode_col(v1) - 1;
if (this.m_lastCol != -1) {
lastCol = this.m_lastCol;
}
// 循环设置边框
for (let i = 0; i <= lastCol; i++) {
const letter = XLSX.utils.encode_col(i);
for (let j = 0; j <= lastRow; j++) {
const name = `${letter}${j}`;
if (!this.worksheet[name]) this.worksheet[name] = { t: "s", v: "" };
this.worksheet[name] = {
...this.worksheet[name],
t: "s",
v:
this.worksheet[name].v == undefined ? "" : this.worksheet[name].v,
s: {
...this.worksheet[name].s,
border: {
// 设置边框
top: { style: "thin" },
bottom: { style: "thin" },
left: { style: "thin" },
right: { style: "thin" },
},
alignment: {
vertical: "center",
horizontal: "center",
...this.worksheet[name].s?.alignment,
},
},
};
}
}
}
XLSX.utils.book_append_sheet(this.workbook, this.worksheet, "Sheet1", true);
const wbout = XLSX.write(this.workbook, {
bookType: "xlsx",
bookSST: false,
type: "binary",
});
const XlsxBlob = new Blob([this.s2ab(wbout)], {
type: "application/octet-stream",
});
this.openDownloadDialog(
XlsxBlob,
name + TimeUtil.format(new Date(), "YYYYMMDDHHmmss") + ".xlsx",
);
};
//多sheet导出
public exportBySheets = ({
autoWidth = true,
skipRow = 0, //默认跳过计算的宽度行数,1代表从第2行开始计算列的宽度
border = true,
name = "",
}) => {
for (const key in this.mysheets) {
const mysheet = this.mysheets[key];
const wsArray = Object.keys(mysheet);
const wObj: any = {};
wsArray.forEach((v) => {
if (v[0] == "!") return false;
const w = this.getCellWidth(mysheet[v].v);
const v1 = v.replace(/[0-9]+/g, "");
const v2 = Number(v.replace(/[A-Z]+/g, "")); //行数
if ((!wObj[v1] || wObj[v1] < w) && v2 > skipRow) {
wObj[v1] = w;
}
});
// 自动宽度设置
if (autoWidth) {
mysheet["!cols"] = Object.keys(wObj).map((v, index) => {
const width = wObj[XLSX.utils.encode_col(index)];
// 设置最小宽度10,最大宽度50
const clampedWidth = Math.max(10, Math.min(50, width));
return { wch: clampedWidth };
});
} else {
mysheet["!cols"] = Object.keys(wObj).map((v, index) => {
return { wch: 20 };
});
}
console.log("cols = ", mysheet["!cols"]);
// 设置边框
if (border) {
const ref = mysheet["!ref"].split(":");
const v1 = ref[1].replace(/[0-9]+/g, "");
const lastRow = Number(ref[1].replace(/[A-Z]+/g, ""));
// 判断最右侧空格
const wsArray = Object.keys(mysheet);
const isValue = wsArray.some((v) => {
if (v.indexOf(v1) != -1) {
if (mysheet[v].v) {
return true;
}
}
return false;
});
let lastCol = isValue
? XLSX.utils.decode_col(v1)
: XLSX.utils.decode_col(v1) - 1;
if (this.m_lastCol != -1) {
lastCol = this.m_lastCol;
}
// 循环设置边框
for (let i = 0; i <= lastCol; i++) {
const letter = XLSX.utils.encode_col(i);
for (let j = 0; j <= lastRow; j++) {
const name = `${letter}${j}`;
if (!mysheet[name]) mysheet[name] = { t: "s", v: "" };
mysheet[name] = {
...mysheet[name],
t: "s",
v: mysheet[name].v == undefined ? "" : mysheet[name].v,
s: {
...mysheet[name].s,
border: {
// 设置边框
top: { style: "thin" },
bottom: { style: "thin" },
left: { style: "thin" },
right: { style: "thin" },
},
alignment: {
vertical: "center",
horizontal: "center",
...mysheet[name].s?.alignment,
},
},
};
}
}
}
XLSX.utils.book_append_sheet(this.workbook, mysheet, key, true);
}
const wbout = XLSX.write(this.workbook, {
bookType: "xlsx",
bookSST: false,
type: "binary",
});
const XlsxBlob = new Blob([this.s2ab(wbout)], {
type: "application/octet-stream",
});
this.openDownloadDialog(
XlsxBlob,
name + TimeUtil.format(new Date(), "YYYYMMDDHHmmss") + ".xlsx",
);
};
}
时间格式化函数:src\utils\TimeUtil.ts
javascript
import moment from "moment";
export default class TimeUtil {
/**
* @des 时间格式化函数
* @des 月(M)、日(d)、小时(h)、分(m)、秒(s)、季度(q) 可以用 1-2 个占位符,
* @des 年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字)
* @param {Date|number|string} date Date对象或者时间戳
* @param {string} fmt 格式化字符串 ("YYYY-MM-DD HH:mm:ss") ==> 2006-07-02 08:09:04
* @returns {string} 格式化后的字符串
*/
static format(date: string | Date | number, fmt = "YYYY-MM-DD HH:mm:ss") {
if (!date) return "";
const d = moment(date).format(fmt);
return ~d.indexOf("Invalid") ? "" : d;
}
}
useTableCommon 使用指南
📋 功能概述
useTableCommon 是一个封装了表格通用功能的组合式函数(Composable),适用于单级表头的表格场景。它提供了以下核心功能:
- 全屏展示:一键切换全屏/退出全屏,自动适配窗口大小
- 列管理:可视化配置列的显示/隐藏/冻结/排序,支持持久化保存
- 单表头导出:按当前可见列配置导出 Excel,支持自定义格式化和条件样式
🎯 适用场景
- 单级表头表格(扁平数据结构)
- 需要列管理功能的表格
- 需要导出功能的表格
- 需要全屏展示的表格
📦 API 参数
typescript
useTableCommon(options: {
configKey: string; // 必填:配置存储的 key
topLevelColumns: ColumnConfig[]; // 必填:表格列定义
getFieldConfig?: (key: string) => Promise<any>; // 可选:获取列配置的 API
saveFieldConfig?: (params: { page: string; config: any }) => Promise<any>; // 可选:保存列配置的 API
exportApi?: (params: ExportApiParams) => Promise<any>; // 可选:导出数据的 API
exportFileName?: string; // 可选:导出文件名
exportFieldFormatters?: Record<string, (value: any, record: any) => any>; // 可选:字段格式化函数
exportFieldStyles?: Record<string, (value: any, record: any) => { color?: string; bgColor?: string } | null>; // 可选:条件样式函数
})
🔧 ColumnConfig 类型定义
typescript
interface ColumnConfig {
prop: string; // 字段名,对应数据对象的 key
label: string; // 表头显示文字
show: boolean; // 是否显示
fixed: boolean; // 是否固定在左侧
sortable: boolean; // 是否可排序
required: boolean; // 是否必填(必填列不能隐藏)
order: number; // 显示顺序
width?: string | number; // 列宽度
minWidth?: string | number; // 最小宽度
[key: string]: any; // 其他自定义属性
}
💡 使用示例
1. 基础配置
typescript
import { useTableCommon } from "@/composables/useTableCommon";
import type { ColumnConfig } from "@/components/TableColumnManager/index.vue";
// 定义表格列
const topLevelColumns: ColumnConfig[] = [
{
prop: "index",
label: "序号",
show: true,
fixed: true,
sortable: false,
required: true,
order: 0,
width: 60,
},
{
prop: "name",
label: "姓名",
show: true,
fixed: false,
sortable: true,
required: false,
order: 1,
minWidth: 120,
},
{
prop: "salary",
label: "薪资(元)",
show: true,
fixed: false,
sortable: true,
required: false,
order: 5,
minWidth: 120,
},
];
// 使用 composable
const {
isFullscreen, // 是否全屏
tableInfoRef, // 表格容器引用
columnManagerVisible, // 列管理弹窗可见性
refreshKey, // 强制刷新 key
windowHeight, // 窗口高度
columnConfig, // 当前列配置
defaultColumnConfig, // 默认列配置
isColVisible, // 判断列是否可见
isColFixed, // 判断列是否固定
handleFullScreen, // 切换全屏
handleTableSetting, // 打开列管理
handleColumnConfigConfirm, // 确认列配置
handleExport, // 导出 Excel
loadFieldConfig, // 加载列配置
setupFullscreenListeners, // 注册全屏监听
removeFullscreenListeners, // 移除全屏监听
mergeColumnConfig, // 合并列配置
} = useTableCommon({
configKey: "employee-list",
topLevelColumns,
});
2. 完整配置(含导出)
typescript
const { handleExport } = useTableCommon({
configKey: "employee-list",
topLevelColumns,
// 获取列配置 API
getFieldConfig: async (key) => {
const res = await api.getFieldConfig(key);
return res.data;
},
// 保存列配置 API
saveFieldConfig: async (params) => {
await api.saveFieldConfig(params);
return { success: true };
},
// 导出数据 API
exportApi: async (params) => {
// params: { pageNo: 1, pageSize: -1 }
const res = await api.getEmployeeList(params);
return res;
},
// 导出文件名
exportFileName: "员工信息表",
// 字段格式化函数
exportFieldFormatters: {
// 薪资添加千分位
salary: (value) => {
if (value == null) return "";
return Number(value).toLocaleString("zh-CN");
},
// 绩效评分添加单位
performance: (value) => {
if (value == null) return "";
return `${value}分`;
},
},
// 条件样式函数
exportFieldStyles: {
// 薪资大于 20000 显示红色
salary: (value) => {
if (Number(value) > 20000) {
return { color: "#FF0000", bgColor: "#FFF2F0" };
}
return null;
},
// 绩效低于 60 显示红色背景
performance: (value) => {
if (Number(value) < 60) {
return { color: "#9C0006", bgColor: "#FFC7CE" };
}
return null;
},
},
});
3. 模板中使用
typescript
<template>
<div ref="tableInfoRef" class="table-container">
<!-- 工具栏 -->
<div class="toolbar">
<el-button @click="handleTableSetting">管理显示字段</el-button>
<el-button @click="handleFullScreen">
{{ isFullscreen ? "退出全屏" : "全屏展示" }}
</el-button>
<el-button type="primary" @click="handleExport">导出 Excel</el-button>
</div>
<!-- 表格 -->
<el-table
:key="refreshKey"
:data="tableData"
:max-height="tableMaxHeight"
ref="tableRef"
>
<el-table-column
prop="index"
label="序号"
width="60"
fixed="left"
align="center"
/>
<el-table-column
prop="name"
label="姓名"
:visible="isColVisible('name')"
:fixed="isColFixed('name')"
min-width="120"
align="center"
/>
<el-table-column
prop="salary"
label="薪资(元)"
:visible="isColVisible('salary')"
:fixed="isColFixed('salary')"
min-width="120"
align="center"
>
<template #default="{ row }">
<span :class="{ 'salary-highlight': row.salary > 20000 }">
{{ formatNumber(row.salary) }}
</span>
</template>
</el-table-column>
</el-table>
<!-- 列管理弹窗 -->
<TableColumnManager
v-model:visible="columnManagerVisible"
:columns="columnConfig"
:default-columns="defaultColumnConfig"
@confirm="handleColumnConfigConfirm"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from "vue";
// 表格数据
const tableData = ref([]);
// 计算表格最大高度
const tableMaxHeight = computed(() => {
return isFullscreen.value ? `${windowHeight.value - 120}px` : "500px";
});
// 组件挂载
onMounted(() => {
setupFullscreenListeners();
loadFieldConfig();
// 加载数据...
});
// 组件卸载
onUnmounted(() => {
removeFullscreenListeners();
});
</script>
⚠️ 注意事项
1. 变量声明顺序
重要 :tableMaxHeight 必须在 useTableCommon 解构之后定义,因为它依赖 isFullscreen 和 windowHeight。
typescript
// ❌ 错误:会报错 "isFullscreen is not defined"
const tableMaxHeight = computed(() => {
return isFullscreen.value ? '...' : '...'
})
const { isFullscreen, windowHeight } = useTableCommon({...})
// ✅ 正确
const { isFullscreen, windowHeight } = useTableCommon({...})
const tableMaxHeight = computed(() => {
return isFullscreen.value ? '...' : '...'
})
2. 导出 API 规范
导出 API 必须支持 pageSize: -1 参数,表示查询全部数据:
typescript
exportApi: async (params) => {
// params: { pageNo: 1, pageSize: -1 }
const res = await api.getData(params);
return res;
};
返回数据格式支持两种:
res.data.records(推荐)res.data
3. 条件样式颜色格式
颜色值必须为十六进制格式(带 #),且在设置样式时会自动去掉 #:
typescript
exportFieldStyles: {
salary: (value) => {
if (value > 20000) {
return {
color: "#FF0000", // ✅ 正确
bgColor: "#FFF2F0", // ✅ 正确
};
}
return null;
};
}
4. 事件监听清理
必须在组件卸载时调用 removeFullscreenListeners() 清理事件监听:
typescript
onUnmounted(() => {
removeFullscreenListeners();
});
🎨 样式建议
表格容器样式
css
.table-container {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
&.fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
border-radius: 0;
margin: 0;
}
}
条件样式示例
css
// 薪资高亮
.salary-highlight {
color: #ff0000;
background-color: #fff2f0;
padding: 2px 6px;
border-radius: 4px;
}
// 绩效异常
.performance-abnormal {
color: #9c0006;
background-color: #ffc7ce;
padding: 2px 6px;
border-radius: 4px;
}
🔗 相关组件
- TableColumnManager:列管理弹窗组件
- TableExportUtil:Excel 导出工具类
📚 完整示例
参考 HeaderExport.vue 文件查看完整的使用示例。
更新日期 :2026-06-05
版本:1.0.0
多行表头导出
先看效果
tip:表格展示的数据和导出的 Excel 数据是 mock 数据,数据是随机的,所以数据对不上。
| 页面渲染 | 导出 Excel |
|---|---|
![]() |
|
![]() |
|
完整代码和封装的组件
完整示例:src\views\TestExport\HeadersExport.vue
javascript
<template>
<!-- 表格外层容器,用于全屏操作 -->
<div
ref="tableInfoRef"
class="table-container"
:class="{ fullscreen: isFullscreen }"
>
<!-- 工具栏 -->
<div class="toolbar">
<div class="toolbar-left">
<h2>多表头导出示例</h2>
<p class="description">
演示 useTableExportComplex
的完整功能:多级表头、嵌套数据、条件样式标记异常数据
</p>
</div>
<div class="toolbar-right">
<!-- 管理显示字段按钮 -->
<el-button
type="primary"
plain
@click="handleTableSetting"
:icon="Setting"
>
管理显示字段
</el-button>
<!-- 全屏切换按钮 -->
<el-button
type="primary"
plain
@click="handleFullScreen"
:icon="isFullscreen ? Minus : FullScreen"
>
{{ isFullscreen ? "退出全屏" : "全屏展示" }}
</el-button>
<!-- 导出按钮 -->
<el-button type="primary" @click="handleExport" :icon="Download">
导出 Excel
</el-button>
</div>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<el-table
:key="refreshKey"
:data="tableData"
border
:max-height="tableMaxHeight"
v-loading="loading"
ref="tableRef"
>
<!-- 序号列(固定) -->
<el-table-column
prop="index"
label="序号"
width="60"
fixed="left"
align="center"
/>
<!-- 基本信息列组 -->
<el-table-column
prop="basic"
label="基本信息"
:visible="isColVisible('basic')"
align="center"
>
<el-table-column
prop="productName"
label="产品名称"
min-width="150"
align="center"
/>
<el-table-column
prop="batchNo"
label="批次号"
min-width="120"
align="center"
/>
<el-table-column
prop="material"
label="材质"
min-width="100"
align="center"
/>
</el-table-column>
<!-- 尺寸列组(带公差范围) -->
<el-table-column
prop="dimension"
label="尺寸参数"
:visible="isColVisible('dimension')"
align="center"
>
<el-table-column
prop="length"
label="长度(mm)"
min-width="120"
align="center"
>
<template #default="{ row }">
<span :class="{ abnormal: isLengthAbnormal(row) }">
{{ formatNumber(row.dimension.length) }}
</span>
</template>
</el-table-column>
<el-table-column
prop="width"
label="宽度(mm)"
min-width="120"
align="center"
>
<template #default="{ row }">
<span :class="{ abnormal: isWidthAbnormal(row) }">
{{ formatNumber(row.dimension.width) }}
</span>
</template>
</el-table-column>
<el-table-column
prop="thickness"
label="厚度(mm)"
min-width="120"
align="center"
>
<template #default="{ row }">
<span :class="{ abnormal: isThicknessAbnormal(row) }">
{{ formatNumber(row.dimension.thickness) }}
</span>
</template>
</el-table-column>
</el-table-column>
<!-- 性能指标列组 -->
<el-table-column
prop="performance"
label="性能指标"
:visible="isColVisible('performance')"
align="center"
>
<el-table-column
prop="tensileStrength"
label="抗拉强度(MPa)"
min-width="140"
align="center"
>
<template #default="{ row }">
<span :class="{ abnormal: isTensileAbnormal(row) }">
{{ formatNumber(row.performance.tensileStrength) }}
</span>
</template>
</el-table-column>
<el-table-column
prop="yieldStrength"
label="屈服强度(MPa)"
min-width="140"
align="center"
>
<template #default="{ row }">
<span :class="{ abnormal: isYieldAbnormal(row) }">
{{ formatNumber(row.performance.yieldStrength) }}
</span>
</template>
</el-table-column>
<el-table-column
prop="elongation"
label="延伸率(%)"
min-width="120"
align="center"
>
<template #default="{ row }">
<span :class="{ abnormal: isElongationAbnormal(row) }">
{{ formatNumber(row.performance.elongation) }}
</span>
</template>
</el-table-column>
</el-table-column>
<!-- 平铺列:检测日期 -->
<el-table-column
prop="inspectDate"
label="检测日期"
:visible="isColVisible('inspectDate')"
min-width="120"
align="center"
/>
<!-- 平铺列:检测状态 -->
<el-table-column
prop="status"
label="检测状态"
:visible="isColVisible('status')"
width="100"
align="center"
>
<template #default="{ row }">
<span
:class="{
'status-pass': row.status === '合格',
'status-fail': row.status === '不合格',
}"
>
{{ row.status }}
</span>
</template>
</el-table-column>
</el-table>
</div>
<!-- 列管理弹窗 -->
<TableColumnManager
v-model:visible="columnManagerVisible"
:columns="columnConfig"
:default-columns="defaultColumnConfig"
@confirm="handleColumnConfigConfirm"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from "vue";
import type { ColumnConfig } from "@/components/TableColumnManager/index.vue";
import TableColumnManager from "@/components/TableColumnManager/index.vue";
import { useTableCommon } from "@/composables/useTableCommon";
import { useTableExportComplex } from "@/composables/useTableExportComplex";
import type { ExportColumnDef } from "@/composables/useTableExportComplex";
import { Setting, FullScreen, Minus, Download } from "@element-plus/icons-vue";
/**
* 模拟表格数据(嵌套结构)
*/
const tableData = ref<Record<string, any>[]>([]);
const loading = ref(false);
/**
* 生成模拟数据(带有嵌套结构的产品检测数据)
*/
const generateMockData = () => {
const materials = ["Q235", "Q345", "Q460", "SS400", "A36"];
const statuses = ["合格", "不合格"];
const data: Record<string, any>[] = [];
for (let i = 1; i <= 50; i++) {
// 基准尺寸
const baseLength = 10000;
const baseWidth = 1500;
const baseThickness = 20;
// 带公差的尺寸值(部分数据超出公差范围以模拟异常)
const length =
baseLength +
(Math.random() > 0.8
? Math.random() > 0.5
? 15
: -15
: Math.random() * 10 - 5);
const width =
baseWidth +
(Math.random() > 0.85
? Math.random() > 0.5
? 8
: -8
: Math.random() * 6 - 3);
const thickness =
baseThickness +
(Math.random() > 0.88
? Math.random() > 0.5
? 1.5
: -1.5
: Math.random() * 1 - 0.5);
// 性能指标
const tensileStrength = Math.floor(Math.random() * 200) + 400;
const yieldStrength = Math.floor(Math.random() * 150) + 250;
const elongation = Math.floor(Math.random() * 15) + 15;
// 判断是否有异常
const hasAbnormal =
Math.abs(length - baseLength) > 10 ||
Math.abs(width - baseWidth) > 6 ||
Math.abs(thickness - baseThickness) > 1 ||
tensileStrength < 420 ||
yieldStrength < 260 ||
elongation < 18;
data.push({
index: i,
productName: `钢板${String(i).padStart(4, "0")}`,
batchNo: `B${2024}${String(Math.floor(Math.random() * 100) + 1).padStart(
3,
"0",
)}`,
material: materials[Math.floor(Math.random() * materials.length)],
dimension: {
length: Number(length.toFixed(2)),
width: Number(width.toFixed(2)),
thickness: Number(thickness.toFixed(2)),
lengthTolerance: "±10",
widthTolerance: "±6",
thicknessTolerance: "±1",
},
performance: {
tensileStrength,
yieldStrength,
elongation,
},
inspectDate: `2024-${String(Math.floor(Math.random() * 12) + 1).padStart(
2,
"0",
)}-${String(Math.floor(Math.random() * 28) + 1).padStart(2, "0")}`,
status: hasAbnormal ? "不合格" : "合格",
});
}
return data;
};
/**
* 格式化数字
*/
const formatNumber = (val: number): string => {
if (val == null) return "";
return val.toLocaleString("zh-CN", { maximumFractionDigits: 2 });
};
/**
* 判断长度是否异常
*/
const isLengthAbnormal = (row: any): boolean => {
return Math.abs(row.dimension.length - 10000) > 10;
};
/**
* 判断宽度是否异常
*/
const isWidthAbnormal = (row: any): boolean => {
return Math.abs(row.dimension.width - 1500) > 6;
};
/**
* 判断厚度是否异常
*/
const isThicknessAbnormal = (row: any): boolean => {
return Math.abs(row.dimension.thickness - 20) > 1;
};
/**
* 判断抗拉强度是否异常
*/
const isTensileAbnormal = (row: any): boolean => {
return row.performance.tensileStrength < 420;
};
/**
* 判断屈服强度是否异常
*/
const isYieldAbnormal = (row: any): boolean => {
return row.performance.yieldStrength < 260;
};
/**
* 判断延伸率是否异常
*/
const isElongationAbnormal = (row: any): boolean => {
return row.performance.elongation < 18;
};
/**
* 模拟导出 API
*/
const mockExportApi = async (params: { pageNo: number; pageSize: number }) => {
await new Promise((resolve) => setTimeout(resolve, 800));
const data = generateMockData();
return {
data: {
records: data,
total: data.length,
},
};
};
/**
* 表格列配置(用于列管理)
* 注意:这里只定义一级列(分组列和平铺列),子列由导出定义处理
*/
const topLevelColumns: ColumnConfig[] = [
{
prop: "index",
label: "序号",
show: true,
fixed: true,
sortable: false,
required: true,
order: 0,
width: 60,
},
{
prop: "basic",
label: "基本信息",
show: true,
fixed: false,
sortable: false,
required: false,
order: 1,
minWidth: 370,
},
{
prop: "dimension",
label: "尺寸参数",
show: true,
fixed: false,
sortable: false,
required: false,
order: 2,
minWidth: 360,
},
{
prop: "performance",
label: "性能指标",
show: true,
fixed: false,
sortable: false,
required: false,
order: 3,
minWidth: 400,
},
{
prop: "inspectDate",
label: "检测日期",
show: true,
fixed: false,
sortable: true,
required: false,
order: 4,
minWidth: 120,
},
{
prop: "status",
label: "检测状态",
show: true,
fixed: false,
sortable: true,
required: false,
order: 5,
width: 100,
},
];
/**
* 使用 useTableCommon 组合式函数(用于全屏和列管理)
*/
const {
isFullscreen,
tableInfoRef,
tableRef,
columnManagerVisible,
refreshKey,
windowHeight,
columnConfig,
defaultColumnConfig,
isColVisible,
isColFixed,
handleFullScreen,
handleTableSetting,
handleColumnConfigConfirm,
setupFullscreenListeners,
removeFullscreenListeners,
loadFieldConfig,
} = useTableCommon({
configKey: "headers-export-demo",
topLevelColumns,
getFieldConfig: async () => {
await new Promise((resolve) => setTimeout(resolve, 300));
return {
data: {
config: {
columns: {},
},
},
};
},
saveFieldConfig: async (params) => {
await new Promise((resolve) => setTimeout(resolve, 300));
console.log("保存列配置:", params);
return { success: true };
},
});
/**
* 导出列定义(支持多级表头结构)
* 这是 useTableExportComplex 的核心配置
*/
const exportColumnDefs: ExportColumnDef[] = [
// 基本信息组(平铺列,自动展开)
{
prop: "basic",
label: "基本信息",
children: [
{
label: "产品名称",
accessor: (row: any) => row.productName,
},
{
label: "批次号",
accessor: (row: any) => row.batchNo,
},
{
label: "材质",
accessor: (row: any) => row.material,
},
],
},
// 尺寸参数组(带公差范围和异常判断)
{
prop: "dimension",
label: "尺寸参数",
children: [
{
label: "长度(mm)",
accessor: (row: any) =>
`${row.dimension.length} ~ ${row.dimension.lengthTolerance}`,
isAbnormal: (row: any) => Math.abs(row.dimension.length - 10000) > 10,
},
{
label: "宽度(mm)",
accessor: (row: any) =>
`${row.dimension.width} ~ ${row.dimension.widthTolerance}`,
isAbnormal: (row: any) => Math.abs(row.dimension.width - 1500) > 6,
},
{
label: "厚度(mm)",
accessor: (row: any) =>
`${row.dimension.thickness} ~ ${row.dimension.thicknessTolerance}`,
isAbnormal: (row: any) => Math.abs(row.dimension.thickness - 20) > 1,
},
],
},
// 性能指标组(带异常判断)
{
prop: "performance",
label: "性能指标",
children: [
{
label: "抗拉强度(MPa)",
accessor: (row: any) => row.performance.tensileStrength,
isAbnormal: (row: any) => row.performance.tensileStrength < 420,
},
{
label: "屈服强度(MPa)",
accessor: (row: any) => row.performance.yieldStrength,
isAbnormal: (row: any) => row.performance.yieldStrength < 260,
},
{
label: "延伸率(%)",
accessor: (row: any) => row.performance.elongation,
isAbnormal: (row: any) => row.performance.elongation < 18,
},
],
},
// 平铺列:检测日期
{
prop: "inspectDate",
label: "检测日期",
},
// 平铺列:检测状态
{
prop: "status",
label: "检测状态",
},
];
/**
* 使用 useTableExportComplex 组合式函数(专用于多级表头导出)
*/
const { handleExport } = useTableExportComplex({
columnDefs: exportColumnDefs,
columnConfig,
loadFieldConfig,
exportApi: mockExportApi,
exportFileName: "产品检测报告",
});
/**
* 表格最大高度计算
* 全屏模式下减去工具栏高度,非全屏模式下使用固定高度
* 必须在 useTableCommon 解构后定义,因为依赖 isFullscreen 和 windowHeight
*/
const tableMaxHeight = computed(() => {
return isFullscreen.value ? `${windowHeight.value - 150}px` : "500px";
});
/**
* 组件挂载时初始化
*/
onMounted(() => {
setupFullscreenListeners();
loadFieldConfig();
loading.value = true;
setTimeout(() => {
tableData.value = generateMockData();
loading.value = false;
}, 500);
});
/**
* 组件卸载时清理
*/
onUnmounted(() => {
removeFullscreenListeners();
});
</script>
<style lang="scss" scoped>
.table-container {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
&.fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
border-radius: 0;
margin: 0;
}
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #eee;
.toolbar-left {
h2 {
margin: 0 0 4px 0;
font-size: 18px;
font-weight: 600;
color: #303133;
}
.description {
margin: 0;
font-size: 13px;
color: #909399;
}
}
.toolbar-right {
display: flex;
gap: 12px;
}
}
.table-wrapper {
flex: 1;
padding: 16px 20px;
overflow: auto;
}
:deep(.el-table) {
width: 100%;
}
.abnormal {
color: #9c0006;
background-color: #ffc7ce;
padding: 2px 6px;
border-radius: 4px;
}
/* 状态样式 */
.status-pass {
color: #67c23a;
}
.status-fail {
color: #9c0006;
}
</style>
封装的多级标题导出hooks:src\composables\useTableExportComplex.ts
javascript
/**
* 复杂表格导出组合式函数
*
* 专用于多级表头、嵌套数据、条件样式的表格导出场景。
* 与 useTableCommon 互不干扰------useTableCommon 只支持扁平一级表头。
*
* 核心能力:
* 1. 多级表头:自动生成两行表头(父级 + 子级),并设置合并单元格
* 2. 嵌套数据提取:通过 accessor 函数从 row 中提取任意深度嵌套的值
* 3. 组合字段:如 "xxx ~ yyy" 格式的公差列,通过 accessor 自行拼接
* 4. 条件样式:异常数据标记为红色背景 + 深红字体
*
* @param options.columnDefs - 导出列定义(含多级结构)
* @param options.columnConfig - 列可见性配置(受"管理显示字段"控制)
* @param options.loadFieldConfig - 刷新列配置的函数
* @param options.exportApi - 导出数据的 API 函数
* @param options.exportFileName - 导出文件名(不含扩展名)
*/
import { type Ref } from "vue";
import { ElMessage } from "element-plus";
import { TableExportUtil } from "@/utils/TableExportUtil";
import type { ColumnConfig } from "@/components/TableColumnManager/index.vue";
/**
* 叶子列定义(实际数据列)
* @param label - 子列表头文字
* @param accessor - 从 row 中提取值的函数
* @param isAbnormal - (可选)判断该单元格是否异常,用于标记红色样式
*/
export interface ExportLeafDef {
label: string;
accessor: (row: any) => any;
isAbnormal?: (row: any) => boolean;
}
/**
* 导出列定义(支持平铺和多级)
* @param prop - 对应 ColumnConfig 的 prop,用于匹配可见性
* @param label - 表头文字(平铺列直接显示,分组列为父级表头)
* @param children - (可选)子列定义数组,有 children 表示分组列
* @param accessor - (可选)平铺列的数据提取函数,默认 (row) => row[prop]
* @param isAbnormal - (可选)平铺列的异常判断
*/
export interface ExportColumnDef {
prop: string;
label: string;
children?: ExportLeafDef[];
accessor?: (row: any) => any;
isAbnormal?: (row: any) => boolean;
}
export interface ExportApiParams {
pageNo: number;
pageSize: number;
[key: string]: any;
}
/**
* 将列索引转为 Excel 列字母(0→A, 1→B, 25→Z, 26→AA ...)
*/
function encodeCol(col: number): string {
let result = "";
let n = col;
while (n >= 0) {
result = String.fromCharCode(65 + (n % 26)) + result;
n = Math.floor(n / 26) - 1;
}
return result;
}
/**
* 格式化数字,保留两位小数
* 若值为 null/undefined/空字符串/非数字,原样返回
*/
function formatNumber(val: any): any {
if (val == null || val === "") return val;
const num = Number(val);
if (isNaN(num)) return val;
return String(parseFloat(num.toFixed(2)));
}
export function useTableExportComplex(options: {
columnDefs: ExportColumnDef[];
columnConfig: Ref<ColumnConfig[]>;
loadFieldConfig: () => Promise<void>;
exportApi?: (params: ExportApiParams) => Promise<any>;
exportFileName?: string;
}) {
async function handleExport() {
if (!options.exportApi) {
ElMessage.warning("未配置导出功能");
return;
}
try {
ElMessage.info("正在导出数据...");
// 先刷新列配置,确保导出的列与"管理显示字段"最新配置一致
await options.loadFieldConfig();
// ==================== 1. 构建可见列的叶子列列表 ====================
const visibleDefs = options.columnDefs.filter((def) => {
const cfg = options.columnConfig.value.find((c) => c.prop === def.prop);
return cfg ? cfg.show : true;
});
/**
* leafColumns: 扁平化的叶子列列表,用于:
* - 构建 Excel 列顺序
* - 提取每行数据
* - 判断异常单元格
*/
const leafColumns: {
parentLabel: string;
label: string;
accessor: (row: any) => any;
isAbnormal?: (row: any) => boolean;
isGrouped: boolean;
}[] = [];
for (const def of visibleDefs) {
if (def.children && def.children.length > 0) {
// 分组列:展开为多个叶子列
for (const child of def.children) {
leafColumns.push({
parentLabel: def.label,
label: child.label,
accessor: child.accessor,
isAbnormal: child.isAbnormal,
isGrouped: true,
});
}
} else {
// 平铺列:自身即为叶子列
leafColumns.push({
parentLabel: def.label,
label: def.label,
accessor: def.accessor || ((row: any) => row[def.prop]),
isAbnormal: def.isAbnormal,
isGrouped: false,
});
}
}
const totalCols = leafColumns.length;
if (totalCols === 0) {
ElMessage.warning("没有可导出的列");
return;
}
// ==================== 2. 获取全部数据 ====================
const res = await options.exportApi({
pageNo: 1,
pageSize: -1,
});
const records: Record<string, any>[] =
res.data?.records || res.data || [];
if (!records || records.length === 0) {
ElMessage.warning("没有可导出的数据");
return;
}
// ==================== 3. 构建表头行 + 合并范围 ====================
const headerKeys = Array.from(
{ length: totalCols },
(_, i) => `col_${i}`,
);
// 第 1 行:平铺列显示标签,分组列显示父标签(子列位置留空)
// 第 2 行:平铺列留空(会与第1行合并),分组列显示子标签
const headerRow1: Record<string, string> = {};
const headerRow2: Record<string, string> = {};
for (let i = 0; i < totalCols; i++) {
const leaf = leafColumns[i];
if (leaf.isGrouped) {
headerRow1[headerKeys[i]] = leaf.parentLabel;
headerRow2[headerKeys[i]] = leaf.label;
} else {
headerRow1[headerKeys[i]] = leaf.label;
headerRow2[headerKeys[i]] = "";
}
}
// 合并范围
const merges: string[] = [];
// 遍历可见列定义,计算每列的起始位置
let colIdx = 0;
for (const def of visibleDefs) {
if (!def.children || def.children.length === 0) {
// 平铺列:纵向合并第1行和第2行
const colLetter = encodeCol(colIdx);
merges.push(`${colLetter}1:${colLetter}2`);
colIdx += 1;
} else {
// 分组列:横向合并第1行的子列范围
const startCol = colIdx;
const endCol = colIdx + def.children.length - 1;
if (endCol > startCol) {
merges.push(`${encodeCol(startCol)}1:${encodeCol(endCol)}1`);
}
colIdx = endCol + 1;
}
}
// ==================== 4. 构建数据行 + 收集异常坐标 ====================
const dataRows: Record<string, any>[] = [];
const abnormalPositions: string[] = [];
for (let rowIdx = 0; rowIdx < records.length; rowIdx++) {
const row = records[rowIdx];
const dataRow: Record<string, any> = {};
for (let colI = 0; colI < totalCols; colI++) {
const leaf = leafColumns[colI];
const rawValue = leaf.accessor(row);
dataRow[headerKeys[colI]] = formatNumber(rawValue);
// 异常单元格:行号 = rowIdx + 3(第1、2行为表头)
if (leaf.isAbnormal && leaf.isAbnormal(row)) {
const colLetter = encodeCol(colI);
abnormalPositions.push(`${colLetter}${rowIdx + 3}`);
}
}
dataRows.push(dataRow);
}
// ==================== 5. 导出 Excel ====================
const exportUtil = new TableExportUtil();
exportUtil.addJson([headerRow1, headerRow2, ...dataRows], headerKeys);
exportUtil.setMerges(merges);
// 异常单元格标记红色背景 + 深红字体
if (abnormalPositions.length > 0) {
exportUtil.setStyle((ws: any) => {
abnormalPositions.forEach((pos) => {
if (ws[pos]) {
ws[pos].s = {
...ws[pos].s,
fill: {
patternType: "solid",
fgColor: { rgb: "FFC7CE" },
},
font: {
...(ws[pos].s?.font || {}),
color: { rgb: "9C0006" },
},
};
}
});
});
}
exportUtil.export({
name: options.exportFileName || "导出",
autoWidth: true,
border: true,
skipRow: 2, // 跳过2行表头再计算列宽
});
ElMessage.success("导出成功");
} catch (error) {
console.error("导出失败:", error);
ElMessage.error("导出失败");
}
}
return { handleExport };
}
useTableExportComplex 使用指南
📋 功能概述
useTableExportComplex 是一个专门用于多级表头、嵌套数据表格导出的组合式函数(Composable)。它提供了以下核心能力:
- 多级表头:自动生成两行表头(父级 + 子级),并设置合并单元格
- 嵌套数据提取 :通过
accessor函数从 row 中提取任意深度嵌套的值 - 组合字段 :如 "xxx ~ yyy" 格式的公差列,通过
accessor自行拼接 - 条件样式:异常数据自动标记为红色背景 + 深红字体
🎯 适用场景
- 多级表头表格(分组列 + 子列)
- 嵌套数据结构(如
row.dimension.length) - 需要组合显示的字段(如公差范围)
- 需要标记异常数据的表格
📦 API 参数
typescript
useTableExportComplex(options: {
columnDefs: ExportColumnDef[]; // 必填:导出列定义(含多级结构)
columnConfig: Ref<ColumnConfig[]>; // 必填:列可见性配置
loadFieldConfig: () => Promise<void>; // 必填:刷新列配置的函数
exportApi?: (params: ExportApiParams) => Promise<any>; // 可选:导出数据 API
exportFileName?: string; // 可选:导出文件名
})
🔧 ExportColumnDef 类型定义
typescript
interface ExportLeafDef {
label: string; // 子列表头文字
accessor: (row: any) => any; // 从 row 中提取值的函数
isAbnormal?: (row: any) => boolean; // 判断该单元格是否异常
}
interface ExportColumnDef {
prop: string; // 对应 ColumnConfig 的 prop
label: string; // 表头文字(分组列为父级,平铺列直接显示)
children?: ExportLeafDef[]; // 子列定义(有 children 表示分组列)
accessor?: (row: any) => any; // 平铺列的数据提取函数
isAbnormal?: (row: any) => boolean; // 平铺列的异常判断
}
💡 使用示例
1. 基础配置(分组列 + 平铺列)
typescript
import { useTableExportComplex } from "@/composables/useTableExportComplex";
import type { ExportColumnDef } from "@/composables/useTableExportComplex";
// 导出列定义
const exportColumnDefs: ExportColumnDef[] = [
// 分组列:基本信息
{
prop: "basic",
label: "基本信息",
children: [
{
label: "产品名称",
accessor: (row) => row.productName,
},
{
label: "批次号",
accessor: (row) => row.batchNo,
},
{
label: "材质",
accessor: (row) => row.material,
},
],
},
// 分组列:尺寸参数(带异常判断)
{
prop: "dimension",
label: "尺寸参数",
children: [
{
label: "长度(mm)",
accessor: (row) => row.dimension.length,
isAbnormal: (row) => Math.abs(row.dimension.length - 10000) > 10,
},
{
label: "宽度(mm)",
accessor: (row) => row.dimension.width,
isAbnormal: (row) => Math.abs(row.dimension.width - 1500) > 6,
},
],
},
// 平铺列:检测日期
{
prop: "inspectDate",
label: "检测日期",
},
// 平铺列:检测状态
{
prop: "status",
label: "检测状态",
},
];
// 使用 composable
const { handleExport } = useTableExportComplex({
columnDefs: exportColumnDefs,
columnConfig, // 来自 useTableCommon 的 columnConfig
loadFieldConfig, // 来自 useTableCommon 的 loadFieldConfig
exportApi: async (params) => {
const res = await api.getData(params);
return res;
},
exportFileName: "产品检测报告",
});
2. 组合字段示例(公差范围)
typescript
const exportColumnDefs: ExportColumnDef[] = [
{
prop: "dimension",
label: "尺寸参数",
children: [
{
label: "长度(mm)",
// 组合显示:数值 ~ 公差
accessor: (row) =>
`${row.dimension.length} ~ ${row.dimension.lengthTolerance}`,
isAbnormal: (row) => Math.abs(row.dimension.length - 10000) > 10,
},
{
label: "宽度(mm)",
accessor: (row) =>
`${row.dimension.width} ~ ${row.dimension.widthTolerance}`,
isAbnormal: (row) => Math.abs(row.dimension.width - 1500) > 6,
},
],
},
];
3. 嵌套数据提取示例
typescript
const exportColumnDefs: ExportColumnDef[] = [
{
prop: "performance",
label: "性能指标",
children: [
{
label: "抗拉强度(MPa)",
// 提取嵌套数据:row.performance.tensileStrength
accessor: (row) => row.performance.tensileStrength,
isAbnormal: (row) => row.performance.tensileStrength < 420,
},
{
label: "屈服强度(MPa)",
accessor: (row) => row.performance.yieldStrength,
isAbnormal: (row) => row.performance.yieldStrength < 260,
},
],
},
];
4. 完整配置(与 useTableCommon 配合使用)
typescript
import { useTableCommon } from "@/composables/useTableCommon";
import { useTableExportComplex } from "@/composables/useTableExportComplex";
// 1. 定义列管理配置(用于列可见性控制)
const topLevelColumns: ColumnConfig[] = [
{
prop: "index",
label: "序号",
show: true,
fixed: true,
sortable: false,
required: true,
order: 0,
width: 60,
},
{
prop: "basic",
label: "基本信息",
show: true,
fixed: false,
sortable: false,
required: false,
order: 1,
minWidth: 370,
},
{
prop: "dimension",
label: "尺寸参数",
show: true,
fixed: false,
sortable: false,
required: false,
order: 2,
minWidth: 360,
},
];
// 2. 使用 useTableCommon 获取列管理功能
const {
isFullscreen,
tableInfoRef,
columnConfig,
defaultColumnConfig,
isColVisible,
handleFullScreen,
handleTableSetting,
handleColumnConfigConfirm,
setupFullscreenListeners,
removeFullscreenListeners,
loadFieldConfig,
} = useTableCommon({
configKey: "product-inspection",
topLevelColumns,
getFieldConfig: async (key) => {
const res = await api.getFieldConfig(key);
return res.data;
},
saveFieldConfig: async (params) => {
await api.saveFieldConfig(params);
return { success: true };
},
});
// 3. 定义导出列配置
const exportColumnDefs: ExportColumnDef[] = [
{
prop: "basic",
label: "基本信息",
children: [
{ label: "产品名称", accessor: (row) => row.productName },
{ label: "批次号", accessor: (row) => row.batchNo },
{ label: "材质", accessor: (row) => row.material },
],
},
{
prop: "dimension",
label: "尺寸参数",
children: [
{
label: "长度(mm)",
accessor: (row) => row.dimension.length,
isAbnormal: (row) => Math.abs(row.dimension.length - 10000) > 10,
},
],
},
];
// 4. 使用 useTableExportComplex 获取导出功能
const { handleExport } = useTableExportComplex({
columnDefs: exportColumnDefs,
columnConfig, // 来自 useTableCommon
loadFieldConfig, // 来自 useTableCommon
exportApi: async (params) => {
const res = await api.getData(params);
return res;
},
exportFileName: "产品检测报告",
});
5. 模板中使用
vue
<template>
<div ref="tableInfoRef" class="table-container">
<!-- 工具栏 -->
<div class="toolbar">
<el-button @click="handleTableSetting">管理显示字段</el-button>
<el-button @click="handleFullScreen">
{{ isFullscreen ? "退出全屏" : "全屏展示" }}
</el-button>
<el-button type="primary" @click="handleExport">导出 Excel</el-button>
</div>
<!-- 表格 -->
<el-table
:key="refreshKey"
:data="tableData"
:max-height="tableMaxHeight"
border
>
<el-table-column
prop="index"
label="序号"
width="60"
fixed="left"
align="center"
/>
<!-- 基本信息分组列 -->
<el-table-column
prop="basic"
label="基本信息"
:visible="isColVisible('basic')"
align="center"
>
<el-table-column
prop="productName"
label="产品名称"
min-width="150"
align="center"
/>
<el-table-column
prop="batchNo"
label="批次号"
min-width="120"
align="center"
/>
<el-table-column
prop="material"
label="材质"
min-width="100"
align="center"
/>
</el-table-column>
<!-- 尺寸参数分组列 -->
<el-table-column
prop="dimension"
label="尺寸参数"
:visible="isColVisible('dimension')"
align="center"
>
<el-table-column
prop="length"
label="长度(mm)"
min-width="120"
align="center"
>
<template #default="{ row }">
<span :class="{ abnormal: isLengthAbnormal(row) }">
{{ formatNumber(row.dimension.length) }}
</span>
</template>
</el-table-column>
</el-table-column>
</el-table>
<!-- 列管理弹窗 -->
<TableColumnManager
v-model:visible="columnManagerVisible"
:columns="columnConfig"
:default-columns="defaultColumnConfig"
@confirm="handleColumnConfigConfirm"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from "vue";
// 异常判断函数(与导出逻辑一致)
const isLengthAbnormal = (row: any): boolean => {
return Math.abs(row.dimension.length - 10000) > 10;
};
// 格式化数字
const formatNumber = (val: number): string => {
if (val == null) return "";
return val.toLocaleString("zh-CN", { maximumFractionDigits: 2 });
};
// 组件挂载
onMounted(() => {
setupFullscreenListeners();
loadFieldConfig();
// 加载数据...
});
// 组件卸载
onUnmounted(() => {
removeFullscreenListeners();
});
</script>
<style lang="scss" scoped>
.abnormal {
color: #9c0006;
background-color: #ffc7ce;
padding: 2px 6px;
border-radius: 4px;
}
</style>
⚠️ 注意事项
1. 与 useTableCommon 配合使用
useTableExportComplex 需要依赖 useTableCommon 提供的 columnConfig 和 loadFieldConfig,因此必须先使用 useTableCommon:
typescript
// ✅ 正确顺序
const { columnConfig, loadFieldConfig } = useTableCommon({...})
const { handleExport } = useTableExportComplex({
columnConfig,
loadFieldConfig,
...
})
2. prop 字段必须匹配
ExportColumnDef 的 prop 字段必须与 ColumnConfig 的 prop 字段一致,用于匹配列可见性:
typescript
// ColumnConfig(列管理)
const topLevelColumns: ColumnConfig[] = [
{ prop: 'basic', label: '基本信息', ... },
{ prop: 'dimension', label: '尺寸参数', ... }
]
// ExportColumnDef(导出)
const exportColumnDefs: ExportColumnDef[] = [
{ prop: 'basic', label: '基本信息', children: [...] }, // ✅ prop 一致
{ prop: 'dimension', label: '尺寸参数', children: [...] } // ✅ prop 一致
]
3. 异常判断函数必须返回布尔值
isAbnormal 函数必须返回 boolean 类型:
typescript
{
label: '长度(mm)',
accessor: (row) => row.dimension.length,
isAbnormal: (row) => Math.abs(row.dimension.length - 10000) > 10 // ✅ 返回 boolean
}
4. 导出 API 规范
导出 API 必须支持 pageSize: -1 参数,表示查询全部数据:
typescript
exportApi: async (params) => {
// params: { pageNo: 1, pageSize: -1 }
const res = await api.getData(params);
return res;
};
返回数据格式支持两种:
res.data.records(推荐)res.data
5. 数字格式化
useTableExportComplex 会自动对数字进行格式化(保留两位小数),无需手动处理:
typescript
// 原始数据
row.dimension.length = 10005.678;
// 导出时自动格式化为
10005.68;
6. 变量声明顺序
tableMaxHeight 必须在 useTableCommon 解构之后定义:
typescript
// ✅ 正确
const { isFullscreen, windowHeight } = useTableCommon({...})
const tableMaxHeight = computed(() => {
return isFullscreen.value ? `${windowHeight.value - 150}px` : '500px'
})
🎨 样式建议
异常单元格样式
scss
.abnormal {
color: #9c0006;
background-color: #ffc7ce;
padding: 2px 6px;
border-radius: 4px;
}
表格容器样式
scss
.table-container {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
&.fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
border-radius: 0;
margin: 0;
}
}
🔗 与 useTableCommon 的区别
| 特性 | useTableCommon | useTableExportComplex |
|---|---|---|
| 表头类型 | 单级表头 | 多级表头(分组列 + 子列) |
| 数据结构 | 扁平数据 | 支持嵌套数据 |
| 字段提取 | 直接通过 prop |
通过 accessor 函数 |
| 条件样式 | 自定义 exportFieldStyles |
内置 isAbnormal 标记 |
| 导出功能 | 内置 | 专用于导出 |
| 列管理 | 内置 | 依赖 useTableCommon |
📚 完整示例
参考 HeadersExport.vue 文件查看完整的使用示例。
更新日期 :2026-06-05
版本:1.0.0



