feat: Excel 高阶接入——格式/筛选/表格样式/隐藏/尾空单元格/列标识/选中区域/关闭/加载/写列(16 id)

This commit is contained in:
yu 2026-10-29 10:10:34 +08:00
parent fc1207cddd
commit 26c682d2f9

View File

@ -611,3 +611,191 @@ 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 } {
if (typeof raw === "object" && raw !== null) return raw as ReturnType<typeof parseFormatSpec>;
const text = String(raw ?? "").trim();
if (!text) return {};
try {
const parsed = JSON.parse(text) as Record<string, unknown>;
if (typeof parsed === "object" && parsed) return parsed as ReturnType<typeof parseFormatSpec>;
} 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]);
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,
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);
const headerRow = sheet.getRow(1);
headerRow.font = { ...headerRow.font, bold: true };
headerRow.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFD9E8F5" } };
headerRow.commit();
sheet.autoFilter = { from: { row: 1, column: 1 }, to: { row: Math.max(1, sheet.rowCount), column: Math.max(1, sheet.columnCount) } };
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 };
});