返回公众号历史文章目录

文章 / 公众号历史文章

【开发文档分享】有AI编程加持,是时候重视WPS的JSA宏的价值了

上一篇推文中,分享了WPS多维表格的二次开发的接口文档,反响不错,在AI编程能力越发强大的今天,普通Excel/WPS中高级用户群体,真的挺有必要再进一步,将通过氛围编程(vibe coding)的方式,驾驭一下AI直接写代码。 【开发文档分享】论二次开发能力,WPS多维表格遥遥领先 当然上一篇谈及

上一篇推文中,分享了WPS多维表格的二次开发的接口文档,反响不错,在AI编程能力越发强大的今天,普通Excel/WPS中高级用户群体,真的挺有必要再进一步,将通过氛围编程(vibe coding)的方式,驾驭一下AI直接写代码。

【开发文档分享】论二次开发能力,WPS多维表格遥遥领先

当然上一篇谈及的是WPS多维表格,很多人遇见了一款工具,就把它当前瑞士军刀般来使用,什么需求,什么场景都用一个工具完成,其实是非常不合适的,例如在多维表格群体里,笔者能够感受到的氛围是大家都很排斥着用传统表格了,觉得多维表格,数据结构化程度高了,还可以直接有仪表盘出报告,没有传统表格什么事了,这也只能说有点天真。

传统表格的生态,仍然是非常旺盛的,因其面向的是单元格级别的颗粒度,可以无限自由地构建数据处理和报表。在新的OFFICE365新函数、动态数组公式效果的加持下,可以做出非常强大的效果。

还有,它是本地客户端的工具,可以和本地资源做连接的,这比起线上表格,有了太广阔的扩展空间了,无论是批量性读取其他系统导出的表格、文本等文件,还有对网页非结构化数据的抓取能力,都是在线表格类产品无法相比的。

当然最关键的,它还有VBA/JSA对象模型的生态,可以将绝大多数的界面操作转换为自动化后台代码完成。

有点扯远了,本篇分享的是WPS的JSA开发文档,老规矩,有兴趣的可加笔者微信 190262897 获取。

下面一些小插曲,有兴趣的可以往下看看,笔者亲自实践过,借助开发文档,交由AI来写代码,也是很可行的,当然,前提最好自己懂VBA对象模型、懂点JS语言语法。

本篇其实是一个小财务需求,需要将财务系统的导出xlsx文件,进行再加工,在Excel/WPS环境下方便地查看报告。当然要完全自动化,就需要用代码去驱动,综合评估了使用RPA软件,Excel/WPS的VBA代码和WPS的JSA代码来完成,最终选择了WPS的JSA方式实现。

简单说明下理由:

1、交付方便性:RPA软件,需要安装一个软件,增加交付成本。

2、本篇需求,完全是只以Excel/WPS作交互,RPA软件也没有太大的额外优势需要选择它。

3、交付的效果,只需要点击一个按钮,运行代码后即可,没有界面交互要求,RPA软件作界面(笔者还挺喜欢用RPA软件做简单的交互界面,比起VBA/JSA的古老窗体界面更舒服快捷)的优势也不明显。

4、VBA和JSA的选择上,选择了后者,是为了在数据处理过程中,可以有更现代的数据结构来处理,写出的代码也更加优雅,VBA来写就比较憋屈。

5、交付对象使用的是WPS,交付成VBA也憋屈,默认不自带VBA模块,虽然很容易通过安装实现,但想挑战下极端环境,没有VBA模块的交付场景。

AI编程里,相比JSA、VBA编程,应该更为成熟,毕竟语料更丰富,AI写出来的代码应该更稳,这个结论,估计今天仍然成立。

在人工写代码中,JSA的宏编缉器环境,实在很简陋,很难写代码,极少的智能提示,所以本篇需求,笔者是努力让AI全程来写代码,人只负责复制粘贴,AI写JS的前端项目代码很擅长,但写JSA代码,就非常容易有幻觉,乱写一通,所以,给它一份markdown格式的文档,就显得尤为重要。

当然笔者在今年年初已经爬取了WPS的JSA/WPSJS加载项官方文档,整理成pdf了,这一次重新拿来整理成markdown格式给AI参考,最终AI写出来的代码,正确率也还不错的。

总共整理了4个文件夹

公众号文章插图

公众号文章插图

公众号文章插图

公众号文章插图

为了尽可能验证可行性,本次的项目,使用的是TRAE CN版本,大家都能轻松获取到的,全免费使用的。用的模型以Kimi-K2-0905为主,

分享部分对话过程,写出来的代码,还真不错,个别有点小错误,多轮对话修正下,可以接受,关键是全程不用在WPS的JSA宏编程器上写代码,就算在TRAE CN上小改一下,也有AI智能提示,也是很流畅。

公众号文章插图

公众号文章插图

公众号文章插图

公众号文章插图

公众号文章插图

公众号文章插图

公众号文章插图

公众号文章插图

公众号文章插图

国产模型,多轮对话过程中,建议反复提交文档给它参考,不知道它哪一轮对话又放飞自我,用幻觉来写出各种不存在的接口方法。而为了多次一口次引用多个文档,最后使用了之前开发的ClipboardTool工具,识别出这个文档的剪切板数据类型,将它固化后多次使用,当然最后在Ditto上,也测试成功,可以添加这种自定义剪切板类型来固化它的内容,供下次粘贴使用。

公众号文章插图

最后,附上一个项目结果的效果,从财务系统导出的的文件,读到数据源中,通过函数的方式,将数据引用到报表层来使用,用Filter、TextBefore、LET等现代函数来制作,实现简单,又语义性极强,真的太舒服了,特别是WPS个人版全部函数同步到位后,函数能轻松解决的,还懒得写代码实现了。

公众号文章插图

公众号文章插图

让笔者有兴趣使用JSA来实现,很大原因是,类似以下这样的科目表数据处理,财务的科目是父子结构的,需要处理成这种多级分类,并且有些级别是长短不一的,例如有些到2级就没有了,有些是有3级分类的,还有有些三级分类下只有一个成员,不需要小计统计的等等,笔者看到这些需求,用VBA来写有些头皮发麻,就想着AI驱动下用JS语言来处理应该更流畅。

公众号文章插图

公众号文章插图

最后,附上本篇的JSA代码,大部分是AI写出来的,而且AI写的也很规范,叫它重构,多些注释和日志输出等,也很听话,写出来的代码可读性很不错。每个子方法上还有定义,在TRAE上跳转阅读和悬停也很流畅。

