如何完整的打印有横向滚动条的表格内容
需求说明
需求是这样的,当页面有几个很宽很宽的表格,它可以左右滚动,并且表格字段是根据每个表单配置来的,也就是说表格字段数量是未知的,因此,表格宽度就是未知的,在理想状态下,表格可以无限宽,只要表格字段够多的话。此时通过 window.print() 需要将表单内容通过 A4 纸完全打印出来。
面对这种需求,各位又当如何?
难点描述
- 
由于打印按钮是通过 npm 引入的公共组件提供的,不是当前页面自己的按钮,因此当前页面并不能知道打印按钮的点击事件。那么问题就来了,如果需要在打印前做一些逻辑处理需要怎么做?
 - 
如何将页面中带有左右滚动条的表格内容打印完全,而不丢失内容?
 
如何将表格内容打印完全?
为了能让表单打印完全,主要想到了如下几种实现方式:
- 
将表格宽度设置为固定的宽度,不让它出现滚动条,这样就能打印完全了。显然这个方式是无法达到目的的,因为表格字段是未知的,所以这种方式不可取。
 - 
在打印的时候,将表格去除,将字段展示由表格的展示方式替换为 key: value 的展示方式,如:
 
    // 表示表格第一行
    Full Name: Name  1
    Age: 18
    Column 1: Column no. 1-1
    Column 2: Column no. 2-1
    ...
    Column 7: Column no. 7-1
    Column 8: Column no. 8-1
    // 表示表格第二行
    Full Name: Name  1
    Age: 18
    Column 1: Column no. 1-2
    Column 2: Column no. 2-2
    ...
    Column 7: Column no. 7-2
    Column 8: Column no. 8-2
    ...
    // 表示表格第8行
    Full Name: Name  8
    Age: 18
    Column 1: Column no. 1-8
    Column 2: Column no. 2-8
    ...
    Column 7: Column no. 7-8
    Column 8: Column no. 8-8
    // 表示表格第9行
    Full Name: Name  9
    Age: 18
    Column 1: Column no. 1-9
    Column 2: Column no. 2-9
    ...
    Column 7: Column no. 7-9
    Column 8: Column no. 8-9
通过上述方式,虽然能够将表格数据打印完全,但是存在以下两个问题:
- 
由于表单字段一行一个,会导致打印内容过长,原本可能只需要 2 张 A4 纸,现在需要 5、6 张 A4 纸。
 - 
这种打印方式打印出来的内容,不容易阅读,不能像 Table 一样直接了当。
 
- 进行表格拆分,将一个表格在打印的时候拆分多个表格。这样既方便阅读,同时也能将内容打印完全。同时打印纸张也相对第二种方式减少许多。
 
- 表格具体的拆分思路就是将一个 column 的内容拆分成多个 column,然后进行打印即可:
 
