electron导入导出多种文件


1. 导入导出 Word

1.1 导入 Word

import fs from "fs";
import path from "path";
import mammoth from "mammoth";

// 导入Word文件
const importWord = () => {
    openDialog("showOpenDialog", {
        title: "导入Word文件",
        properties: ["openFile", "showHiddenFiles"],
        filters: [{ name: ".docx", extensions: ["docx"] }],
    }).then(async ({ filePaths }) => {
        if (filePaths.length) {
            const fext = path.extname(filePaths[0]);
            if (fext === ".docx") {
                try {
                    const text = await importDocx(filePaths[0]);
                    rawWordContent.value = text;
                    ElMessage.success("导入成功");
                } catch (err) {
                    console.error("Word导入错误:", err);
                    ElMessage.error("导入失败,请检查文件格式");
                }
            } else {
                ElMessage.warning("暂不支持该格式,仅支持docx");
            }
        }
    });
};

// 处理docx文件,解析 docx 文件,提取原始纯文
const importDocx = async filePath => {
    // 读取文件为 Buffer
    const buffer = fs.readFileSync(filePath);
    // 使用 mammoth 提取纯文本(保留段落/换行基本格式)
    const result = await mammoth.extractRawText({ buffer });
    // 返回原始文本(无任何表格/结构化处理)
    const text = result.value;
    return text;
};

注意:Word 导入需要安装 mammoth,依赖:npm install mammoth

1.2 导出 Word

import Docxtemplater from "docxtemplater";
import PizZip from "pizzip";
import PizZipUtils from "pizzip/utils/index.js";

// 导出 docx 文件
const exportDocx = () => {
    openDialog("showSaveDialog", {
        title: "导出word文件",
        defaultPath: `结果_${new Date().getTime()}.docx`,
        // 在 Windows 和 Linux 上, 打开对话框不能同时是文件选择器和目录选择器, 因此如果在这些平台上将 properties 设置为["openFile"、"openDirectory"], 则将显示为目录选择器。
        properties: ["openFile"],
        // 文件下载扩展名
        filters: [{ name: ".docx", extensions: ["docx"] }],
        // 点击保存回调
    }).then(({ filePath }) => {
        if (filePath) {
            try {
                /******* 导出表格数据 ********/
                // 检测文件扩展名是否正确
                outFileDocx(filePath);

                /******* 导出为纯文本 ********/
                // 同步导出版本:适合小文本,逻辑更简洁
                fs.writeFileSync(filePath, rawWordContent.value, "utf8");
                ElMessage.success("导出成功!");
                deskNotifnAndOpenFolder("导出结果", filePath);

                // 异步导出版本:写入纯文本
                fs.writeFile(filePath, rawWordContent.value, "utf8", err => {
                    if (err) {
                        console.error("TXT 导出失败:", err);
                        ElMessage.error(`导出失败:${err.message || "写入文件出错"}`);
                    } else {
                        ElMessage.success("Word 纯文本导出为 TXT 成功!");
                        // 可选:打开文件所在文件夹(保持原有逻辑)
                        deskNotifnAndOpenFolder("导出结果", filePath);
                    }
                });
            } catch (err) {
                console.error("同步导出失败:", err);
                ElMessage.error(`导出失败:${err.message}`);
            }
        }
    });
};

const loadFile = (url, callback) => {
    PizZipUtils.getBinaryContent(url, callback);
};

const outFileDocx = async filePath => {
    loadFile("标记导出模版.docx", async (error, content) => {
        if (error) {
            throw error;
        }

        let signExport = [];
        tableData.value.forEach(item => {
            let str = `名称: ${item.text},开始时间:${Number(item.start).toFixed(
                3
            )},结束时间:${Number(item.end).toFixed(3)}`;
            signExport.push(str);
        });

        var zip = new PizZip(content);
        var doc = new Docxtemplater()
            .loadZip(zip)
            .setData({
                list: signExport,
            })
            .render();
        var out = doc.getZip().generate({
            type: "blob",
            mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        });
        const buffer = Buffer.from(await out.arrayBuffer());
        fs.writeFile(filePath, buffer, async err => {
            // 失败
            if (err) {
                ElMessage.error("导出失败");
            } else {
                ElMessage.success("导出成功");
                deskNotifnAndOpenFolder("导出成功", filePath);
            }
        });
    });
};