公众号文章插图

/** * WPS JSA 财务利润报表生成宏 - 重构版 * * 功能说明: * 1. 从"会计科目_导入"文件中读取会计科目数据 * 2. 处理成本数据并写入成本表 * 3. 处理损益类数据并写入成本表 * 4. 合并相同内容的单元格 * 5. 处理辅助余额表数据 * * 代码结构: * - 主函数:main() - 程序入口,协调各模块 * - 文件操作模块:文件查找、工作簿处理 * - 数据处理模块:成本数据处理、损益类数据处理 * - 辅助余额表模块:辅助余额表数据处理 * - 工具函数模块:单元格合并、边框设置等 */  
// 全局常量定义const START_ROW = 17; // 数据起始行号(从17行开始填充数据)  
/** * 主函数 - 程序入口 * 协调各个功能模块,处理主要业务流程 */function main() {  try {    console.log("=== 财务利润报表生成程序开始 ===");  
    // 步骤1:初始化环境    let context = initializeEnvironment();  
    // 步骤2:查找并打开会计科目源文件    let sourceFile = findTargetFile(context.currentPath, "会计科目_导入", ".xlsx");    if (!sourceFile) {      alert("未找到以'会计科目_导入'开头的xlsx文件!");      return;    }  
    let sourceWkb = openOrGetWorkbook(sourceFile);  
    // 步骤3:读取会计科目数据    let accountingData = readAccountingData(sourceWkb);    if (!accountingData) {      sourceWkb.Close(false);      return;    }  
    // 步骤4:处理成本数据    let costResult = processCostData(accountingData);    console.log("成本数据处理完成,共" + costResult.length + "行");  
    // 步骤5:处理损益类数据    let profitLossResult = processProfitLossData(accountingData);    console.log("损益类数据处理完成,共" + profitLossResult.length + "行");  
    // 步骤6:写入结果数据    writeResultsToWorkbook(context.actWkb, costResult, profitLossResult, accountingData);  
    // 步骤7:处理辅助余额表    processAuxiliaryBalanceSheet(context.actWkb, context.currentPath);  
    // 步骤7:处理辅助余额表    processAuxiliaryBalanceSheet(context.actWkb, context.currentPath);    //对 汇总表 的表头进行数字文本化处理,单元格内容是数字的,将它设置成文本    let summarySht = context.actWkb.Worksheets("汇总表");    let headerRange = summarySht.Range("名称_表头区域");    headerRange.NumberFormat = "@";    // 新增:对headerRange第三行内容作判断,如果是数字则前面加'强制转文本    let thirdRow = headerRange.Rows(3);    for (let c = 1; c <= thirdRow.Columns.Count; c++) {      let cell = thirdRow.Cells(1, c);      let val = cell.Value2;      // 更准确的数字判断      if (val !== null && val !== undefined && val !== "" && !isNaN(val) && !isNaN(parseFloat(val))) {        cell.Value2 = "'" + val;      }    }  
    // 步骤8:清理资源    cleanupResources(sourceWkb);  
    console.log("=== 财务利润报表生成程序完成 ===");  } catch (error) {    console.log("发生错误: " + error.message);    alert("处理过程中发生错误: " + error.message);  }}  
/** * 初始化环境 * 获取当前工作簿和路径信息 * @returns {Object} 包含actWkb和currentPath的对象 */function initializeEnvironment() {  let actWkb = Application.ActiveWorkbook;  let currentPath = actWkb.Path;  
  console.log("当前工作簿: " + actWkb.Name);  console.log("当前路径: " + currentPath);  
  return {    actWkb: actWkb,    currentPath: currentPath  };}  
/** * 查找目标文件 * 在指定路径下查找符合前缀和后缀条件的文件 * @param {string} searchPath - 搜索路径 * @param {string} filePrefix - 文件前缀 * @param {string} fileExtension - 文件扩展名 * @returns {string|null} 找到的文件完整路径,未找到返回null */function findTargetFile(searchPath, filePrefix, fileExtension) {  console.log(`查找文件:路径=${searchPath}, 前缀=${filePrefix}, 扩展名=${fileExtension}`);  
  let files = FileSystem.readdirSync(searchPath);  
  for (let i = 0; i < files.length; i++) {    let fileName = files[i];    if (fileName.startsWith(filePrefix) && fileName.endsWith(fileExtension)) {      let fullPath = searchPath + "\\" + fileName;      console.log("找到目标文件: " + fullPath);      return fullPath;    }  }  
  console.log("未找到符合条件的文件");  return null;}  
