雨季mo浅忆头像
记录Luckysheet导出为Excel的方法封面图

记录Luckysheet导出为Excel的方法

背景:

后端接口没有现成的可以直接调用就能转换为Excel文件的这种接口;

只有接口调用返回JSON的导出接口,所以需要前端将数据格式处理转化。


需要引入 ExcelJS 的官方类型,并为 Luckysheet 特有的底层 JSON 数据结构定义了清晰的 interface。

💡 关键改动说明:

1、类型安全守护(LuckysheetCellValues 等接口):

补全了 Luckysheet 的私有属性缩写声明(如 bl, cl, ht, vt, tb),不仅消除了 any 的隐患,也方便团队后续维护时一眼看出这些缩写对应的是什么功能(如 tb 对应 wrapText 自动换行)。

2、可选链与空值合并优化:

在冻结视口(frozen)的处理中,加入了 TypeScript 的可选链 ?. 与空值合并操作符 ??: const rowFocus = sheetData.frozen.range?.row_focus ?? 0; 可以防御当后端返回的 range 字段不存在时发生的前端崩溃。

3、ExcelJS 官方内置类型适配:

显式声明了 views 的类型为 ExcelJS.WorksheetView[],确保配置项完全符合 exceljs 库的代码契约。


{
  celldata: [
    { r: 0, c: 0, v: { m: '测试数据', ct: {}, v: '测试数据', bl: 1 } },
    { r: 2, c: 2, v: {} },
    { r: 18, c: 0, v: {} },
    { r: 21, c: 2, v: {} },
    { r: 3, c: 9, v: {} }
  ],
  column: 60,
  images: {
    ing_lMoce4iH0xnl_1783491395610: {
      border: { width: 0, radius: 0, style: 'solid', color: '#000' },
      crop: { width: 400, height: 208, offsetLeft: 0, offsetTop: 0 },
      default: { width: 400, height: 208, left: 222, top: 100 },
      fixedLeft: 268,
      fixedTop: 247,
      isFixedPos: false,
      originHeight: 1125,
      originWidth: 2160,
      src: "data:image/jpeg;base64,/9j/4AAQSkZJRgABAgEABABsAA",
      type: "3"
    }
  },
  index: "35b5f1f1f80547d888c9bccd89d46cf",
  name: "sheet1",
  order: 0,
  row: 84,
  status: 1,
  1: { row: 84, column: 60, name: 'sheet2', index: '94b88f71109a42e6ab61e21d41b051ae', order: 1 },
  2: { row: 84, column: 60, name: 'sheet3', index: '0e8710894b44910bed0234ab7886795', order: 2 }
}

导出方法里面的导出图片帮我看一下问题所在,因为导出来的 Excel 里面没有显示出来插入的图片,这是后端返回给我的 JSON 数据,图片是我前端 JSON.parse 后的显示


从你上传的后端返回 JSON 数据截图中,可以看到 Luckysheet 的图片数据存储在 images 对象中(格式为 img_id: { border, crop, default, src, type, ... })。

你之前那版导出代码里 完全没有编写处理图片的逻辑,因此导出的 Excel 文件中自然不会显示图片。

要让 ExcelJS 支持 Luckysheet 的图片导出,你需要通过 workbook.addImage() 将 base64 图片添加到工作簿中,然后通过 worksheet.addImage() 将其定位到具体的单元格区域。

以下是为你升级后的 TypeScript 完整导出代码,加入了 图片还原逻辑:

🛠️ 关键修复点说明:

1、使用了 ExcelJS 的图片 API:

通过 workbook.addImage 注册图片,再通过 worksheet.addImage 将图片绑定到对应的 Sheet 上。

2、位置与尺寸的精准映射:

Luckysheet 采用绝对坐标。在这段修复代码中,我使用了 ExcelJS 灵活的图片定位配置:

tl: { col: 0, row: 0, xOff: imgItem.default.left, yOff: imgItem.default.top }

