高效办公利器:前端实现表格导出excel格式 + 自定义水印的完整方案

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文件的展示

相关推荐
叫我詹躲躲2 小时前
为什么Bun.js能在3秒内启动一个完整的Web应用?
前端·javascript·bun
Olrookie2 小时前
若依前后端分离版学习笔记(十七)——ruoyi开发规范&流程,请求流程,依赖引入,组件注册&通信
前端·笔记
Keepreal4962 小时前
谈谈对闭包的理解以及常见用法
前端·javascript
luckymiaow2 小时前
告别环境配置地狱!UniApp Android 本地 一键打包神器
前端
Keepreal4962 小时前
JS加载时机
前端·javascript
itslife2 小时前
从头看 vite 源码 - 调试
前端
叫我詹躲躲2 小时前
ES2025:10个让你眼前一亮的JavaScript新特性
前端·javascript
乐~~~2 小时前
解决avue-input-tree组件重置数据不回显/重置失败
前端·javascript·vue.js
你的电影很有趣2 小时前
lesson68:JavaScript 操作 HTML 元素、属性与样式全指南
开发语言·前端·javascript