- SetCellFormat V1/V2/V3:格式解析补厂商 JSON 键映射(FontColor/FontSize/ BoldAndItalic 位标志/BackgroundColor 带透明度/DigitalFormat.formatstring/ FontFormat 字体名),纯格式代码(如 0.00)回落为数字格式——此前全部被静默忽略 - ShowTableStyle:行级样式(row.font/fill+commit)在 exceljs 中会覆盖该行 已设置的单元格样式,改为逐单元格设置表头样式 - 经 18 节点覆盖流程实测:V3 红字宋体加粗、V2 蓝字、V1 数字格式 0.00 均落盘
827 lines
40 KiB
TypeScript
827 lines
40 KiB
TypeScript
import ExcelJS from "exceljs";
|
||
import path from "node:path";
|
||
import { register } from "../engine/registry";
|
||
import { RuntimeError, type CapabilityHandler } from "../engine/types";
|
||
import { acquireWorkbook, releaseWorkbook } from "./excel-store";
|
||
|
||
type WorkbookSource = { workbook: ExcelJS.Workbook; filePath: string };
|
||
type CopiedRange = { values: unknown[][]; styles: Partial<ExcelJS.Style>[][] };
|
||
|
||
let copiedRange: CopiedRange | undefined;
|
||
|
||
function value(args: Record<string, unknown>, keys: string[], fallback: unknown = ""): unknown {
|
||
for (const key of keys) {
|
||
const candidate = args[key];
|
||
if (candidate !== undefined && candidate !== null && candidate !== "") return candidate;
|
||
}
|
||
return fallback;
|
||
}
|
||
|
||
function text(args: Record<string, unknown>, keys: string[], fallback = ""): string {
|
||
return String(value(args, keys, fallback));
|
||
}
|
||
|
||
function truthy(candidate: unknown, fallback = false): boolean {
|
||
if (candidate === undefined || candidate === null || candidate === "") return fallback;
|
||
return [true, 1, "1", "true", "True", "TRUE", "yes", "是", "开启"].includes(candidate as never);
|
||
}
|
||
|
||
function numberValue(candidate: unknown, fallback: number): number {
|
||
const result = Number(candidate);
|
||
return Number.isFinite(result) ? result : fallback;
|
||
}
|
||
|
||
function requiredPath(candidate: unknown, nodeId: string, componentId: string): string {
|
||
const raw = typeof candidate === "object" && candidate !== null ? (candidate as { path?: unknown }).path : candidate;
|
||
const filePath = String(raw ?? "").trim();
|
||
if (!filePath) {
|
||
throw new RuntimeError("validation_error", "Excel path is empty", {
|
||
nodeId,
|
||
componentId,
|
||
recommendation: "Configure an Excel workbook path"
|
||
});
|
||
}
|
||
return path.resolve(filePath);
|
||
}
|
||
|
||
async function loadWorkbook(args: Record<string, unknown>, nodeId: string, componentId: string): Promise<WorkbookSource> {
|
||
const filePath = requiredPath(value(args, ["workbook", "objExcel", "objExcelWorkBook", "file", "path"]), nodeId, componentId);
|
||
const workbook = await acquireWorkbook(filePath);
|
||
return { workbook, filePath };
|
||
}
|
||
|
||
function worksheetOf(workbook: ExcelJS.Workbook, args: Record<string, unknown>): ExcelJS.Worksheet {
|
||
const name = text(args, ["sheet", "sheetName"]);
|
||
const sheet = name ? workbook.getWorksheet(name) : workbook.worksheets[0];
|
||
if (!sheet) throw new Error(`Sheet not found: ${name || "first sheet"}`);
|
||
return sheet;
|
||
}
|
||
|
||
async function saveWhenRequested(source: WorkbookSource, args: Record<string, unknown>, fallback = false): Promise<void> {
|
||
if (truthy(value(args, ["save", "isSave"]), fallback)) await source.workbook.xlsx.writeFile(source.filePath);
|
||
}
|
||
|
||
function columnNumber(candidate: unknown): number {
|
||
const raw = String(candidate ?? "A").trim().toUpperCase();
|
||
if (/^\d+$/.test(raw)) return Math.max(1, Number(raw));
|
||
if (!/^[A-Z]+$/.test(raw)) throw new Error(`Invalid column: ${raw}`);
|
||
let result = 0;
|
||
for (const character of raw) result = result * 26 + character.charCodeAt(0) - 64;
|
||
return result;
|
||
}
|
||
|
||
function columnLetter(index: number): string {
|
||
let current = Math.max(1, Math.trunc(index));
|
||
let result = "";
|
||
while (current > 0) {
|
||
current -= 1;
|
||
result = String.fromCharCode(65 + current % 26) + result;
|
||
current = Math.floor(current / 26);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function cellValue(candidate: ExcelJS.CellValue): unknown {
|
||
if (candidate && typeof candidate === "object" && "result" in candidate) return candidate.result ?? "";
|
||
if (candidate && typeof candidate === "object" && "text" in candidate) return candidate.text;
|
||
return candidate ?? "";
|
||
}
|
||
|
||
function listValue(candidate: unknown): unknown[] {
|
||
if (Array.isArray(candidate)) return candidate;
|
||
const raw = String(candidate ?? "").trim();
|
||
if (!raw) return [];
|
||
try {
|
||
const parsed = JSON.parse(raw);
|
||
return Array.isArray(parsed) ? parsed : [parsed];
|
||
} catch {
|
||
try {
|
||
const parsed = JSON.parse(raw.replace(/'/g, '"'));
|
||
return Array.isArray(parsed) ? parsed : [parsed];
|
||
} catch {
|
||
return raw.split(",").map((item) => item.trim());
|
||
}
|
||
}
|
||
}
|
||
|
||
function tableValue(candidate: unknown): unknown[][] {
|
||
return listValue(candidate).map((item) => Array.isArray(item) ? item : [item]);
|
||
}
|
||
|
||
function rangeBounds(sheet: ExcelJS.Worksheet, range: string): { top: number; left: number; bottom: number; right: number } {
|
||
const normalized = range.trim() || `A1:${columnLetter(Math.max(1, sheet.columnCount))}${Math.max(1, sheet.rowCount)}`;
|
||
const [start, end = start] = normalized.split(":").map((part) => part.trim());
|
||
const first = sheet.getCell(start);
|
||
const last = sheet.getCell(end);
|
||
return {
|
||
top: Math.min(Number(first.row), Number(last.row)),
|
||
left: Math.min(Number(first.col), Number(last.col)),
|
||
bottom: Math.max(Number(first.row), Number(last.row)),
|
||
right: Math.max(Number(first.col), Number(last.col))
|
||
};
|
||
}
|
||
|
||
function explicitRange(args: Record<string, unknown>, sheet: ExcelJS.Worksheet): string {
|
||
const direct = text(args, ["excelRng", "range", "cellPosRange", "cellPos", "sortRange"]);
|
||
if (direct) return direct;
|
||
const top = numberValue(value(args, ["startRow", "row"]), 1);
|
||
const left = columnNumber(value(args, ["startColumn", "column"]));
|
||
const bottom = numberValue(value(args, ["lastRow", "endRow"]), top);
|
||
const right = columnNumber(value(args, ["lastColumn", "endColumn"], left));
|
||
return `${columnLetter(left)}${top}:${columnLetter(right)}${bottom || Math.max(1, sheet.rowCount)}`;
|
||
}
|
||
|
||
function cellsInRange(sheet: ExcelJS.Worksheet, range: string): ExcelJS.Cell[] {
|
||
const bounds = rangeBounds(sheet, range);
|
||
const cells: ExcelJS.Cell[] = [];
|
||
for (let row = bounds.top; row <= bounds.bottom; row += 1) {
|
||
for (let column = bounds.left; column <= bounds.right; column += 1) cells.push(sheet.getCell(row, column));
|
||
}
|
||
return cells;
|
||
}
|
||
|
||
function cloneStyle(style: Partial<ExcelJS.Style>): Partial<ExcelJS.Style> {
|
||
return JSON.parse(JSON.stringify(style)) as Partial<ExcelJS.Style>;
|
||
}
|
||
|
||
function colorArgb(candidate: unknown): string {
|
||
if (Array.isArray(candidate)) {
|
||
const [red = 0, green = 0, blue = 0] = candidate.map((item) => Math.max(0, Math.min(255, Number(item))));
|
||
return `FF${red.toString(16).padStart(2, "0")}${green.toString(16).padStart(2, "0")}${blue.toString(16).padStart(2, "0")}`.toUpperCase();
|
||
}
|
||
const raw = String(candidate ?? "#000000").trim();
|
||
if (raw.startsWith("[")) {
|
||
try { return colorArgb(JSON.parse(raw)); } catch { return "FF000000"; }
|
||
}
|
||
const hex = raw.replace(/^#/, "").toUpperCase();
|
||
if (/^[0-9A-F]{8}$/.test(hex)) return hex;
|
||
return `FF${/^[0-9A-F]{6}$/.test(hex) ? hex : "000000"}`;
|
||
}
|
||
|
||
function comparison(left: unknown, right: unknown): number {
|
||
if (typeof left === "number" && typeof right === "number") return left - right;
|
||
return String(left ?? "").localeCompare(String(right ?? ""), "zh-CN", { numeric: true, sensitivity: "base" });
|
||
}
|
||
|
||
function registerMany(ids: string[], handler: CapabilityHandler): void {
|
||
register(ids, handler);
|
||
}
|
||
|
||
const writeRow: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const start = text(args, ["cellPos"]);
|
||
const row = start ? Number(sheet.getCell(start).row) : numberValue(value(args, ["row", "rowIndex"]), 1);
|
||
const column = start ? Number(sheet.getCell(start).col) : columnNumber(value(args, ["column"]));
|
||
listValue(value(args, ["contents", "value"])).forEach((item, index) => { sheet.getCell(row, column + index).value = item as ExcelJS.CellValue; });
|
||
await saveWhenRequested(source, args);
|
||
return `${columnLetter(column)}${row}`;
|
||
};
|
||
|
||
const writeColumn: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const start = text(args, ["cellPos"]);
|
||
const row = start ? Number(sheet.getCell(start).row) : numberValue(value(args, ["row"]), 1);
|
||
const column = start ? Number(sheet.getCell(start).col) : columnNumber(value(args, ["column"]));
|
||
listValue(value(args, ["contents", "value"])).forEach((item, index) => { sheet.getCell(row + index, column).value = item as ExcelJS.CellValue; });
|
||
await saveWhenRequested(source, args);
|
||
return `${columnLetter(column)}${row}`;
|
||
};
|
||
|
||
const writeRange: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const row = numberValue(value(args, ["row", "startRow"]), 1);
|
||
const column = columnNumber(value(args, ["column", "startColumn"]));
|
||
tableValue(value(args, ["contents", "value"])).forEach((items, rowOffset) => {
|
||
items.forEach((item, columnOffset) => { sheet.getCell(row + rowOffset, column + columnOffset).value = item as ExcelJS.CellValue; });
|
||
});
|
||
await saveWhenRequested(source, args);
|
||
return `${columnLetter(column)}${row}`;
|
||
};
|
||
|
||
const readRow: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const start = text(args, ["cellPos"]);
|
||
const row = start ? Number(sheet.getCell(start).row) : numberValue(value(args, ["row", "rowIndex"]), 1);
|
||
const firstColumn = start ? Number(sheet.getCell(start).col) : 1;
|
||
const result: unknown[] = [];
|
||
for (let column = firstColumn; column <= Math.max(firstColumn, sheet.columnCount); column += 1) result.push(cellValue(sheet.getCell(row, column).value));
|
||
return result;
|
||
};
|
||
|
||
const readColumn: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const start = text(args, ["cellPos"]);
|
||
const row = start ? Number(sheet.getCell(start).row) : 1;
|
||
const column = start ? Number(sheet.getCell(start).col) : columnNumber(value(args, ["column"]));
|
||
const result: unknown[] = [];
|
||
for (let current = row; current <= Math.max(row, sheet.rowCount); current += 1) result.push(cellValue(sheet.getCell(current, column).value));
|
||
return result;
|
||
};
|
||
|
||
const readRange: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const bounds = rangeBounds(sheet, explicitRange(args, sheet));
|
||
const result: unknown[][] = [];
|
||
for (let row = bounds.top; row <= bounds.bottom; row += 1) {
|
||
const values: unknown[] = [];
|
||
for (let column = bounds.left; column <= bounds.right; column += 1) values.push(cellValue(sheet.getCell(row, column).value));
|
||
result.push(values);
|
||
}
|
||
return result;
|
||
};
|
||
|
||
const readColumnCount: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const row = numberValue(value(args, ["row"]), 1);
|
||
let last = 0;
|
||
for (let column = 1; column <= sheet.columnCount; column += 1) {
|
||
if (cellValue(sheet.getCell(row, column).value) !== "") last = column;
|
||
}
|
||
return last;
|
||
};
|
||
|
||
const findOrReplace: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const needle = text(args, ["what", "input"]);
|
||
const replacement = text(args, ["replacement", "replace"]);
|
||
const mode = text(args, ["mode"], "0");
|
||
const replaceMode = mode === "1" || mode.includes("替换") || replacement !== "";
|
||
const exact = truthy(value(args, ["wholeWord"])) || text(args, ["lookAt"]) === "1";
|
||
const matchCase = truthy(value(args, ["matchCase"]));
|
||
const range = text(args, ["range"]);
|
||
const cells = cellsInRange(sheet, range || `A1:${columnLetter(Math.max(1, sheet.columnCount))}${Math.max(1, sheet.rowCount)}`);
|
||
const matches: string[] = [];
|
||
for (const cell of cells) {
|
||
const original = String(cellValue(cell.value));
|
||
const haystack = matchCase ? original : original.toLowerCase();
|
||
const target = matchCase ? needle : needle.toLowerCase();
|
||
const matched = exact ? haystack === target : haystack.includes(target);
|
||
if (!matched) continue;
|
||
matches.push(cell.address);
|
||
if (replaceMode) {
|
||
const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||
cell.value = exact ? replacement : original.replace(new RegExp(escaped, matchCase ? "g" : "gi"), replacement);
|
||
}
|
||
if (truthy(value(args, ["matchFirst"]))) break;
|
||
}
|
||
await saveWhenRequested(source, args);
|
||
return matches;
|
||
};
|
||
|
||
const sortRange: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const range = text(args, ["sortRange", "range"]) || `A1:${columnLetter(Math.max(1, sheet.columnCount))}${Math.max(1, sheet.rowCount)}`;
|
||
const bounds = rangeBounds(sheet, range);
|
||
const hasHeader = truthy(value(args, ["header"]));
|
||
const dataStart = bounds.top + (hasHeader ? 1 : 0);
|
||
const rows: unknown[][] = [];
|
||
for (let row = dataStart; row <= bounds.bottom; row += 1) {
|
||
const values: unknown[] = [];
|
||
for (let column = bounds.left; column <= bounds.right; column += 1) values.push(cellValue(sheet.getCell(row, column).value));
|
||
rows.push(values);
|
||
}
|
||
let sortOffset = 0;
|
||
const rule = value(args, ["rule"]);
|
||
if (rule) {
|
||
try {
|
||
const parsed = typeof rule === "string" ? JSON.parse(rule) as Record<string, unknown> : rule as Record<string, unknown>;
|
||
sortOffset = Math.max(0, columnNumber(value(parsed, ["column", "key"], bounds.left)) - bounds.left);
|
||
} catch { /* use first column */ }
|
||
}
|
||
const descending = ["2", "-1", "desc", "descending", "降序"].includes(text(args, ["sortOrder"]).toLowerCase());
|
||
rows.sort((left, right) => comparison(left[sortOffset], right[sortOffset]) * (descending ? -1 : 1));
|
||
rows.forEach((values, rowOffset) => values.forEach((item, columnOffset) => {
|
||
sheet.getCell(dataStart + rowOffset, bounds.left + columnOffset).value = item as ExcelJS.CellValue;
|
||
}));
|
||
await saveWhenRequested(source, args);
|
||
return rows;
|
||
};
|
||
|
||
function criterionMatches(candidate: unknown, criterion: unknown, condition: string): boolean {
|
||
const left = String(candidate ?? "");
|
||
const right = String(criterion ?? "");
|
||
if (["contains", "包含", "1"].includes(condition)) return left.includes(right);
|
||
if (["notContains", "不包含", "2"].includes(condition)) return !left.includes(right);
|
||
if (["greater", ">", "3"].includes(condition)) return Number(candidate) > Number(criterion);
|
||
if (["less", "<", "4"].includes(condition)) return Number(candidate) < Number(criterion);
|
||
if (["notEqual", "!=", "5"].includes(condition)) return left !== right;
|
||
return left === right;
|
||
}
|
||
|
||
const filterRows: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
let column = columnNumber(value(args, ["column"], "A"));
|
||
let criterion = value(args, ["criteria"], "");
|
||
let condition = text(args, ["condition"], "equal");
|
||
const rule = value(args, ["rule"]);
|
||
if (rule) {
|
||
try {
|
||
const parsed = typeof rule === "string" ? JSON.parse(rule) as Record<string, unknown> : rule as Record<string, unknown>;
|
||
column = columnNumber(value(parsed, ["column"], column));
|
||
criterion = value(parsed, ["criteria", "value"], criterion);
|
||
condition = text(parsed, ["condition", "operator"], condition);
|
||
} catch { /* use direct fields */ }
|
||
}
|
||
const startRow = numberValue(value(args, ["startRow"]), 1);
|
||
const endRowRaw = numberValue(value(args, ["endRow"]), sheet.rowCount);
|
||
const endRow = endRowRaw < 0 ? sheet.rowCount : endRowRaw;
|
||
const headerRow = truthy(value(args, ["onHeader"])) ? startRow : 0;
|
||
for (let row = startRow; row <= endRow; row += 1) {
|
||
if (row === headerRow) continue;
|
||
sheet.getRow(row).hidden = !criterionMatches(cellValue(sheet.getCell(row, column).value), criterion, condition);
|
||
}
|
||
sheet.autoFilter = { from: { row: startRow, column: 1 }, to: { row: endRow, column: Math.max(1, sheet.columnCount) } };
|
||
await saveWhenRequested(source, args);
|
||
return { column: columnLetter(column), criterion, condition };
|
||
};
|
||
|
||
const clearFilter: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
for (let row = 1; row <= sheet.rowCount; row += 1) sheet.getRow(row).hidden = false;
|
||
sheet.autoFilter = undefined;
|
||
await saveWhenRequested(source, args);
|
||
return true;
|
||
};
|
||
|
||
const deleteDuplicates: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const columns = listValue(value(args, ["columns"], "A")).map(columnNumber);
|
||
const hasHeader = truthy(value(args, ["header"]));
|
||
const seen = new Set<string>();
|
||
const duplicates: number[] = [];
|
||
for (let row = hasHeader ? 2 : 1; row <= sheet.rowCount; row += 1) {
|
||
const key = JSON.stringify(columns.map((column) => cellValue(sheet.getCell(row, column).value)));
|
||
if (seen.has(key)) duplicates.push(row);
|
||
else seen.add(key);
|
||
}
|
||
duplicates.reverse().forEach((row) => sheet.spliceRows(row, 1));
|
||
await saveWhenRequested(source, args);
|
||
return duplicates.length;
|
||
};
|
||
|
||
function copyCells(sheet: ExcelJS.Worksheet, range: string): CopiedRange {
|
||
const bounds = rangeBounds(sheet, range);
|
||
const values: unknown[][] = [];
|
||
const styles: Partial<ExcelJS.Style>[][] = [];
|
||
for (let row = bounds.top; row <= bounds.bottom; row += 1) {
|
||
const valueRow: unknown[] = [];
|
||
const styleRow: Partial<ExcelJS.Style>[] = [];
|
||
for (let column = bounds.left; column <= bounds.right; column += 1) {
|
||
const cell = sheet.getCell(row, column);
|
||
valueRow.push(cell.value);
|
||
styleRow.push(cloneStyle(cell.style));
|
||
}
|
||
values.push(valueRow);
|
||
styles.push(styleRow);
|
||
}
|
||
return { values, styles };
|
||
}
|
||
|
||
const copyRange: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const range = explicitRange(args, sheet);
|
||
copiedRange = copyCells(sheet, range);
|
||
const destinationColumn = value(args, ["startColumn2"]);
|
||
const destinationRow = value(args, ["startRow2"]);
|
||
if (destinationColumn !== "" && destinationRow !== "") {
|
||
const startColumn = columnNumber(destinationColumn);
|
||
const startRow = numberValue(destinationRow, 1);
|
||
copiedRange.values.forEach((items, rowOffset) => items.forEach((item, columnOffset) => {
|
||
const cell = sheet.getCell(startRow + rowOffset, startColumn + columnOffset);
|
||
cell.value = item as ExcelJS.CellValue;
|
||
cell.style = cloneStyle(copiedRange!.styles[rowOffset][columnOffset]);
|
||
}));
|
||
await saveWhenRequested(source, args);
|
||
}
|
||
return copiedRange.values;
|
||
};
|
||
|
||
const pasteRange: CapabilityHandler = async (_ctx, node, args) => {
|
||
if (!copiedRange) throw new RuntimeError("validation_error", "No copied Excel range is available", { nodeId: node.id, componentId: node.componentId });
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const start = text(args, ["Start"]);
|
||
const row = start ? Number(sheet.getCell(start).row) : numberValue(value(args, ["row"]), 1);
|
||
const column = start ? Number(sheet.getCell(start).col) : columnNumber(value(args, ["column"]));
|
||
const transpose = truthy(value(args, ["transpose"]));
|
||
const values = transpose ? copiedRange.values[0].map((_, index) => copiedRange!.values.map((items) => items[index])) : copiedRange.values;
|
||
values.forEach((items, rowOffset) => items.forEach((item, columnOffset) => {
|
||
const cell = sheet.getCell(row + rowOffset, column + columnOffset);
|
||
cell.value = item as ExcelJS.CellValue;
|
||
if (!transpose) cell.style = cloneStyle(copiedRange!.styles[rowOffset][columnOffset]);
|
||
}));
|
||
await saveWhenRequested(source, args);
|
||
return `${columnLetter(column)}${row}`;
|
||
};
|
||
|
||
const createSheet: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const name = text(args, ["sheet", "sheetName", "name"], `Sheet${source.workbook.worksheets.length + 1}`);
|
||
if (source.workbook.getWorksheet(name)) throw new Error(`Sheet already exists: ${name}`);
|
||
source.workbook.addWorksheet(name);
|
||
await saveWhenRequested(source, args);
|
||
return name;
|
||
};
|
||
|
||
const deleteSheet: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
if (source.workbook.worksheets.length <= 1) throw new Error("Cannot delete the only worksheet");
|
||
source.workbook.removeWorksheet(sheet.id);
|
||
await saveWhenRequested(source, args);
|
||
return sheet.name;
|
||
};
|
||
|
||
const activateSheet: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const mode = text(args, ["mode"]);
|
||
const index = mode.includes("序号") ? numberValue(value(args, ["index"]), 1) : 0;
|
||
const sheet = index ? source.workbook.worksheets[index - 1] : worksheetOf(source.workbook, args);
|
||
if (!sheet) throw new Error(`Sheet not found: ${index}`);
|
||
source.workbook.views = [{
|
||
activeTab: source.workbook.worksheets.indexOf(sheet),
|
||
firstSheet: 0,
|
||
visibility: "visible",
|
||
x: 0,
|
||
y: 0,
|
||
width: 12000,
|
||
height: 8000
|
||
}];
|
||
await saveWhenRequested(source, args);
|
||
return sheet.name;
|
||
};
|
||
|
||
const getSheetNames: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
return source.workbook.worksheets.map((sheet) => sheet.name);
|
||
};
|
||
|
||
function setFontColor(cell: ExcelJS.Cell, argb: string): void {
|
||
cell.font = { ...(cell.font || {}), color: { argb } };
|
||
}
|
||
|
||
function setBackground(cell: ExcelJS.Cell, argb: string): void {
|
||
cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb } };
|
||
}
|
||
|
||
const setColor = (kind: "font" | "background", rangeMode: boolean): CapabilityHandler => async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const range = rangeMode
|
||
? explicitRange(args, sheet)
|
||
: text(args, ["cellPos"]) || `${text(args, ["column"], "A")}${numberValue(value(args, ["row"]), 1)}`;
|
||
const argb = colorArgb(value(args, ["color", "cellColor"]));
|
||
for (const cell of cellsInRange(sheet, range)) {
|
||
if (kind === "font") setFontColor(cell, argb);
|
||
else setBackground(cell, argb);
|
||
}
|
||
await saveWhenRequested(source, args);
|
||
return argb;
|
||
};
|
||
|
||
const readBackground: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const cell = sheet.getCell(numberValue(value(args, ["row"]), 1), columnNumber(value(args, ["column"])));
|
||
const fill = cell.fill as ExcelJS.FillPattern | undefined;
|
||
return fill?.type === "pattern" ? fill.fgColor?.argb ?? "" : "";
|
||
};
|
||
|
||
const autoFill: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const sourceRange = text(args, ["sourceRange", "excelRng"]);
|
||
const destination = text(args, ["destination"]);
|
||
const sourceBounds = rangeBounds(sheet, sourceRange);
|
||
const destinationBounds = rangeBounds(sheet, destination);
|
||
const sourceValues = copyCells(sheet, sourceRange).values;
|
||
const vertical = sourceBounds.left === sourceBounds.right;
|
||
for (let row = destinationBounds.top; row <= destinationBounds.bottom; row += 1) {
|
||
for (let column = destinationBounds.left; column <= destinationBounds.right; column += 1) {
|
||
const sourceRow = (row - destinationBounds.top) % sourceValues.length;
|
||
const sourceColumn = (column - destinationBounds.left) % sourceValues[0].length;
|
||
let next = sourceValues[sourceRow][sourceColumn];
|
||
const sequence = vertical ? sourceValues.map((items) => items[0]) : sourceValues[0];
|
||
const offset = vertical ? row - sourceBounds.top : column - sourceBounds.left;
|
||
if (sequence.length >= 2 && sequence.every((item) => typeof item === "number")) {
|
||
next = Number(sequence[0]) + (Number(sequence[1]) - Number(sequence[0])) * offset;
|
||
}
|
||
sheet.getCell(row, column).value = next as ExcelJS.CellValue;
|
||
}
|
||
}
|
||
await saveWhenRequested(source, args);
|
||
return destination;
|
||
};
|
||
|
||
const splitColumn: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const sourceColumn = columnNumber(value(args, ["column"]));
|
||
const destination = text(args, ["destination"]);
|
||
const destinationColumn = destination ? Number(sheet.getCell(destination).col) : sourceColumn;
|
||
const startRow = numberValue(value(args, ["startRow"]), 1);
|
||
const endRow = numberValue(value(args, ["endRow"]), sheet.rowCount);
|
||
const delimiters: string[] = [];
|
||
if (truthy(value(args, ["tab"]), true)) delimiters.push("\\t");
|
||
if (truthy(value(args, ["semicolon"]))) delimiters.push(";");
|
||
if (truthy(value(args, ["comma"]))) delimiters.push(",");
|
||
if (truthy(value(args, ["space"]))) delimiters.push("\\s+");
|
||
if (truthy(value(args, ["other"]))) delimiters.push(text(args, ["otherChar"]).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
||
const separator = new RegExp(delimiters.filter(Boolean).join("|") || "\\t");
|
||
for (let row = startRow; row <= endRow; row += 1) {
|
||
String(cellValue(sheet.getCell(row, sourceColumn).value)).split(separator).forEach((item, offset) => {
|
||
sheet.getCell(row, destinationColumn + offset).value = item;
|
||
});
|
||
}
|
||
await saveWhenRequested(source, args);
|
||
return `${columnLetter(destinationColumn)}${startRow}`;
|
||
};
|
||
|
||
const closeWorkbook: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
if (truthy(value(args, ["save", "isSave"]), true)) await source.workbook.xlsx.writeFile(source.filePath);
|
||
releaseWorkbook(source.filePath);
|
||
return source.filePath;
|
||
};
|
||
|
||
const deleteRange: CapabilityHandler = async (_ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const range = explicitRange(args, sheet);
|
||
for (const cell of cellsInRange(sheet, range)) cell.value = null;
|
||
await saveWhenRequested(source, args);
|
||
return range;
|
||
};
|
||
|
||
export const EXCEL_COMMON_IDS = [
|
||
"Excel_WriteColumn", "Excel_WriteColumnNew", "Excel_Cs_WriteColumn",
|
||
"Excel_WriteRow", "Excel_WriteRowNew", "Excel_Cs_WriteRow", "Excel_Cs_WriteRowV2",
|
||
"Excel_Cs_WriteRangeV2", "Excel_GetRowData", "Excel_GetRowDataNew", "Excel_Cs_ReadRow",
|
||
"Excel_GetColData", "Excel_Cs_ReadColumnCount", "Excel_Cs_ReadRange",
|
||
"Excel_Cs_Find", "Excel_Cs_FindV2", "Excel_Cs_Search",
|
||
"Excel_Cs_Sort", "Excel_Cs_SortV1", "Excel_Cs_Filter", "Excel_Cs_ExcelFilter",
|
||
"Excel_Cs_ClearExcelFilter", "Excel_Cs_EnableExcelFilter", "Excel_Cs_DeleteDuplicates",
|
||
"Excel_CopyRange", "Excel_Cs_CopyRange", "Excel_Cs_CopyRangeV2", "Excel_Cs_CopyRangeV3",
|
||
"Excel_PasteRange", "Excel_Cs_PastRange", "Excel_Cs_PastRangeV2",
|
||
"Excel_CreateSheet", "Excel_DeleteSheet", "Excel_Cs_Delete", "Excel_ActiveSheet", "Excel_Cs_Active", "Excel_Cs_GetName",
|
||
"Excel_SetCellFontColor", "Excel_Cs_SetCellFont", "Excel_SetRangeFontColor", "Excel_Cs_SetRangeFont", "Excel_Cs_SetRangeFontV2",
|
||
"Excel_SetCellColor", "Excel_Cs_SetCellBackground", "Excel_SetRangeColor", "Excel_Cs_SetRangeBackground", "Excel_Cs_SetRangeBackgroundV2",
|
||
"Excel_Cs_ReadCellBackground", "Excel_Cs_AutoFill", "Excel_Cs_AutoFillV2",
|
||
"Excel_Cs_ColumnSplit", "Excel_Cs_ColumnSplitV2", "Excel_Close", "Excel_Cs_Close",
|
||
"Excel_DeleteRange", "Excel_Cs_DeleteRange", "Excel_Cs_DeleteRangeV2", "Excel_Cs_DeleteRangeV3"
|
||
] as const;
|
||
|
||
registerMany(["Excel_WriteRow", "Excel_WriteRowNew", "Excel_Cs_WriteRow", "Excel_Cs_WriteRowV2"], writeRow);
|
||
registerMany(["Excel_WriteColumn", "Excel_WriteColumnNew", "Excel_Cs_WriteColumn"], writeColumn);
|
||
register("Excel_Cs_WriteRangeV2", writeRange);
|
||
registerMany(["Excel_GetRowData", "Excel_GetRowDataNew", "Excel_Cs_ReadRow"], readRow);
|
||
register("Excel_GetColData", readColumn);
|
||
register("Excel_Cs_ReadColumnCount", readColumnCount);
|
||
register("Excel_Cs_ReadRange", readRange);
|
||
registerMany(["Excel_Cs_Find", "Excel_Cs_FindV2", "Excel_Cs_Search"], findOrReplace);
|
||
registerMany(["Excel_Cs_Sort", "Excel_Cs_SortV1"], sortRange);
|
||
registerMany(["Excel_Cs_Filter", "Excel_Cs_ExcelFilter"], filterRows);
|
||
registerMany(["Excel_Cs_ClearExcelFilter", "Excel_Cs_EnableExcelFilter"], clearFilter);
|
||
register("Excel_Cs_DeleteDuplicates", deleteDuplicates);
|
||
registerMany(["Excel_CopyRange", "Excel_Cs_CopyRange", "Excel_Cs_CopyRangeV2", "Excel_Cs_CopyRangeV3"], copyRange);
|
||
registerMany(["Excel_PasteRange", "Excel_Cs_PastRange", "Excel_Cs_PastRangeV2"], pasteRange);
|
||
register("Excel_CreateSheet", createSheet);
|
||
registerMany(["Excel_DeleteSheet", "Excel_Cs_Delete"], deleteSheet);
|
||
registerMany(["Excel_ActiveSheet", "Excel_Cs_Active"], activateSheet);
|
||
register("Excel_Cs_GetName", getSheetNames);
|
||
registerMany(["Excel_SetCellFontColor", "Excel_Cs_SetCellFont"], setColor("font", false));
|
||
registerMany(["Excel_SetRangeFontColor", "Excel_Cs_SetRangeFont", "Excel_Cs_SetRangeFontV2"], setColor("font", true));
|
||
registerMany(["Excel_SetCellColor", "Excel_Cs_SetCellBackground"], setColor("background", false));
|
||
registerMany(["Excel_SetRangeColor", "Excel_Cs_SetRangeBackground", "Excel_Cs_SetRangeBackgroundV2"], setColor("background", true));
|
||
register("Excel_Cs_ReadCellBackground", readBackground);
|
||
registerMany(["Excel_Cs_AutoFill", "Excel_Cs_AutoFillV2"], autoFill);
|
||
registerMany(["Excel_Cs_ColumnSplit", "Excel_Cs_ColumnSplitV2"], splitColumn);
|
||
registerMany(["Excel_Close", "Excel_Cs_Close"], closeWorkbook);
|
||
registerMany(["Excel_DeleteRange", "Excel_Cs_DeleteRange", "Excel_Cs_DeleteRangeV2", "Excel_Cs_DeleteRangeV3"], deleteRange);
|
||
|
||
|
||
// ===== Excel 高阶(批四) =====
|
||
function cellAddress(addr: string): { row: number; column: number } {
|
||
const m = addr.toUpperCase().match(/^([A-Z]+)(\d+)$/);
|
||
if (!m) throw new Error("Invalid cell address: " + addr);
|
||
return { row: Number(m[2]), column: columnNumber(m[1]) };
|
||
}
|
||
|
||
function toArgb(color: string): string {
|
||
const hex = color.replace(/^#/, "").toUpperCase();
|
||
return hex.length === 6 ? "FF" + hex : hex.padStart(8, "F");
|
||
}
|
||
|
||
function parseFormatSpec(raw: unknown): { bold?: boolean; italic?: boolean; underline?: boolean; fontSize?: number; color?: string; bgColor?: string; numFmt?: string; fontName?: string } {
|
||
if (typeof raw === "object" && raw !== null) return normalizeFormatSpec(raw as Record<string, unknown>);
|
||
const text = String(raw ?? "").trim();
|
||
if (!text) return {};
|
||
try {
|
||
const parsed = JSON.parse(text) as Record<string, unknown>;
|
||
if (typeof parsed === "object" && parsed) return normalizeFormatSpec(parsed);
|
||
} catch { /* fall through to keywords */ }
|
||
const spec: ReturnType<typeof parseFormatSpec> = {};
|
||
const lower = text.toLowerCase();
|
||
if (/加粗|bold/.test(lower)) spec.bold = true;
|
||
if (/斜体|italic/.test(lower)) spec.italic = true;
|
||
if (/下划线|underline/.test(lower)) spec.underline = true;
|
||
const sizeMatch = text.match(/(\d+)号|font-size[::]\s*(\d+)/i);
|
||
if (sizeMatch) spec.fontSize = Number(sizeMatch[1] || sizeMatch[2]);
|
||
// 无关键词命中时视为数字格式代码(如 "0.00"、"G/通用格式"、"yyyy-mm-dd")
|
||
if (!Object.keys(spec).length) spec.numFmt = text;
|
||
return spec;
|
||
}
|
||
|
||
/** 兼容厂商格式 JSON(FontColor/BoldAndItalic/FontSize/BackgroundColor/DigitalFormat)与内部 camelCase 键 */
|
||
function normalizeFormatSpec(parsed: Record<string, unknown>): ReturnType<typeof parseFormatSpec> {
|
||
const spec: ReturnType<typeof parseFormatSpec> = {};
|
||
const flags = Number(parsed.BoldAndItalic ?? 0);
|
||
if (flags & 1) spec.bold = true;
|
||
if (flags & 2) spec.italic = true;
|
||
if (parsed.FontFormat) spec.fontName = String(parsed.FontFormat);
|
||
if (parsed.FontSize !== undefined && parsed.FontSize !== null && String(parsed.FontSize).trim() !== "") spec.fontSize = Number(parsed.FontSize);
|
||
if (parsed.FontColor) spec.color = String(parsed.FontColor);
|
||
if (parsed.BackgroundColor) spec.bgColor = String(parsed.BackgroundColor).split(",")[0].trim();
|
||
const digital = parsed.DigitalFormat as { formatstring?: unknown } | undefined;
|
||
if (digital?.formatstring && String(digital.formatstring).trim() !== "") spec.numFmt = String(digital.formatstring);
|
||
for (const key of ["bold", "italic", "underline", "fontSize", "color", "bgColor", "numFmt"] as const) {
|
||
if (parsed[key] !== undefined) (spec as Record<string, unknown>)[key] = parsed[key];
|
||
}
|
||
return spec;
|
||
}
|
||
|
||
registerMany(["Excel_Cs_SetCellFormat", "Excel_Cs_SetCellFormat_V2", "Excel_Cs_SetCellFormat_V3"], 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", "cellPos", "rng"], "A1");
|
||
const spec = parseFormatSpec(args.format);
|
||
const addresses: string[] = [];
|
||
const parts = range.toUpperCase().split(":");
|
||
if (parts.length === 2 && parts[0] !== parts[1]) {
|
||
const from = cellAddress(parts[0]);
|
||
const to = cellAddress(parts[1]);
|
||
for (let r = from.row; r <= to.row; r += 1) {
|
||
for (let cIdx = from.column; cIdx <= to.column; cIdx += 1) {
|
||
addresses.push(columnLetter(cIdx) + r);
|
||
}
|
||
}
|
||
} else {
|
||
addresses.push(parts[0]);
|
||
}
|
||
for (const addr of addresses) {
|
||
const cell = sheet.getCell(addr);
|
||
if (spec.bold !== undefined || spec.italic !== undefined || spec.underline !== undefined || spec.fontSize !== undefined || spec.color) {
|
||
cell.font = {
|
||
...cell.font,
|
||
bold: spec.bold,
|
||
italic: spec.italic,
|
||
underline: spec.underline,
|
||
size: spec.fontSize,
|
||
name: spec.fontName ?? cell.font?.name,
|
||
color: spec.color ? { argb: toArgb(spec.color) } : undefined
|
||
};
|
||
}
|
||
if (spec.numFmt) cell.numFmt = spec.numFmt;
|
||
if (spec.bgColor) cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: toArgb(spec.bgColor) } };
|
||
}
|
||
await saveWhenRequested(source, args);
|
||
ctx.log("已设置 " + addresses.length + " 个单元格格式", "info", node.id);
|
||
return { formatted: addresses.length, range };
|
||
});
|
||
|
||
register("Excel_Cs_ExcelFilter", async (ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const rule = text(args, ["rule"]);
|
||
const startRow = numberValue(args.startRow, 1);
|
||
const endRow = numberValue(args.endRow, sheet.rowCount || 1);
|
||
const range = rule && rule.includes(":") ? rule.toUpperCase() : "A" + startRow + ":" + columnLetter(sheet.columnCount || 1) + endRow;
|
||
sheet.autoFilter = range;
|
||
await saveWhenRequested(source, args);
|
||
ctx.log("已设置自动筛选:" + range, "info", node.id);
|
||
return { filter: range };
|
||
});
|
||
|
||
register("Excel_Cs_ShowTableStyle", async (ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
// 必须逐单元格设置:exceljs 的行级样式(row.font/row.fill + commit)会覆盖
|
||
// 该行已设置的单元格样式(如先执行过的设置单元格格式)
|
||
const lastCol = Math.max(1, sheet.columnCount);
|
||
for (let c = 1; c <= lastCol; c += 1) {
|
||
const cell = sheet.getRow(1).getCell(c);
|
||
cell.font = { ...cell.font, bold: true };
|
||
cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFD9E8F5" } };
|
||
}
|
||
sheet.autoFilter = { from: { row: 1, column: 1 }, to: { row: Math.max(1, sheet.rowCount), column: lastCol } };
|
||
await saveWhenRequested(source, args, true);
|
||
ctx.log("已应用表格样式(表头加粗 + 筛选)", "info", node.id);
|
||
return { styled: true };
|
||
});
|
||
|
||
register("Excel_Cs_Hide", async (ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const mode = String(value(args, ["mode"], "row")).toLowerCase();
|
||
const range = text(args, ["range"]);
|
||
const hide = !/取消|取消隐藏|unhide|show/i.test(mode + range);
|
||
if (mode.includes("col") || mode.includes("列")) {
|
||
const targets = range ? range.split(",") : [text(args, ["column"])];
|
||
for (const t of targets) {
|
||
if (!t.trim()) continue;
|
||
const letter = t.trim().replace(/^\d+:?/, "").toUpperCase() || t.trim().toUpperCase();
|
||
sheet.getColumn(columnNumber(letter)).hidden = hide;
|
||
}
|
||
} else {
|
||
const targets = range ? range.split(":") : [text(args, ["row"])];
|
||
if (targets.length === 2) {
|
||
const from = Number(targets[0]); const to = Number(targets[1]);
|
||
for (let r = Math.min(from, to); r <= Math.max(from, to); r += 1) sheet.getRow(r).hidden = hide;
|
||
} else {
|
||
const r = Number(targets[0]);
|
||
if (Number.isFinite(r) && r > 0) sheet.getRow(r).hidden = hide;
|
||
}
|
||
}
|
||
await saveWhenRequested(source, args);
|
||
ctx.log((hide ? "已隐藏" : "已取消隐藏") + "(" + mode + " " + range + ")", "info", node.id);
|
||
return { hidden: hide };
|
||
});
|
||
|
||
register("Excel_Cs_GetLastEmptyCell", async (ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const column = columnLetter(numberValue(args.column, 1));
|
||
let lastFilled = 0;
|
||
for (let r = 1; r <= sheet.rowCount; r += 1) {
|
||
const v = sheet.getRow(r).getCell(numberValue(args.column, 1)).value;
|
||
if (v !== null && v !== undefined && v !== "") lastFilled = r;
|
||
}
|
||
const emptyRow = lastFilled + 1;
|
||
return { address: column + emptyRow, row: emptyRow, column: numberValue(args.column, 1) };
|
||
});
|
||
|
||
register("Excel_GetColumnId", async (_ctx, _node, args) => columnLetter(numberValue(args.num, 1)));
|
||
|
||
register(["Excel_Cs_SelectRange", "Excel_Cs_SelectRangeV2", "Excel_Cs_SelectRangeV3", "Excel_SelectRange"], async (ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const cellPos = text(args, ["cellPos", "range", "Start"], "A1");
|
||
const parts = cellPos.toUpperCase().split(":");
|
||
const from = cellAddress(parts[0]);
|
||
const to = parts[1] ? cellAddress(parts[1]) : from;
|
||
const rows: unknown[][] = [];
|
||
for (let r = from.row; r <= to.row; r += 1) {
|
||
const row: unknown[] = [];
|
||
for (let cIdx = from.column; cIdx <= to.column; cIdx += 1) {
|
||
row.push(sheet.getRow(r).getCell(cIdx).value ?? "");
|
||
}
|
||
rows.push(row);
|
||
}
|
||
ctx.log("已读取区域 " + cellPos + "(" + rows.length + " 行)", "info", node.id);
|
||
return rows;
|
||
});
|
||
|
||
register("Excel_Cs_CloseV2", async (ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const save = !/不保存|discard|false/i.test(text(args, ["closeAction"]));
|
||
if (save) await source.workbook.xlsx.writeFile(source.filePath);
|
||
releaseWorkbook(source.filePath);
|
||
ctx.log("已关闭工作簿" + (save ? "(已保存)" : "(未保存)"), "info", node.id);
|
||
return { closed: true, saved: save };
|
||
});
|
||
|
||
register(["Excel_Cs_Load", "Excel_Cs_LoadV2"], async (ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheets = source.workbook.worksheets.map(w => w.name);
|
||
ctx.log("已加载工作簿:" + source.filePath + "(" + sheets.length + " 个 Sheet)", "info", node.id);
|
||
return { filePath: source.filePath, sheets };
|
||
});
|
||
|
||
register("Excel_WriteColumn", async (ctx, node, args) => {
|
||
const source = await loadWorkbook(args, node.id, node.componentId);
|
||
const sheet = worksheetOf(source.workbook, args);
|
||
const start = cellAddress(text(args, ["cellPos"], "A1").toUpperCase());
|
||
const valuesRaw = value(args, ["value"]);
|
||
const values = Array.isArray(valuesRaw) ? valuesRaw : [valuesRaw];
|
||
const flat = Array.isArray(values) ? values.flat() : [values];
|
||
flat.forEach((item, offset) => {
|
||
sheet.getRow(start.row + offset).getCell(start.column).value = item as ExcelJS.CellValue;
|
||
});
|
||
await saveWhenRequested(source, args, true);
|
||
ctx.log("已写入 " + flat.length + " 行(" + columnLetter(start.column) + start.row + " 起)", "info", node.id);
|
||
return { written: flat.length };
|
||
});
|