// 将一个 columns 的内容按照参数 size 拆分成多个 column
const chunkedColumns = (columns: ColumnType[], size: number) => {
  const columnList = [];
  for (let i = 0; i < columns.length; i += size) {
    const chunk = columns.slice(i, i + size);
    columnList.push(chunk);
  }
  return columnList;
};
判断是否开启打印,拆分表格进行打印
找到了将表格打印完全的方案,剩下的就是如何判断是否开启打印,然后在开启打印时将表格进行拆分,然后进行打印,就大功告成了。
而想要知道是否开启打印,我们可以监听 beforeprint 和 afterprint 事件,在事件触发时,我们可以做一些逻辑处理,如下:
import React, { useMemo, useState, useEffect } from 'react';
import { Table } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import styles from './index.less';
interface DataType {
  key: React.Key;
  name: string;
  age: number;
  address: string;
}
interface ColumnType {
  title: string;
  key: string;
  dataIndex?: string;
  width?: number | string;
  fixed?: string;
  render?: () => void;
}
const PrintPage: React.FC = () => {
  const [isPrint, setIsPrint] = useState(false);
  // 监听打印事件
  useEffect(() => {
    window.addEventListener('beforeprint', beforePrint);
    window.addEventListener('afterprint', afterPrint);
    return () => {
      window.removeEventListener('beforeprint', beforePrint);
      window.removeEventListener('afterprint', afterPrint);
    };
  }, []);
  const beforePrint = () => {
    setIsPrint(true);
  };
  const afterPrint = () => {
    setIsPrint(false);
  };
  const chunkedColumns = (columns: ColumnType[], size: number) => {
    const columnList = [];
    for (let i = 0; i < columns.length; i += size) {
      const chunk = columns.slice(i, i + size);
      columnList.push(chunk);
    }
    return columnList;
  };
  // 判断 isPrint,如果为 true 则表示开启了打印
  const columns: ColumnType[] | ColumnType[][] = useMemo(() => {
    const width = isPrint ? 200 : 'auto';
    const list = [
      {
        title: 'Full Name',
        width,
        dataIndex: 'name',
        key: 'name',
      },
      {
        title: 'Age',
        width,
        dataIndex: 'age',
        key: 'age',
      },
      {
        title: 'Column 1',
        dataIndex: 'address',
        key: '1',
        width,
      },
      {
        title: 'Column 2',
        dataIndex: 'address',
        key: '2',
        width,
      },
      {
        title: 'Column 3',
        dataIndex: 'address',
        key: '3',
        width,
      },
      {
        title: 'Column 4',
        dataIndex: 'address',
        key: '4',
        width,
      },
      {
        title: 'Column 5',
        dataIndex: 'address',
        key: '5',
        width,
      },
      {
        title: 'Column 6',
        dataIndex: 'address',
        key: '6',
        width,
      },
      {
        title: 'Column 7',
        dataIndex: 'address',
        key: '7',
        width,
      },
      {
        title: 'Column 8',
        dataIndex: 'address',
        key: '8',
        width: 200,
        fixed: 'right',
      },
    ];
    if (isPrint) {
      return chunkedColumns(list, 4);
    }
    return list;
  }, [isPrint]);
  const dataSource: DataType[] = [];
  for (let i = 0; i < 5; i++) {
    dataSource.push({
      key: i,
      name: `Name ${i}`,
      age: 18,
      address: `Column no. ${i}`,
    });
  }
  const is2DArray = (arr: ColumnType[][]) => {
    return Array.isArray(arr) && arr.length > 0 && Array.isArray(arr[0]);
  };
  return (
    <div className={styles.PrintPage}>
      {is2DArray(columns as ColumnType[][]) ? (
        columns.map((i) => {
          return (
            <Table
              columns={i as ColumnsType<DataType>}
              dataSource={dataSource}
              pagination={false}
            />
          );
        })
      ) : (
        <Table
          columns={columns as ColumnsType<DataType>}
          dataSource={dataSource}
          pagination={false}
          scroll={{ x: 1500 }}
        />
      )}
    </div>
  );
};
export default PrintPage;
写完上述代码,怀着必胜的信念,通过 Ctrl + P 开启打印,表格成功拆分,内容完美展现,OK 大功告成。

接着页面中加入打印按钮,通过 window.print() 事件触发打印:
const PrintPage: React.FC = () => {
  // ...
  const onPrint = () => {
    window.print();
  }
  return (
    <div className={styles.Users}>
      {is2DArray(columns as ColumnType[][]) ? (
        columns.map((i) => {
          return (
            <Table
              columns={i as ColumnsType<DataType>}
              dataSource={dataSource}
              pagination={false}
            />
          );
        })
      ) : (
        <Table
          columns={columns as ColumnsType<DataType>}
          dataSource={dataSource}
          pagination={false}
          scroll={{ x: 1500 }}
        />
      )}
      <div className={styles.printAction}>
        <Button type="primary" className={styles.printBtn} onClick={onPrint}>
          打印
        </Button>
      </div>
    </div>
  );
};
export default PrintPage;
点击打印按钮进行打印,发现打印预览中的内容是这样的,并没有在打印时拆分表格:

