WPS JS宏 通用方法整理汇总 实时更新

发布于:2025-08-13 ⋅ 阅读:(11) ⋅ 点赞:(0)

我是将这个放到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}`);
    }