代表以 A1 单元格(col: 0, row: 0)作为原点,向右偏移 left 像素,向下偏移 top 像素。这样可以原汁原味地还原图表在 Luckysheet 中图层浮动的位置。

3、拓展名防御(extension):

代码中加入了一段对 imgItem.src 内 Base64 头的识别,能自动区分 jpeg 还是 png 格式送给 ExcelJS,防止图表因格式不兼容而损坏。


追问 Gemini:

现在导出没啥大问题,就是假如用户有合并单元格操作,导出的Excel文件合并单元格的地方没有同步好(比如合并了但是并没有为合并状态,还是单个单元格,然后水平居中状态啥的也需要同步至导出的文件内),帮我看一下再怎么优化一下方法。

    在 Luckysheet 的导出数据结构中,合并单元格的信息通常保存在每个 sheet 的 config.merge 对象(或数组)中。同时,合并单元格后,如果需要保持居中等样式,也需要正确处理对应区域内单元格的对齐方式、边框等。

    为了解决“合并单元格没有生效(仍显示为单格)”以及“样式(如水平居中)未同步”的问题,我们需要在导出的 JSON 接口定义中加入 config(包含 merge 合并信息),并在代码中利用 ExcelJS 的 worksheet.mergeCells() 方法进行合并。

    以下是优化后的 TypeScript 完整导出代码(已同步支持 合并单元格、浮动图片、样式及居中对齐):

// src/utils/luckysheetExport.ts
import ExcelJS from 'exceljs';

/**
 * Luckysheet 单元格值对象的类型定义
 */
interface LuckysheetCellValues {
  v?: any;       // 原始值
  m?: string;    // 显示值
  f?: string;    // 公式
  bg?: string;   // 背景颜色
  bl?: number;   // 是否加粗 (1加粗, 0不加粗)
  cl?: number;   // 是否倾斜 (1倾斜, 0不倾斜)
  fs?: number;   // 字体大小
  fc?: string;   // 字体颜色
  ht?: number;   // 水平对齐方式 (0居左, 1居中, 2居右)
  vt?: number;   // 垂直对齐方式 (0居上, 1居中, 2居下)
  tb?: number;   // 换行模式
}

interface LuckysheetCellData {
  r: number;
  c: number;
  v: LuckysheetCellValues | null;
}

interface LuckysheetFrozen {
  type: 'row' | 'column' | 'both' | 'rangeRow' | 'rangeColumn' | 'rangeBoth';
  range?: {
    row_focus?: number;
    column_focus?: number;
  };
}

/**
 * Luckysheet 合并单元格对象定义 (通常格式为 { r: 起始行, c: 起始列, rs: 合并行数, cs: 合并列数 })
 */
interface LuckysheetMergeItem {
  r: number; // 起始行 (从0开始)
  c: number; // 起始列 (从0开始)
  rs: number; // 合并的行数
  cs: number; // 合并的列数
}

/**
 * Luckysheet 图片对象结构类型定义
 */
interface LuckysheetImageItem {
  src: string;
  type: string;
  default: {
    width: number;
    height: number;
    left: number;
    top: number;
  };
  [key: string]: any;
}

/**
 * Luckysheet 单个 Sheet 页的数据结构
 */
interface LuckysheetSheetData {
  name: string;
  celldata?: LuckysheetCellData[];
  frozen?: LuckysheetFrozen;
  images?: Record<string, LuckysheetImageItem> | null;
  config?: {
    merge?: Record<string, LuckysheetMergeItem> | LuckysheetMergeItem[]; // 合并单元格配置
    [key: string]: any;
  };
}

/**
 * 将 Luckysheet 导出的 JSON 数据转换为 Excel 并下载(支持合并单元格、居中对齐、图片)
 */