/** * 打开或获取已打开的工作簿 * 如果工作簿已打开则直接返回,否则打开新工作簿 * @param {string} filePath - 工作簿文件路径 * @returns {Object} 工作簿对象 */function openOrGetWorkbook(filePath) {  // 检查是否已打开  let allWorkbooks = Application.Workbooks;  for (let i = 1; i <= allWorkbooks.Count; i++) {    let wkb = allWorkbooks.Item(i);    if (wkb.FullName === filePath) {      console.log("工作簿已打开,直接使用: " + wkb.Name);      return wkb;    }  }  
  // 未打开则新建  let newWkb = Application.Workbooks.Open(filePath);  console.log("已打开工作簿: " + newWkb.Name);  return newWkb;}  
/** * 读取会计科目数据 * 从源工作簿的"会计科目"工作表中读取数据 * @param {Object} sourceWkb - 源工作簿对象 * @returns {Array|null} 会计科目数据数组,失败返回null */function readAccountingData(sourceWkb) {  try {    let sourceSheet = sourceWkb.Sheets.Item("会计科目");    if (!sourceSheet) {      alert("未找到'会计科目'工作表!");      return null;    }  
    let lastRow = sourceSheet.UsedRange.Rows.Count;    let dataRange = sourceSheet.Range("F10:H" + lastRow);    let dataArray = dataRange.Value2;  
    console.log("读取数据范围: F10:H" + lastRow);    console.log("数据行数: " + dataArray.length);  
    return dataArray;  } catch (error) {    console.log("读取会计科目数据失败: " + error.message);    alert("读取会计科目数据失败: " + error.message);    return null;  }}  
/** * 将处理结果写入工作簿 * 将成本数据和损益类数据分别写入指定位置 * @param {Object} targetWkb - 目标工作簿 * @param {Array} costResult - 成本处理结果 * @param {Array} profitLossResult - 损益类处理结果 * @param {Array} subjectData - 原始会计科目数据 */function writeResultsToWorkbook(targetWkb, costResult, profitLossResult, subjectData) {  let costSheet = targetWkb.Sheets.Item("成本表");  // 写入成本数据  let costRows = costResult.length;  let costCols = costResult[0].length;  let costRange = costSheet.Range("A" + START_ROW).Resize(costRows, costCols);  
  let clearCostRange = Application.Intersect(costRange.CurrentRegion, costRange.CurrentRegion.Offset(1, 0));  clearCostRange.Clear();  //科目编码处理成文本  costSheet.Columns("D:D").NumberFormat = "@";  costRange.Value2 = costResult;  console.log("成本数据已写入【成本表】A" + START_ROW + "开始的" + costRows + "行" + costCols + "列区域");  
  // 写入损益类数据  let profitLossRows = profitLossResult.length;  let profitLossCols = profitLossResult[0].length;  let profitLossRange = costSheet.Range("H" + START_ROW).Resize(profitLossRows, profitLossCols);  let clearProfitLossRange = Application.Intersect(profitLossRange.CurrentRegion, profitLossRange.CurrentRegion.Offset(1, 0));  clearProfitLossRange.Clear();  //科目编码处理成文本  costSheet.Columns("J:J").NumberFormat = "@";  profitLossRange.Value2 = profitLossResult;  console.log("损益类数据已写入【成本表】H" + START_ROW + "开始的" + profitLossRows + "行" + profitLossCols + "列区域");  
  // 激活目标工作簿  targetWkb.Activate();  
  // 合并单元格和设置边框  mergeIdenticalCells(costSheet, costResult, "A" + (START_ROW - 1), 0);  mergeIdenticalCells(costSheet, costResult, "B" + (START_ROW - 1), 1);  setRangeBorder(costSheet.Range("A" + (START_ROW - 1)).CurrentRegion);  
  mergeIdenticalCells(costSheet, profitLossResult, "H" + (START_ROW - 1), 0);  setRangeBorder(costSheet.Range("H" + (START_ROW - 1)).CurrentRegion);  // 添加E13:F13和K13:L13的公式并填充到CurrentRegion的最后一行  addFormulasToCostSheet(costSheet);  
  //对C列和H列出现小计字样的进行加粗显示  formatSubtotalCells(costSheet);}  
/** * 对C列和H列中内容为"小计:"的单元格进行格式化 * 包括扩展为一行三列并加粗显示 * @param {Object} sheet - 工作表对象 */function formatSubtotalCells(sheet) {  console.log("开始格式化C列和H列中的小计单元格...");  
  // 获取工作表的最后一行  let lastRow = sheet.UsedRange.Rows.Count;  console.log("工作表最后行号: " + lastRow);  
  // 处理C列中的小计单元格(从起始行到最后一行)  formatSubtotalInRange(sheet, "C", START_ROW, lastRow);  
  // 处理H列中的小计单元格(从起始行到最后一行)  formatSubtotalInRange(sheet, "I", START_ROW, lastRow);  
  console.log("小计单元格格式化完成");}  
/** * 在指定列范围内查找并格式化小计单元格 * @param {Object} sheet - 工作表对象 * @param {string} columnLetter - 列字母(如"C"或"H") * @param {number} startRow - 开始行号 * @param {number} endRow - 结束行号 */function formatSubtotalInRange(sheet, columnLetter, startRow, endRow) {  console.log("开始处理" + columnLetter + "列,范围:" + columnLetter + startRow + ":" + columnLetter + endRow);  
  // 遍历指定范围内的每一行  for (let row = startRow; row <= endRow; row++) {    let cell = sheet.Range(columnLetter + row);    let cellValue = cell.Value2; // 使用Text属性获取显示文本  
    // 检查单元格内容是否为"小计:"    if (cellValue === "小计:") {      console.log("找到小计单元格: " + columnLetter + row + ",值: " + cellValue);  
      // 扩展单元格为一行三列      let expandRange = sheet.Range(columnLetter + row).Resize(1, 4);      // 设置加粗格式      expandRange.Font.Bold = true;  
      console.log("已格式化单元格: " + columnLetter + row + ",扩展为一行三列并加粗");    }  }}  