之所以通过 window.print() 打印未生效,这是因为 window.print 触发时,打印状态未改变,导致表格在打印时未进行拆分。既然这样,那就将 window.print 放到定时器中执行,看是否生效:
const PrintPage: React.FC = () => {
  // ...
  let timer: ReturnType<typeof setTimeout> | null = null;
  const onPrint = () => {
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
    timer = setTimeout(() => {
      window.print();
    });
  };
  return (
    <div className={styles.Users}>
      {is2DArray(columns as ColumnType[][]) ? (
        columns.map((i) => {
          return (
            <Table
              columns={i as ColumnsType<DataType>}
              dataSource={dataSource}
              pagination={false}
            />
          );
        })
      ) : (
        <Table
          columns={columns as ColumnsType<DataType>}
          dataSource={dataSource}
          pagination={false}
          scroll={{ x: 1500 }}
        />
      )}
      <div className={styles.printAction}>
        <Button type="primary" className={styles.printBtn} onClick={onPrint}>
          打印
        </Button>
      </div>
    </div>
  );
};
export default PrintPage;
将 window.print() 放到定时器中,发现还是没有生效。那就只能将改变打印状态的 setIsPrint(true) 放到第 onPrint 函数中执行了,于是最终将代码改成如下形式:
import React, { useMemo, useState, useEffect } from 'react';
import { Button, Table } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import styles from './index.less';
interface DataType {
  key: React.Key;
  name: string;
  age: number;
  address: string;
}
interface ColumnType {
  title: string;
  key: string;
  dataIndex?: string;
  width?: number | string;
  fixed?: string;
  render?: () => void;
}
const PrintPage: React.FC = () => {
  const [isPrint, setIsPrint] = useState(false);
  useEffect(() => {
    window.addEventListener('afterprint', afterPrint);
    return () => {
      window.removeEventListener('afterprint', afterPrint);
    };
  }, []);
  const afterPrint = () => {
    setIsPrint(false);
  };
  const chunkedColumns = (columns: ColumnType[], size: number) => {
    const columnList = [];
    for (let i = 0; i < columns.length; i += size) {
      const chunk = columns.slice(i, i + size);
      columnList.push(chunk);
    }
    return columnList;
  };
  const columns: ColumnType[] | ColumnType[][] = useMemo(() => {
    const width = isPrint ? 200 : 'auto';
    const list = [
      {
        title: 'Full Name',
        width,
        dataIndex: 'name',
        key: 'name',
      },
      {
        title: 'Age',
        width,
        dataIndex: 'age',
        key: 'age',
      },
      {
        title: 'Column 1',
        dataIndex: 'address',
        key: '1',
        width,
      },
      {
        title: 'Column 2',
        dataIndex: 'address',
        key: '2',
        width,
      },
      {
        title: 'Column 3',
        dataIndex: 'address',
        key: '3',
        width,
      },
      {
        title: 'Column 4',
        dataIndex: 'address',
        key: '4',
        width,
      },
      {
        title: 'Column 5',
        dataIndex: 'address',
        key: '5',
        width,
      },
      {
        title: 'Column 6',
        dataIndex: 'address',
        key: '6',
        width,
      },
      {
        title: 'Column 7',
        dataIndex: 'address',
        key: '7',
        width,
      },
      {
        title: 'Column 8',
        dataIndex: 'address',
        key: '8',
        width: 200,
        fixed: 'right',
      },
    ];
    if (isPrint) {
      return chunkedColumns(list, 4);
    }
    return list;
  }, [isPrint]);
  const dataSource: DataType[] = [];
  for (let i = 0; i < 5; i++) {
    dataSource.push({
      key: i,
      name: `Name ${i}`,
      age: 18,
      address: `Column no. ${i}`,
    });
  }
  let timer: ReturnType<typeof setTimeout> | null = null;
  const onPrint = () => {
    setIsPrint(true);
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
    timer = setTimeout(() => {
      window.print();
    });
  };
  return (
    <div className={styles.PrintPage}>
      {isPrint ? (
        columns.map((i, index) => {
          return (
            <Table
              key={index}
              columns={i as ColumnsType<DataType>}
              dataSource={dataSource}
              pagination={false}
            />
          );
        })
      ) : (
        <Table
          columns={columns as ColumnsType<DataType>}
          dataSource={dataSource}
          pagination={false}
          scroll={{ x: 1500 }}
        />
      )}
      <div className={styles.printAction}>
        <Button type="primary" className={styles.printBtn} onClick={onPrint}>
          打印
        </Button>
      </div>
    </div>
  );
};
export default PrintPage;
通过这种实现方式在上述这个 demo 中,看起来想要的效果是实现了,但是我真正的项目中,通过上述方式是无法达到效果的,原因主要有以下几点:
- 
由于项目中的打印按钮是放在一个公共 npm 组件中的,该组件并没有向外暴露打印按钮的事件,因此,无法像上述示例中一样,在
onPrint方法中通过setPrint(true)将打印状态设置为true。 - 
在我真实的项目中,在
beforeprint事件中,通过setPrint(true)更改isPrint打印状态,会导致页面出现卡死,这是因为setPrint(true)之后会导致页面重新渲染,而window.print()方法会阻止页面渲染,从而导致页面出现卡死。但是和上述 demo 一样,直接通过Ctrl + P触发打印,是能成功拆分表格进行打印的,但是通过window.print()就不行,具体原因目前我还没有找到准确的答案,各位如果想知道具体原因,可以自行查证。 

