Files
PRA-only-demo/src/main/capabilities/excel-common.ts
T
yu 8cedf45628 fix: 修复设置单元格格式不生效与表格样式覆盖单元格样式
- 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 均落盘
2026-10-29 15:57:57 +08:00

827 lines
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };
});