diff --git a/src/main/capabilities/office-aliases.test.ts b/src/main/capabilities/office-aliases.test.ts index 2f5228d..f89e59a 100644 --- a/src/main/capabilities/office-aliases.test.ts +++ b/src/main/capabilities/office-aliases.test.ts @@ -1,6 +1,8 @@ import { readFile, mkdtemp, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import ExcelJS from "exceljs"; +import { fileURLToPath } from "node:url"; import { afterEach, describe, expect, it } from "vitest"; import type { Catalog, CatalogComponent, FlowNode } from "../../shared/types"; import type { RunContext } from "../engine/types"; @@ -19,7 +21,7 @@ afterEach(async () => Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursiv describe("common Excel aliases", () => { it("registers only real catalog aliases", async () => { - const catalog = JSON.parse(await readFile("resources/catalog.json", "utf8")) as Catalog; + const catalog = JSON.parse(await readFile(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../resources/catalog.json"), "utf8")) as Catalog; const ids = new Set(catalog.components.map((item) => item.id)); expect(EXCEL_COMMON_ALIASES.length).toBeGreaterThanOrEqual(25); for (const id of EXCEL_COMMON_ALIASES) { expect(ids.has(id), id).toBe(true); expect(getHandler(id), id).toBeTruthy(); } @@ -82,4 +84,34 @@ describe("common Excel aliases", () => { const savedAs = await run("Excel_SaveAs", { objExcel: created, filename: "rel-target.xlsx", isAbsolutePath: "False" }) as { path: string }; expect(savedAs.path).toBe(path.resolve(directory, "rel-target.xlsx")); }); + + it("combines the Excel_Cs_SaveAs directory path with the name field", async () => { + const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-excel-saveas-cs-")); dirs.push(directory); + const created = await run("Excel_NewExcel", { filePath: directory, name: "cs-origin.xlsx" }) as { path: string }; + const targetDir = path.join(directory, "nested"); + const savedAs = await run("Excel_Cs_SaveAs", { workbook: created, path: targetDir, name: "cs-target.xlsx" }) as { path: string }; + expect(savedAs.path).toBe(path.resolve(targetDir, "cs-target.xlsx")); + const reopened = new ExcelJS.Workbook(); + await reopened.xlsx.readFile(savedAs.path); + expect(reopened.worksheets.length).toBeGreaterThan(0); + }); + + it("clears a range through the Excel_ClearRange cellPos key", async () => { + const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-excel-clear-")); dirs.push(directory); + const created = await run("Excel_NewExcel", { filePath: directory, name: "clear.xlsx" }) as { path: string }; + await run("Excel_WriteRange", { objExcel: created, sheet: "Sheet1", cellPos: "A1", data: [["甲", "乙"], ["丙", "丁"]] }); + await run("Excel_ClearRange", { objExcel: created, sheet: "Sheet1", cellPos: "A1:B2", isFormat: "False", isSave: "True" }); + const reopened = await run("Excel_OpenExcel", { filePath: created.path }) as { id: string }; + await expect(run("Excel_Cs_ReadContentV2", { workbook: reopened, sheet: "Sheet1", excelRng: "A1:B2" })).resolves.toEqual([["", ""], ["", ""]]); + }); + + it("clears through the Excel_Cs_ClearRange startRow/startColumn keys", async () => { + const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-excel-clear-cs-")); dirs.push(directory); + const created = await run("Excel_NewExcel", { filePath: directory, name: "clear-cs.xlsx" }) as { path: string }; + await run("Excel_WriteRange", { objExcel: created, sheet: "Sheet1", cellPos: "A1", data: [["1", "2"], ["3", "4"]] }); + await run("Excel_Cs_ClearRange", { objExcel: created, sheet: "Sheet1", startRow: "2", startColumn: "A", lastRow: "2", lastColumn: "B", mode: "内容", save: "True" }); + const reopened = await run("Excel_OpenExcel", { filePath: created.path }) as { id: string }; + await expect(run("Excel_Cs_ReadContentV2", { workbook: reopened, sheet: "Sheet1", excelRng: "A2:B2" })).resolves.toEqual([["", ""]]); + await expect(run("Excel_Cs_ReadContentV2", { workbook: reopened, sheet: "Sheet1", excelRng: "A1:B1" })).resolves.toEqual([["1", "2"]]); + }); }); diff --git a/src/main/capabilities/office-file-extended.ts b/src/main/capabilities/office-file-extended.ts index d6dcec6..789beb3 100644 --- a/src/main/capabilities/office-file-extended.ts +++ b/src/main/capabilities/office-file-extended.ts @@ -164,12 +164,30 @@ register("Excel_Cs_DeleteColumn", async (_ctx, node, args) => { register("Excel_Cs_ClearRangeV4", async (_ctx, node, args) => { const source = await loadWorkbook(args, node.id, node.componentId); const sheet = worksheetOf(source.workbook, args); - const range = text(args, ["excelRng", "range"]); + // 兼容各别名版本的键:V4=excelRng、V2/V3=range、Legacy Excel_ClearRange=cellPos、V1=startRow/startColumn/lastRow/lastColumn + let range = text(args, ["excelRng", "range", "cellPos"]); + if (!range) { + const startRow = text(args, ["startRow"]); + const startColumn = text(args, ["startColumn"]); + if (startRow && startColumn) { + const lastRow = text(args, ["lastRow"], startRow); + const lastColumn = text(args, ["lastColumn"], startColumn); + range = `${startColumn}${startRow}:${lastColumn}${lastRow}`; + } + } if (!range) throw new RuntimeError("validation_error", "Range is empty", { nodeId: node.id, componentId: node.componentId }); const [start, end = start] = range.split(":"); const first = sheet.getCell(start); const last = sheet.getCell(end); - for (let row = first.row; row <= last.row; row += 1) for (let column = first.col; column <= last.col; column += 1) sheet.getCell(row, column).value = null; + // 清除模式:全部=内容+格式,格式=仅格式,其余(内容/默认)=仅内容;Legacy isFormat=True 同样清格式 + const mode = text(args, ["clearType", "mode"], "内容"); + const clearFormat = mode.includes("全部") || mode.includes("格式") || truthy(value(args, ["isFormat"])); + const clearValue = !mode.includes("格式"); + for (let row = first.row; row <= last.row; row += 1) for (let column = first.col; column <= last.col; column += 1) { + const cell = sheet.getCell(row, column); + if (clearValue) cell.value = null; + if (clearFormat) cell.style = {}; + } await saveWhenRequested(source, args); return range; }); diff --git a/src/main/capabilities/office.ts b/src/main/capabilities/office.ts index 134b06d..9786cb2 100644 --- a/src/main/capabilities/office.ts +++ b/src/main/capabilities/office.ts @@ -178,9 +178,13 @@ const saveExcel: CapabilityHandler = async (_ctx, node, args) => { const saveAsExcel: CapabilityHandler = async (_ctx, node, args) => { const { handle, workbook } = await resolveWorkbook(args, node.id, node.componentId); - const raw = required(value(args, ["path", "file", "savePath", "filename", "saveFilePath"]), "另存为路径", node.id, node.componentId); + // CS 版(Excel_Cs_SaveAs)用 path=目录 + name=文件名;Legacy(Excel_SaveAs)用 filename=完整路径 + const rawName = text(args, ["name"]); + const raw = rawName + ? path.resolve(text(args, ["path", "file", "savePath"], ""), rawName) + : required(value(args, ["path", "file", "savePath", "filename", "saveFilePath"]), "另存为路径", node.id, node.componentId); // Legacy Excel_SaveAs 用 isAbsolutePath 标记 filename 是否为绝对路径,相对路径基于原文件目录 - const absolute = bool(value(args, ["isAbsolutePath"]), true); + const absolute = rawName ? true : bool(value(args, ["isAbsolutePath"]), true); const previousPath = handle.path; handle.path = absolute ? path.resolve(raw) : path.resolve(path.dirname(previousPath), raw); await fs.mkdir(path.dirname(handle.path), { recursive: true }); diff --git a/src/renderer/src/canvas/FlowCanvas.tsx b/src/renderer/src/canvas/FlowCanvas.tsx index 1d2ed1f..9e52c4b 100644 --- a/src/renderer/src/canvas/FlowCanvas.tsx +++ b/src/renderer/src/canvas/FlowCanvas.tsx @@ -19,7 +19,6 @@ import { useEditorStore } from "../store/editorStore"; import { useRunStore } from "../store/runStore"; import { RpaNode, type RpaNodeData } from "./RpaNode"; import { ContainerNode, containerSlots, type ContainerNodeData } from "./ContainerNode"; -import { MousePointerClick } from "lucide-react"; const nodeTypes = { rpa: RpaNode, container: ContainerNode }; @@ -256,16 +255,6 @@ function CanvasInner({ catalog, onAddAt }: { catalog: Catalog; onAddAt: (compone maskColor="rgba(242,245,248,.72)" /> - - {flow.nodes.length <= 1 && ( -