前端vue如何将项目中excel模板内容做替换?

前端vue项目public/static/template目录下有个test.xlsx文件,需要对导出的模板文件内容进行更换,同时要保证其它文本、样式不变,目前用下面工具库可以实现替换内容导出,但是导出的内容font字体乱码了image.png


这是原文件的样式,需要将required替换成必填,同时保证样式还是红色字体也不变
image.png


import XLSX from 'xlsx';
import XLSXStyle from "xlsx-style";
import {saveAs} from 'file-saver';
async downloadTemplate1() {
      try {
        this.loading = true;

        // 1. 获取模板文件
        const workbook = await fetchTemplate(this.templatePath);

        // 2. 替换国际化键值
        const processedWorkbook = replaceI18nKeys(workbook);

        // 3. 生成并下载文件
        createExcel(processedWorkbook, this.title + this.$i('inkey.xdl.page.template'));

        this.$message.success('模板下载成功');

      } catch (error) {
        console.error('模板下载失败:', error);
        this.$message.error(`模板下载失败: ${error.message}`);
      } finally {
        this.loading = false;
      }
    },
/**
 * 从服务器获取模板文件(可选保留,模板解析用)
 */
export async function fetchTemplate(templatePath) {
    try {
        if (!templatePath || templatePath.trim() === '') {
            throw new Error('模板文件路径为空,请检查templatePath配置');
        }
        console.log('开始获取模板文件:', templatePath);
        const response = await fetch(templatePath);
        if (!response.ok) {
            throw new Error(`模板文件加载失败: 状态码${response.status},路径${templatePath}`);
        }
        const contentType = response.headers.get('content-type');
        const validXlsxTypes = [
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
            'application/octet-stream'
        ];
        if (!validXlsxTypes.some(type => contentType?.includes(type))) {
            throw new Error(`非标准XLSX文件,MIME类型:${contentType}`);
        }
        const arrayBuffer = await response.arrayBuffer();
        if (!arrayBuffer || arrayBuffer.byteLength === 0) {
            throw new Error('模板文件内容为空,文件可能损坏');
        }
        const uint8Array = new Uint8Array(arrayBuffer);
        const workbook = XLSXStyle.read(uint8Array, {
            type: 'buffer',
            cellDates: true,
            cellFormula: true,
            cellNF: true,
            cellStyles: true, // 保留模板样式
            sheetStubs: true,
            raw: false
        });
        console.log(workbook)
        const fixedWorkbook = fixChineseFontNameDeep(workbook);
        console.log(fixedWorkbook)
        return fixedWorkbook;
    } catch (error) {
        console.error('获取模板文件失败:', error);
        throw new Error(`获取模板文件失败: ${error.message || '文件解析异常,请检查模板文件格式'}`);
    }
}

export function createExcel(workbook, fileName, options = {}) {
    try {
        // 在写入之前再次确保字体名称正确
        const finalWorkbook = fixChineseFontName(workbook);

        if (!finalWorkbook || !finalWorkbook.SheetNames || finalWorkbook.SheetNames.length === 0) {
            throw new Error('工作簿无效,无工作表信息');
        }

        const writeOptions = {
            bookType: 'xlsx',
            type: 'binary',
            bookSST: false,
            compression: false,
            Props: finalWorkbook.Props || {
                Title: fileName || 'Excel文件',
                Author: '系统',
                CreatedDate: new Date()
            },
            cellStyles: true,
            STYLES: true,
            // 添加编码相关选项
            codepage: 65001, // UTF-8 代码页
            ...options
        };

        // 使用修复后的工作簿
        const excelData = XLSXStyle.write(finalWorkbook, writeOptions);

        // 验证输出
        console.log('生成Excel数据大小:', excelData.length, '字符');

        const blob = new Blob([s2ab(excelData)], {
            type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8"
        });

        saveAs(blob, `${fileName || 'ExcelTemplate'}.xlsx`);
        console.log('Excel模板导出成功,样式已保留');

        return true;

    } catch (error) {
        console.error('下载Excel模板失败:', error);
        throw error;
    }
}
/**
 * 核心:修复Excel字体名称乱码(深度修复,模板解析/新建Excel通用)
 */