最终实现方案
经过深思熟虑之后,发现之前的实现路存在问题,因为区分是否开启打印,不是只能通过 js 来判断的,还可以通过 css 来区分,想到这思路如同泉涌,便有了最终的实现方案,即:将打印情况下展示的表格和正常展示的表格拆分为两个组件,PrintTable 和 NormalTable。在 indx.tsx 中导入这两个组件,通过 css 的 @media print 来区分是否打印,从而实现打印时和正常展示时表格的切换:
- PrintTable.tsx 内容如下:
 
import React, { useMemo } from 'react';
import { Table } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import styles from './index.less';
interface DataType {
  key: React.Key;
  name: string;
  age: number;
  address: string;
}
interface ColumnType {
  title: string;
  key: string;
  dataIndex?: string;
  width?: number | string;
  fixed?: string;
  render?: () => void;
}
const PrintTable: React.FC<IProps> = () => {
  const chunkedColumns = (columns: ColumnType[], size: number) => {
    const columnList = [];
    for (let i = 0; i < columns.length; i += size) {
      const chunk = columns.slice(i, i + size);
      columnList.push(chunk);
    }
    return columnList;
  };
  const columns: ColumnType[] | ColumnType[][] = useMemo(() => {
    const width = 200;
    const list = [
      {
        title: 'Full Name',
        width,
        dataIndex: 'name',
        key: 'name',
      },
      {
        title: 'Age',
        width,
        dataIndex: 'age',
        key: 'age',
      },
      {
        title: 'Column 1',
        dataIndex: 'address',
        key: '1',
        width,
      },
      {
        title: 'Column 2',
        dataIndex: 'address',
        key: '2',
        width,
      },
      {
        title: 'Column 3',
        dataIndex: 'address',
        key: '3',
        width,
      },
      {
        title: 'Column 4',
        dataIndex: 'address',
        key: '4',
        width,
      },
      {
        title: 'Column 5',
        dataIndex: 'address',
        key: '5',
        width,
      },
      {
        title: 'Column 6',
        dataIndex: 'address',
        key: '6',
        width,
      },
      {
        title: 'Column 7',
        dataIndex: 'address',
        key: '7',
        width,
      },
      {
        title: 'Column 8',
        dataIndex: 'address',
        key: '8',
        width: 200,
        fixed: 'right',
      },
    ];
    return chunkedColumns(list, 4);
  }, []);
  const dataSource: DataType[] = [];
  for (let i = 0; i < 5; i++) {
    dataSource.push({
      key: i,
      name: `Name ${i}`,
      age: 18,
      address: `Column no. ${i}`,
    });
  }
  return (
    <div className={styles.PrintTable}>
      {columns.map((i) => {
        return (
          <Table
            key={i[0].key}
            columns={i as ColumnsType<DataType>}
            dataSource={dataSource}
            pagination={false}
          />
        );
      })}
    </div>
  );
};
export default PrintTable;
- NormalTable.tsx 内容如下:
 