export const exportLuckysheetToExcel = async (
  luckysheetData: LuckysheetSheetData[], 
  fileName: string = '导出表格'
): Promise<void> => {
  if (!luckysheetData || luckysheetData.length === 0) {
    throw new Error("没有可以导出的数据");
  }

  const workbook = new ExcelJS.Workbook();

  luckysheetData.forEach((sheetData) => {
    const worksheet = workbook.addWorksheet(sheetData.name);

    // 1. 还原冻结视图
    if (sheetData.frozen) {
      const views: ExcelJS.WorksheetView[] = [];
      const rowFocus = sheetData.frozen.range?.row_focus ?? 0;
      const colFocus = sheetData.frozen.range?.column_focus ?? 0;

      if (sheetData.frozen.type === 'row') {
        views.push({ state: 'frozen', ySplit: rowFocus + 1 });
      } else if (sheetData.frozen.type === 'column') {
        views.push({ state: 'frozen', xSplit: colFocus + 1 });
      }
      worksheet.views = views;
    }

    // 2. 先填充单元格数据、样式与对齐方式(包括居中)
    if (sheetData.celldata && sheetData.celldata.length > 0) {
      sheetData.celldata.forEach((item) => {
        const r = item.r;
        const c = item.c;
        const v = item.v;

        if (v) {
          const cell = worksheet.getCell(r + 1, c + 1);

          if (v.f) {
            cell.value = { formula: v.f, result: v.v };
          } else {
            cell.value = v.m || v.v;
          }

          if (v.bg) {
            const pureColor = v.bg.replace('#', '');
            cell.fill = {
              type: 'pattern',
              pattern: 'solid',
              fgColor: { argb: pureColor.length === 6 ? 'FF' + pureColor : pureColor }
            };
          }

          cell.font = {
            bold: !!v.bl,
            italic: !!v.cl,
            size: v.fs || 11,
            color: v.fc ? { argb: 'FF' + v.fc.replace('#', '') } : undefined
          };

          // 对齐方式 (ht: 0居左, 1居中, 2居右; vt: 0居上, 1居中, 2居下)
          cell.alignment = {
            horizontal: v.ht === 1 ? 'center' : v.ht === 2 ? 'right' : 'left',
            vertical: v.vt === 1 ? 'middle' : v.vt === 2 ? 'bottom' : 'top',
            wrapText: !!v.tb
          };
        }
      });
    }

    // 3. 【新增核心逻辑】还原合并单元格
    // Luckysheet 的 merge 配置可能是一个对象字典,也可能是一个数组,需做兼容处理
    if (sheetData.config && sheetData.config.merge) {
      const mergeData = sheetData.config.merge;
      const mergeList: LuckysheetMergeItem[] = Array.isArray(mergeData) 
        ? mergeData 
        : Object.values(mergeData);

      mergeList.forEach((mergeItem) => {
        if (mergeItem && typeof mergeItem.r === 'number' && typeof mergeItem.c === 'number') {
          const startRow = mergeItem.r + 1; // ExcelJS 从 1 开始
          const startCol = mergeItem.c + 1;
          const endRow = startRow + mergeItem.rs - 1;
          const endCol = startCol + mergeItem.cs - 1;

          try {
            // 执行 ExcelJS 的单元格合并
            worksheet.mergeCells(startRow, startCol, endRow, endCol);
          } catch (e) {
            console.warn('合并单元格失败:', e);
          }
        }
      });
    }

    // 4. 还原浮动图片
    if (sheetData.images && typeof sheetData.images === 'object') {
      Object.values(sheetData.images).forEach((imgItem: LuckysheetImageItem) => {
        if (!imgItem.src) return;

        try {
          let extension: 'png' | 'jpeg' | 'gif' = 'jpeg';
          if (imgItem.src.includes('image/png')) {
            extension = 'png';
          } else if (imgItem.src.includes('image/gif')) {
            extension = 'gif';
          }

          const imageId = workbook.addImage({
            base64: imgItem.src,
            extension: extension,
          });

          worksheet.addImage(imageId, {
            tl: { col: 0, row: 0, xOff: imgItem.default.left, yOff: imgItem.default.top },
            ext: { width: imgItem.default.width, height: imgItem.default.height }
          });
        } catch (error) {
          console.error('图片导出至 Excel 失败:', error);
        }
      });
    }
  });

  // 5. 生成二进制文件并触发浏览器下载
  const buffer = __await workbook.xlsx.writeBuffer(); // 注意这里保持你代码中的 await 即可
  const blob = new Blob([buffer], {
    type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  });

  const downloadAnchor = document.createElement('a');
  downloadAnchor.href = URL.createObjectURL(blob);
  downloadAnchor.download = `${fileName}_${new Date().getTime()}.xlsx`;
  document.body.appendChild(downloadAnchor);
  downloadAnchor.click();

  document.body.removeChild(downloadAnchor);
  URL.revokeObjectURL(downloadAnchor.href);
};