function fixChineseFontNameDeep(workbook) {
    // 修复字体名称,直接替换乱码为正确的字体名
    if (workbook.Styles && workbook.Styles.Fonts) {
        workbook.Styles.Fonts.forEach(font => {
            if (font.name && typeof font.name === 'string') {
                // 直接映射常见乱码到正确字体
                if (font.name === '\等线') font.name = '等线';
                else if (font.name === '微软雅黑') font.name = '微软雅黑';
                else if (font.name === '宋ä½"') font.name = '宋体';
                else if (font.name === '黑ä½"') font.name = '黑体';
                else {
                    // 尝试解码 latin1 转 UTF-8
                    try {
                        const latin1Bytes = [];
                        for (let i = 0; i < font.name.length; i++) {
                            latin1Bytes.push(font.name.charCodeAt(i));
                        }
                        const decoder = new TextDecoder('utf-8');
                        const utf8Str = decoder.decode(new Uint8Array(latin1Bytes));
                        // 解码出中文则替换
                        if (/[\u4e00-\u9fa5]/.test(utf8Str)) font.name = utf8Str;
                    } catch (e) {
                        console.warn('字体解码失败:', font.name, e);
                    }
                }
            }
        });
    }
    return workbook;
}

/**
 * 二次修复字体:包含主题字体+单元格样式(写入前最终校验)
 */
function fixChineseFontName(workbook) {
    // 修复样式表字体
    if (workbook.Styles && workbook.Styles.Fonts) {
        workbook.Styles.Fonts.forEach(font => {
            if (font.name && typeof font.name === 'string') {
                const original = font.name;
                const fixed = fixEncoding(original);
                if (fixed !== original) {
                    console.log(`修复样式字体: ${original} -> ${fixed}`);
                    font.name = fixed;
                }
            }
        });
    }

    // 修复主题字体(东亚/拉丁)
    if (workbook.Theme && workbook.Theme.themeElements) {
        const theme = workbook.Theme.themeElements;
        // 拉丁字体
        if (theme.fontScheme && theme.fontScheme.latin) {
            const original = theme.fontScheme.latin.typeface;
            if (original) {
                const fixed = fixEncoding(original);
                if (fixed !== original) theme.fontScheme.latin.typeface = fixed;
            }
        }
        // 东亚字体(中文核心)
        if (theme.fontScheme && theme.fontScheme.ea) {
            const original = theme.fontScheme.ea.typeface;
            if (original) {
                const fixed = fixEncoding(original);
                if (fixed !== original) theme.fontScheme.ea.typeface = fixed;
            }
        }
    }
    return workbook;
}

/**
 * 编码修复工具:UTF-8/GBK/硬编码映射(兜底)
 */
function fixEncoding(text) {
    if (!text || typeof text !== 'string') return text;
    // 已是中文直接返回
    if (/[\u4e00-\u9fa5]/.test(text)) return text;

    // 硬编码映射常见字体乱码/英文
    const fontMapping = {
        '等线': '等线',
        '微软雅黑': '微软雅黑',
        '宋ä½"': '宋体',
        '黑ä½"': '黑体',
        'kaiti': '楷体',
        'fangsong': '仿宋',
        'simsun': '宋体',
        'microsoft yahei': '微软雅黑',
        'dengxian': '等线',
    };
    const lowerText = text.toLowerCase();
    for (const [garbled, chinese] of Object.entries(fontMapping)) {
        if (lowerText === garbled.toLowerCase()) return chinese;
    }

    // 尝试UTF-8解码
    const tryUTF8Decode = (str) => {
        try {
            const decoded = decodeURIComponent(escape(str));
            if (/[\u4e00-\u9fa5]/.test(decoded)) return decoded;
        } catch (e) {}
        return str;
    };
    let result = tryUTF8Decode(text);
    if (/[\u4e00-\u9fa5]/.test(result)) return result;

    // 尝试GBK/GB2312解码
    const tryGBKDecode = (str) => {
        try {
            const bytes = [];
            for (let i = 0; i < str.length; i++) bytes.push(str.charCodeAt(i) & 0xFF);
            const encodings = ['gbk', 'gb2312', 'gb18030'];
            for (const encoding of encodings) {
                try {
                    const decoder = new TextDecoder(encoding);
                    const res = decoder.decode(new Uint8Array(bytes));
                    if (/[\u4e00-\u9fa5]/.test(res)) return res;
                } catch (e) { continue; }
            }
        } catch (e) { console.warn('GBK解码失败:', e); }
        return str;
    };
    result = tryGBKDecode(text);
    return /[\u4e00-\u9fa5]/.test(result) ? result : text;
}