注意:Word 按照模版导出表格数据需要安装 pizzipdocxtemplater ,依赖:npm install pizzip docxtemplater

其他 word 导出方法,详见Vue 项目中导出 word 的 2 种方法

2. 导入导出 Excel

导入导出 Excel 均需要安装 xlsx ,依赖:npm install xlsx

2.1 导入 Excel

import fs from "fs";
import path from "path";
import { write, utils, read } from "xlsx";
import { snowflake } from "@/utils/class/snowflake.js";

// 导入 excel 文件
const importExcel = () => {
    openDialog("showOpenDialog", {
        title: "导入excel文件",
        // 选择文件, 隐藏文件也显示出来
        properties: ["openFile", "showHiddenFiles"],
        filters: [
            { name: ".xlsx", extensions: ["xlsx"] },
            { name: ".xls", extensions: ["xls"] },
        ],
    }).then(async ({ filePaths }) => {
        if (filePaths.length) {
            const fext = path.extname(filePaths[0]);
            if (fext === ".xlsx" || fext === ".xls") {
                importXlsx(filePaths[0], list => {
                    tableData.value = list;
                });
                ElMessage.success("导入成功");
            } else {
                console.log("暂不支持该格式");
            }
        }
    });
};

// 处理 xlsx
const importXlsx = (filePath, callback) => {
    fs.readFile(filePath, (err, data) => {
        if (err) return;
        // 将文件数据转换为工作簿对象
        const workbook = read(data, { type: "buffer" });
        // 获取工作簿的第一个工作表
        const sheetName = workbook.SheetNames[0];
        const worksheet = workbook.Sheets[sheetName];
        // 将工作表转换为JSON对象
        const jsonData = utils.sheet_to_json(worksheet);
        let list = [];
        for (let index = 0; index < jsonData.length; index++) {
            const element = jsonData[index];
            let params = {
                id: String(snowflake.generateId()),
                start: Number(element["开始时间"]) * 1000,
                end: Number(element["结束时间"]) * 1000,
                text: element["说话内容"],
                speaker: element["说话人"],
            };
            list.push(params);
        }
        callback(list);
    });
};

2.2 导出 Excel

import fs from "fs";
import { write, utils, read } from "xlsx";