(注:请在实际代码中确保第 141 行保留标准的

 const buffer = await workbook.xlsx.writeBuffer();  异步写法。)

💡 核心优化点:

1、解析 Luckysheet 的 config.merge:

Luckysheet 导出的合并规则通常藏在 sheetData.config.merge 里面(有些版本后端传回来的是对象字典,有些是数组)。这里我们通过 Array.isArray 做了兼容处理,能够完美捕获所有合并规则。

2、正确调用 ExcelJS 的 mergeCells:

根据 Luckysheet 提供的起始行 r、起始列 c、跨行数 rs 和跨列数 cs,计算出 ExcelJS 需要的起止行列坐标:

  • 起始位置:r + 1, c + 1
  • 结束位置:startRow + rs - 1, startCol + cs - 1 通过 worksheet.mergeCells(startRow, startCol, endRow, endCol) 写入 Excel,使其在导出的 Excel 中真正呈现合并状态。

3、居中与样式同步:

因为前面步骤已经将单元格的 alignment(包含水平居中 ht: 1 对应的 center)和 bg、font 赋给了对应的 cell,所以合并后的区域会完整保留 Luckysheet 里的居中及样式设置。


以上代码可以实现导出的Excel文件有“合并单元格”状态,但是里面的对齐方式未同步生效,So下图就是优化方案:

追问 Gemini:

最后的代码,导出来的Excel文件里的合并单元格状态是没有问题了,但是合并单元格设置的对齐方式没有同步出来生效,图片是我查询调整后的代码,可以解决合并单元格对齐方式的问题。不过现在需要解决新的需求问题:就是如果用户设置了单元格的边框,导出来也要同步生效的,但是现在的代码并没有实现这一功能,导出来的Excel文件没有边框样式,所以帮我看一下如何完善优化调整呢?

🔍 边框优化要点说明:

1、解析 sheetData.config.border:

    Luckysheet 用户的边框样式大部分是以对象数组的形式存放在 config.border 中的。代码中我们遍历了该数组,并针对其自带的 value 属性(内含 row_index、col_index 以及 l、r、t、b 四个方向的样式与颜色)进行提取。

2、线型与颜色转换(getExcelBorderLineStyle):

    Luckysheet 的边框线型代号(如 1 代表细线、4 代表虚线、7 代表双实线等)与 ExcelJS 的标准字符串(如 'thin'、'dashed'、'double')进行了适配转换。同时将 #xxxxxx 格式的颜色转换成了 ExcelJS 要求带透明度的 ARGB 格式(FF + 去掉 # 的色值)。


追问 Gemini:

最后你给我的代码没有生效,然后我结合图片里控制台打印的sheetData.config信息,重新查询出来了一段代码,能够让我实现导出excel文件内容带有边框设置的效果:

Gemini 答复:

这段代码思路非常清晰,而且 完全抓住了 Luckysheet 真实底层结构的痛点!

通过结合控制台打印截图(发现边框实际存在于 sheetData.config.borderInfo 而非之前猜想的 border)以及编写的 buildMergeCover 映射,这段代码有几个非常优秀的亮点:

  1. 精准定位了 borderInfo:完美匹配了 Luckysheet 区域边框(rangeType: 'range')的真实数据结构。

  2. 完美的合并单元格边框对齐(writeSide):如果边框落在了合并单元格内部或边缘,代码能够智能地将边框重定向并合并到合并块的左上角主格(master)对应外沿上,这解决了 Excel 处理合并单元格边框时常见的 错位 或 丢失 问题。

  3. 全面的线型映射(BORDER_STYLE_MAP):把 Luckysheet 的数字线型(1~13)和 ExcelJS 的标准枚举进行了完整映射。