/**
 * ArrayBuffer转换工具(xlsx-style写入必备)
 */
function s2ab(s) {
    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;
}
阅读 635
1 个回答

刚开始AI给的是import XLSX from 'xlsx';import XLSXStyle from "xlsx-style";这套方案,这个库怎么调都有问题,后来换成"exceljs": "^4.4.0"这个库可以了,但是这个库AI给的代码也是各种问题,富文本那个判断一直有问题,但也基本上人工调试通了,后面有些判断暂时没验证,导出的内容全部都替换,样式全部保留,目前看没什么问题,主要代码贴下面

async downloadI18nTemplate() {
      const arrayBuffer = await fetchExcelTemplate(this.templatePath);
      await readExcelReplaceTextAndExport(arrayBuffer, this.title);
    },
import ExcelJS from 'exceljs';
import { saveAs } from 'file-saver';
import {replaceCellI18nKey} from "@/utils/excelUtils";

/**
 * 核心方法:读取Excel并替换文本,保留所有样式后导出
 * @param {File|ArrayBuffer} excelSource - 入参:本地File文件(input上传)/服务器请求的ArrayBuffer
 * @param {string} exportFileName - 导出的文件名(无.xlsx后缀)
 */
export async function readExcelReplaceTextAndExport(excelSource, exportFileName = '导出文件') {
    try {
        // 1. 初始化ExcelJS工作簿(用于写文件+保留样式)
        const workbook = new ExcelJS.Workbook();
        let buffer;

        // 处理入参:本地File文件 → ArrayBuffer
        if (excelSource instanceof File) {
            buffer = await excelSource.arrayBuffer();
        } else if (excelSource instanceof ArrayBuffer) {
            buffer = excelSource; // 服务器请求的ArrayBuffer直接使用
        } else {
            throw new Error('入参必须是本地File文件或ArrayBuffer');
        }

        // 2. ExcelJS加载原始Excel文件(核心:**完整保留所有样式**)
        await workbook.xlsx.load(buffer);

        // 3. 遍历所有工作表,逐单元格替换文本(**只改文本,不碰任何样式**)
        workbook.worksheets.forEach(worksheet => {
            // 遍历每一行:includeEmpty: true 必须开启,否则带样式的空单元格/特殊格式单元格会被过滤
            worksheet.eachRow({ includeEmpty: true }, (row) => {
                // 遍历行内每一个单元格:同样开启includeEmpty: true
                row.eachCell({ includeEmpty: true }, (cell) => {
                    // 跳过真正的空单元格(值为null/undefined,无任何内容)
                    if (cell.value === null || cell.value === undefined) return;

                    // ========== 新增:打印单元格信息(方便调试,可保留/删除) ==========
                    // 场景1:富文本(type=8)→ 你的带颜色单元格核心处理(最高优先级!)
                    if (cell.type === ExcelJS.ValueType.RichText) {
                        // 富文本的value是数组,每个项包含text(文字)和font(样式/颜色)
                        if (cell.value && typeof cell.value === 'object' && Array.isArray(cell.value.richText)) {
                            cell.value.richText = cell.value.richText.map(richItem => {
                                // 只替换text字段,所有font/color样式完全保留
                                if (typeof richItem.text === 'string') {
                                    return { ...richItem, text: replaceCellI18nKey(richItem.text) };
                                }
                                return richItem;
                            });
                        }
                        // 情况2:通用富文本结构 → 直接是数组 [ {...} ](兼容其他Excel格式)
                        else if (Array.isArray(cell.value)) {
                            cell.value = cell.value.map(richItem => {
                                if (typeof richItem.text === 'string') {
                                    return { ...richItem, text: replaceCellI18nKey(richItem.text) };
                                }
                                return richItem;
                            });
                        }
                    }
                    // 场景2:普通文本(type=3)→ 无样式纯文本
                    else if (cell.type === ExcelJS.ValueType.String && typeof cell.value === 'string') {
                        const newText = replaceCellI18nKey(cell.value);
                        if (newText !== cell.value) cell.value = newText;
                    }
                    // 场景3:共享字符串(type=7)→ Excel优化存储的带样式文本
                    else if (cell.type === ExcelJS.ValueType.SharedString) {
                        const sharedStr = workbook.sharedStrings.get(cell.value);
                        if (typeof sharedStr === 'string') {
                            const newText = replaceCellI18nKey(sharedStr);
                            if (newText !== sharedStr) cell.value = newText;
                        }
                        // 共享字符串也可能是富文本数组
                        else if (Array.isArray(sharedStr)) {
                            cell.value = sharedStr.map(richItem => ({
                                ...richItem,
                                text: typeof richItem.text === 'string' ? replaceCellI18nKey(richItem.text) : richItem.text
                            }));
                        }
                    }
                    // 场景4:公式单元格(type=6)→ 公式原文/计算值
                    else if (cell.type === ExcelJS.ValueType.Formula) {
                        // 替换公式原文(cell.formula)和公式计算值(cell.value)
                        if (typeof cell.formula === 'string') {
                            cell.formula = replaceCellI18nKey(cell.formula);
                        }
                        if (typeof cell.value === 'string') {
                            cell.value = replaceCellI18nKey(cell.value);
                        }
                    }
                    // 场景5:其他可显示文本类型(日期/数字转文本,可选)
                    else if ([ExcelJS.ValueType.Number, ExcelJS.ValueType.Date].includes(cell.type)) {
                        // 如需替换数字/日期格式的文本,可开启(默认关闭,避免误改)
                        // const newText = replaceCellI18nKey(String(cell.value));
                        // if (newText !== String(cell.value)) cell.value = newText;
                    }

                    // 场景4:公式单元格(可选,如需替换公式内文本可开启)
                    // else if (cell.type === ExcelJS.ValueType.Formula && typeof cell.formula === 'string') {
                    //   const newFormula = replaceCellI18nKey(cell.formula);
                    //   if (newFormula !== cell.formula) cell.formula = newFormula;
                    // }
                });
            });
        });

        // 4. ExcelJS生成二进制文件(保留所有样式)
        const excelBuffer = await workbook.xlsx.writeBuffer();
        const blob = new Blob([excelBuffer], {
            type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
        });

        // 5. 下载文件
        saveAs(blob, `${exportFileName}.xlsx`);
        return true;
    } catch (error) {
        console.error('Excel处理失败:', error);
        return false;
    }
}

/**
 * 辅助方法:从服务器获取Excel模板(fetch请求),返回ArrayBuffer
 * @param {string} templateUrl - 服务器Excel模板地址
 */
export async function fetchExcelTemplate(templateUrl) {
    try {
        const response = await fetch(templateUrl);
        if (!response.ok) throw new Error(`模板请求失败,状态码:${response.status}`);
        const arrayBuffer = await response.arrayBuffer();
        if (!arrayBuffer || arrayBuffer.byteLength === 0) throw new Error('模板文件为空');
        return arrayBuffer;
    } catch (error) {
        console.error('获取Excel模板失败:', error);
        return null;
    }
}
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题