/** * 处理辅助余额表 * 查找辅助余额表文件,处理其中的数据并写入当前工作簿 * @param {Object} actWkb - 当前活动工作簿 * @param {string} currentPath - 当前工作路径 */function processAuxiliaryBalanceSheet(actWkb, currentPath) {  console.log("开始处理辅助余额表...");  
  // 查找辅助余额表文件  let balanceFile = findTargetFile(currentPath, "科目辅助全景查询", ".xlsx");  if (!balanceFile) {    console.log("未找到以'科目辅助全景查询'开头的xlsx文件,跳过辅助余额表处理");    return;  }  
  // 打开或获取辅助余额表工作簿  let balanceWkb = openOrGetWorkbook(balanceFile);  
  try {    // 获取"辅助余额表"工作表(假设一定存在)    let balanceSht = actWkb.Sheets.Item("科目辅助全景查询");    console.log("找到'科目辅助全景查询'工作表");  
    // 查找名为"表_科目辅助全景查询"的ListObject    let balanceTable = null;    try {      balanceTable = balanceSht.ListObjects.Item("表_科目辅助全景查询");      console.log("找到'表_科目辅助全景查询'ListObject");  
      // 清空ListObject的数据内容(保留表头)      if (balanceTable.DataBodyRange) {        balanceTable.DataBodyRange.Clear();        console.log("已清空'表_科目辅助全景查询'的数据内容");      }  
      // 将ListObject大小调整为2行(含标题行)      let currentColumns = balanceTable.Range.Columns.Count;      let newRange = balanceSht.Range(balanceTable.Range.Cells(1, 1), balanceTable.Range.Cells(2, currentColumns));      balanceTable.Resize(newRange);  
      balanceSht.Columns.Item("B:B").NumberFormat = "@";      balanceSht.Columns.Item("D:D").NumberFormat = "@";      balanceSht.Columns.Item("F:F").NumberFormat = "@";      balanceSht.Columns.Item("H:H").NumberFormat = "@";      balanceSht.Columns.Item("J:J").NumberFormat = "@";      balanceSht.Columns.Item("L:L").NumberFormat = "@";      balanceSht.Columns.Item("N:N").NumberFormat = "@";      console.log("已将'表_科目辅助全景查询'调整为2行大小(含标题行)");    } catch (tableError) {      console.log("未找到'表_科目辅助全景查询'ListObject: " + tableError.message);      throw tableError;    }  
    // 读取源数据    let balanceSourceSheet = balanceWkb.Sheets.Item(1);    let balanceLastRow = balanceSourceSheet.UsedRange.Rows.Count - 1; // 有合计行,减少一行    let balanceLastCol = balanceSourceSheet.UsedRange.Columns.Count;    let lastColLetter = getColumnLetter(balanceLastCol);    console.log("科目辅助全景查询源数据最后行号: " + balanceLastRow);    console.log("科目辅助全景查询源数据最后列号: " + balanceLastCol + " (" + lastColLetter + ")");  
    let balanceDataRange = balanceSourceSheet.Range("A15:" + lastColLetter + balanceLastRow);    let arrValue = balanceDataRange.Value2;  
    console.log("读取科目辅助全景查询数据范围: A15:" + lastColLetter + balanceLastRow);    console.log("数据数组维度: " + arrValue.length + "行 x " + (arrValue[0] ? arrValue[0].length : 0) + "列");  
    // 写入数据到A2单元格开始的位置    let targetRange = balanceTable.Range.Offset(1, 0).Resize(arrValue.length, arrValue[0].length);    targetRange.Value2 = arrValue;    balanceTable.Resize(Application.Union(balanceTable.Range, targetRange.Resize(targetRange.Rows.Count, balanceTable.Range.Columns.Count)));  
    console.log("已将科目辅助全景查询数据写入当前工作簿的'科目辅助全景查询'工作表,从A2开始");    console.log("写入数据范围: A2:" + lastColLetter + (arrValue.length + 1));  
    let formulaProjectId = "=LEFT([@项目编码],5)";    let mainProjectIdColumn = balanceTable.ListColumns("主项目编码").DataBodyRange;    mainProjectIdColumn.Formula = formulaProjectId;  
    let formulaCustomerCheck = '=IF(LEFT([@客户编码],1)="1",TRUE,FALSE)';    let customerCheckColumn = balanceTable.ListColumns("客户判断").DataBodyRange;    customerCheckColumn.Formula = formulaCustomerCheck;  
    let formulaTax = "=IFERROR(VLOOKUP([@项目编码],表_子项目信息,4,0),0)";    let taxColumn = balanceTable.ListColumns("项目税率").DataBodyRange;    taxColumn.Formula = formulaTax;  
    let formulaNonTaxIncome = "=[@本期发生-借方本币]/(1+[@项目税率])";    let nonTaxIncomeColumn = balanceTable.ListColumns("不含税收入金额").DataBodyRange;    nonTaxIncomeColumn.Formula = formulaNonTaxIncome;  
    // 处理配置表(包含项目表、公司表和子项目信息表)    processSettingTable(actWkb, arrValue);  } catch (error) {    console.log("处理科目辅助全景查询时发生错误: " + error.message);    throw error;  } finally {    // 确保关闭科目辅助全景查询工作簿    if (balanceWkb) {      try {        balanceWkb.Close(false);        console.log("已关闭科目辅助全景查询源工作簿");      } catch (closeError) {        console.log("关闭工作簿时发生错误: " + closeError.message);      }    }  }}  
/** * 添加E13和J13的公式并填充到CurrentRegion的最后一行 * @param {Object} costSheet - 成本表工作表对象 */function addFormulasToCostSheet(costSheet) {  console.log("开始添加E" + START_ROW + ":F" + START_ROW + "和K" + START_ROW + ":L" + START_ROW + "的公式...");  
  try {    // 获取E列起始行的CurrentRegion范围    let eStartCurrentRegion = costSheet.Range("E" + START_ROW).CurrentRegion;    let e13LastRow = eStartCurrentRegion.Rows.Count + eStartCurrentRegion.Row - 1;    console.log("E" + START_ROW + "的CurrentRegion范围最后一行: " + e13LastRow);  
    // 设置E列起始行的公式    let e13Formula = "=IFS(LEN(D" + START_ROW + ")=6,TRUE,AND(LEN(D" + START_ROW + ")=8,COUNTIF($D$" + START_ROW + ":$D$" + e13LastRow + ",LEFT(D" + START_ROW + ',6)&"*")=1),TRUE,TRUE,FALSE)';    let f13Formula =      '=IF(AND(名称_公司="",LEFT(D' +      START_ROW +      ',4)="4406"),0,SUM(FILTER(表_科目辅助全景查询[本期发生-借方本币],((表_科目辅助全景查询[主项目编码]=名称_项目编号)+(名称_项目编号=""))*((表_科目辅助全景查询[核算账簿]=成本表!名称_公司)+(成本表!名称_公司=""))*(表_科目辅助全景查询[科目编码]=D' +      START_ROW +      "),0)))";  
    costSheet.Range("E" + START_ROW).Formula = e13Formula;    costSheet.Range("F" + START_ROW).Formula = f13Formula;    console.log("已设置E" + START_ROW + "、F" + START_ROW + "单元格公式");  
    // 对E列起始行到F{lastRow}范围进行FillDown填充    let efFillRange = costSheet.Range("E" + START_ROW + ":F" + e13LastRow);    efFillRange.FillDown();    console.log("已完成E" + START_ROW + ":F" + e13LastRow + "范围的FillDown填充");    efFillRange.Columns.Item(2).NumberFormat = '_ * #,##0.00_ ;_ * -#,##0.00_ ;_ * "-"??_ ;_ @_ ';    // 获取K列起始行的CurrentRegion范围    let kStartCurrentRegion = costSheet.Range("K" + START_ROW).CurrentRegion;    let k13LastRow = kStartCurrentRegion.Rows.Count + kStartCurrentRegion.Row - 1;    console.log("K" + START_ROW + "的CurrentRegion范围最后一行: " + k13LastRow);  
    // 设置K列起始行的公式    let k13Formula = "=IFS(LEN(J" + START_ROW + ")=4,TRUE,AND(LEN(J" + START_ROW + ")=6,COUNTIF($J$" + START_ROW + ":$J$" + k13LastRow + ",LEFT(J" + START_ROW + ',4)&"*")=1),TRUE,TRUE,FALSE)';    let l13Formula =      '=SUM(FILTER(表_科目辅助全景查询[本期发生-借方本币],((表_科目辅助全景查询[主项目编码]=名称_项目编号)+(名称_项目编号=""))*((表_科目辅助全景查询[核算账簿]=成本表!名称_公司)+(成本表!名称_公司=""))*(表_科目辅助全景查询[科目编码]=J' +      START_ROW +      "),0))";    costSheet.Range("K" + START_ROW).Formula = k13Formula;    costSheet.Range("L" + START_ROW).Formula = l13Formula;    console.log("已设置K" + START_ROW + "、L" + START_ROW + "单元格公式");  
    // 对K列起始行到L{lastRow}范围进行FillDown填充    let klFillRange = costSheet.Range("K" + START_ROW + ":L" + k13LastRow);    klFillRange.FillDown();    console.log("已完成K" + START_ROW + ":L" + k13LastRow + "范围的FillDown填充");    klFillRange.Columns.Item(2).NumberFormat = '_ * #,##0.00_ ;_ * -#,##0.00_ ;_ * "-"??_ ;_ @_ ';    // 设置L5单元格的公式,使用动态的e13LastRow和k13LastRow    let l5Formula =      "=SUMIF(E" + START_ROW + ":E" + e13LastRow + ",TRUE,F" + START_ROW + ":F" + e13LastRow + ")+SUMIF(K" + START_ROW + ":K" + k13LastRow + ",TRUE,L" + START_ROW + ":L" + k13LastRow + ")";    costSheet.Range("L5").Formula = l5Formula;    console.log("已设置L5单元格公式: " + l5Formula);  
    console.log("E" + START_ROW + "、J" + START_ROW + "、K" + START_ROW + "和L5的公式添加完成");  } catch (error) {    console.log("添加公式时发生错误: " + error.message);    throw error;  }}  
/** * 处理配置表(包含项目表、公司表和子项目信息表) * 1. 从辅助余额表数据中提取第3、4列数据,去重后写入配置表的"表_项目"ListObject * 2. 处理公司表:从辅助余额表数据中提取第5列数据,去重后写入配置表的"表_公司"ListObject * 3. 处理子项目信息表:使用项目表处理后的数据 * @param {Object} actWkb - 当前活动工作簿 * @param {Array} arrValue - 辅助余额表数据数组 */function processSettingTable(actWkb, arrValue) {  console.log("开始处理配置表中的项目表...");  
  // 定义uniqueData变量在外层作用域,供后续processSubprojectInfoTable函数使用  let uniqueData = [];  
  try {    // 获取配置表工作表    let configSheet = actWkb.Sheets.Item("配置表");    console.log("找到'配置表'工作表");  
    // 从arrValue中提取第3、4列数据(JavaScript数组是0-based索引)    let projectData = [];    for (let i = 0; i < arrValue.length; i++) {      if (arrValue[i][3] && arrValue[i][4]) {        // 第3列和第4列都有值时,添加到projectData数组        projectData.push([arrValue[i][3], arrValue[i][4]]);      }    }  
    console.log("提取项目数据,共" + projectData.length + "行");  
    // 去重处理    let seen = new Set();  
    for (let i = 0; i < projectData.length; i++) {      let key = projectData[i][0] + "|" + projectData[i][1];      if (!seen.has(key)) {        seen.add(key);        uniqueData.push(projectData[i]);      }    }  
    console.log("去重后项目数据,共" + uniqueData.length + "行");  
    // 查找名为"表_项目"的ListObject    let projectTable = null;    try {      projectTable = configSheet.ListObjects.Item("表_项目");      console.log("找到'表_项目'ListObject");  
      // 清空整个数据区域      if (projectTable.DataBodyRange) {        projectTable.DataBodyRange.Clear();        console.log("已清空'表_项目'ListObject的所有数据内容");      }  
      // 将ListObject调整为2行大小(保留标题行和1行数据行)      let currentColumns = projectTable.Range.Columns.Count;      let newRange = configSheet.Range(projectTable.Range.Cells(1, 1), projectTable.Range.Cells(2, currentColumns));      projectTable.Resize(newRange);      console.log("已将'表_项目'ListObject调整为2行大小");      newRange.Columns.Item(1).EntireColumn.NumberFormat = "@";      // 将去重后的数组写入ListObject.DataRange.Cells(1)开始的位置      if (uniqueData.length > 0) {        // 筛选第1列成员长度小于7的成员        let filteredUniqueData = [];        for (let i = 0; i < uniqueData.length; i++) {          if (uniqueData[i][0] && String(uniqueData[i][0]).length < 7) {            filteredUniqueData.push(uniqueData[i]);          }        }        console.log("筛选后项目数据(第1列长度<7),共" + filteredUniqueData.length + "行");  
        let targetCell = projectTable.Range.Offset(1, 0).Cells(1); // 第1个数据单元格        let targetRange = targetCell.Resize(filteredUniqueData.length, filteredUniqueData[0].length);        targetRange.Value2 = filteredUniqueData;        projectTable.Resize(Application.Union(projectTable.Range, targetRange.Resize(targetRange.Rows.Count, projectTable.Range.Columns.Count)));  
        console.log("已将去重后的项目数据写入'表_项目'ListObject,从DataRange.Cells(1)开始");        console.log("写入数据范围: " + targetRange.Address());  
        // 重新设置第3列的公式        // 使用ListColumn.DataBodyRange.Formula设置整列公式(FormulaArray是只读的)        let savedFormula = '=[@项目编码]&"-"&[@项目名称]';        let column3Range = projectTable.ListColumns(3).DataBodyRange;        if (column3Range) {          column3Range.Formula = savedFormula;          console.log("已重新设置第3列的公式: " + savedFormula);        }  
        // 更新定义名称"名称_项目引用"的引用区域为projectTable第3列的数据体区域        try {          let projectNameRef = actWkb.Names.Item("名称_项目引用");          projectNameRef.RefersTo = projectTable.ListColumns(3).DataBodyRange;          console.log("已更新定义名称'名称_项目引用',引用区域为: " + projectTable.ListColumns(3).DataBodyRange.Address());        } catch (nameError) {          console.log("更新定义名称'名称_项目引用'时出错: " + nameError.message);        }      }    } catch (tableError) {      console.log("未找到'表_项目'ListObject: " + tableError.message);      throw tableError;    }  } catch (error) {    console.log("处理配置表项目表时发生错误: " + error.message);    throw error;  }  
  // 继续处理公司表和子项目信息表  processCompanyTable(actWkb, arrValue);  
  // 处理子项目信息表(使用项目表处理后的uniqueData)  processSubprojectInfoTable(actWkb, uniqueData);  
  console.log("配置表处理完成(包含项目表、公司表和子项目信息表)");}  
/** * 处理配置表中的公司表 * 从辅助余额表数据中提取第5列数据,去重后写入配置表的"表_公司"ListObject * @param {Object} actWkb - 当前活动工作簿 * @param {Array} arrValue - 辅助余额表数据数组 */function processCompanyTable(actWkb, arrValue) {  console.log("开始处理配置表中的公司表...");  
  try {    // 获取配置表工作表    let configSheet = actWkb.Sheets.Item("配置表");    console.log("找到'配置表'工作表");  
    // 从arrValue中提取第1列数据(JavaScript数组是0-based索引,所以是第0列)    let companyData = [];    for (let i = 0; i < arrValue.length; i++) {      if (arrValue[i][0]) {        // 第1列有值时,添加到companyData数组        companyData.push([arrValue[i][0]]);      }    }  
    console.log("提取公司数据,共" + companyData.length + "行");  
    // 去重处理    let uniqueData = [];    let seen = new Set();  
    for (let i = 0; i < companyData.length; i++) {      let key = companyData[i][0];      if (!seen.has(key)) {        seen.add(key);        uniqueData.push(companyData[i]);      }    }  
    console.log("去重后公司数据,共" + uniqueData.length + "行");  
    // 查找名为"表_公司"的ListObject    let companyTable = null;    try {      companyTable = configSheet.ListObjects.Item("表_公司");      console.log("找到'表_公司'ListObject");  
      // 清空ListObject的数据内容      if (companyTable.DataBodyRange) {        companyTable.DataBodyRange.Clear();        console.log("已清空'表_公司'ListObject的数据内容");      }  
      // 将ListObject调整为2行大小(保留标题行和1行数据行)      let currentColumns = companyTable.Range.Columns.Count;      let newRange = configSheet.Range(companyTable.Range.Cells(1, 1), companyTable.Range.Cells(2, currentColumns));      companyTable.Resize(newRange);      console.log("已将'表_公司'ListObject调整为2行大小");  
      // 将去重后的数组写入ListObject.DataRange.Cells(1)开始的位置      if (uniqueData.length > 0) {        let targetCell = companyTable.Range.Offset(1, 0).Cells(1); // 第1个数据单元格        let targetRange = targetCell.Resize(uniqueData.length, uniqueData[0].length);        targetRange.Value2 = uniqueData;        companyTable.Resize(Application.Union(companyTable.Range, targetRange.Resize(targetRange.Rows.Count, companyTable.Range.Columns.Count)));  
        console.log("已将去重后的公司数据写入'表_公司'ListObject,从DataRange.Cells(1)开始");        console.log("写入数据范围: " + targetRange.Address());  
        // 更新定义名称"名称_公司引用"的引用区域为companyTable第1列的数据体区域        try {          let companyNameRef = actWkb.Names.Item("名称_公司引用");          companyNameRef.RefersTo = companyTable.ListColumns(1).DataBodyRange;          console.log("已更新定义名称'名称_公司引用',引用区域为: " + companyTable.ListColumns(1).DataBodyRange.Address());        } catch (nameError) {          console.log("更新定义名称'名称_公司引用'时出错: " + nameError.message);        }      }    } catch (tableError) {      console.log("未找到'表_公司'ListObject: " + tableError.message);  
      throw tableError;    }  } catch (error) {    console.log("处理配置表公司表时发生错误: " + error.message);  
    throw error;  }}  
/** * 处理配置表中的子项目信息表 * 从uniqueData数组中过滤出第1列长度为7位的成员,写入配置表的"表_子项目信息"ListObject * @param {Object} actWkb - 当前活动工作簿 * @param {Array} uniqueData - 已经去重的项目数据(来自processProjectTable) */function processSubprojectInfoTable(actWkb, uniqueData) {  console.log("开始处理配置表中的子项目信息表...");  
  try {    // 获取配置表工作表    let configSheet = actWkb.Sheets.Item("配置表");    console.log("找到'配置表'工作表");  
    // 过滤uniqueData,找出第1列以1、2或3开头的成员    let filteredData = [];    const validPrefixes = ["1", "2", "3"]; // 定义有效前缀  
    for (let i = 0; i < uniqueData.length; i++) {      if (uniqueData[i][0]) {        const firstChar = String(uniqueData[i][0]).charAt(0);        if (validPrefixes.includes(firstChar)) {          filteredData.push(uniqueData[i]);        }      }    }  
    console.log("过滤后子项目信息数据(找出第1列以1、2或3开头的成员),共" + filteredData.length + "行");  
    // 查找名为"表_子项目信息"的ListObject    let subprojectTable = null;    try {      subprojectTable = configSheet.ListObjects.Item("表_子项目信息");      console.log("找到'表_子项目信息'ListObject");  
      // 在ListObject的Range.Offset(Range.Rows.Count, 0).Cells(1)位置赋值      let currentRows = subprojectTable.Range.Rows.Count;      let targetCell = subprojectTable.Range.Offset(currentRows, 0).Cells(1);  
      // 将过滤后的数组写入目标位置,先resize为数组的行、列大小      if (filteredData.length > 0) {        let targetRange = targetCell.Resize(filteredData.length, filteredData[0].length);        targetRange.Value2 = filteredData;        subprojectTable.Resize(Application.Union(subprojectTable.Range, targetRange.Resize(targetRange.Rows.Count, subprojectTable.Range.Columns.Count)));        console.log("已将过滤后的子项目信息数据写入'表_子项目信息'ListObject");        console.log("写入起始位置: " + targetCell.Address());        console.log("写入数据范围: " + targetRange.Address());  
        // 使用WPSJS API的RemoveDuplicates方法按第1列去重        try {          let beforeCount = targetRange.Rows.Count;          targetRange.RemoveDuplicates([1]); // 按第1列去重          let afterCount = targetRange.Rows.Count;          let removedCount = beforeCount - afterCount;  
          console.log("子项目信息表去重完成:");          console.log("- 去重前记录数: " + beforeCount);          console.log("- 去重后记录数: " + afterCount);          console.log("- 已删除重复记录: " + removedCount);  
          if (removedCount > 0) {            console.log("子项目信息表按第1列成功去除 " + removedCount + " 条重复记录");          } else {            console.log("子项目信息表未发现重复记录");          }        } catch (dedupError) {          console.log("使用RemoveDuplicates去重时出错: " + dedupError.message);          console.log("已跳过去重步骤,保留原始数据");        }      }    } catch (tableError) {      console.log("未找到'表_子项目信息'ListObject: " + tableError.message);  
      throw tableError;    }  } catch (error) {    console.log("处理配置表子项目信息表时发生错误: " + error.message);  
    throw error;  }}  
