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 按照模版导出表格数据需要安装
pizzip和docxtemplater,依赖: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("导出失败");
});
});
};