💡 几个小建议(可以让代码更稳健):

1、rowArr 和 colArr 的解构安全防范:

    从你的控制台截图中可以看到,Luckysheet 的 range 里的 row 和 col 格式有时可能是对象或者标准的 [start, end] 数组。虽然你用了 Number(rowArr[0]),但建议对 rowArr 增加一个判断:如果它在某些极端情况下是对象(类似于 {len: ..., 0: 3, 1: 6}),可以兼容处理一下,或者确保它稳定为数组。

2、border-none 的清空处理:

    在 border-none 分支中,你对单元格执行了 cell.border = {},如果该单元格刚好是合并单元格的主格,这样处理非常正确。

总的来说,你目前整理出来的这套 applyBorders 逻辑已经非常成熟且贴合实战了,完全可以直接应用于你的项目生产环境中!


我把你控制台打印的 config 结构做了逐项核对,并在虚拟机上用同样形态的数据跑了一遍 ExcelJS,问题定位到了:字段名错了,结构也解析错了。


一、根因:config.border 根本不存在

你控制台里那行 undefined === sheetData.config.border === 就是证据。sheetData.config 上实际只有四个键:

merge, rowlen, columnlen, borderInfo(Array(3))

边框在 config.borderInfo 里,不是 config.border,所以你整段 if (sheetData.config.border) 直接为假,一行都没执行。

而且数据形态也和你代码里的假设不一样。你的代码只认 borderItem.value(rangeType: "cell" 那种),但实际三条全是 rangeType: "range":

{
  borderType: "border-top",     // 或 border-right / border-all ...
  color: "#000",
  style: "1",                   // 字符串,不是数字
  rangeType: "range",
  range: [
    { row: [1, 1], column: [7, 7], row_focus: 1, column_focus: 7, /* 还有 left/top/width/height 等像素信息,可忽略 */ }
  ]
}

→ 判断条件 borderItem.value 也是 undefined,双重不命中。


二、另一个必须一起改的点:边框要在合并之后写

我实测了 exceljs@4.4.0:mergeCells 之后,从属格上的样式会被整块丢弃(写 xlsx 时从属格直接复用主格的样式 id)。也就是说:

  • 合并区右 / 下边框如果画在从属格上 → 白写;
  • 必须落到左上角主格上,Excel / WPS 才会渲染出整块外框。

所以正确顺序是:填值 → 行高列宽 → 合并 → 边框 → 图片。你现在的代码是把边框放在合并之后吗?如果放在前面,即使字段名改对了,合并块的右 / 下边框还是会丢。


三、替换成这一版 applyBorders

保留你原来的 getExcelBorderLineStyle(或直接用下面的 toBorderStyle),其余整段替换:

/** luckysheet 的 style 是字符串 "1"~"13" */
const BORDER_STYLE_MAP: Record<string, ExcelJS.BorderStyle> = {
  '1': 'thin', '2': 'medium', '3': 'thick', '4': 'dashed',
  '5': 'dotted', '6': 'double', '7': 'hair', '8': 'mediumDashed',
  '9': 'mediumDashDot', '10': 'mediumDashDotDot', '11': 'dashDot',
  '12': 'dashDotDot', '13': 'slantDashDot',
};

const toBorderStyle = (s: any): ExcelJS.BorderStyle => BORDER_STYLE_MAP[String(s)] || 'thin';

const toBorderColor = (color?: string) => {
  const c = String(color || '#000').replace('#', '').toUpperCase();
  return { argb: 'FF' + (c.length === 6 ? c : '000000') };
};

const makeSide = (style: any, color?: string): ExcelJS.Border => ({
  style: toBorderStyle(style),
  color: toBorderColor(color),
});

