我是将这个放到WPS的加载项中,可以跨文档调用一些通用方法。
/*
方法目录
注意:加载项中的这些方法,主要用于js宏运行中的方法调用公共模块。其中Excompute_开头的是计算函数,在Excel表格中可以 = 方法名直接当普通函数使用
Folder_getAllFiles(folderPath) 遍历目标文件夹,返回所有文件路径的数组
Folder_deleteAllFolderAndFiles(folderPath) 清空文件所有内容(包括文件和子文件夹)
Folder_splitTable(sheetName, colNumber, outputPath) 按指定列拆分数据为独立文件
Folder_mergeAllTables(folderPath,sheetName) 合并指定文件下所有表格,未指定时默认第一个sheet页,无返回值
Folder_selectFile(title, filter) 通用文件选择器,获取文件路径
Excel_deleteEmptyRows(ws1) 删除工作表中的所有空白行,参数为工作表对象,无返回值
Excel_getRealLastRowAndCol(ws1, number) 获取表格真实的最后一行或最后一列(排除合并单元格干扰),第二参数为1,返回最后一行,其他值返回最后一列
Excel_deleteRowsByColumnValues(sheetName, col, strArray) 删除指定列中包含目标字符串数组中任意值的行,参数2:要检查的列标识(例如:"A"、"B")。参数3:用于匹配删除行的字符串数组
Excel_filterByDateRange(sheetName, colNumber, startDate, endDate) 保留指定日期范围内的行,删除范围外的行,参数4,为结束日期(格式:yyyy-mm-dd,可选,默认当前日期0点)
Excompute_dateStrToDateValue(str) 将"yyyy-mm-dd"或"yyyy-mm-dd hh:MM:ss"字符串转换为Excel日期序列号, 返回Excel日期序列号
Excompute_dateValueToDateStr(excelValue) 将Excel日期序列号转换为字符串,返回字符串(纯日期:"yyyy-mm-dd";带时间:"yyyy-mm-dd hh:MM:ss")
scheduleTask(funcName, delayMs) 定时执行任务,参数为要执行的函数名,延迟毫秒数
Word_getRangeStartIndex(filePath, text, type) 在指定路径的Word文档中查找目标文字,返回第一个匹配项的位置索引,参数3:1=返回起始位置,2=返回结束位置
Word_deleteEmptyLines(filePath) 删除指定路径的Word文档中的空白行
insertColoredText(mergeDoc, text, colors) Word文档中插入带有指定颜色的文本(支持单个字符设置不同颜色)
createWordApp(visible = true) 创建Word应用实例,参数默认为 true
generateDocuments(keepHeaders, 1) 表格所有数据生成一个合并Word文档,支持单元格多种文字颜色
generateDocuments(keepHeaders, generateMode) 按指定列的值作为文件名,每行生成一个Word文件,支持单元格多种文字颜色
generateTextDocuments(keepHeaders, 1) 表格所有数据生成一个合并Word文档,纯文本输出,数据更快
generateTextDocuments(keepHeaders, generateMode) 按指定列的值作为文件名,每行生成一个Word文件,纯文本输出,数据更快
batchGenerateWord() 从Excel读取数据,批量生成Word文档(替换模板占位符,文本输出)
bgrToRgb(bgr) 将表格BGR颜色格式转换为Word的RGB颜色格式
Excel_getRange(ws) 种方式定义Range对象,方法无返回值,学习或直接复制使用
Excel_iterateRange(ws) 多种方式遍历Range对象,方法无返回值,学习或直接复制使用
Excel_setHeaderFormat(sheetName) 关于字体、背景色、边框等格式设置,及清除格式,学习或直接复制使用
Word_functions() 关于Word文档的一些对象及方法使用,学习或直接复制使用
*/
//----------------------------------------------------操作文件类方法,统一前缀:Folder_ ,调用时用Application.Run("方法名", 参数1,参数2);--------------------------------------------------------------
/*
* 遍历目标文件夹,返回所有文件路径的数组
* @param {string} folderPath - 目标文件夹路径
* @returns {Array<string>} - 文件路径数组
*/
function Folder_getAllFiles(folderPath) {
// 检查根路径是否存在
if (Dir(folderPath, 16) === "") {
console.error("根路径不存在:", folderPath);
return []; // 返回空数组而非undefined
}
const fileArray = [];
walk(folderPath, fileArray);
return fileArray;
// 递归遍历文件夹
function walk(path, array) {
try {
// 规范化路径(确保以\结尾)
if (!path.endsWith('\\')) {
path += '\\';
}
let fileName = Dir(path, 16); // 先查找子文件夹
const subFolders = [];
// 遍历当前层级的所有项
while (fileName) {
if (fileName !== '.' && fileName !== '..') {
const fullPath = path + fileName;
const attributes = GetAttr(fullPath);
// 正确判断文件夹:使用位运算检查属性是否包含文件夹标志(16)
if ((attributes & 16) === 16) {
subFolders.push(fullPath);
//console.log("找到子文件夹:", fullPath);
} else {
array.push(fullPath);
//console.log("找到文件:", fullPath);
}
}
fileName = Dir(); // 获取下一项
}
// 递归处理子文件夹
for (const folder of subFolders) {
walk(folder + '\\', array);
}
} catch (error) {
console.error("遍历出错:", error.message, "路径:", path);
// 继续执行而非中断整个遍历
}
}
}
/**
* 清空文件所有内容(包括文件和子文件夹)
* @param {string} folderPath - 目标文件夹路径
*/
function Folder_deleteAllFolderAndFiles(folderPath) {
// 规范化根路径(确保结尾是反斜杠,避免拼接错误)
const rootPath = folderPath.endsWith('\\') ? folderPath : folderPath + '\\';
try {
// 检查并创建根文件夹
if (Dir(rootPath, 16) === "") {
alert("根路径不存在,已创建:" + rootPath);
MkDir(rootPath);
return;
} else {
console.log("根路径已存在,开始清空内容:" + rootPath);
}
// 收集所有子文件夹路径(用于后续删除空文件夹)
const subFolders = [];
// 递归遍历并删除所有文件和收集子文件夹
walk(rootPath, subFolders);
// 按路径深度从深到浅排序(确保先删深层文件夹)
subFolders.sort((a, b) => {
const depthA = (a.match(/\\/g) || []).length; //字符串.match(正则表达式);在字符串中查找与正则表达式匹配的内容,并返回匹配结果,返回一个数组或NULL
const depthB = (b.match(/\\/g) || []).length;
return depthB - depthA; // 降序排序
});
// 删除所有空的子文件夹
let deletedCount = 0;
for (let i = 0; i < subFolders.length; i++) {
if (isFolderEmpty(subFolders[i])) {
try {
RmDir(subFolders[i]);
console.log("已删除空文件夹:" + subFolders[i]);
deletedCount++;
} catch (e) {
console.error("删除文件夹失败:" + subFolders[i] + ",原因:" + e.message);
}
} else {
console.log("跳过非空文件夹:" + subFolders[i]);
}
}
alert(`操作完成\n- 已删除所有文件\n- 已删除 ${deletedCount} 个空文件夹\n- 路径:${rootPath}`);
} catch (e) {
alert("执行失败:" + e.message + "\n路径:" + rootPath);
}
/**
* 递归遍历文件夹,删除所有文件并收集子文件夹路径
* @param {string} path - 当前遍历的文件夹路径
* @param {Array} folderArray - 用于收集子文件夹路径的数组
*/
function walk(path, folderArray) {
try {
// 确保路径以反斜杠结尾
const currentPath = path.endsWith('\\') ? path : path + '\\';
let itemName = Dir(currentPath, 16); // 16=只查找文件夹
const childFolders = []; // 临时存储当前层级的子文件夹
// 第一次遍历:收集子文件夹和删除文件
while (itemName) {
if (itemName !== '.' && itemName !== '..') {
const fullPath = currentPath + itemName;
const attributes = GetAttr(fullPath);
// 判断是否为文件夹(属性16表示文件夹)
if ((attributes & 16) === 16) {
childFolders.push(fullPath); // 记录子文件夹
folderArray.push(fullPath); // 加入全局数组
console.log("发现子文件夹:" + fullPath);
} else {
// 处理文件:删除(含只读文件处理)
deleteFile(fullPath);
}
}
itemName = Dir(); // 继续遍历下一项
}
// 递归处理子文件夹
for (let i = 0; i < childFolders.length; i++) {
walk(childFolders[i], folderArray);
}
} catch (e) {
console.error("遍历出错:" + e.message + ",路径:" + path);
}
}
/**
* 删除文件(处理只读属性和异常)
* @param {string} filePath - 要删除的文件路径
*/
function deleteFile(filePath) {
try {
// 检查文件是否为只读
const attributes = GetAttr(filePath);
if ((attributes & 1) === 1) { // 1=只读属性
SetAttr(filePath, 0); // 移除只读属性
console.log("已移除只读属性:" + filePath);
}
Kill(filePath);
console.log("已删除文件:" + filePath);
} catch (e) {
console.error("删除文件失败:" + filePath + ",原因:" + e.message);
}
}
/**
* 判断文件夹是否为空(不含文件和子文件夹)
* @param {string} folderPath - 要检查的文件夹路径
* @returns {boolean} 空则返回true,否则返回false
*/
function isFolderEmpty(folderPath) {
const path = folderPath.endsWith('\\') ? folderPath : folderPath + '\\';
let item = Dir(path + "*.*"); // 查找所有类型的项目
while (item) {
if (item !== '.' && item !== '..') {
// 存在有效项目(文件或子文件夹)
return false;
}
item = Dir(); // 继续查找
}
return true; // 为空文件夹
}
}
/**
* 按指定列拆分数据为独立文件
* @param {string} sheetName - 源数据表名
* @param {number} colNumber - 用于拆分的列号(1开始,如1=A列)
* @param {string} outputPath - 输出文件夹路径
*/
function Folder_splitTable(sheetName, colNumber, outputPath) {
try {
// 1. 验证源工作表
const sourceSheet = ThisWorkbook.Worksheets(sheetName);
if (!sourceSheet) {
throw new Error(`未找到工作表:${sheetName}`);
}
// 2. 获取源数据范围,自定义方法
const lastRow = Excel_getRealLastRowAndCol(sourceSheet, 1);
const lastCol = Excel_getRealLastRowAndCol(sourceSheet, 2);
if (lastRow < 2) {
throw new Error("源表至少需要表头和1行数据");
}
const colLetter = String.fromCharCode(64 + lastCol); // String.fromCharCode(65) → 返回 "A"(因为 A 的 Unicode 编码是 65)
// 3. 处理输出路径
const safeOutputPath = outputPath.endsWith('\\') ? outputPath : outputPath + '\\';
Folder_deleteAllFolderAndFiles(safeOutputPath); // 确保文件夹存在并清空,自定义方法
// 4. 提取拆分列的唯一值(用临时表去重)
const tempSheet = ThisWorkbook.Worksheets.Add();
tempSheet.Name = "临时表";
// 复制拆分列数据
sourceSheet.Range(
sourceSheet.Cells(1, colNumber),
sourceSheet.Cells(lastRow, colNumber)
).Copy(tempSheet.Range("A1"));
tempSheet.Columns(1).RemoveDuplicates(1, xlYes); // 去重
const uniqueCount = tempSheet.Cells(tempSheet.Rows.Count, 1).End(xlUp).Row;
if (uniqueCount < 2) {
throw new Error("拆分列无有效数据(至少需要1个分类值)");
}
// 5. 准备日期(用于文件名)
const today = new Date();
const dateStr = `${today.getFullYear()}.${String(today.getMonth() + 1).padStart(2, '0')}.${String(today.getDate()).padStart(2, '0')}`;
// 6. 按唯一值拆分数据
Application.DisplayAlerts = false;
let fileCount = 0;
for (let i = 2; i <= uniqueCount; i++) { // 从第2行开始(跳过表头)
const splitValue = tempSheet.Cells(i, 1).Value2;
if (!splitValue) continue; // 跳过空值
// 处理文件名(移除非法字符)
const safeValue = String(splitValue).replace(/[\\/:*?"<>|]/g, "");
const savePath = `${safeOutputPath}${safeValue}_数据_${dateStr}.xlsx`;
// 创建新工作簿并复制表头
const newWorkbook = Workbooks.Add();
const newSheet = newWorkbook.Worksheets(1);
sourceSheet.Range(`A1:${colLetter}1`).Copy(newSheet.Range("A1")); // 复制表头
// 用自动筛选批量复制数据(高效)
// sourceSheet.Range(); 定位需要筛选的数据范围
// .AutoFilter(colNumber, splitValue); Excel 对象模型的筛选方法,作用是对上面的范围启用自动筛选,并设置筛选条件
sourceSheet.Range(`A1:${colLetter}${lastRow}`).AutoFilter(colNumber, splitValue);
try {
// 复制筛选后的可见行(跳过表头),即选中筛选符合条件的数据行复制到目标表格
sourceSheet.Range(`A2:${colLetter}${lastRow}`)
.SpecialCells(xlCellTypeVisible)
.Copy(newSheet.Range("A2"));
} catch (e) {
console.log(`分类【${splitValue}】无匹配数据,已跳过`);
}
sourceSheet.AutoFilterMode = false; // 取消筛选
// 保存并关闭
newWorkbook.SaveAs(savePath);
newWorkbook.Close();
fileCount++;
}
// 7. 清理临时表
tempSheet.Delete();
Application.DisplayAlerts = true;
alert(`拆分完成,共生成 ${fileCount} 个文件\n路径:${safeOutputPath}`);
} catch (e) {
Application.DisplayAlerts = true;
alert(`拆分失败:${e.message}`);
}
}
/**
* 合并指定文件夹下所有表格
* @param {string} folderPath - 目标文件夹路径
* @param {string} [sheetName] - 要合并的工作表名称(可选,默认使用第一个工作表)
*/
function Folder_mergeAllTables(folderPath, sheetName) {
try {
// 1. 获取目标文件夹中的所有文件(依赖自定义方法 Folder_getAllFiles)
const findFolderArray = Folder_getAllFiles(folderPath);
if (findFolderArray.length === 0) {
throw new Error("目标文件夹中未找到任何文件,请检查路径是否正确,比如: \ 问题");
}
// 2. 创建合并结果工作簿并新建工作表
const newWork = Workbooks.Add();
const ws = newWork.Worksheets;
const ws1 = ws.Add();
ws1.Name = "合并结果";
// 3. 初始化变量(记录当前合并位置、是否为第一个文件的表头)
let currentStartRow = 1; // 合并表中当前待粘贴的起始行
let isFirstFile = true; // 标记是否为第一个文件(用于保留一次表头)
// 4. 遍历所有文件并合并
for (let i = 0; i < findFolderArray.length; i++) {
const filePath = findFolderArray[i];
console.log(`正在处理第 ${i + 1} 个文件:${filePath}`);
// 打开当前工作簿(捕获打开失败的情况)
let workBook;
try {
workBook = Workbooks.Open(filePath);
} catch (e) {
console.warn(`文件【${filePath}】打开失败,已跳过:${e.message}`);
continue;
}
// 获取目标工作表(指定sheetName或默认第一个)
let targetSheet;
try {
targetSheet = sheetName
? workBook.Worksheets(sheetName) // 使用指定工作表
: workBook.Worksheets(1); // 默认使用第一个工作表(索引1)
} catch (e) {
workBook.Close(false); // 不保存关闭
console.warn(`文件【${filePath}】中未找到目标工作表,已跳过:${e.message}`);
continue;
}
// 获取目标工作表的有效数据范围(使用自定义方法)
const lastCol = Excel_getRealLastRowAndCol(targetSheet, 2); // 最后一列
const lastRow = Excel_getRealLastRowAndCol(targetSheet, 1); // 最后一行
// 确定复制的起始行:第一个文件保留表头(从1开始),后续文件跳过表头(从2开始)
const copyStartRow = isFirstFile ? 1 : 2;
// 复制数据(注意:Cells必须指定所属工作表,否则可能引用错误)
const sourceRange = targetSheet.Range(
targetSheet.Cells(copyStartRow, 1), // 起始单元格(指定所属工作表)
targetSheet.Cells(lastRow, lastCol) // 结束单元格(指定所属工作表)
);
// 粘贴到合并表的当前位置
sourceRange.Copy(ws1.Cells(currentStartRow, 1));
// 更新下一次粘贴的起始行(当前行 + 本次复制的行数)
currentStartRow += (lastRow - copyStartRow + 1);
// 第一个文件处理完毕,后续文件不再保留表头
isFirstFile = false;
// 关闭当前工作簿(不保存原文件,避免意外修改)
workBook.Close(false);
console.log(`文件【${filePath}】处理完成`);
}
// 5. 保存合并结果
const savePath = `${folderPath}\\合并表格.xlsx`;
newWork.SaveAs(savePath);
// newWork.Close(true); // 根据需要决定是否自动关闭
alert(`合并完成!共处理 ${findFolderArray.length} 个文件,结果已保存至:\n${savePath}`);
} catch (e) {
alert(`合并失败:${e.message}`);
console.error("合并过程出错:", e);
}
}
//----------------------------------------------------操作Excel类方法,统一前缀:Ecxel_ ,调用时用Application.Run("方法名", 参数1,参数2);--------------------------------------------------------------
/**
* 删除工作表中的所有空白行
* @param {Worksheet} ws1 - 要处理的工作表对象
* @returns {void} 直接修改传入的工作表,不返回值
*/
function Excel_deleteEmptyRows(ws1) {
// 获取工作表中使用的最后一行(基于A列)
var lastRow = ws1.Cells(ws1.Rows.Count, 1).End(xlUp).Row;
// 从最后一行向上遍历,确保删除行不会影响后续索引
for (let y = lastRow; y > 0; y--) {
// 使用 CountA 函数检查整行是否没有任何内容
if (Application.WorksheetFunction.CountA(ws1.Rows(y)) === 0) {
// 如果整行为空,则删除该行
ws1.Rows(y).Delete();
}
}
}
/**
* 获取表格真实的最后一行或最后一列(排除合并单元格干扰)
* @param {Worksheet} ws1 - 要处理的工作表对象
* @param {number} number - 1=返回最后一行,其他值=返回最后一列
* @returns {number} 最后一行行号或最后一列列号
*/
function Excel_getRealLastRowAndCol(ws1, number) {
if (!ws1) {
throw new Error("未传入有效的工作表对象");
}
//UsedRange属性:快速定位工作表中 “有实际数据的范围”
//这个属性有个缺点,就算是空表lastRow和lastCol不会是0,而是1
const usedRange = ws1.UsedRange;
const maxColInUsed = usedRange.Columns.Count;
const maxRowInUsed = usedRange.Rows.Count;
let lastRow = 0;
let lastCol = 0;
// 计算最后一行
for (let col = 1; col <= maxColInUsed; col++) {
const colLastRow = ws1.Cells(ws1.Rows.Count, col).End(xlUp).Row;
if (colLastRow > lastRow) {
lastRow = colLastRow;
}
}
// 计算最后一列
for (let row = 1; row <= maxRowInUsed; row++) {
const rowLastCol = ws1.Cells(row, ws1.Columns.Count).End(xlToLeft).Column;
if (rowLastCol > lastCol) {
lastCol = rowLastCol;
}
}
return number === 1 ? lastRow : lastCol;
}
/**
* 通用:将"yyyy-mm-dd"或"yyyy-mm-dd hh:MM:ss"字符串转换为Excel日期序列号
* @param {string} str - 日期字符串(纯日期或带时间)
* @returns {number} Excel日期序列号(含时间则带小数)
*/
function Excompute_dateStrToDateValue(str) {
// 拆分日期和时间(无时间则默认00:00:00)
const parts = str.split(' ');
const datePart = parts[0];
const timePart = parts[1] || "00:00:00"; // 无时间则补全
// 解析日期部分(年、月、日)------map(Number) 是 JavaScript 数组的一个常用方法,作用是将数组中的每个元素转换为数字类型
const [year, month, day] = datePart.split('-').map(Number);
// 解析时间部分(时、分、秒)
const [hours, minutes, seconds] = timePart.split(':').map(Number);
// 验证格式有效性---------------isNaN 是 JavaScript 的内置函数,全称是 “Is Not a Number”,作用是检查一个值是否不是有效的数字
if (
isNaN(year) || isNaN(month) || isNaN(day) ||
isNaN(hours) || isNaN(minutes) || isNaN(seconds) ||
month < 1 || month > 12 || day < 1 || day > 31 ||
hours < 0 || hours > 23 || minutes < 0 || minutes > 59 || seconds < 0 || seconds > 59
) {
throw new Error(`无效格式:${str},请使用"yyyy-mm-dd"或"yyyy-mm-dd hh:MM:ss"`);
}
// 构造UTC时间(自动处理纯日期/带时间)
const utcDateTime = new Date(Date.UTC(
year,
month - 1, // 月份0-based
day,
hours,
minutes,
seconds
));
if (isNaN(utcDateTime.getTime())) {
throw new Error(`无效日期:${str}`);
}
// 转换为Excel序列号
return utcDateTime.getTime() / (24 * 60 * 60 * 1000) + 25569;
}
/**
* 通用:将Excel日期序列号转换为字符串(带时间则显示,否则仅日期)
* @param {number} excelValue - Excel日期序列号
* @returns {string} 字符串(纯日期:"yyyy-mm-dd";带时间:"yyyy-mm-dd hh:MM:ss")
*/
function Excompute_dateValueToDateStr(excelValue) {
if (typeof excelValue !== 'number' || isNaN(excelValue)) { //typeof 是 JavaScript 的运算符,用于判断变量的类型(如 number、string、undefined 等)。
return "";
}
// 转换为UTC时间戳
const utcTimestamp = (excelValue - 25569) * 24 * 60 * 60 * 1000;
const utcDate = new Date(utcTimestamp); //Fri Jul 18 2025
// 提取UTC日期和时间
const year = utcDate.getUTCFullYear(); // 返回 UTC 时间的年份(四位数,如 2025)
const month = String(utcDate.getUTCMonth() + 1).padStart(2, '0'); //返回 UTC 时间的月份(0-11,0 表示 1 月,11 表示 12 月);+ 1 将其转换为正常月份(1-12)
const day = String(utcDate.getUTCDate()).padStart(2, '0'); //String() 将数字转为字符串; padStart(2, '0') 在字符串前面补零,确保长度为 2(如 7 → 07)
const hours = String(utcDate.getUTCHours()).padStart(2, '0');
const minutes = String(utcDate.getUTCMinutes()).padStart(2, '0');
const seconds = String(utcDate.getUTCSeconds()).padStart(2, '0');
// 判断是否有时间(小时、分、秒均为0则仅返回日期)
const hasTime = !(hours === "00" && minutes === "00" && seconds === "00");
return hasTime
? `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
: `${year}-${month}-${day}`;
}
/*
* 删除指定列中包含目标字符串数组中任意值的行
* @param {string} sheetName - 要操作的工作表名称
* @param {string} col - 要检查的列标识(例如:"A"、"B")
* @param {Array<string>} strArray - 用于匹配删除行的字符串数组
*/
function Excel_deleteRowsByColumnValues(sheetName, col, strArray) {
// 获取目标工作表对象
var ws = ThisWorkbook.Worksheets(sheetName);
// 根据指定列确定最后一行数据
var lastRow = ws.Cells(ws.Rows.Count, col).End(xlUp).Row;
// 存储要删除的行号(从下往上收集)
var rowsToDelete = [];
// 从最后一行向上遍历
for (var row = lastRow; row >= 1; row--) {
// 获取当前行指定列的单元格值
var cellValue = ws.Range(col + row).Value2;
// 处理空值并转换为字符串以确保比较一致性
var val = cellValue ? cellValue.toString() : "";
// 检查单元格值是否匹配数组中的任何字符串
for (var i = 0; i < strArray.length; i++) {
if (val === strArray[i]) {
// 将行添加到删除列表并退出内部循环以避免重复
rowsToDelete.push(row);
break;
}
}
}
// 删除标记的行(从下往上删除以防止行索引偏移)
for (var j = 0; j < rowsToDelete.length; j++) {
ws.Rows(rowsToDelete[j]).Delete();
}
}
/**
* 根据日期范围筛选数据(保留指定日期范围内的行,删除范围外的行)
* @param {string} sheetName - 工作表名称
* @param {number} colNumber - 日期所在列的列号(1开始,如11=K列)
* @param {string} startDate - 开始日期(格式:yyyy-mm-dd)
* @param {string} endDate - 结束日期(格式:yyyy-mm-dd,可选,默认当前日期0点)
*/
function Excel_filterByDateRange(sheetName, colNumber, startDate, endDate) {
try {
// 1. 初始化工作表对象
const ws1 = ThisWorkbook.Worksheets(sheetName);
if (!ws1) {
throw new Error(`未找到工作表:${sheetName}`);
}
// 2. 处理结束日期默认值(若未提供,默认为当前日期)
if (!endDate) {
const today = new Date();
today.setHours(0, 0, 0, 0); // 清零时间部分,确保为当天0点
endDate = today.toISOString().split('T')[0]; // 转为"yyyy-mm-dd"格式
}
// 3. 将日期字符串转换为Excel日期序列号(自定义方法)
const startDateValue = Excel_dateStrToDateValue(startDate); // 开始日期的Excel值
const endDateValue = Excel_dateStrToDateValue(endDate); // 结束日期的Excel值
// 4. 获取日期列的最后一行(避免空行干扰)
const lastRow = ws1.Cells(ws1.Rows.Count, colNumber).End(xlUp).Row;
if (lastRow < 2) { // 至少需要表头(1行)+1行数据
throw new Error("数据行数不足(至少需要1行数据)");
}
// 5. 存储需要删除的行号(从下往上收集,避免删除时索引错乱)
const rowsToDelete = [];
// 6. 遍历所有数据行(从最后一行往上到第2行,保留第1行表头)
for (let row = lastRow; row >= 2; row--) {
// 将列号转为列字母(如11→"K"),拼接行号得到单元格地址(如"K5")
const colLetter = String.fromCharCode(64 + colNumber); // 64是ASCII中"A"的前一位
const dateCell = ws1.Range(colLetter + row);
const dateValue = dateCell.Value2; // 获取单元格的原始值(Excel日期序列号或字符串)
// 标记是否需要删除当前行(默认不删除)
let shouldDelete = false;
// 处理Excel原生日期(数值类型,如44927代表2023-01-01)
if (typeof dateValue === 'number' && !isNaN(dateValue)) {
// 若日期小于开始日期 或 大于结束日期,则需要删除
shouldDelete = dateValue < startDateValue || dateValue > endDateValue;
}
// 处理文本格式的日期(字符串类型,如"2023-01-01")
else if (typeof dateValue === 'string' && dateValue.trim() !== '') {
try {
// 将文本日期转为Excel日期序列号
const excelDate = Excel_dateStrToDateValue(dateValue.trim());
shouldDelete = excelDate < startDateValue || excelDate > endDateValue;
} catch (e) {
// 文本格式无效时,视为需要删除(可根据需求调整)
console.log(`行${row}的日期格式无效:${dateValue},将被删除`);
shouldDelete = true;
}
}
// 空值或其他类型,视为需要删除
else {
shouldDelete = true;
}
// 若需要删除,记录行号
if (shouldDelete) {
rowsToDelete.push(row);
}
}
// 7. 批量删除标记的行(从下往上删,避免行号偏移)
for (const row of rowsToDelete) {
ws1.Rows(row).Delete();
}
alert(`筛选完成,共删除 ${rowsToDelete.length} 行数据`);
} catch (e) {
alert(`执行失败:${e.message}`);
console.error(e);
}
}
//----------------------------------------------------其他无归类方法---------------------------------------------------------------------------------------------------------------------------
/**
* 辅助函数:定时执行任务
* @param {string} funcName - 要执行的函数名
* @param {number} delayMs - 延迟毫秒数
*/
function scheduleTask(funcName, delayMs) {
const runTime = new Date();
runTime.setTime(runTime.getTime() + delayMs);
Application.OnTime(runTime, funcName); // 用Date对象作为时间参数(避免格式错误)
}
//----------------------------------------------------操作Excel类方法,统一前缀:Ecxel_ ,无法调用只用于用到时复制,比如遍历某一列的值后是直接使用的不适合返回值-----------------------------------
/**
* 多种方式定义Range对象,方法无返回值,学习或直接复制使用
* @param {Worksheet} ws - 工作表对象
*/
function Excel_getRange(ws) {
// 1.Excel 风格的单元格地址字符串(如 A1、A1:B10)定位区域
var singleCell = ws.Range("A1");
var range1 = ws.Range("A1:B10"); // A1到B10的矩形区域
var range2 = ws.Range("A1:A5, C1:C5"); // A1-A5和C1-C5两个区域
//2.行号和列号 定义
var cell = ws.Cells(1, 1); // 第1行第1列(即A1)
var range = ws.Range(ws.Cells(1, 1), ws.Cells(10, 2)); // 第1-10行,第1-2列(即A1:B10)
//3.整行 / 整列 定义
var row1 = ws.Rows(1); // 第1行
var rows = ws.Rows("1:5"); // 第1到5行
var colA = ws.Columns(1); // 第1列(即A列)
var cols = ws.Columns("A:C"); // A到C列
}
/**
* 多种方式遍历Range对象,方法无返回值,学习或直接复制使用
* 注意:如果是对整个工作表的遍历,遍历单列比如'K'列,可直接 ws1.Range("K" + row) ,
* 但对于Range来说,只能使用 range.Cells(row,2) ,这个2代表range区域的第2列,
* 这种区别在于:子区域和工作表对象 对 Range 语法支持不同
* @param {Worksheet} ws - 工作表对象
*/
function Excel_iterateRange(ws) {
var range = ws.Range("A1:C5"); // 示例区域
// 方式1:外层行循环,内层列循环(推荐)
for (var row = 1; row <= range.Rows.Count; row++) {
for (var col = 1; col <= range.Columns.Count; col++) {
var cellValue = range.Cells(row, col).Value2;
// 处理单元格值(如打印、计算)
console.log(`行=${row}, 列=${col}, 值=${cellValue}`);
}
}
// 方式2:For Each 循环,遍历区域内的每个单元格(按行优先顺序),若需获取行列索引(绝对位置,相对于整个工作表),需通过 range.Cells(cell).Row 和 range.Cells(cell).Column 计算
for (var cell in range.Cells) {
var cellValue = range.Cells(cell).Value2;
console.log(`值=${cellValue}`);
}
// 方式3:For Each 循环,按行 / 列批量处理,适合整行 / 整列操作(如检查每行的合计)
// 遍历每一行
for (var row in range.Rows) {
var rowRange = range.Rows(row);
// 处理整行(如计算每行的总和)
var sum = Application.WorksheetFunction.Sum(rowRange);
console.log(`行 ${row} 的总和=${sum}`);
}
// 遍历每一列
for (var col in range.Columns) {
var colRange = range.Columns(col);
// 处理整列(如查找最大值)
var max = Application.WorksheetFunction.Max(colRange);
console.log(`列 ${col} 的最大值=${max}`);
}
}
/**
* 关于字体、背景色、边框等格式设置,及清除格式,学习或直接复制使用
* @param {string} sheetName - 工作表名称
*/
function Excel_setHeaderFormat(sheetName) {
const ws = ThisWorkbook.Worksheets(sheetName);
const headerRange = ws.Range("A1:D1"); // 表头区域(A1到D1)
// 设置字体
headerRange.Font.Name = "微软雅黑";
headerRange.Font.Size = 12;
headerRange.Font.Bold = true; //加粗
headerRange.Font.Color = RGB(255, 255, 255); // 黑色 RGB(0, 0, 0);白色 RGB(255, 255, 255);红色 RGB(255, 0, 0);绿色 RGB(0, 255, 0);
// 蓝色 RGB(0, 0, 255);黄色 RGB(255, 255, 0);紫色 RGB(255, 0, 255);灰色 RGB(128, 128, 128)
// 设置背景色
headerRange.Interior.Color = RGB(0, 102, 204); // 蓝色背景
// 设置对齐方式
headerRange.HorizontalAlignment = xlCenter; // xlLeft 左对齐、xlCenter 居中、xlRight 右对齐
headerRange.VerticalAlignment = xlCenter; // xlTop 顶端、xlCenter 居中、xlBottom 底端
headerRange.WrapText = true; // 自动换行
// 设置边框
const borders = headerRange.Borders;
borders.LineStyle = xlContinuous; // 实线边框
borders.Weight = xlThin; // 细边框
borders.Color = RGB(0, 0, 0); // 黑色边框
// 设置行高和列宽
headerRange.RowHeight = 25;
ws.Range("A:D").ColumnWidth = 18;
//清除区域内的所有格式(保留数据)
headerRange.ClearFormats();
//设置数字格式,比如:四舍五入整数("0"),四舍五入保留两位小数("0.00"),百分比(0%)
headerRange.NumberFormat = "yyyy-mm-dd";
}
/**
* 关于Word文档的一些对象及方法使用
*/
function Word_functions() {
//1、word文档对象--------------------------------------------------------------------------------------------------------------------------------------------------
//启动 WPS 文字(kwps)或兼容的 Word 类应用(ket),返回应用实例。
let wordApp = CreateObject("kwps.application") || CreateObject("ket.application");
wordApp.Visible = false; //控制应用是否在前台显示。false用于后台静默处理,true用于可视化操作。
wordApp.Quit(); //关闭 Word/WPS 应用,释放资源。
//创建一个新的空白文档,返回文档对象。
const docs = wordApp.Documents.Add();
const docx = wordApp.Documents.Open(templatePath); //打开指定路径的 Word 文档,返回文档对象。
wordApp.Documents.Count; //获取已打开Word文档数量
wordApp.Documents.Item(i); //遍历已打开Word文档数量,获取文档对象
docx.Close(false); //关闭当前文档,false表示不保存修改;若需保存则传true或省略
docx.SaveAs2(savePath); //将文档保存到指定路径。
docx.FullName; //获取文件绝对路径
//2、word中的文字--------------------------------------------------------------------------------------------------------------------------------------------------
const allContent = docx.Content; // 获取整个文档的文字范围
docx.Paragraphs.Count; /*文档中的文字按段落划分,Paragraphs是所有段落的集合,可通过索引访问单个段落(索引从 1 开始)。
获取文档总段落数,段落按Enter划分,一个word写完按了2次Enter,就是3个段落,注意:
包含在段落中的元素:嵌入型图片(InlineShape)、纯文本、文本格式(如字体、颜色等);
不包含在段落中的元素:浮动型图片(Shape)、表格(Table)、自选图形、文本框等独立对象。*/
const paraRange = docx.Paragraphs.Item(i).Range; //获取段落Range对象
const paraText = paraRange.Text; //获取该段落文字
const startPos = 10; // 起始位置
const endIndex = 20; // 结束位置
const customRange = docx.Range(startPos, endIndex); // 自定义文字范围
customRange.Text = "替换这段文字"; // 修改范围内的文字
customRange.Font.Name = "微软雅黑"; //设置字体
customRange.Font.Size = 12;
rangeFind = customRange.Find; //查找替换对象
//3、word文档对象的查找对象-------------------------------------------------------------------------------------------------------------------------------------------
const findObj = docx.Content.Find; // 获取文档内容的“查找替换”对象(相当于打开“查找替换”对话框)
findObj.ClearFormatting(); //设置查找时,只认 “文本内容”,不管文字是什么字体、颜色、大小;
findObj.Replacement.ClearFormatting(); //设置替换后的内容,使用默认格式(或继承原位置的格式)
findObj.Text = placeholder; // 要查找的内容(如"{$姓名}")
findObj.Replacement.Text = replaceValue; // 替换后的内容
findObj.Execute({ Replace: 2 }); // 2表示"全部替换"
//4、word文档内容复制、粘贴---------------------------------------------------------------------------------------------------------------------------------------------
docx.Content.Copy(); // 复制整个文档内容到剪贴板
// 定位到目标文档末尾,执行粘贴操作
const endPos = docx.Content.End;
const endRange = docx.Range(endPos, endPos); //可通过指定起始和结束位置创建自定义范围(如某段中的部分文字)
endRange.InsertAfter("\f"); // "\f"代表分页符
endRange.Paste(); // 粘贴剪贴板内容到该位置
//5、word文档获取表格对象,表格是结构化数据容器,通过 **Tables集合 ** 管理,每个表格对应一个Table对象。---------------------------------------------------------------------
const table = docx.Tables.Item(t); // 获取第t个表格(t为索引,从1开始)
const cell = table.Cell(1, 1); // 获取表格第1行第1列单元格
cell.Range.Copy(); // 复制单元格内容
const cellText = cell.Range.Text.trim(); // 获取单元格文本(去除首尾空格)
if (!cellText) table.Delete(); // 若单元格为空,删除整个表格
const totalTables = docx.Tables.Count; // 获取总表格数
const firstTable = docx.Tables(1); // 获取第1个表格
const totalRows = firstTable.Rows.Count; // 表格行数
const totalCols = firstTable.Columns.Count; // 表格列数
const firstRow = firstTable.Rows(1); // 第1行
cell.Range.Text = "新内容"; //修改单元格内容
firstTable.Rows.Add(); //插入行 / 列(在表格末尾插入一行)
firstTable.Cell(1,1).Merge(firstTable.Cell(1,2)); //合并单元格(合并第 1 行 1-2 列)
//6、word中的图片对象,分为嵌入型图片(InlineShape) 和浮动型图片(Shape)-------------------------------------------------------------------------------------------------
const totalInlinePics = docx.InlineShapes.Count; // 嵌入型图片总数
const firstInlinePic = docx.InlineShapes(1); // 获取第1个嵌入型图片
firstInlinePic.Width = 200; //调整大小
firstInlinePic.Height = 150;
firstInlinePic.Delete(); //删除图片
firstInlinePic.Replace("D:/新图片.png") //替换图片
const totalShapes = docx.Shapes.Count; // 浮动对象总数(含图片、图形等)
const firstShape = docx.Shapes(1); // 获取第1个浮动对象
// 判断是否为图片(Shape可能是图形、文本框等)
if (firstShape.Type === 13) { // 13 = wdShapePicture(图片类型)
firstShape.LinkFormat.SourceFullName; // 链接图片的路径
}
firstShape.Left = 100; firstShape.Top = 50 ; //移动位置(距离页面左侧和顶部的距离)
firstShape.ConvertToInlineShape(); //转换为嵌入型
//7、word中的图形对象(自选图形、线条等),图形(如矩形、箭头、流程图元素)属于 **Shape对象 **(与浮动型图片同属一个集合),通过Shapes集合管理,区分于图片的核心是Type属性。--------
// 遍历所有浮动对象,筛选出图形(非图片)
for (let i = 1; i <= docx.Shapes.Count; i++) {
const shape = docx.Shapes(i);
// 图形类型(如矩形=1,直线=9,箭头=20等,不同版本可能有差异)
if (shape.Type !== 13) { // 排除图片(13=图片类型)
console.log("图形类型:", shape.Type);
console.log("图形文字:", shape.TextFrame.TextRange.Text); // 图形内的文字(如矩形中的文字)
}
}
shape.Fill.ForeColor.RGB = RGB(255, 0, 0); //(填充为红色)
shape.Line.Weight = 2; //(线条粗细为 2 磅)
shape.TextFrame.TextRange.Text = "新文字"; //添加文字
//8、word中生成的图片对象,Word 中插入的折线图(通过 “插入→图表” 生成的图表),本质上是一种特殊的Shape对象(浮动型对象),属于Shapes集合(和浮动型图片同属一个集合,但类型不同)。--------
// 遍历文档中所有浮动对象(含图表、图片、图形等),获取到图形对象
for (let i = 1; i <= docx.Shapes.Count; i++) {
const shape = docx.Shapes(i);
// 判断是否为图表(假设Type=7代表图表)
if (shape.Type === 7) {
console.log("找到一个图表(可能是折线图)");
// 进一步判断是否为折线图(通过图表类型名称)
const chart = shape.Chart; // 获取图表对象
console.log("图表类型:", chart.ChartType); // 输出具体类型(如“xlLine”代表折线图)
// 如果确认是折线图,就可以操作它
if (chart.ChartType === 4) { // 假设4代表折线图(具体值需查对应版本文档)
console.log("这是一个折线图");
// 后续操作...
}
}
}
//获取图表数据
const chart = shape.Chart;
const dataRange = chart.DataTable.Workbook.Worksheets(1).UsedRange; // 获取图表数据源范围
console.log("折线图数据:", dataRange.Value); // 输出图表的数值数据
//修改图表样式
chart.ChartTitle.Text = "新的折线图标题"; // 修改标题
chart.Axes(1).Title.Text = "X轴标签"; // 修改X轴标签(1代表X轴,2代表Y轴)
//调整尺寸和位置
shape.Width = 400; // 宽度(磅)
shape.Left = 100; // 距离页面左侧的距离(磅)
//删除折线图
shape.Delete(); // 删除整个图表对象
}
/**
* 在指定路径的Word文档中查找目标文字,返回第一个匹配项的位置索引
* 支持多匹配提醒,兼容WPS/Word,自动释放资源
* @param {String} path - 文档完整路径(如"C:/docs/template.docx")
* @param {String} text - 要查找的文本(如"修改后描述:")
* @param {number} type - 1=返回起始位置,2=返回结束位置
* @returns {number|null} 位置索引或null(未找到/出错)
*/
function Word_getRangeStartIndex(filePath, text, type) {
// 正则表达式 /\\/g 匹配所有反斜杠,替换为 /
const path = filePath.replace(/\\/g, '/');
let wordApp; // 提升作用域,让外部可访问
let doc; // 提升作用域
try {
// 1. 尝试获取实例并检查文档是否已打开
wordApp = CreateObject("kwps.application") || CreateObject("word.application");
let isOpen = false;
// 遍历已打开文档
for (let i = 1; i <= wordApp.Documents.Count; i++) {
const openedDoc = wordApp.Documents.Item(i);
if (openedDoc.FullName.replace(/\\/g, '/') === path) {
doc = openedDoc;
isOpen = true;
break;
}
}
// 2. 未打开则直接打开
if (!isOpen){
doc = wordApp.Documents.Open(path);
}
} catch {
console.log("WPS应用启动失败或文件路径问题");
}
try {
// 2. 参数校验
if (!text || typeof text !== "string") {
alert("查找文本不能为空");
return null;
}
if (type !== 1 && type !== 2) {
alert("类型参数错误:1=起始索引,2=结束索引");
return null;
}
// 3. 初始化变量(记录匹配信息)
let matchCount = 0;
let firstStart = null;
let firstEnd = null;
let currentStart = 0; // 当前查找起始位置
// 4. 循环查找所有匹配项
while (true) {
// 4.1 创建当前区间的查找范围(从currentStart到文档末尾)
const searchRange = doc.Range(currentStart, doc.Content.End);
if (searchRange.Start >= searchRange.End) break; // 范围无效时退出
// 4.2 基于当前范围创建新的查找对象(关键:避免复用旧对象)
const findObj = searchRange.Find;
// 4.3 配置查找参数(每次创建新对象都需重新设置)
findObj.ClearFormatting();
findObj.Replacement.ClearFormatting();
findObj.Text = text; // 查找文本
findObj.Forward = true; // 向前查找
findObj.Wrap = 0; // 范围结束后停止
findObj.MatchWholeWord = true; // 全字匹配
findObj.MatchCase = false; // 不区分大小写
// 4.4 执行查找
const isFound = findObj.Execute();
if (!isFound) break; // 未找到更多匹配项
// 4.5 处理找到的匹配项
matchCount++;
const foundRange = findObj.Parent; // 指向查找到的文本对应的范围对象(Range),而不是段落的Range
// 获取该文本所在的段落
//const paragraph = foundRange.Paragraphs(1);
// 段落的完整范围(包含整个段落内容)
//const paragraphRange = paragraph.Range;
// 段落的起始/结束位置
//const paraStart = paragraphRange.Start;
//const paraEnd = paragraphRange.End;
// 记录第一个匹配项的位置
if (matchCount === 1) {
firstStart = foundRange.Start;
firstEnd = foundRange.End;
}
// 更新下一次查找的起始位置(避免重复匹配)
currentStart = foundRange.End + 1;
}
// 5. 处理查找结果
if (matchCount === 0) {
alert(`未找到文本:${text}`);
return null;
} else if (matchCount > 1) {
alert(`找到${matchCount}处匹配内容,返回第一个匹配项的位置`);
}
// 6. 返回结果
return type === 1 ? firstStart : firstEnd;
} catch (e) {
alert(`执行错误:${e.message}\n可能原因:文档格式错误或权限不足`);
return null;
} finally {
// 7. 强制释放资源(无论成功失败都执行)
if (doc) {
try { doc.Close(false); } catch (e) {} // 关闭文档(不保存)
}
if (wordApp) {
try { wordApp.Quit(); } catch (e) {} // 退出应用
}
}
}
// 删除word文档中空白行
function Word_deleteEmptyLines(filePath) {
// 正则表达式 /\\/g 匹配所有反斜杠,替换为 /
const path = filePath.replace(/\\/g, '/');
let wordApp; // 提升作用域,让外部可访问
let doc; // 提升作用域
try {
// 尝试获取实例并检查文档是否已打开
wordApp = CreateObject("kwps.application") || CreateObject("word.application");
let isOpen = false;
// 遍历已打开文档
for (let i = 1; i <= wordApp.Documents.Count; i++) {
const openedDoc = wordApp.Documents.Item(i);
if (openedDoc.FullName.replace(/\\/g, '/') === path) {
doc = openedDoc;
isOpen = true;
break;
}
}
// 未打开则直接打开
if (!isOpen){
doc = wordApp.Documents.Open(path);
}
} catch {
console.log("WPS应用启动失败或文件路径问题");
}
try {
// 打印当前文档状态(方便调试)
console.log(`处理文档:${doc.Name},段落数:${doc.Paragraphs.Count}`);
// 强制重置文档选择状态(关键:避免光标在表格/特殊区域)
const selection = doc.Application.Selection;
selection.HomeKey(6); // 6=wdStory,移到文档开头
selection.EndKey(6, 1); // 全选文档内容(确保操作范围正确)
selection.Collapse(1); // 1=wdCollapseStart,折叠光标到开头
const find = doc.Content.Find;
// 强制重置所有查找参数(避免之前的设置残留)
find.ClearFormatting();
find.Replacement.ClearFormatting();
find.Forward = true;
find.Wrap = 0; // 0=wdFindStop,不循环
find.MatchCase = false;
find.MatchWholeWord = false;
find.MatchWildcards = false; // 先关闭通配符
find.MatchSoundsLike = false;
find.MatchAllWordForms = false;
// 1. 处理连续硬回车(保留一个)
find.Text = "^p^p";
find.Replacement.Text = "^p";
const replace1 = find.Execute({ Replace: 2 }); // 2=全部替换
console.log(`替换连续硬回车:${replace1 ? "成功" : "无匹配"}`);
// 2. 处理连续软回车(先转为硬回车,再合并)
find.Text = "^l^l"; // ^l是软回车
find.Replacement.Text = "^p";
const replace2 = find.Execute({ Replace: 2 });
console.log(`替换连续软回车:${replace2 ? "成功" : "无匹配"}`);
// 3. 处理包含空格的空行(通配符模式)
find.MatchWildcards = true;
find.Text = "^p[ ]{1,}^p"; // 匹配中间全是空格的空行
find.Replacement.Text = "^p^p";
const replace3 = find.Execute({ Replace: 2 });
console.log(`替换空格空行:${replace3 ? "成功" : "无匹配"}`);
find.MatchWildcards = false; // 用完立即关闭
// 4. 从后往前删空段落(仅含段落标记)
for (let i = doc.Paragraphs.Count; i >= 1; i--) {
try {
const para = doc.Paragraphs.Item(i);
const paraText = para.Range.Text;
// 空段落的特征:长度为1(仅段落标记^p,ASCII 13)
if (paraText.length === 1 && paraText === String.fromCharCode(13)) {
para.Range.Delete();
console.log(`删除空段落:第${i}段`);
}
} catch (paraErr) {
console.log(`处理第${i}段时出错:${paraErr.message}`);
}
}
console.log(`空白行处理完成:${doc.Name}`);
} catch (e) {
// 抛出具体错误位置(方便定位)
console.log(`删除空白行时出错:${e.message}(步骤:${e.stack?.split('\n')[1]?.trim()})`);
}
}
/*
* 通用文件选择函数 - 用于打开文件选择对话框,获取用户选择的单个文件路径
* @param {string} title - 文件选择对话框的标题,用于提示用户选择何种文件
* @param {Array} filter - 文件过滤规则,格式为[显示名称, 扩展名规则],例如["Excel文件", "*.xlsx;*.xls"]
* @returns {string|null} - 返回用户选中的文件路径;若用户取消选择,则返回null
* @throws {Error} - 当文件选择过程中发生错误时,抛出包含错误信息的异常
*/
function Folder_selectFile(title, filter) {
// 示例调用:
// const excelPath = Folder_selectFile("选择Excel数据文件", ["Excel文件", "*.xlsx;*.xls"]);
// const wordTemplatePath = Folder_selectFile("选择Word模板文件", ["Word文件", "*.docx;*.doc"]);
try {
// 创建文件选择对话框对象(3表示msoFileDialogFilePicker类型,即文件选择器)
const fd = Application.FileDialog(3);
// 设置对话框标题,提示用户当前需要选择的文件类型
fd.Title = title;
// 禁用多选功能,只允许选择单个文件
fd.AllowMultiSelect = false;
// 清除对话框中默认的文件过滤规则
fd.Filters.Clear();
// 添加自定义文件过滤规则(显示名称和对应的扩展名)
fd.Filters.Add(filter[0], filter[1]);
// 显示对话框,若用户点击"确定"(返回值为-1)则返回选中的文件路径,否则返回null
return fd.Show() === -1 ? fd.SelectedItems(1) : null;
} catch (e) {
// 捕获并包装错误信息,明确提示文件选择失败
throw new Error(`文件选择失败:${e.message}`);
}
}
/*
* 核心功能:从Excel读取数据,批量生成Word文档(替换模板占位符,文本输出)
* Word模板:带表格表头字段内容的占位符,{$序号},表格按行遍历,替换对应占位符,一行生成一个Word
*/
function batchGenerateWord() {
// 配置项:占位符格式、文件名非法字符
const CONFIG = {
placeholderPrefix: "{$", // 占位符前缀(如{$姓名})
placeholderSuffix: "}", // 占位符后缀
invalidFileNameChars: /[\\/:*?"<>|]/g // 文件名非法字符
};
try {
// 1. 选择Excel数据源和Word模板
const excelPath = Folder_selectFile("选择Excel数据文件", ["Excel文件", "*.xlsx;*.xls"]);
const wordTemplatePath = Folder_selectFile("选择Word模板文件", ["Word文件", "*.docx;*.doc"]);
if (!excelPath || !wordTemplatePath) return; // 取消选择则退出
// 2. 读取Excel数据
const wb = Application.Workbooks.Open(excelPath); // 打开Excel
const ws = wb.ActiveSheet; // 获取活动工作表
// 调用自定义函数获取有效数据的最后一行和列
let lastRow = Excel_getRealLastRowAndCol(ws, 1);
let lastCol = Excel_getRealLastRowAndCol(ws, 2);
// 读取指定范围的单元格数据(逐行逐列)
const range = ws.Range(ws.Cells(1, 1), ws.Cells(lastRow, lastCol));
const allValues = [];
for (let row = 1; row <= range.Rows.Count; row++) {
const currentRow = [];
for (let col = 1; col <= range.Columns.Count; col++) {
// 空单元格转为空字符串
const cell = range.Cells(row, col);
const cellValue = cell.Value2;
currentRow.push(cellValue === null ? "" : String(cellValue));
}
allValues.push(currentRow);
}
// 过滤空行(只保留至少一个非空单元格的行)
const data = [];
for (const row of allValues) {
if (row.some(cell => cell !== "")) data.push(row);
}
// 3. 数据校验(确保格式正确)
if (data.length === 0) throw new Error("Excel无有效数据");
if (data[0].length === 0) throw new Error("表头行无数据");
// 表头处理(转为字符串,去除空格)
const headerRow = data[0].map(cell => (cell?.toString() || "").trim());
const lastColIndex = headerRow.length - 1;
if (headerRow[lastColIndex] !== "文件名称") {
throw new Error(`最后一列表头应为「文件名称」,实际为「${headerRow[lastColIndex] || "空"}」`);
}
if (data.length < 2) throw new Error("至少需要1行表头+1行数据");
// 4. 清洗文件名(替换非法字符)
for (let i = 1; i < data.length; i++) {
const rawName = data[i][lastColIndex] || "";
const cleanedName = rawName.toString().replace(CONFIG.invalidFileNameChars, "_").trim();
if (!cleanedName) throw new Error(`第${i+1}行文件名无效`);
data[i][lastColIndex] = cleanedName;
}
// 5. 启动WPS文字,批量生成文档
let wordApp = CreateObject("kwps.application") || CreateObject("ket.application");
wordApp.Visible = false; // 后台运行
const saveDir = wb.Path; // 保存路径(与Excel同目录)
const totalRows = data.length - 1; // 总数据行数
let completed = 0; // 已完成数量
// 循环生成每个Word
for (let rowIdx = 1; rowIdx < data.length; rowIdx++) {
StatusBar = `生成中(${completed}/${totalRows})...`; // 更新状态栏
const rowData = data[rowIdx];
const fileName = rowData[lastColIndex];
const savePath = `${saveDir}/${fileName}.docx`;
try {
const doc = wordApp.Documents.Open(wordTemplatePath); // 打开模板
const findObj = doc.Content.Find; // 查找替换对象
findObj.ClearFormatting();
findObj.Replacement.ClearFormatting();
// 替换所有占位符({$字段名} → 实际值)
for (let colIdx = 0; colIdx < lastColIndex; colIdx++) {
// 1. 获取当前列的字段名(来自Excel表头行)
const fieldName = headerRow[colIdx] || `字段${colIdx+1}`;
// 2. 构建完整的占位符(比如 "姓名" → "{$姓名}")
const placeholder = `${CONFIG.placeholderPrefix}${fieldName}${CONFIG.placeholderSuffix}`;
// 3. 获取当前行当前列的实际数据(要替换进去的值)
const replaceValue = rowData[colIdx].toString();
// 4. 配置Word的查找替换功能
findObj.Text = placeholder; // 要找什么(占位符)
findObj.Replacement.Text = replaceValue; // 替换成什么(实际数据)
findObj.Execute({ Replace: 2 }); // 执行替换(2表示"全部替换")
}
doc.SaveAs2(savePath); // 保存文件
doc.Close();
completed++;
} catch (rowErr) {
alert(`第${rowIdx+1}行失败:${rowErr.message}\n已跳过`);
}
}
// 6. 清理资源
StatusBar = ""; // 清空状态栏
wordApp.Quit(); // 关闭WPS文字
wb.Close(false); // 关闭Excel(不保存)
alert(`完成!生成${completed}个文件,路径:${saveDir}`);
} catch (globalErr) {
// 全局错误处理(确保资源释放)
try { if (typeof wordApp !== "undefined") wordApp.Quit(); } catch (e) {}
try { if (typeof wb !== "undefined") wb.Close(false); } catch (e) {}
alert(`失败:${globalErr.message}`);
}
}
/* 辅助函数:向Word文档中插入带有指定颜色的文本(支持单个字符设置不同颜色)
* @param {Object} mergeDoc - Word文档对象,用于操作文档内容
* @param {string} text - 需要插入的文本内容
* @param {Array} colors - 颜色数组,每个元素对应text中对应索引字符的颜色值(需与文本长度一致)
* 无返回值,直接在文档中插入带颜色的文本
*/
function insertColoredText(mergeDoc, text, colors) {
// 校验颜色数组与文本长度是否匹配(确保每个字符都有对应的颜色设置)
if (colors.length !== text.length) {
// 长度不匹配时,输出警告信息并使用默认黑色插入文本
console.warn(`颜色数组长度(${colors.length})与文本长度(${text.length})不匹配,使用默认黑色`);
mergeDoc.Content.InsertAfter(text);
return;
}
// 循环处理每个字符,逐个插入并应用对应颜色
for (let i = 0; i < text.length; i++) {
const char = text.charAt(i); // 获取当前索引的字符
const color = colors[i]; // 获取当前字符对应的颜色值
// 记录插入前文档内容的末尾位置(用于定位新插入的字符)
// 减1是因为Word的Range.End属性指向字符后一位,需调整到实际末尾字符位置
const startPos = mergeDoc.Content.End - 1;
// 在文档末尾插入当前字符
mergeDoc.Content.InsertAfter(char);
// 选中刚插入的字符(从插入前的末尾位置到插入后的末尾位置)
const charRange = mergeDoc.Range(startPos, mergeDoc.Content.End);
// 为选中的字符应用对应的颜色
charRange.Font.Color = color;
}
}
/* 辅助函数:将BGR颜色格式转换为RGB颜色格式(兼容WPS的颜色值表示方式)
* @param {number|null|undefined} bgr - 待转换的BGR颜色值
* @returns {number} 转换后的RGB颜色值,若输入无效则返回0(表示黑色)
*/
function bgrToRgb(bgr) {
// 校验输入有效性:若为null、undefined或非数字,返回默认黑色(0)
if (bgr === null || bgr === undefined || isNaN(bgr)) return 0;
// 处理WPS特殊情况:WPS可能返回负数颜色值(基于32位补码表示)
// 加上0x1000000(即16777216)将负数转换为等效的24位无符号颜色值
if (bgr < 0) bgr += 0x1000000;
// 从BGR值中提取三通道颜色分量(每个分量占8位)
const blue = bgr & 0xFF; // 提取低8位:B(蓝色)分量
const green = (bgr >> 8) & 0xFF; // 右移8位后提取低8位:G(绿色)分量
const red = (bgr >> 16) & 0xFF; // 右移16位后提取低8位:R(红色)分量
// 重组为RGB格式:R分量占高8位,G分量占中8位,B分量占低8位
return (red << 16) | (green << 8) | blue;
}
/*
* 辅助函数:创建Word应用实例
*/
function createWordApp(visible = true) {
let wordApp = CreateObject("kwps.application") || CreateObject("ket.application");
wordApp.Visible = visible;
return wordApp;
}
/*
* 核心方法:生成文档,支持单元格多种文字颜色
* 参数1:keepHeaders - 要保留的表头数组
* 参数2:generateMode - 生成方式(1=合并为一个文档;字符串=按指定列的值作为文件名,每行生成一个)
*/
function generateDocuments(keepHeaders, generateMode) {
// 示例调用:根据需要修改参数
//const keepHeaders = ["总序号", "原条款", "新条款", "原文描述", "修改后描述"];
// 选择Excel文件
const excelPath = selectFile("选择Excel数据文件", ["Excel文件", "*.xlsx;*.xls"]);
if (!excelPath) return;
// 打开Excel并读取数据范围
let wb = Application.Workbooks.Open(excelPath);
let ws = wb.ActiveSheet;
let lastRow = Excel_getRealLastRowAndCol(ws, 1);
let lastCol = Excel_getRealLastRowAndCol(ws, 2);
const range = ws.Range(ws.Cells(1, 1), ws.Cells(lastRow, lastCol));
// 读取单元格数据及颜色信息
const allValuesWithColors = [];
for (let row = 1; row <= range.Rows.Count; row++) {
const currentRow = [];
for (let col = 1; col <= range.Columns.Count; col++) {
const cell = range.Cells(row, col); // 单元格Range对象
// 读取单元格文本内容(确保为字符串)
const cellValue = cell.Value2 === null ? "" : String(cell.Value2);
//cell.Characters().Count;表格单元格获取总字符数,但单元格内容为数字时,读取不到,所以未采用
const charCount = cellValue.length;
const charColors = [];
// 通过Characters(index, 1)遍历每个字符
for (let charIndex = 1; charIndex <= charCount; charIndex++) {
try {
// 读取第charIndex个字符(从1开始),长度为1
const charObj = cell.Characters(charIndex, 1);
// 获取该字符的字体颜色(BGR格式)
const bgrColor = charObj.Font.Color;
// 转换为RGB格式(供Word使用)
const rgbColor = bgrToRgb(bgrColor);
charColors.push(rgbColor);
} catch (e) {
// 单个字符读取失败时,默认黑色
charColors.push(0);
console.log(`单元格(${row},${col})字符${charIndex}颜色读取失败:${e.message}`);
}
}
currentRow.push({
value: cellValue,
colors: charColors
});
}
allValuesWithColors.push(currentRow);
}
// 过滤空行和数据校验
const data = allValuesWithColors.filter(row => row.some(cell => cell.value !== ""));
if (data.length === 0) throw new Error("Excel无有效数据");
if (data[0].length === 0) throw new Error("表头行无数据");
if (data.length < 2) throw new Error("至少需要1行表头+1行数据");
// 获取保存目录
const saveDir = wb.Path;
// 根据生成模式处理
if (generateMode === 1) {
// 生成合并文档
generateMergedDocument(data, keepHeaders, saveDir);
} else if (typeof generateMode === "string") {
// 按指定列生成多个文档
generateMultipleDocuments(data, keepHeaders, saveDir, generateMode);
} else {
throw new Error("无效的生成方式参数,必须是1或表头字符串");
}
// 清理Excel资源
wb.Close(false);
wb = null;
ws = null;
}
// 生成合并文档(所有数据在一个文件中)
function generateMergedDocument(data, keepHeaders, saveDir) {
let wordApp = createWordApp();
let mergeDoc = wordApp.Documents.Add();
// 遍历数据行,插入带颜色的内容
for (let i = 1; i < data.length; i++) {
const currentRow = data[i];
const headerRow = data[0];
mergeDoc.Content.InsertAfter("\n");
for (let j = 0; j < currentRow.length; j++) {
const header = headerRow[j];
const dataCell = currentRow[j];
if (!keepHeaders.includes(header.value)) continue;
// 插入表头(带颜色)
insertColoredText(mergeDoc, `${header.value}:`, header.colors);
// 插入数据(带颜色)
insertColoredText(mergeDoc, dataCell.value, dataCell.colors);
mergeDoc.Content.InsertAfter("\n");
}
}
// 保存并清理资源
const fileName = "带颜色的合并文档_" + new Date().getTime() + ".docx";
const savePath = saveDir + "\\" + fileName;
mergeDoc.SaveAs2(savePath);
mergeDoc.Close();
wordApp.Quit();
alert(`生成成功:${savePath}`);
}
// 生成多个文档(每行数据一个文件)
function generateMultipleDocuments(data, keepHeaders, saveDir, fileNameHeader) {
let wordApp = createWordApp(false); // 批量生成时隐藏界面
const headerRow = data[0];
// 查找文件名所在列的索引
let fileNameColIndex = -1;
for (let j = 0; j < headerRow.length; j++) {
if (headerRow[j].value === fileNameHeader) {
fileNameColIndex = j;
break;
}
}
if (fileNameColIndex === -1) {
wordApp.Quit();
throw new Error(`未找到表头 "${fileNameHeader}",无法生成文件名`);
}
// 遍历数据行生成文档
const generatedFiles = [];
for (let i = 1; i < data.length; i++) {
const currentRow = data[i];
// 获取并处理文件名(替换非法字符)
let fileName = currentRow[fileNameColIndex].value || `未命名_${i}`;
fileName = fileName.replace(/[\\/:*?"<>|]/g, "_") + ".docx"; // 过滤非法字符
const savePath = saveDir + "\\" + fileName;
// 创建新文档并插入内容
let doc = wordApp.Documents.Add();
for (let j = 0; j < currentRow.length; j++) {
const header = headerRow[j];
const dataCell = currentRow[j];
if (!keepHeaders.includes(header.value)) continue;
insertColoredText(doc, `${header.value}:`, header.colors);
insertColoredText(doc, dataCell.value, dataCell.colors);
doc.Content.InsertAfter("\n");
}
// 保存并关闭当前文档
doc.SaveAs2(savePath);
doc.Close();
generatedFiles.push(savePath);
}
// 清理资源
wordApp.Quit();
wordApp = null;
alert(`已生成 ${generatedFiles.length} 个文档:\n${saveDir}`);
}
/*
* 核心方法:生成纯文本文档
* 参数1:keepHeaders - 要保留的表头数组
* 参数2:generateMode - 生成方式(1=合并为一个文档;字符串=按指定列的值作为文件名,每行生成一个)
*/
function generateTextDocuments(keepHeaders, generateMode) {
// 示例调用:根据需要修改参数
//const keepHeaders = ["总序号", "原条款", "新条款", "原文描述", "修改后描述"];
// 1. 选择Excel数据源文件
const excelPath = selectFile("选择Excel数据文件", ["Excel文件", "*.xlsx;*.xls"]);
if (!excelPath) return;
// 2. 读取Excel中的数据
let wb = Application.Workbooks.Open(excelPath);
let ws = wb.ActiveSheet;
// 获取有效数据范围
let lastRow = Excel_getRealLastRowAndCol(ws, 1);
let lastCol = Excel_getRealLastRowAndCol(ws, 2);
const range = ws.Range(ws.Cells(1, 1), ws.Cells(lastRow, lastCol));
// 逐行读取数据(纯文本,不处理颜色)
const allValues = [];
for (let row = 1; row <= range.Rows.Count; row++) {
const currentRow = [];
for (let col = 1; col <= range.Columns.Count; col++) {
const cell = range.Cells(row, col);
// 读取单元格文本内容(确保为字符串)
const cellValue = cell.Value2 === null ? "" : String(cell.Value2);
currentRow.push(cellValue);
}
allValues.push(currentRow);
}
// 过滤空行:仅保留至少包含一个非空单元格的行
const data = []; // 存储过滤后的有效数据(最终用于生成文档)
for (const row of allValues) {
// 用some方法判断行中是否存在非空单元格(只要有一个非空就保留)
if (row.some(cell => cell !== "")) data.push(row);
}
// 数据校验
if (data.length === 0) throw new Error("Excel无有效数据");
if (data[0].length === 0) throw new Error("表头行无数据");
if (data.length < 2) throw new Error("至少需要1行表头+1行数据");
// 保存目录(与Excel同目录)
const saveDir = wb.Path;
// 根据生成模式处理
if (generateMode === 1) {
// 生成合并文档
generateTextMergedDocument(data, keepHeaders, saveDir);
} else if (typeof generateMode === "string") {
// 按指定列生成多个文档
generateTextMultipleDocuments(data, keepHeaders, saveDir, generateMode);
} else {
throw new Error("无效的生成方式参数,必须是1或表头字符串");
}
// 清理Excel资源
wb.Close(false);
wb = null;
ws = null;
}
// 生成合并文档(纯文本,所有数据在一个文件中)
function generateTextMergedDocument(data, keepHeaders, saveDir) {
// 启动WPS并创建文档
let wordApp = createWordApp();
let mergeDoc = wordApp.Documents.Add();
// 遍历数据行插入内容
for (let i = 1; i < data.length; i++) {
const currentRow = data[i];
const headerRow = data[0];
mergeDoc.Content.InsertAfter("\n"); // 记录间换行分隔
// 插入当前行数据(仅保留指定表头)
for (let j = 0; j < currentRow.length; j++) {
const header = headerRow[j];
const cellValue = currentRow[j];
if (keepHeaders.includes(header)) {
const dataText = `${header}:${cellValue}\n`;
mergeDoc.Content.InsertAfter(dataText);
}
}
}
// 保存文档
const fileName = "合并文档_" + new Date().getTime() + ".docx";
const savePath = saveDir + "\\" + fileName;
mergeDoc.SaveAs2(savePath);
// 清理资源
mergeDoc.Close();
wordApp.Quit();
mergeDoc = null;
wordApp = null;
alert(`合并文档已生成:\n${savePath}`);
}
// 生成多个文档(纯文本,每行数据一个文件)
function generateTextMultipleDocuments(data, keepHeaders, saveDir, fileNameHeader) {
let wordApp = createWordApp(false); // 批量生成时隐藏界面
const headerRow = data[0];
// 查找文件名所在列的索引
let fileNameColIndex = -1;
for (let j = 0; j < headerRow.length; j++) {
if (headerRow[j] === fileNameHeader) {
fileNameColIndex = j;
break;
}
}
if (fileNameColIndex === -1) {
wordApp.Quit();
console.log(`未找到表头 "${fileNameHeader}",无法生成文件名`);
}
// 遍历数据行生成文档
const generatedFiles = [];
for (let i = 1; i < data.length; i++) {
const currentRow = data[i];
// 获取并处理文件名(替换非法字符)
let fileName = data[i][fileNameColIndex] || `未命名_${i}`;
fileName = fileName.replace(/[\\/:*?"<>|]/g, "_") + ".docx"; // 过滤非法字符
const savePath = saveDir + "\\" + fileName;
// 创建新文档并插入内容
let doc = wordApp.Documents.Add();
for (let j = 0; j < currentRow.length; j++) {
const header = headerRow[j];
const cellValue = currentRow[j];
if (keepHeaders.includes(header)) {
const dataText = `${header}:${cellValue}\n`;
doc.Content.InsertAfter(dataText);
}
}
// 保存并关闭当前文档
doc.SaveAs2(savePath);
doc.Close();
generatedFiles.push(savePath);
}
// 清理资源
wordApp.Quit();
wordApp = null;
alert(`已生成 ${generatedFiles.length} 个文档:\n${saveDir}`);
}