import React from 'react';
import { Table } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import styles from './index.less';
interface DataType {
  key: React.Key;
  name: string;
  age: number;
  address: string;
}
interface ColumnType {
  title: string;
  key: string;
  dataIndex?: string;
  width?: number | string;
  fixed?: string;
  render?: () => void;
}
const NormalTable: React.FC = () => {
  const columns: ColumnType[] | ColumnType[][] = [
    {
      title: 'Full Name',
      dataIndex: 'name',
      key: 'name',
    },
    {
      title: 'Age',
      dataIndex: 'age',
      key: 'age',
    },
    {
      title: 'Column 1',
      dataIndex: 'address',
      key: '1',
    },
    {
      title: 'Column 2',
      dataIndex: 'address',
      key: '2',
    },
    {
      title: 'Column 3',
      dataIndex: 'address',
      key: '3',
    },
    {
      title: 'Column 4',
      dataIndex: 'address',
      key: '4',
    },
    {
      title: 'Column 5',
      dataIndex: 'address',
      key: '5',
    },
    {
      title: 'Column 6',
      dataIndex: 'address',
      key: '6',
    },
    {
      title: 'Column 7',
      dataIndex: 'address',
      key: '7',
    },
    {
      title: 'Column 8',
      dataIndex: 'address',
      key: '8',
      width: 200,
      fixed: 'right',
    },
  ];
  const dataSource: DataType[] = [];
  for (let i = 0; i < 5; i++) {
    dataSource.push({
      key: i,
      name: `Name ${i}`,
      age: 18,
      address: `Column no. ${i}`,
    });
  }
  return (
    <div className={styles.NormalTable}>
      <Table
        columns={columns as ColumnsType<DataType>}
        dataSource={dataSource}
        pagination={false}
        scroll={{ x: 1500 }}
      />
    </div>
  );
};
export default NormalTable;
- index.tsx 内容如下:
 
import React from "react";
import { Button } from "antd";
import PrintTable from "./PrintTable";
import NormalTable from "./NormalTable";
import styles from "./index.less";
const PrintPage: React.FC = () => {
  return (
    <div className={styles.PrintPage}>
      <PrintTable />
      <NormalTable />
      <div className={styles.printAction}>
        <Button
          type="primary"
          className={styles.printBtn}
          onClick={window.print}
        >
          打印
        </Button>
      </div>
    </div>
  );
};
export default PrintPage;
- index.less 内容如下:
 
.PrintPage {
  position: relative;
  width: 100vw;
  padding: 10px;
  overflow-x: hidden;
  background-color: #fff;
  .PrintTable {
    width: 100vw;
    display: none;
  }
  .NormalTable {
    width: 100vw;
    display: block;
  }
  .printAction {
    display: flex;
    justify-content: flex-end;
    margin-top: 20px;
  }
  @media print {
    .printAction {
      display: none;
    }
    .PrintTable {
      display: block;
    }
    .NormalTable {
      display: none;
    }
  }
}
以上就是我对该需求的最终实现方式。如果各位有更好的实现方式,我们可以相互探讨以下。
转载自:https://juejin.cn/post/7350934681830654006