feat: excel pivot macro picture and workbook COM operations
This commit is contained in:
parent
ae4f4a2460
commit
657bc6d7d0
63
src/main/capabilities/excel-com.test.ts
Normal file
63
src/main/capabilities/excel-com.test.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import ExcelJS from "exceljs";
|
||||
import { getHandler } from "../engine/registry";
|
||||
import type { CatalogComponent, FlowNode } from "../../shared/types";
|
||||
import type { RunContext } from "../engine/types";
|
||||
import "./excel-com";
|
||||
|
||||
const directories: string[] = [];
|
||||
const context = {} as RunContext;
|
||||
const component = {} as CatalogComponent;
|
||||
const node = (id: string): FlowNode => ({ id, componentId: id, position: { x: 0, y: 0 }, values: {}, options: {} });
|
||||
async function run(id: string, args: Record<string, unknown>) {
|
||||
const handler = getHandler(id);
|
||||
expect(handler, id).toBeTruthy();
|
||||
return handler!(context, node(id), args, component);
|
||||
}
|
||||
afterEach(async () => Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true, force: true }))));
|
||||
|
||||
const registered = [
|
||||
"Excel_Cs_Group", "Excel_Cs_DeletePicture", "Excel_Cs_RefreshPivotTable", "Excel_Cs_DeletePictureV2",
|
||||
"Excel_Cs_Encrypt", "Excel_Cs_Move", "Excel_Cs_InsertPicture", "Excel_Cs_Merge", "Excel_InsertImage",
|
||||
"Excel_Cs_InsertPivotView", "Excel_Cs_Interop", "Excel_Cs_InsertPivotTable", "Excel_Cs_RunMacro",
|
||||
"Excel_Loop", "Excel_Cs_Export", "Excel_Cs_ExcelAddin", "Excel_Cs_InsertPictureV2",
|
||||
"Excel_Cs_FilterPivotTable", "Excel_Cs_RunMacroV2", "Excel_Cs_SetSlicer", "Excel_ExecuteMacro",
|
||||
"Excel_Cs_RefreshAll", "Excel_DeleteImage", "Excel_ExportRangeAsPicture"
|
||||
];
|
||||
|
||||
describe("Excel COM capabilities", () => {
|
||||
it("registers every Excel COM capability", () => { for (const id of registered) expect(getHandler(id), id).toBeTruthy(); });
|
||||
|
||||
it("groups rows, moves sheets, reads dynamic properties and loops content", async () => {
|
||||
if (process.platform !== "win32") return;
|
||||
const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-excel-com-")); directories.push(directory);
|
||||
const filePath = path.join(directory, "workbook.xlsx");
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
workbook.addWorksheet("Sheet1").getCell("A1").value = "A1";
|
||||
workbook.addWorksheet("Second").getCell("A1").value = "S1";
|
||||
await workbook.xlsx.writeFile(filePath);
|
||||
|
||||
await expect(run("Excel_Cs_Group", { workbook: { path: filePath }, sheet: "Sheet1", group: 0, groupType: 0, startRow: 1, endRow: 2, save: true })).resolves.toMatchObject({ grouped: true });
|
||||
await expect(run("Excel_Cs_Interop", { workbook: { path: filePath }, obj: 2, sheet: "Sheet1", property: "Name" })).resolves.toMatchObject({ value: "Sheet1" });
|
||||
await expect(run("Excel_Cs_Move", { workbook: { path: filePath }, sheet: "Second", mode: "按序号", index: 1, save: true })).resolves.toMatchObject({ moved: true });
|
||||
await expect(run("Excel_Cs_RefreshAll", { workbook: { path: filePath }, save: true })).resolves.toMatchObject({ refreshed: true });
|
||||
const loop = await run("Excel_Loop", { objExcel: { path: filePath } }) as unknown[][];
|
||||
expect(loop.length).toBeGreaterThanOrEqual(2);
|
||||
expect(loop.flat().join(",")).toContain("A1");
|
||||
}, 120000);
|
||||
|
||||
it("exports a workbook to PDF through Excel COM", async () => {
|
||||
if (process.platform !== "win32") return;
|
||||
const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-excel-pdf-")); directories.push(directory);
|
||||
const filePath = path.join(directory, "source.xlsx");
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
workbook.addWorksheet("Report").getCell("A1").value = "PDF";
|
||||
await workbook.xlsx.writeFile(filePath);
|
||||
const output = await run("Excel_Cs_Export", { workbook: { path: filePath }, option: "全部", path: directory, name: "report.pdf", cover: true }) as string;
|
||||
expect(output).toBe(path.join(directory, "report.pdf"));
|
||||
expect((await readFile(output)).subarray(0, 5).toString()).toMatch(/^%PDF-/);
|
||||
}, 120000);
|
||||
});
|
||||
288
src/main/capabilities/excel-com.ts
Normal file
288
src/main/capabilities/excel-com.ts
Normal file
@ -0,0 +1,288 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import ExcelJS from "exceljs";
|
||||
import { register } from "../engine/registry";
|
||||
import { RuntimeError, type CapabilityHandler } from "../engine/types";
|
||||
import { powershell, psString } from "./desktop";
|
||||
import { releaseWorkbook } from "./excel-store";
|
||||
|
||||
function text(args: Record<string, unknown>, keys: string[], fallback = ""): string {
|
||||
for (const key of keys) {
|
||||
const value = args[key];
|
||||
if (value !== undefined && value !== null && value !== "") return String(value);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function bool(value: unknown, fallback = false): boolean {
|
||||
if (value === undefined || value === null || value === "") return fallback;
|
||||
return [true, 1, "1", "true", "True", "TRUE", "yes", "是", "开启"].includes(value as never);
|
||||
}
|
||||
|
||||
function number(value: unknown, fallback = 0): number { const result = Number(value); return Number.isFinite(result) ? result : fallback; }
|
||||
|
||||
function workbookPath(args: Record<string, unknown>, nodeId: string, componentId: string, keys = ["workbook", "objExcel", "objExcelWorkBook", "destWorkbook"]): string {
|
||||
for (const key of keys) {
|
||||
const raw = args[key];
|
||||
const candidate = raw && typeof raw === "object" ? (raw as { path?: unknown }).path : raw;
|
||||
const value = String(candidate ?? "").trim();
|
||||
if (value && value !== "Excel文件" && value !== "objExcelWorkBook") return path.resolve(value);
|
||||
}
|
||||
throw new RuntimeError("validation_error", "Excel文件路径为空", { nodeId, componentId });
|
||||
}
|
||||
|
||||
function sheetExpression(args: Record<string, unknown>): string {
|
||||
const sheet = text(args, ["sheet", "sheetName"]);
|
||||
return sheet ? `$workbook.Worksheets.Item(${psString(sheet)})` : "$workbook.ActiveSheet";
|
||||
}
|
||||
|
||||
async function runExcelCom(filePath: string, body: string, save = false, readonly = false, timeout = 120000): Promise<unknown> {
|
||||
const openMode = readonly ? "$true" : "$false";
|
||||
const saveMode = save ? "$true" : "$false";
|
||||
const script = [
|
||||
"$excel=$null;$workbook=$null;$rpaOutput=$null",
|
||||
"try {",
|
||||
" $excel=New-Object -ComObject Excel.Application;$excel.Visible=$false;$excel.DisplayAlerts=$false",
|
||||
" $workbook=$excel.Workbooks.Open(" + psString(filePath) + ",0," + openMode + ")",
|
||||
" " + body,
|
||||
" if (" + saveMode + ") { $workbook.Save() }",
|
||||
" if ($null -ne $rpaOutput) { [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(($rpaOutput | ConvertTo-Json -Compress -Depth 6))) | Write-Output }",
|
||||
"} finally {",
|
||||
" if($null -ne $workbook){try{$workbook.Close($false)}catch{};try{[void][Runtime.InteropServices.Marshal]::ReleaseComObject($workbook)}catch{}}",
|
||||
" if($null -ne $excel){try{$excel.Quit()}catch{};try{[void][Runtime.InteropServices.Marshal]::ReleaseComObject($excel)}catch{}}",
|
||||
" [GC]::Collect();[GC]::WaitForPendingFinalizers()",
|
||||
"}"
|
||||
].join("\n");
|
||||
const output = (await powershell(script, timeout)).trim();
|
||||
if (save) releaseWorkbook(filePath);
|
||||
if (!output) return null;
|
||||
try { return JSON.parse(Buffer.from(output, "base64").toString("utf8")); }
|
||||
catch { throw new RuntimeError("excel_com_failed", "Excel COM 输出异常", { detail: output.slice(0, 300) }); }
|
||||
}
|
||||
|
||||
function list(value: unknown): string[] {
|
||||
if (Array.isArray(value)) return value.map((item) => String(item)).filter(Boolean);
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) return [];
|
||||
try { const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed.map(String) : [String(parsed)]; }
|
||||
catch { return raw.split(/[,,;;\s]+/).filter(Boolean); }
|
||||
}
|
||||
const group: CapabilityHandler = async (_ctx, node, args) => {
|
||||
const filePath = workbookPath(args, node.id, node.componentId, ["workbook"]);
|
||||
const ungroup = String(args.group ?? "0") === "1";
|
||||
const columns = String(args.groupType ?? "0") === "1";
|
||||
const start = columns ? text(args, ["startColumn"], "A") : String(number(args.startRow, 1));
|
||||
const end = columns ? text(args, ["endColumn"], text(args, ["startColumn"], "A")) : String(number(args.endRow, number(args.startRow, 1)));
|
||||
const method = ungroup ? "Ungroup" : "Group";
|
||||
const body = `$sheet=${sheetExpression(args)};$start=${psString(start)};$end=${psString(end)};
|
||||
$count=if(${columns ? "$true" : "$false"}){$sheet.UsedRange.Columns.Count}else{$sheet.UsedRange.Rows.Count};
|
||||
foreach($name in @("start","end")){ $v=[int](Get-Variable $name -ValueOnly); if($v -lt 0){ Set-Variable $name ($count+$v+1) } };
|
||||
$range=$sheet.Range("$($start):$($end)");$range.${method}|Out-Null`;
|
||||
await runExcelCom(filePath, body, bool(args.save));
|
||||
return { grouped: !ungroup, type: columns ? "columns" : "rows" };
|
||||
};
|
||||
|
||||
const deletePicture: CapabilityHandler = async (_ctx, node, args) => {
|
||||
const filePath = workbookPath(args, node.id, node.componentId, ["workbook", "objExcel"]);
|
||||
const name = text(args, ["name", "pictureName"]);
|
||||
if (!name) throw new RuntimeError("validation_error", "图片名称为空", { nodeId: node.id, componentId: node.componentId });
|
||||
const body = `$sheet=${sheetExpression(args)};$sheet.Shapes.Item(${psString(name)}).Delete()`;
|
||||
await runExcelCom(filePath, body, bool(args.save));
|
||||
return { deleted: name };
|
||||
};
|
||||
|
||||
const insertPicture: CapabilityHandler = async (_ctx, node, args) => {
|
||||
const filePath = workbookPath(args, node.id, node.componentId, ["workbook", "objExcel"]);
|
||||
const image = path.resolve(text(args, ["path", "pictureName"]));
|
||||
if (!fs.existsSync(image)) throw new RuntimeError("validation_error", "图片文件不存在", { nodeId: node.id, componentId: node.componentId, detail: image });
|
||||
const row = number(args.row, 1); const column = number(args.column, 1);
|
||||
const left = number(args.Left ?? args.left, 20); const top = number(args.Top ?? args.top, 20);
|
||||
const width = number(args.Width ?? args.width, -1); const height = number(args.Height ?? args.height, -1);
|
||||
const scale = width > 0 && height > 0 ? "$true" : "$false";
|
||||
const body = `$sheet=${sheetExpression(args)};$cell=$sheet.Cells(${row},${column});$left=${left}+if($left -gt 0){$cell.Left}else{0};$top=${top}+if($top -gt 0){$cell.Top}else{0};[void]$sheet.Shapes.AddPicture(${psString(image)},$false,${scale},$left,$top,${width},${height})`;
|
||||
await runExcelCom(filePath, body, bool(args.save));
|
||||
return { inserted: image };
|
||||
};
|
||||
|
||||
const encryptExcel: CapabilityHandler = async (_ctx, node, args) => {
|
||||
const filePath = workbookPath(args, node.id, node.componentId, ["workbook"]);
|
||||
const password = String(args.password1 ?? args.password ?? "");
|
||||
const confirm = String(args.password2 ?? password);
|
||||
if (!password) throw new RuntimeError("validation_error", "Excel密码为空", { nodeId: node.id, componentId: node.componentId });
|
||||
if (password !== confirm) throw new RuntimeError("validation_error", "两次输入的Excel密码不一致", { nodeId: node.id, componentId: node.componentId });
|
||||
const temporary = `${filePath}.${Date.now()}.enc.xlsx`;
|
||||
try {
|
||||
await runExcelCom(filePath, `$workbook.SaveAs(${psString(temporary)},51,${psString(password)},${psString(confirm)})`, false);
|
||||
fs.rmSync(temporary, { force: true });
|
||||
// COM SaveAs cannot replace the workbook while it is open, so save beside it and rename after Close.
|
||||
} finally {
|
||||
if (fs.existsSync(temporary)) {
|
||||
fs.rmSync(filePath, { force: true });
|
||||
fs.renameSync(temporary, filePath);
|
||||
}
|
||||
releaseWorkbook(filePath);
|
||||
}
|
||||
return filePath;
|
||||
};
|
||||
|
||||
const moveSheet: CapabilityHandler = async (_ctx, node, args) => {
|
||||
const filePath = workbookPath(args, node.id, node.componentId, ["workbook"]);
|
||||
const mode = text(args, ["mode"], "按序号"); const after = text(args, ["option"], "之前").includes("之后");
|
||||
const targetName = text(args, ["name", "index"]); const position = text(args, ["position", "index"], "第一位");
|
||||
const index = number(args.index, 1);
|
||||
const afterMode = after ? "$true" : "$false";
|
||||
const body = `$sheet=${sheetExpression(args)};$count=$workbook.Worksheets.Count;$targetIndex=0;
|
||||
if(${psString(mode)} -eq "按名称"){ $targetName=${psString(targetName)}; for($i=1;$i -le $count;$i++){ if($workbook.Worksheets.Item($i).Name -eq $targetName){$targetIndex=$i;break} }; if($targetIndex -eq 0){throw "target_sheet_not_found"} }
|
||||
elseif(${psString(mode)} -eq "按位置"){ $p=${psString(position)}; if($p -eq "第一位"){$targetIndex=1}elseif($p -eq "最后一位"){$targetIndex=$count}else{ $delta=-1; if($p -eq "向后移一位"){$delta=1}; $targetIndex=$sheet.Index+$delta } }
|
||||
else { $targetIndex=${index}; if($targetIndex -lt 0){$targetIndex=$count+$targetIndex+1} };
|
||||
$targetIndex=[Math]::Min([Math]::Max(1,$targetIndex),$count);$target=$workbook.Worksheets.Item($targetIndex);
|
||||
if(${afterMode}){ if($target.Index -ne $sheet.Index){ $sheet.Move($null,$target)|Out-Null } }
|
||||
else { if($target.Index -ne $sheet.Index){ $sheet.Move($target,$null)|Out-Null } }`;
|
||||
await runExcelCom(filePath, body, bool(args.save));
|
||||
return { moved: true };
|
||||
};
|
||||
const mergeSheet: CapabilityHandler = async (_ctx, node, args) => {
|
||||
const filePath = workbookPath(args, node.id, node.componentId, ["workbook"]);
|
||||
const destPath = workbookPath(args, node.id, node.componentId, ["destWorkbook", "workbook"]);
|
||||
const horizontal = String(args.option ?? "0") === "1";
|
||||
const horizontalMode = horizontal ? "$true" : "$false";
|
||||
const saveDest = bool(args.save) ? "$true" : "$false";
|
||||
const body = `$source=${sheetExpression(args)};$dest=$excel.Workbooks.Open(${psString(destPath)});$target=$dest.Worksheets.Item(${psString(text(args, ["destSheet"], "Sheet1"))});
|
||||
if(${horizontalMode}){$offset=$target.UsedRange.Columns.Count+1}else{$offset=$target.UsedRange.Rows.Count+1};
|
||||
for($r=1;$r -le $source.UsedRange.Rows.Count;$r++){for($c=1;$c -le $source.UsedRange.Columns.Count;$c++){ if(${horizontalMode}){$target.Cells($r,$offset+$c-1).Value2=$source.Cells($r,$c).Value2}else{$target.Cells($offset+$r-1,$c).Value2=$source.Cells($r,$c).Value2}}};
|
||||
if(${saveDest}){$dest.Save()}`;
|
||||
await runExcelCom(filePath, body, false);
|
||||
return { merged: true, target: destPath };
|
||||
};
|
||||
|
||||
const refreshAll: CapabilityHandler = async (_ctx, node, args) => {
|
||||
const filePath = workbookPath(args, node.id, node.componentId, ["workbook"]);
|
||||
await runExcelCom(filePath, "$workbook.RefreshAll()", bool(args.save));
|
||||
return { refreshed: true };
|
||||
};
|
||||
|
||||
const refreshPivot: CapabilityHandler = async (_ctx, node, args) => {
|
||||
const filePath = workbookPath(args, node.id, node.componentId, ["workbook"]);
|
||||
const name = text(args, ["pivotTable"]); const sheetExpr = sheetExpression(args);
|
||||
const body = name ? `${sheetExpr}.PivotTables(${psString(name)}).RefreshTable()` : `${sheetExpr}.PivotTables() | ForEach-Object { $_.RefreshTable() }`;
|
||||
await runExcelCom(filePath, body, bool(args.save));
|
||||
return { refreshed: true, name: name || "all" };
|
||||
};
|
||||
|
||||
const filterPivot: CapabilityHandler = async (_ctx, node, args) => {
|
||||
const filePath = workbookPath(args, node.id, node.componentId, ["workbook"]);
|
||||
const field = text(args, ["field"]); const values = list(args.fieldValue);
|
||||
if (!field) throw new RuntimeError("validation_error", "筛选字段为空", { nodeId: node.id, componentId: node.componentId });
|
||||
const encoded = Buffer.from(JSON.stringify(values), "utf16le").toString("base64");
|
||||
const body = `$pt=${sheetExpression(args)}.PivotTables(${psString(text(args, ["pivotTable"]))});$pf=$pt.PivotFields(${psString(field)});
|
||||
try{$pf.ClearAllFilters()}catch{};
|
||||
$wanted=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String("${encoded}"))|ConvertFrom-Json;
|
||||
foreach($item in $pf.PivotItems()){ $selected=if($wanted.Count -eq 0){$true}else{$wanted -contains $item.Name}; $item.Visible=$selected }`;
|
||||
await runExcelCom(filePath, body, bool(args.save));
|
||||
return { field, values };
|
||||
};
|
||||
|
||||
const createPivot: CapabilityHandler = async (_ctx, node, args) => {
|
||||
const filePath = workbookPath(args, node.id, node.componentId, ["workbook"]);
|
||||
const chart = node.componentId.includes("PivotView");
|
||||
let rule: Record<string, unknown> = {};
|
||||
try { rule = JSON.parse(String(args.rule || "{}")) as Record<string, unknown>; } catch { /* sourceData can still be supplied separately. */ }
|
||||
const source = text({ ...rule, ...args }, ["SourceData", "sourceData", "source"]);
|
||||
if (!source) throw new RuntimeError("validation_error", "透视表数据源为空", { nodeId: node.id, componentId: node.componentId });
|
||||
const name = text(rule, ["Name", "name"], chart ? "RPAPivotChart" : "RPAPivotTable");
|
||||
const body = `$sheet=${sheetExpression(args)};$cache=$excel.ActiveWorkbook.PivotCaches().Create(1,$sheet.Range(${psString(source)}));
|
||||
$dest=$sheet.Cells(1,20);$pt=$cache.CreatePivotTable($dest,${psString(name)});
|
||||
if(${chart ? "$true" : "$false"}){[void]$sheet.Shapes.AddChart(-4109,20,20,420,260)}`;
|
||||
await runExcelCom(filePath, body, bool(args.save));
|
||||
return { name, chart };
|
||||
};
|
||||
const runMacro = (scriptKey: "script" | "code" | "macroName"): CapabilityHandler => async (_ctx, node, args) => {
|
||||
const filePath = workbookPath(args, node.id, node.componentId, ["workbook", "objExcel"]);
|
||||
const macro = text(args, [scriptKey, "script", "code", "macroName", "name"]);
|
||||
if (!macro) throw new RuntimeError("validation_error", "宏名称为空", { nodeId: node.id, componentId: node.componentId });
|
||||
const parameters = list(args.args ?? args.macroArgs).map((item) => psString(item)).join(",");
|
||||
const body = `$result=$excel.Run(${psString(macro)}${parameters ? "," + parameters : ""});$rpaOutput=@{result=$result}`;
|
||||
return await runExcelCom(filePath, body, bool(args.save), false, 180000);
|
||||
};
|
||||
|
||||
const interop: CapabilityHandler = async (_ctx, node, args) => {
|
||||
const filePath = workbookPath(args, node.id, node.componentId, ["workbook", "objExcel"]);
|
||||
const property = text(args, ["property"]);
|
||||
if (!property) throw new RuntimeError("validation_error", "属性或方法名为空", { nodeId: node.id, componentId: node.componentId });
|
||||
const objectKind = number(args.obj ?? args.object, 0);
|
||||
const range = text(args, ["range"], "A1");
|
||||
const target = objectKind === 0 ? "$excel" : objectKind === 1 ? "$workbook" : objectKind === 2 ? sheetExpression(args) : `${sheetExpression(args)}.Range(${psString(range)})`;
|
||||
const value = args.value;
|
||||
const empty = value === undefined || value === null || value === "";
|
||||
const body = empty
|
||||
? `$target=${target};$rpaOutput=@{value=$target.GetType().InvokeMember(${psString(property)},[Reflection.BindingFlags]::GetProperty,$null,$target,@())}`
|
||||
: `$target=${target};[void]$target.GetType().InvokeMember(${psString(property)},[Reflection.BindingFlags]::SetProperty,$null,$target,@(${psString(String(value))}));$rpaOutput=@{set=$true}`;
|
||||
return await runExcelCom(filePath, body, bool(args.save));
|
||||
};
|
||||
|
||||
const exportPdf: CapabilityHandler = async (_ctx, node, args) => {
|
||||
const filePath = workbookPath(args, node.id, node.componentId, ["workbook"]);
|
||||
const folder = text(args, ["path"]);
|
||||
const name = text(args, ["name"], path.basename(filePath, path.extname(filePath)) + ".pdf");
|
||||
const output = path.join(path.resolve(folder), /\.(pdf)$/i.test(name) ? name : `${name}.pdf`);
|
||||
fs.mkdirSync(path.dirname(output), { recursive: true });
|
||||
if (!bool(args.cover, true) && fs.existsSync(output)) throw new RuntimeError("validation_error", "PDF 文件已存在", { detail: output });
|
||||
const activateOnly = text(args, ["option"], "全部") === "激活页";
|
||||
const body = `if(${activateOnly ? "$true" : "$false"}){$workbook.ActiveSheet.ExportAsFixedFormat(0,${psString(output)})}else{$workbook.ExportAsFixedFormat(0,${psString(output)})}`;
|
||||
await runExcelCom(filePath, body, false);
|
||||
return output;
|
||||
};
|
||||
|
||||
const exportRangePicture: CapabilityHandler = async (_ctx, node, args) => {
|
||||
const filePath = workbookPath(args, node.id, node.componentId, ["workbook"]);
|
||||
const folder = text(args, ["path"]); const name = text(args, ["name"], `excel-range-${Date.now()}`);
|
||||
const output = path.join(path.resolve(folder), /\.(png|jpg|jpeg)$/i.test(name) ? name : `${name}.png`);
|
||||
fs.mkdirSync(path.dirname(output), { recursive: true });
|
||||
const body = `$range=${sheetExpression(args)}.Range(${psString(text(args, ["range"], "A1"))});$range.CopyPicture(1,2)|Out-Null;$shape=${sheetExpression(args)}.ChartObjects().Add(0,0,$range.Width,$range.Height);$chart=$shape.Chart;$chart.Paste();[void]$chart.Export(${psString(output)},"PNG");$shape.Delete()`;
|
||||
await runExcelCom(filePath, body, false);
|
||||
return output;
|
||||
};
|
||||
|
||||
const setSlicer: CapabilityHandler = async (_ctx, node, args) => {
|
||||
const filePath = workbookPath(args, node.id, node.componentId, ["workbook"]);
|
||||
const name = text(args, ["slicerName"]); const values = list(args.slicerOptions);
|
||||
if (!name) throw new RuntimeError("validation_error", "切片器名称为空", { nodeId: node.id, componentId: node.componentId });
|
||||
const encoded = Buffer.from(JSON.stringify(values), "utf16le").toString("base64");
|
||||
const body = `$cache=$workbook.SlicerCaches.Item(${psString(name)});
|
||||
$wanted=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String("${encoded}"))|ConvertFrom-Json;
|
||||
foreach($item in $cache.SlicerItems()){ $item.Selected=if($wanted.Count -eq 0){$true}else{$wanted -contains $item.Name} }`;
|
||||
await runExcelCom(filePath, body, bool(args.save));
|
||||
return { slicer: name, values };
|
||||
};
|
||||
|
||||
const excelLoop: CapabilityHandler = async (_ctx, node, args) => {
|
||||
const filePath = workbookPath(args, node.id, node.componentId, ["objExcel", "workbook"]);
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.readFile(filePath);
|
||||
const rows: unknown[][] = [];
|
||||
for (const sheet of workbook.worksheets) {
|
||||
for (let row = 1; row <= sheet.rowCount; row += 1) {
|
||||
const cells: unknown[] = [];
|
||||
for (let column = 1; column <= sheet.columnCount; column += 1) cells.push(sheet.getCell(row, column).value ?? "");
|
||||
if (cells.some((cell) => cell !== "")) rows.push(cells);
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
};
|
||||
|
||||
register("Excel_Cs_Group", group);
|
||||
register("Excel_Cs_Encrypt", encryptExcel);
|
||||
register(["Excel_Cs_DeletePicture", "Excel_Cs_DeletePictureV2", "Excel_DeleteImage"], deletePicture);
|
||||
register(["Excel_Cs_InsertPicture", "Excel_Cs_InsertPictureV2", "Excel_InsertImage"], insertPicture);
|
||||
register("Excel_Cs_Move", moveSheet);
|
||||
register("Excel_Cs_Merge", mergeSheet);
|
||||
register(["Excel_Cs_RefreshAll", "Excel_Cs_RefreshPivotTable"], async (ctx, node, args, component) => node.componentId === "Excel_Cs_RefreshPivotTable" ? refreshPivot(ctx, node, args, component) : refreshAll(ctx, node, args, component));
|
||||
register("Excel_Cs_FilterPivotTable", filterPivot);
|
||||
register(["Excel_Cs_InsertPivotTable", "Excel_Cs_InsertPivotView"], createPivot);
|
||||
register(["Excel_Cs_RunMacro", "Excel_Cs_RunMacroV2", "Excel_ExecuteMacro"], runMacro("script"));
|
||||
register("Excel_Cs_Interop", interop);
|
||||
register("Excel_Cs_Export", exportPdf);
|
||||
register("Excel_Cs_ExcelAddin", runMacro("code"));
|
||||
register("Excel_Cs_SetSlicer", setSlicer);
|
||||
register("Excel_Loop", excelLoop);
|
||||
register("Excel_ExportRangeAsPicture", exportRangePicture);
|
||||
@ -29,6 +29,7 @@ import "./system-extended";
|
||||
import "./office-file-extended";
|
||||
import "./office-aliases";
|
||||
import "./excel-common";
|
||||
import "./excel-com";
|
||||
import "./window-extended";
|
||||
import "./data-extended";
|
||||
import "./utility-extended";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user