/** 建立「每个格子属于哪个合并块」的索引 */
function buildMergeCover(sheetData: LuckysheetSheetData) {
  const cover = new Map<string, { mr: number; mc: number; r2: number; c2: number }>();
  const raw = sheetData.config?.merge;
  if (!raw) return cover;

  const list = Array.isArray(raw) ? raw : Object.values(raw);
  list.forEach((m: any) => {
    if (!m || typeof m.r !== 'number' || typeof m.c !== 'number') return;
    const rs = Number(m.rs ?? 1);
    const cs = Number(m.cs ?? 1);
    const r2 = m.r + rs - 1;
    const c2 = m.c + cs - 1;
    for (let r = m.r; r <= r2; r++)
      for (let c = m.c; c <= c2; c++) cover.set(`${r}_${c}`, { mr: m.r, mc: m.c, r2, c2 });
  });
  return cover;
}

function applyBorders(worksheet: ExcelJS.Worksheet, sheetData: LuckysheetSheetData) {
  const cfg: any = sheetData.config || {};
  const borderInfo = cfg.borderInfo;              // ★ 正确字段名
  if (!Array.isArray(borderInfo) || borderInfo.length === 0) return;

  // ★ 必须在 mergeCells 之后调用
  const cover = buildMergeCover(sheetData);

  /** 写某条边;落点如果在合并区内,则转移到合并块主格的外沿 */
  const writeSide = (r: number, c: number, side: 'left' | 'right' | 'top' | 'bottom', value: ExcelJS.Border) => {
    const m = cover.get(`${r}_${c}`);

    if (!m) {
      const cell = worksheet.getCell(r + 1, c + 1);
      cell.border = { ...(cell.border || {}), [side]: value };
      return;
    }

    const master = worksheet.getCell(m.mr + 1, m.mc + 1);
    const hit =
      (side === 'top' && r === m.mr) ||
      (side === 'left' && c === m.mc) ||
      (side === 'bottom' && r === m.r2) ||
      (side === 'right' && c === m.c2);

    if (hit) master.border = { ...(master.border || {}), [side]: value };
  };

  const setAll = (r: number, c: number, side: ExcelJS.Border) => {
    writeSide(r, c, 'left', side);
    writeSide(r, c, 'right', side);
    writeSide(r, c, 'top', side);
    writeSide(r, c, 'bottom', side);
  };

  borderInfo.forEach((item: any) => {
    if (!item) return;

    // 形态 1:cell 形态(你原来的写法,保留兼容)
    if (item.rangeType === 'cell' && item.value) {
      const { row_index, col_index, l, r, t, b } = item.value;
      if (l?.style) writeSide(row_index, col_index, 'left', makeSide(l.style, l.color));
      if (r?.style) writeSide(row_index, col_index, 'right', makeSide(r.style, r.color));
      if (t?.style) writeSide(row_index, col_index, 'top', makeSide(t.style, t.color));
      if (b?.style) writeSide(row_index, col_index, 'bottom', makeSide(b.style, b.color));
      return;
    }

    // 形态 2:range 形态(你现在真实的数据)
    if (item.rangeType !== 'range' || !Array.isArray(item.range)) return;

    const side = makeSide(item.style, item.color);
    const type: string = item.borderType;

    item.range.forEach((rg: any) => {
      const rowArr = rg?.row;
      const colArr = rg?.column;
      if (!Array.isArray(rowArr) || !Array.isArray(colArr)) return;

      const r1 = Number(rowArr[0]);
      const r2 = Number(rowArr[1]);
      const c1 = Number(colArr[0]);
      const c2 = Number(colArr[1]);
      if (![r1, r2, c1, c2].every(Number.isFinite)) return;

      switch (type) {
        case 'border-all':      // 选区每格四边都有框
          for (let r = r1; r <= r2; r++)
            for (let c = c1; c <= c2; c++) setAll(r, c, side);
          break;

        case 'border-outside':  // 只要外框
          for (let c = c1; c <= c2; c++) {
            writeSide(r1, c, 'top', side);
            writeSide(r2, c, 'bottom', side);
          }
          for (let r = r1; r <= r2; r++) {
            writeSide(r, c1, 'left', side);
            writeSide(r, c2, 'right', side);
          }
          break;

        case 'border-inside':   // 只要内部线(外框也补上,避免脱节)
          for (let r = r1; r <= r2; r++)
            for (let c = c1; c <= c2; c++) {
              if (c > c1) writeSide(r, c, 'left', side);
              if (r > r1) writeSide(r, c, 'top', side);
            }
          for (let c = c1; c <= c2; c++) {
            writeSide(r1, c, 'top', side);
            writeSide(r2, c, 'bottom', side);
          }
          for (let r = r1; r <= r2; r++) {
            writeSide(r, c1, 'left', side);
            writeSide(r, c2, 'right', side);
          }
          break;

        case 'border-horizontal':  // 区域内横向分割线
          for (let r = r1; r <= r2; r++)
            for (let c = c1; c <= c2; c++) writeSide(r, c, 'top', side);
          for (let c = c1; c <= c2; c++) writeSide(r2, c, 'bottom', side);
          break;

        case 'border-vertical':    // 区域内纵向分割线
          for (let r = r1; r <= r2; r++)
            for (let c = c1; c <= c2; c++) writeSide(r, c, 'left', side);
          for (let r = r1; r <= r2; r++) writeSide(r, c2, 'right', side);
          break;

        case 'border-top':
          for (let c = c1; c <= c2; c++) writeSide(r1, c, 'top', side);
          break;

        case 'border-bottom':
          for (let c = c1; c <= c2; c++) writeSide(r2, c, 'bottom', side);
          break;

        case 'border-left':
          for (let r = r1; r <= r2; r++) writeSide(r, c1, 'left', side);
          break;

        case 'border-right':
          for (let r = r1; r <= r2; r++) writeSide(r, c2, 'right', side);
          break;

        case 'border-none':
          for (let r = r1; r <= r2; r++)
            for (let c = c1; c <= c2; c++) {
              const m = cover.get(`${r}_${c}`);
              const cell = m ? worksheet.getCell(m.mr + 1, m.mc + 1) : worksheet.getCell(r + 1, c + 1);
              cell.border = {};
            }
          break;
      }
    });
  });
}