// 导出 excel 文件
const exportExcel = () => {
    if (tableData.value.length === 0) return ElMessage.warning("当前没有数据");
    const wopt = {
        bookType: "xlsx",
        bookSST: true,
        type: "array",
    };
    const workBook = {
        SheetNames: ["Sheet1"],
        Sheets: {},
        Props: {},
    };

    let signExport = [];
    tableData.value.forEach(item => {
        signExport.push({
            开始时间: (Number(item.start) / 1000).toFixed(3),
            结束时间: (Number(item.end) / 1000).toFixed(3),
            说话人: item.speaker,
            说话内容: item.text,
        });
    });

    workBook.Sheets["Sheet1"] = utils.json_to_sheet(signExport);
    // 打开选择文件对话框,非模态
    openDialog("showSaveDialog", {
        title: "保存",
        defaultPath: "导出结果.xlsx",
        properties: ["openFile"],
        filters: [{ name: "xlsx", extensions: ["xlsx"] }],
        // 点击保存回调
    }).then(async data => {
        let { canceled, filePath } = data;
        if (!canceled) {
            filePath = filePath.replace(/\//g, "\\");
            let filename = filePath.substring(0, filePath.lastIndexOf("."));
            let excelModel = new Blob([write(workBook, wopt)], {
                type: "application/octet-stream",
            });
            let reader = new FileReader();
            reader.readAsDataURL(excelModel);
            reader.addEventListener("loadend", function () {
                let arg = {
                    baseCode: reader.result,
                    fileType: "excel",
                    filename: filename,
                };
                let dataBuffer = Buffer.from(arg.baseCode.split("base64,")[1], "base64");
                fs.writeFile(filePath, dataBuffer, err => {
                    if (err) {
                        console.error(err);
                        ElMessage.warning("文件被占用,导出失败");
                    } else {
                        ElMessage.success("导出成功");
                        deskNotifnAndOpenFolder("标记导出结果", filePath);
                    }
                });
            });
        }
    });
};

3. 导入导出 Text

3.1 导入 Text

import fs from "fs";
import path from "path";

// 导入 word 文件
const importText = () => {
    openDialog("showOpenDialog", {
        title: "导入word文件",
        properties: ["openFile", "showHiddenFiles"],
        filters: [{ name: ".txt", extensions: ["txt"] }],
    }).then(async ({ filePaths }) => {
        if (filePaths.length) {
            const fext = path.extname(filePaths[0]);
            if (fext === ".txt") {
                importTxt(filePaths[0], rawContent => {
                    rawTextContent.value = rawContent;
                    ElMessage.success("导入成功");
                });
            } else {
                ElMessage.warning("暂不支持该格式");
            }
        }
    });
};

// 处理文本文件
const importTxt = (filePath, callback) => {
    fs.readFile(filePath, "utf8", (err, data) => {
        if (err) {
            console.error("读取文本文件错误:", err);
            ElMessage.error("读取文件失败");
            callback(""); // 出错时返回空字符串,避免回调异常
            return;
        }

        // 直接返回原始内容,不做任何分割/格式化处理
        callback(data);
    });
};

3.2 导出 Text

import fs from "fs";

// 导出文本文件
const exportText = () => {
    // 校验导出内容:为空时提示
    if (!rawTextContent.value || rawTextContent.value.trim() === "") {
        return ElMessage.warning("暂无可导出的文本内容");
    }

    openDialog("showSaveDialog", {
        title: "导出文本文件",
        defaultPath: `文本导出_${new Date().getTime()}.txt`,
        properties: ["openFile"],
        filters: [{ name: ".txt", extensions: ["txt"] }],
    }).then(async ({ filePath, canceled }) => {
        if (canceled || !filePath) return; // 用户取消保存则退出

        // 异步写入:避免阻塞主线程,大文件
        try {
            // 直接写入原始文本内容
            fs.writeFile(filePath, rawTextContent.value, "utf8", err => {
                if (err) {
                    console.error("导出 TXT 失败:", err);
                    ElMessage.error(`导出失败:${err.message || "未知错误"}`);
                } else {
                    ElMessage.success("文本导出成功");
                    // 可选:打开文件所在文件夹(保持原有逻辑)
                    deskNotifnAndOpenFolder("导出结果", filePath);
                }
            });
        } catch (err) {
            console.error("导出 TXT 异常:", err);
            ElMessage.error(`导出异常:${err.message || "未知错误"}`);
        }

        // 同步写入:适合小文件,逻辑更简洁
        try {
            fs.writeFileSync(filePath, rawTextContent.value, "utf8");
            ElMessage.success("文本导出成功");
            deskNotifnAndOpenFolder("导出结果", filePath);
        } catch (err) {
            console.error("同步导出 TXT 失败:", err);
            ElMessage.error(`导出失败:${err.message || "未知错误"}`);
        }
    });
};

4. 导入导出 TextGrid

4.1 导入 TextGrid

import fs from "fs";
import path from "path";
import { snowflake } from "@/utils/class/snowflake.js";

// 导入 TextGrid 文件
const importTextGrid = () => {
    // 打开选择文件对话框,非模态
    openDialog("showOpenDialog", {
        title: "textgrid文件",
        // 选择文件, 隐藏文件也显示出来
        properties: ["openFile", "showHiddenFiles"],
        filters: [{ name: "textgrid", extensions: ["textgrid"] }],
    }).then(async ({ filePaths }) => {
        if (filePaths.length) {
            const fext = path.extname(filePaths[0]);
            if (fext === ".textgrid") {
                const result = parseTextGrid(filePaths[0]);
                if (result) {
                    tableData.value = result?.items || [];
                } else {
                    ElMessage.warning("解析TextGrid文件时出错");
                }
            } else {
                console.log("暂不支持该格式");
            }
        }
    });
};

const parseTextGrid = filePath => {
    try {
        // 读取文件内容
        const content = fs.readFileSync(filePath, "utf8");
        const lines = content.split("\n").map(line => line.trim());

        // 存储解析结果
        const result = {
            fileName: "",
            xmin: 0,
            xmax: 0,
            size: 0,
            intervals: 0,
            items: [],
        };

        // 标记是否在item块中
        let inItemBlock = false;
        let currentItem = null;
        let xminFirst = true;
        let xmaxFirst = true;
        // 解析内容
        lines.forEach(line => {
            // 匹配文件基本信息
            if (line.startsWith("File name =")) {
                result.fileName = line.split("=")[1].trim().replace(/"/g, "");
            } else if (line.startsWith("xmin =") && xminFirst) {
                result.xmin = parseFloat(line.split("=")[1].trim());
                xminFirst = false;
            } else if (line.startsWith("xmax =") && xmaxFirst) {
                result.xmax = parseFloat(line.split("=")[1].trim());
                xmaxFirst = false;
            } else if (line.startsWith("size =")) {
                result.size = parseInt(line.split("=")[1].trim());
            } else if (line.startsWith("intervals =")) {
                result.intervals = parseInt(line.split("=")[1].trim());
            }
            // 检测item块开始
            else if (line.startsWith("item []:")) {
                inItemBlock = true;
            }
            // 解析intervals条目
            else if (inItemBlock) {
                // 匹配intervals [数字]: 格式
                const intervalMatch = line.match(/^intervals\s*\[\s*(\d+)\s*\]\s*:/i);
                if (intervalMatch) {
                    // 如果已有当前item,先保存
                    if (currentItem) {
                        result.items.push(currentItem);
                    }
                    // 创建新的item对象
                    currentItem = {
                        id: "region_" + String(snowflake.generateId()),
                        text: "",
                        speaker: "",
                        start: 0,
                        end: 0,
                    };
                }
                // 解析item内部属性
                else if (currentItem) {
                    if (line.startsWith("xmin =")) {
                        currentItem.start = parseFloat(line.split("=")[1].trim());
                    } else if (line.startsWith("xmax =")) {
                        currentItem.end = parseFloat(line.split("=")[1].trim());
                    } else if (line.startsWith("text =")) {
                        const textPart = line.split("=", 2)[1].trim();
                        currentItem.text = textPart.replace(/^["'](.*)["']$/, "$1");
                    } else if (line.startsWith("speaker =")) {
                        const speakerPart = line.split("=", 2)[1].trim();
                        currentItem.speaker = speakerPart.replace(/^["'](.*)["']$/, "$1");
                    }
                }
            }
        });

        // 添加最后一个item
        if (currentItem) {
            result.items.push(currentItem);
        }
        return result;
    } catch (error) {
        console.error("解析TextGrid文件时出错:", error);
        return null;
    }
};

4.2 导出 TextGrid

import fs from "fs";
import path from "path";

// 导出 TextGrid 文件
const exportTextGrid = () => {
    if (tableData.value.length === 0) return ElMessage.warning("当前没有数据");
    // 打开选择文件对话框,非模态
    openDialog("showSaveDialog", {
        title: "导出textgrid文件",
        defaultPath: "结果_" + new Date().getTime() + ".textgrid",
        properties: ["openFile"],
        filters: [{ name: "textgrid", extensions: ["textgrid"] }],
        // 点击保存回调
    }).then(async data => {
        const { canceled, filePath } = data;
        if (!canceled) {
            let sortData = tableData.value.sort((a, b) => {
                return a.start - b.start;
            });
            let tempData = [];
            for (let i = 0; i < sortData.length; i++) {
                let el = sortData[i];
                let el1 = "";
                if (i !== sortData.length - 1) {
                    el1 = sortData[i + 1];
                }
                if (el1 && el.end !== el1.start) {
                    tempData.push(
                        {
                            xmin: Number(el.start).toFixed(3),
                            xmax: Number(el.end).toFixed(3),
                            text: el.text,
                            speaker: el.speaker,
                        },
                        {
                            xmin: Number(el.end).toFixed(3),
                            xmax: Number(el1.start).toFixed(3),
                            text: "",
                            speaker: "",
                        }
                    );
                } else {
                    tempData.push({
                        xmin: Number(el.start).toFixed(3),
                        xmax: Number(el.end).toFixed(3),
                        text: el.text,
                        speaker: el.speaker,
                    });
                }
            }
            let textGridContent = await convertToTextGrid(tempData, filePath);
            var textContent = new Blob([textGridContent], {
                type: "text/plain",
            });
            let reader = new FileReader();
            reader.readAsDataURL(textContent);
            reader.addEventListener("loadend", function () {
                let arg = {
                    baseCode: reader.result,
                    fileType: "textgrid",
                };
                let dataBuffer = Buffer.from(arg.baseCode.split("base64,")[1], "base64");
                fs.writeFile(filePath, dataBuffer, err => {
                    if (err) {
                        ElMessage.warning("文件被占用,导出失败");
                    } else {
                        ElMessage.success("导出成功");
                        deskNotifnAndOpenFolder("导出结果", filePath);
                    }
                });
            });
        }
    });
};

// 处理 TextGrid 格式
const convertToTextGrid = (data, filePath) => {
    const size = countTargetTypes(tableData.value);
    let xmin = Math.min(...data.map(el => el.xmin));
    let xmax = Math.max(...data.map(el => el.xmax));
    const fileName = path.basename(filePath, ".wav");
    let textGridContent = `File name = "${fileName}"\n`;
    textGridContent += 'File type = "ooTextFile"\n';
    textGridContent += 'Object class = "TextGrid"\n\n';
    textGridContent += `xmin = ${xmin}\n`;
    textGridContent += `xmax = ${xmax}\n`;
    textGridContent += "tiers? <exists>\n";
    textGridContent += `size = ${size}\n`;
    textGridContent += `intervals = ${data.length}\n`;
    textGridContent += "item []:\n";
    for (let i = 0; i < data.length; i++) {
        textGridContent += `        intervals [${i + 1}]:\n`;
        textGridContent += `            xmin = ${data[i].xmin}\n`;
        textGridContent += `            xmax = ${data[i].xmax}\n`;
        textGridContent += `            text = \"${data[i].text}\"\n`;
        textGridContent += `            speaker = \"${data[i].speaker}\"\n`;
    }
    return textGridContent;
};

// 统计target值的种类总数
function countTargetTypes(arr) {
    const uniqueTargets = new Set();
    arr.forEach(item => {
        uniqueTargets.add(item.speaker);
    });
    return uniqueTargets.size;
}

5. electron 导出的 2 种方法:主进程操作和渲染进程操作

5.1 渲染进程(页面)处理文件的保存操作

// electron 主进程
import { ipcMain, dialog } from "electron";

ipcMain.handle("openDialog", (event, ...args) => {
    const [type, ...option] = args;
    return dialog[type](win, ...option);
});

封装的openDialog

import { ipcRenderer } from "electron";

/***
 * 调用原生文件对话框
 * @param type 'showOpenDialog', 'showSaveDialog'
 * @param option 详见electron dialog https://www.electronjs.org/zh/docs/latest/api/dialog
 * @returns {Promise<any>|null}
 */
export function openDialog(type, option) {
    const types = ["showOpenDialog", "showSaveDialog"];
    if (!types.includes(type)) {
        console.log("打开对话框的类型,不在当前定义范围内");
        return new Promise((_, reject) => reject("打开对话框的类型,不在当前定义范围内"));
    }
    return ipcRenderer.invoke("openDialog", type, option);
}
import fs from "fs";
import { openDialog } from "@/utils/dialogUtils";

const exportTextGrid = () => {
    openDialog("showSaveDialog", {
        title: "导出文件",
        defaultPath: "结果_" + new Date().getTime() + ".textgrid",
        properties: ["openFile"],
        filters: [{ name: "textgrid", extensions: ["textgrid"] }],
        // 点击保存回调
    }).then(async data => {
        const { canceled, filePath } = data;
        if (!canceled) {
            let textGridContent = tableData.value;
            var textContent = new Blob([textGridContent], {
                type: "text/plain",
            });
            let reader = new FileReader();
            reader.readAsDataURL(textContent);
            reader.addEventListener("loadend", function () {
                let arg = {
                    baseCode: reader.result,
                    fileType: "textgrid",
                };
                let dataBuffer = Buffer.from(arg.baseCode.split("base64,")[1], "base64");
                fs.writeFile(filePath, dataBuffer, err => {
                    if (err) {
                        ElMessage.warning("文件被占用,导出失败");
                    } else {
                        ElMessage.success("导出成功");
                        deskNotifnAndOpenFolder("导出结果", filePath);
                    }
                });
            });
        }
    });
};

5.2 在主进程处理文件的保存操作

// electron 主进程
import { ipcMain, dialog } from "electron";
import fs from "fs";

// 导出 xlsx 等文件
ipcMain.on("saveDialog", (event, arg) => {
    const extensionType = {
        excel: [
            { name: ".xlsx", extensions: ["xlsx"] },
            { name: ".xls", extensions: ["xls"] },
        ],
        textgrid: [{ name: ".textgrid", extensions: ["textgrid"] }],
        docx: [
            { name: ".docx", extensions: ["docx"] },
            { name: ".doc", extensions: ["doc"] },
        ],
        txt: [{ name: ".txt", extensions: ["txt"] }],
    };
    try {
        const fileInfo = arg;
        if (isLinux()) {
            if (
                !fileInfo["filename"].endsWith(extensionType[arg.fileType][0].name) &&
                !fileInfo["filename"].endsWith(extensionType[arg.fileType][1].name)
            ) {
                fileInfo["filename"] = fileInfo["filename"] + extensionType[arg.fileType][0].name;
            }
        }
    } catch (e) {
        console.log(e);
    }
    dialog
        .showSaveDialog(win, {
            title: "导出",
            // 在 Windows 和 Linux 上, 打开对话框不能同时是文件选择器和目录选择器, 因此如果在这些平台上将 properties 设置为["openFile"、"openDirectory"], 则将显示为目录选择器。
            properties: ["openFile"],
            // 默认情况下使用的绝对目录路径、绝对文件路径、文件名
            defaultPath: arg.filename,
            // 文件下载扩展名
            filters: [...extensionType[arg.fileType]],
            // 点击保存回调
        })
        .then(({ filePath, canceled }) => {
            // filePath存在则为保存路径 否为undefined
            // 去掉头部无用字段并将base64转码成buffer
            // console.log('ilePath存在则为保存路径 否为undefined', filePath)
            if (filePath) {
                let dataBuffer = Buffer.from(arg.baseCode.split("base64,")[1], "base64");
                // 检测文件扩展名是否正确
                let typeFlag = extensionType[arg.fileType].some(
                    item => item.extensions[0] === filePath.substring(filePath.lastIndexOf(".") + 1)
                );
                if (typeFlag) {
                    fs.writeFile(filePath, dataBuffer, err => {
                        // 失败
                        if (err) {
                            // 向渲染进程发送消息通知失败
                            win.webContents.send("defeatedDialog");
                        }
                    });
                    // 成功 向渲染进程发送消息通知成功
                    win.webContents.send("succeedDialog", filePath);
                    // 判断是否存在保存路径
                } else if (filePath !== undefined) {
                    dialog.showMessageBox({
                        type: "error",
                        title: "系统提示",
                        message: "系统检测出文件类型异常,请检查并重新选择或填写",
                    });
                }
            } else if (canceled) {
                win.webContents.send("canceledDialog");
            }
        });
});
// 渲染进程
import path from "path";
const { ipcRenderer } = require("electron");

// 导出
const exportTextGrid = () => {
    let textGridContent = tableData.value;
    let file = new Blob([textGridContent], { type: "text/plain" });

    let reader = new FileReader();
    reader.readAsDataURL(file);
    reader.addEventListener("loadend", function () {
        ipcRenderer.send("saveDialog", {
            baseCode: reader.result,
            fileType: "textgrid",
            filename: "结果_" + new Date().getTime() + ".textgrid",
        });
        ipcRenderer.once("succeedDialog", async (event, filePath) => {
            ElMessage.success("导出成功");
            deskNotifnAndOpenFolder("表格导出", filePath);
        });
        ipcRenderer.once("defeatedDialog", () => {
            ElMessage.error("导出失败");
        });
    });
};

文章作者: 弈心
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 弈心 !
评论
  目录