/** * 将列号转换为列字母(如1->A,26->Z,27->AA) * @param {number} columnNumber - 列号(从1开始) * @returns {string} 列字母 */function getColumnLetter(columnNumber) {  let letter = "";  while (columnNumber > 0) {    columnNumber--;    letter = String.fromCharCode(65 + (columnNumber % 26)) + letter;    columnNumber = Math.floor(columnNumber / 26);  }  return letter;}  
/** * 清理资源 * 关闭不再需要的工作簿等资源 * @param {Object} workbook - 需要关闭的工作簿 */function cleanupResources(workbook) {  if (workbook) {    try {      workbook.Close(false);      console.log("已关闭工作簿: " + workbook.Name);    } catch (error) {      console.log("关闭工作簿失败: " + error.message);    }  }}  
// ==================== 数据处理函数 ====================  
/** * 处理成本数据(基于转换脚本.js逻辑) * 过滤4401或4403开头的成本科目,按层级结构组织数据 * @param {Array} subjectData - 会计科目原始数据 * @returns {Array} 处理后的成本数据结构 */function processCostData(subjectData) {  // 过滤4401或4403开头的成本科目(新格式:第1列是科目编码,第3列是会计要素名称)  let costSubjects = [];  for (let i = 0; i < subjectData.length; i++) {    let item = subjectData[i];    let code = String(item[0]); // 第1列是科目编码    if ((code.startsWith("4401") || code.startsWith("4403") || code.startsWith("4406")) && item[2] === "成本") {      // 第3列是会计要素名称      costSubjects.push(item);    }  }  
  // 按层级结构组织数据,同时记录科目编码  let structure = {};  let codeToNameMap = {}; // 用于存储科目编码到名称的映射  
  for (let i = 0; i < costSubjects.length; i++) {    let item = costSubjects[i];    let code = String(item[0]); // 第1列是科目编码    let name = item[1]; // 第2列是科目名称  
    // 记录编码到名称的映射    codeToNameMap[code] = name;  
    // 根据编码长度判断层级    if (code.length === 4) {      // 一级科目      if (!structure[code]) {        structure[code] = {          name: name,          code: code,          children: {}        };      }    } else if (code.length === 6) {      // 二级科目      let parentCode = code.substring(0, 4);      if (structure[parentCode]) {        if (!structure[parentCode].children[code]) {          structure[parentCode].children[code] = {            name: name,            code: code,            children: {}          };        }      }    } else if (code.length === 8) {      // 三级科目      let parentCode = code.substring(0, 4);      let subParentCode = code.substring(0, 6);      if (structure[parentCode] && structure[parentCode].children[subParentCode]) {        structure[parentCode].children[subParentCode].children[code] = {          name: name,          code: code        };      }    }  }  
  // 转换为整理结构格式(包含科目编号)  let result = [];  
  let firstLevelCodes = Object.keys(structure);  for (let i = 0; i < firstLevelCodes.length; i++) {    let firstLevelCode = firstLevelCodes[i];    let firstLevel = structure[firstLevelCode];  
    let secondLevelCodes = Object.keys(firstLevel.children);    for (let j = 0; j < secondLevelCodes.length; j++) {      let secondLevelCode = secondLevelCodes[j];      let secondLevel = firstLevel.children[secondLevelCode];      let thirdLevelCount = Object.keys(secondLevel.children).length;  
      if (thirdLevelCount === 0) {        // 没有三级分类的二级分类,二级和三级都叫相同的名字        result.push([          firstLevel.name,          secondLevel.name,          secondLevel.name,          secondLevel.code // 添加二级科目编号        ]);      } else if (thirdLevelCount === 1) {        // 只有一个三级分类时,不添加小计        let thirdLevelCodes = Object.keys(secondLevel.children);        for (let k = 0; k < thirdLevelCodes.length; k++) {          let thirdLevelCode = thirdLevelCodes[k];          let thirdLevel = secondLevel.children[thirdLevelCode];          result.push([            firstLevel.name,            secondLevel.name,            thirdLevel.name,            thirdLevel.code // 添加三级科目编号          ]);        }      } else {        // 有多个三级分类的,正常添加三级科目        let thirdLevelCodes = Object.keys(secondLevel.children);        for (let k = 0; k < thirdLevelCodes.length; k++) {          let thirdLevelCode = thirdLevelCodes[k];          let thirdLevel = secondLevel.children[thirdLevelCode];          result.push([            firstLevel.name,            secondLevel.name,            thirdLevel.name,            thirdLevel.code // 添加三级科目编号          ]);        }  
        // 小计时返回当前二级分类的科目编号        result.push([          firstLevel.name,          secondLevel.name,          "小计:",          secondLevel.code // 小计时使用二级科目编号        ]);      }    }  }  
  return result;}  
/** * 处理损益类数据(基于转换脚本_损益类.js逻辑) * 过滤损益类科目,应用排除规则,按层级结构组织数据 * @param {Array} subjectData - 会计科目原始数据 * @returns {Array} 处理后的损益类数据结构 */function processProfitLossData(subjectData) {  // 过滤损益类科目,应用排除规则  let profitLossSubjects = [];  for (let i = 0; i < subjectData.length; i++) {    let item = subjectData[i];    let code = String(item[0]); // 第1列是科目编码    let category = item[2]; // 第3列是会计要素名称  
    // 必须是损益类    if (category !== "损益") {      continue;    }  
    // 排除5001、5301开头的    if (code.startsWith("5001") || code.startsWith("5301")) {      continue;    }  
    // 5401开头的处理:包含一级科目5401,二级科目只取540102,不取540101    if (code.startsWith("5401")) {      // 一级科目5401要包含      if (code === "5401") {        profitLossSubjects.push(item);        continue;      }      // 二级科目只取540102      if (code === "540102") {        profitLossSubjects.push(item);      }      continue;    }  
    profitLossSubjects.push(item);  }  
  // 按层级结构组织数据(损益类只有一级和二级)  let structure = {};  
  for (let i = 0; i < profitLossSubjects.length; i++) {    let item = profitLossSubjects[i];    let code = String(item[0]); // 第1列是科目编码    let name = item[1]; // 第2列是科目名称  
    // 根据编码长度判断层级(损益类只有4位和6位编码)    if (code.length === 4) {      // 一级科目      if (!structure[code]) {        structure[code] = {          name: name,          code: code,          children: {}        };      }    } else if (code.length === 6) {      // 二级科目      let parentCode = code.substring(0, 4);      if (structure[parentCode]) {        structure[parentCode].children[code] = {          name: name,          code: code        };      }    }  }  
  // 转换为整理结构格式  let result = [];  
  let firstLevelCodes = Object.keys(structure);  for (let i = 0; i < firstLevelCodes.length; i++) {    let firstLevelCode = firstLevelCodes[i];    let firstLevel = structure[firstLevelCode];    let secondLevelCount = Object.keys(firstLevel.children).length;  
    if (secondLevelCount === 0) {      // 没有二级科目的一级科目,直接添加本身,不添加小计      result.push([        firstLevel.name,        firstLevel.name,        firstLevel.code // 一级科目编码      ]);    } else if (secondLevelCount === 1) {      // 只有一个二级科目时,添加二级科目本身,不添加小计      let secondLevelCodes = Object.keys(firstLevel.children);      for (let j = 0; j < secondLevelCodes.length; j++) {        let secondLevelCode = secondLevelCodes[j];        let secondLevel = firstLevel.children[secondLevelCode];        result.push([          firstLevel.name,          secondLevel.name,          secondLevel.code // 二级科目编码        ]);      }    } else {      // 有多个二级科目的,先添加所有二级科目      let secondLevelCodes = Object.keys(firstLevel.children);      for (let j = 0; j < secondLevelCodes.length; j++) {        let secondLevelCode = secondLevelCodes[j];        let secondLevel = firstLevel.children[secondLevelCode];        result.push([          firstLevel.name,          secondLevel.name,          secondLevel.code // 二级科目编码        ]);      }  
      // 添加小计行,使用一级科目编码      result.push([        firstLevel.name,        "小计:",        firstLevel.code // 一级科目编码      ]);    }  }  
  return result;}  
// ==================== 工具函数 ====================  
/** * 合并表头单元格(第一行相同内容的单元格) * 基于转置后的数组第一行数据,找出相同内容的列并合并 * @param {Object} sheet - 工作表对象 * @param {Array} transposedArray - 转置后的数组(第一行包含要合并的表头数据) * @param {string} startCell - 起始单元格(如"E1") */function mergeHeaderCells(sheet, transposedArray, startCell) {  if (!transposedArray || transposedArray.length === 0 || !transposedArray[0]) {    console.log("转置数组为空或无效,跳过表头合并");    return;  }  
  let firstRow = transposedArray[0]; // 第一行是表头数据  let startCol = sheet.Range(startCell).Column;  let currentValue = null;  let mergeStartCol = 0;  let mergeCount = 0;  
  console.log("开始合并表头单元格,从" + startCell + "开始,共" + firstRow.length + "列");  
  for (let i = 0; i < firstRow.length; i++) {    let value = firstRow[i];  
    if (i === 0) {      currentValue = value;      mergeCount++;    } else if (value !== currentValue) {      // 处理前一个合并组      if (mergeCount > 1) {        let mergeRange = sheet.Range(sheet.Cells(1, startCol + mergeStartCol), sheet.Cells(1, startCol + mergeStartCol + mergeCount - 1));        console.log("合并表头单元格地址:" + mergeRange.Address() + ",值:" + currentValue);        mergeRange.Merge(false);      }  
      // 开始新的合并组      currentValue = value;      mergeStartCol = i;      mergeCount = 1;    } else {      // 相同值,增加合并计数      mergeCount++;    }  }  
  // 处理最后一组合并  if (mergeCount > 1) {    let mergeRange = sheet.Range(sheet.Cells(1, startCol + mergeStartCol), sheet.Cells(1, startCol + mergeStartCol + mergeCount - 1));    console.log("合并最后一组表头单元格地址:" + mergeRange.Address() + ",值:" + currentValue);    mergeRange.Merge(false);  }  
  console.log("表头单元格合并完成");}  
/** * 合并相同内容的单元格 * 对指定列中连续相同内容的单元格进行合并 * @param {Object} sheet - 工作表对象 * @param {Array} dataArray - 数据数组 * @param {string} startCell - 起始单元格(如"A12") * @param {number} columnIndex - 要合并的列索引 */function mergeIdenticalCells(sheet, dataArray, startCell, columnIndex) {  let startRow = sheet.Range(startCell).Row; // 从第12行开始,不从第13行,会出现第1个合并单元格后,算offset不准  let currentValue = null;  let mergeStartRow = 1; //这里从1开始,将12移到13位置  let mergeCount = 0;  
  for (let i = 0; i < dataArray.length; i++) {    let value = dataArray[i][columnIndex];  
    if (i == 0) {      currentValue = value;      mergeCount++;    } else if (value !== currentValue) {      // 处理前一个合并组      if (mergeCount > 1) {        let mergeRange = sheet.Range(startCell).Offset(mergeStartRow, 0).Resize(mergeCount, 1);        console.log("合并单元格地址:" + mergeRange.Address());        mergeRange.Merge(false);        console.log("合并单元格: " + startCell + " 行" + (startRow + mergeStartRow) + "-" + (startRow + mergeStartRow + mergeCount - 1) + " 列" + columnIndex + " 值: " + currentValue);      }  
      // 开始新的合并组      currentValue = value;      mergeStartRow = i + 1;      mergeCount = 1;    } else {      // 相同值,增加合并计数      mergeCount++;    }  }  
  // 处理最后一组合并  if (mergeCount > 1) {    let mergeRange = sheet.Range(startCell).Offset(mergeStartRow, 0).Resize(mergeCount, 1);    console.log("合并单元格地址:" + mergeRange.Address());    mergeRange.Merge(false);    console.log("合并最后一组单元格: " + startCell + " 行" + (startRow + mergeStartRow) + "-" + (startRow + mergeStartRow + mergeCount - 1) + " 列" + columnIndex + " 值: " + currentValue);  }}  
/** * 设置区域边框 * 为指定区域设置统一的边框样式 * @param {Object} rng - 要设置边框的区域对象 */function setRangeBorder(rng) {  // 定义所有边框类型  const borderTypes = [xlEdgeLeft, xlEdgeTop, xlEdgeBottom, xlEdgeRight, xlInsideVertical, xlInsideHorizontal];  
  // 设置边框样式和粗细  borderTypes.forEach((borderType) => {    let border = rng.Borders.Item(borderType);    border.Weight = xlThin;    border.LineStyle = xlContinuous;    border.ColorIndex = xlColorIndexAutomatic;    border.TintAndShade = 0;  });}