1.需求背景
在工作中,难免会遇到需要将页面表格导出到excel上,可能有时还会在excel表格加一个特有水印,这到底是需要前端实现还是后端实现呢?当然前端实现是可以通过exceljs库 来实现表格的导出,通过file-saver库实现自定义水印,这样也会大大提高团队效率
2.准备工作
2.1 下载exceljs库
npm install exceljs --save
2.2 下载file-saver库
npm install file-saver --save
3.项目实现
3.1 新建工具方法
新建utils/excel.js文件

在项目中导入依赖库
js
// 导入依赖库
import ExcelJS from 'exceljs';
import { saveAs } from 'file-saver';
生成带水印的图片的函数
js
/**
* 生成带水印的图片buffer(基于Canvas)
* @param {string[]} texts - 水印文本数组(支持多行)
* @param {Object} options - 水印配置
* @returns {Promise<ArrayBuffer>} 水印图片的buffer
*/
async function generateWatermarkImage(texts = [], options = {}) {
// 默认配置
const {
width = 200, // 水印图片宽度
height = 150, // 水印图片高度
fontSize = 14, // 字体大小
color = 'rgba(180, 180, 180, 0.3)', // 水印颜色(半透明灰色)
rotate = -30, // 旋转角度(负角度为逆时针)
fontFamily = 'Arial'
} = options;
// 创建Canvas元素
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = width;
canvas.height = height;
// 设置水印样式
ctx.fillStyle = color;
ctx.font = `${fontSize}px ${fontFamily}`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 旋转画布
ctx.translate(width / 2, height / 2);
ctx.rotate(rotate * Math.PI / 180);
ctx.translate(-width / 2, -height / 2);
// 绘制多行文本
texts.forEach((text, index) => {
// 每行文本的垂直偏移量(根据行数自动调整)
const yOffset = (index - (texts.length - 1) / 2) * fontSize * 1.5;
ctx.fillText(text, width / 2, height / 2 + yOffset);
});
// 将Canvas转换为图片buffer
return new Promise((resolve, reject) => {
canvas.toBlob(blob => {
if (!blob) {
reject(new Error('水印图片生成失败'));
return;
}
// 转换blob为ArrayBuffer
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsArrayBuffer(blob);
}, 'image/png');
});
}
导出excel文件并下载的函数(里面调用了生成水印的函数)
js
/**
* 导出Excel文件(带水印和单元格样式)
* @param {Array} data - 表格数据
* @param {string} fileName - 导出文件名
*/
export async function exportExcel(data, fileName = 'excel.xlsx') {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Sheet1');
// 生成水印图片并添加到工作表
const watermarkImage = await generateWatermarkImage(['excel水印1', 'excel水印2']);
const imageId = workbook.addImage({
buffer: watermarkImage,
extension: 'png'
});
worksheet.addBackgroundImage(imageId);
// 处理表格数据
data.forEach((rowData, rowIndex) => {
rowData.forEach((cellData, colIndex) => {
const {
value,
colSpan = 1,
rowSpan = 1,
offset = { row: 0, col: 0 },
push = { rows: 0, cols: 0 },
width,
height,
bgColor,
bold,
size
} = cellData;
// 计算单元格位置
const rowPosition = rowIndex + 1 + offset.row + push.rows;
const colPosition = colIndex + 1 + offset.col + push.cols;
const cell = worksheet.getCell(rowPosition, colPosition);
// 设置单元格值
cell.value = value;
// 处理单元格合并
if (colSpan > 1 || rowSpan > 1) {
worksheet.mergeCells(
rowPosition,
colPosition,
rowPosition + rowSpan - 1,
colPosition + colSpan - 1
);
}
// 设置单元格样式
cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
if (bgColor) {
cell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: bgColor }
};
}
cell.font = {
bold: !!bold, // !! 是双重非运算符,作用是将变量转换为布尔值
size: size || 12
};
if (width) {
worksheet.getColumn(colPosition).width = width;
}
if (height) {
worksheet.getRow(rowPosition).height = height;
}
cell.border = {
top: { style: 'thin', color: { argb: '000000' } },
left: { style: 'thin', color: { argb: '000000' } },
bottom: { style: 'thin', color: { argb: '000000' } },
right: { style: 'thin', color: { argb: '000000' } }
};
});
});
// 生成文件并下载
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
saveAs(blob, fileName);
}
3.2 导出逻辑
在页面中引入exportExcel工具方法,这个方法第一个参数是导出的数据,第二个参数是excel文件的名称,需要将导出的数据转化为二维数组 (第一个为表头 ,第二个为内容)
js
<script setup>
import { ref } from 'vue';
import { exportExcel } from './utils/excel';
// 原始数据
const tableData = ref([
{ name: 'iPhone 17', price: 5999, sales: '20w' },
{ name: 'iPhone 17 Pro', price: 8999, sales: '15w' },
{ name: 'iPhone 17 Pro Max', price: 9999, sales: '41w' },
{ name: 'iPhone 17 Plus', price: 6999, sales: '12w' },
{ name: 'iPhone 17 1TB版', price: 7999, sales: '8w' },
{ name: 'Xiaomi 17', price: 5799, sales: '200w' },
{ name: 'Xiaomi 17 Pro', price: 6299, sales: '150w' },
{ name: 'Xiaomi 17 Ultra', price: 7499, sales: '90w' },
{ name: 'Xiaomi 17 Pro Max', price: 7999, sales: '65w' },
{ name: 'Xiaomi 17 Lite', price: 3299, sales: '280w' },
{ name: 'Xiaomi Mix Fold 5', price: 12999, sales: '18w' },
{ name: 'Xiaomi Redmi K80', price: 2499, sales: '350w' },
{ name: 'Xiaomi Redmi Note 14 Pro', price: 1999, sales: '420w' },
]);
// 处理导出
const handleExport = async () => {
// 转换数据格式以适应exportExcel函数的要求
// 这里需要转化为二维数组(第一个为表头,第二个为内容)
const exportData = [
// 表头行
[
{ value: '商品名称', width:50,bold: true, bgColor: 'F5F7FA' },
{ value: '价格', width:50,bold: true, bgColor: 'F5F7FA' },
{ value: '销量', width:50,bold: true, bgColor: 'F5F7FA' }
],
// 内容行
...tableData.value.map(item => [
{ value: item.name },
{ value: item.price },
{ value: item.sales }
])
];
// 调用导出函数
exportExcel(exportData, `iPhone和xiaomi手机销量.xlsx`);
};
</script>
3.3 效果展示
页面表格展示

下载的excel文件

excel文件的展示