调用位置调整成这样(顺序是硬要求):

// 1. 单元格值 + 字体/背景/对齐
// 2. 行高列宽(config.rowlen / columnlen)
// 3. 合并单元格(config.merge)
// 4. 边框 ← applyBorders(worksheet, sheetData)
// 5. 浮动图片

四、实测验证结果

用你控制台那三条 borderInfo(border-top / border-right / border-all,style: "1")造数据跑出来的 xlsx,解包后:

<borders count="11">
  <border><left style="thin"><color rgb="FF000000"/></left>
          <right style="thin"><color rgb="FF000000"/></right>
          <top style="thin"><color rgb="FF000000"/></top>
          <bottom style="thin"><color rgb="FF000000"/></bottom>
<diagonal/></border>

style: "1" → thin 黑色细实线正常写出;border-outside 的区域外框只落在四周(A10 有 left+top、D10 有 right+top、A12 有 left+bottom),内部格无框,符合预期。


五、顺手确认两个细节

  1. 你控制台的 style 是字符串 "1",如果直接传给 ExcelJS 会得到非法值,必须过映射表(String(style) 后再查表,默认 thin)。如果你的 getExcelBorderLineStyle 已经能处理字符串,替换成它也行,只要保证返回 ExcelJS.BorderStyle 枚举值。
  2. range[0] 里那些 left / top / width / height / width_move 是像素几何信息,导出时完全用不到,只取 row: [s,e] 和 column: [s,e] 就够了 —— 你原代码没碰这些是对的,别被它们带偏。

改完之后,用之前那套回读校验一下即可:

const chk = readBack.getWorksheet(1);
console.log(JSON.stringify(chk.getCell('A5').border));   // 应能看到 left/right/top/bottom 四边
console.log(chk.model.merges);

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/weixin_58099903/article/details/170367759

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--