diff --git a/src/main/capabilities/excel-common.ts b/src/main/capabilities/excel-common.ts index 7b4e6b9..e61a1aa 100644 --- a/src/main/capabilities/excel-common.ts +++ b/src/main/capabilities/excel-common.ts @@ -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; + const text = String(raw ?? "").trim(); + if (!text) return {}; + try { + const parsed = JSON.parse(text) as Record; + if (typeof parsed === "object" && parsed) return parsed as ReturnType; + } catch { /* fall through to keywords */ } + const spec: ReturnType = {}; + 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 }; +});