项目实战:解决html2canvas+jsPDF导出pdf分页内容截断问题

该文章已生成可运行项目,

html2canvas + jspdf方案是前端实现页面打印的一种常用方案,但是在实践过程中,遇到的最大问题就是分页截断的问题:当页面元素超过一页A4纸的时候,连续的页面就会因为分页而导致内容被截断,进而影响了pdf的可读性。

由于网上关于分页截断的解决思路比较少,所以特意将此次的解决方案记录下来。

1、使用jsPDF和html2canvas创建简单的PDF文件

首先,我们开始使用 JSPDF 和 html2canvas 生成一个简单的 PDF文件。

1.1 创建一个jsPDF实例

创建一个 JSPDF 实例,设置页面的大小、方向和其他参数。参考官网可以写一个很简单的实例。

var doc = new jsPDF({
  orientation: 'landscape',
  unit: 'in',
  format: [4, 2]
}

doc.text('Hello world!', 1, 1)
doc.save('two-by-four.pdf')

生成一个pdf文件,并且在文件中写入一定内容,其实JSPDF这个库就能做到。

但是很多业务场景下,我们的目标内容会更复杂,而且还要考虑样式,所以最好的方式是引入html2canvas这个库,将页面元素转换成base64数据,然后贴在pdf中(使用addImage方法),这样就能保证页面的内容。

引入了html2canvas库后,我们更多关注是利用现成组件库、框架或者原生html和css实现更复杂的页面内容。

1.2 引入html2canvas

使用 html2canvas 捕捉 HTML 内容或特定的 HTML 元素,并将其转换为 Canvas。其中,html2canvas 函数的主要用法是:

html2canvas(element, options);
  • element: 要渲染为 canvas 的 HTML 元素。这可以是一个 DOM 元素,也可以是一个选择器字符串,表示需要渲染的元素。
  • options(可选): 一个包含配置选项的对象,用于定制 html2canvas 的行为。
    以下是一些常见的配置选项:
    • allowTaint(默认值: false): 是否允许加载跨域的图片,默认为 false。如果设为 true,html2canvas 将尝试加载跨域的图片,但在某些情况下可能会受到浏览器的限
    • backgroundColor(默认值: #ffffff): canvas 的背景颜色。
    • useCORS(默认值: false): 是否使用 CORS(Cross-Origin Resource Sharing)来加载图片。如果设置为 true,则 html2canvas 将尝试使用 CORS 来加载图片。
    • logging(默认值: false): 是否输出日志信息到控制台。
    • width 和 height: canvas 的宽度和高度。如果未指定,则默认为目标元素的宽度和高度。
    • scale(默认值: window.devicePixelRatio): 缩放因子,决定 canvas 的分辨率。

下面是一个简单的demo,可以看到html2canvas能够将dom元素转化为一张base64图片,将鼠标选中元素,可以感受到图片和文字的不同。

<div id="capture" style="padding: 10px; background: #f5da55">
    <h4 style="color: #000; ">Hello world!</h4>
</div>
html2canvas(document.querySelector("#capture")).then(canvas => {
    document.body.appendChild(canvas)
});

在这里插入图片描述

1.3 将html2canvas转化的图片放到pdf中

这一步我们需要使用JSPDF 的addImage方法,其语法如下:

addImage(imageData, format, x, y, width, height, alias, compression)
  • imageData - 要添加的图像数据。可以是图像的 URL、图像的 base64 编码字符串或图像的二进制数据
  • format - 图像的格式。可以是 “JPEG”、“PNG” 或 “TIFF”。
  • x - 图像在 PDF 文档中的 x 坐标。
  • y - 图像在 PDF 文档中的 y 坐标。
  • width - 图像的宽度。
  • height - 图像的高度。
  • alias - 图像的别名。此别名可用于在 PDF 文档中引用图像。
  • compression - 图像的压缩级别。可以是 “NONE”、“FAST” 或 “SLOW”。
import jsPDF from 'jspdf';

export default function addImageUsage() {
  const doc = new jsPDF();
  const imageData = 【替换成base64数据流】;
  doc.addImage(imageData, 'png', 0, 0, 10, 10);
  doc.addImage(imageData, 'png', 100, 100, 10, 10);
  doc.addImage(imageData, 'png', 200, 200, 10, 10);

  drawNet(doc);

  doc.save('test.pdf');
}

const drawNet = (doc) => {
  const gap = 10;
  const start = [0, 0];
  const end = [595.28, 841.89];

  // 所有横线
  for (let i = start[0]; i < end[0]; i = i + gap) {
    doc.line(i, 0, i, end[0]);
  }
  // 所有纵线
  for (let j = start[1]; j < end[1]; j = j + gap) {
    doc.line(0, j, end[1], j);
  }
};

此示例将在 PDF 文档(默认是A4纸大小,宽高为[595.28, 841.89]像素)的 (10, 10) 、(100, 100) 、(200, 200) 坐标处,添加一张png 图像。图像的宽度和高度将分别为 10 和 10 像素,为了了解pdf中的坐标系统,此示例还在pdf文档中生成了间距为10px的网格系统。

在这里插入图片描述

1.4 jsPDF和html2canvas结合起来用

了解了上面的三个关键点,接下来我们将这三个步骤串联起来,实现一个基本的html→pdf的方案。大致步骤如下:

    1. 写一个基本html页面
    1. 创建jspdf实例
    1. 获取页面的dom节点,使用html2canvas将其转化为base64数据流
    1. 将base64数据流装载到jspdf提供的addImage方法中
    1. 保存pdf

基于这5个步骤,可以实现基本的页面打印。

import html2canvas from 'html2canvas';
import jsPDF, { RGBAData } from 'jspdf';

// 将元素转化为canvas元素
// 通过 放大 提高清晰度
// width为内容宽度
async function toCanvas(element: HTMLElement) {
  if (!element) return { width: 0, height: 0 };

  // canvas元素
  const canvas = await html2canvas(element, {
    scale: window.devicePixelRatio * 2, // 增加清晰度
    useCORS: true // 允许跨域
  });

  // 获取canvas转化后的宽高
  const { width: canvasWidth, height: canvasHeight } = canvas;

  // 转化成图片Data
  const canvasData = canvas.toDataURL('image/jpeg', 1.0);

  return { width: canvasWidth, height: canvasHeight, data: canvasData };
}

/**
 * 生成pdf(A4多页pdf截断问题, 包括页眉、页脚 和 上下左右留空的护理)
 */
export async function generatePDF({
  /** pdf内容的dom元素 */
  element,

  /** pdf文件名 */
  filename
}) {
  if (!(element instanceof HTMLElement)) {
    return;
  }

  const pdf = new jsPDF();

  // 一页的高度, 转换宽度为一页元素的宽度
  const {
    width: imageWidth,
    height: imageHeight,
    data
  } = await toCanvas(element);

  // 添加图片
  function addImage(
    _x: number,
    _y: number,
    pdfInstance: jsPDF,
    base_data:
    | string
    | HTMLImageElement
    | HTMLCanvasElement
    | Uint8Array
    | RGBAData,
    _width: number,
    _height: number
  ) {
    pdfInstance.addImage(base_data, 'JPEG', _x, _y, _width, _height);
  }

  addImage(0, 0, pdf, data!, imageWidth, imageHeight);

  return pdf.save(filename);
}

2、多页:比例缩放+循环移位

通常,在我们的实践中,会发现2个问题:

  • 生成的pdf内容与实际的页面元素比例不一致
  • 页面内容超出一页pdf的高度,但是生成的pdf只有一页,没有展示全部的页面信息

这两个问题的解决方案是等比例缩放+循环移位:

2.1 等比例缩放

通过比例缩放,实现页面内容等比例展示在pdf文档中。

令页面元素的宽高为x, y(转化成canvas图片的宽高),pdf文档的宽高为w, h。因为高度可以通过加页延伸,所以可以按照宽度进行缩放,缩放后的图片高度可以通过下列公式计算:

y_scaled=(w/x)\*yy\_{scaled} = (w / x) \* yy_scaled=(w/x)\*y

2.2 循环移位

如果页面的高度超出了pdf文档的高度,即y > h,使用addPage方法添加一页即可。但是在新的一页中,我们的图片内容的高度需要调整。

假设y = 2 * h,这意味我们需要两页才能完整得展示页面内容。在一页pdf中,图片在起始位置插入即可,即:

PDF.addImage(pageData, 'JPEG', 0, 0, x, y)// 注意x,y 是缩放后的大小

在第二页pdf中,图片的纵向位置需要调整一页pdf的高度,即:

PDF.addImage(pageData, 'JPEG', 0, -h, x, y)// 注意x,y 是缩放后的大小

通过循环计算剩余高度,然后不停调整纵向位置移动base64的图片位置,可以解决多页的问题。

3、分页截断的挑战

尽管 JSPDF 和 html2canvas 是功能强大的工具,但是他们也有很多槽点,比如得手动分页,手动处理分页截断的问题。等你实践到这一步,就开始面临分页截断的问题,类似的问题也有网友在Github上提出,但是底下依然没有很好的解决思路。

处理分页截断的原理就是在使用addImage之前,将html进行分页,通过维护一个高度位置数据,来记录每次循环迭代addImage的位置。

从高到低遍历维护一个分页数组pages,该数组记录每一页的起始位置,如:pages[0] 对应 第一页起始位置,pages[1] 对应第二页起始位置。

在这里插入图片描述
接下来我们重点讨论如何将页面进行切割,然后生成pages这个数组。

假设页面的高度是1500,pdf宽高是[500, 900],如果不用处理分页截断的问题,我们可以想到第一页(0-900)是用来承载页面从高度为0到900的信息;

第二页(900-1800)是用来承载页面从高度900到1500的,所以pages数组为[0, 900]。

如果要处理分页截断呢,这时候就需要计算页面元素的距离pdf文档起始位置的高度h1,以及该元素的内部高度h2,通过这两个高度来判断这个元素要不要放在下一页,防止截断,示意图如下:

在这里插入图片描述

如果h1 + h2 > 页面高度, 这时候说明这个元素不处理的就会被分页截断,所以应该要把这个元素放到第二页去渲染,这就意味着pages记录的数据要变化,示意图如下,可以看到pages[1]我们往上调整了,比第二页pdf的起始位置更高。

在这里插入图片描述
说明渲染第二页pdf的时候,要从h1开始渲染,pages数组为[0, h1],解释为第一页pdf渲染页面高度区域为0-900, 第二页pdf渲染html高度区域为h1-1500。注意到第一页渲染的时候到尾部的时候,会有部分内容和第二页头部内容重合。 因为h1到900这部分的内容肯定会渲染,这部分内容一直都是页面元素,我们改变pages[1]的值的原因只是创建一个副本,让页面看起来内容没有被截断。

为了解决这个问题(为了美观),我们用填充一块白色区域遮掉它!此处使用jspdf的rect和setFillColor方法,把重合的区域遮白处理。

pdf.setFillColor(255, 255, 255);
pdf.rect(x, y, Math.ceil(_width), Math.ceil(_height), 'F');

上面我们谈到了h1和h2,其中h1是元素盒子的上边距到打印区域的高度(比例缩放后的高度),h2是元素盒子的内部高度。

计算h1: getBoundingClientRect方法

const rect = contentElement.getBoundingClientRect() || {};
const topDistance = rect.top;
return topDistance;

在这里插入图片描述
计算h2: offsetHeight方法
在这里插入图片描述
值得注意的是,因为打印区域的html元素不一定是从窗口顶部开始,所以为了计算实际的h1(元素到打印区域的顶部距离),可以采用这样的方法:

  • 用getBoundingClientRect方法计算元素到窗口顶部的距离
  • 循环打印之前将pages信息针对第一个元素进行一个高度校准。
// 对pages进行一个值的修正,因为pages生成是根据根元素来的,根元素并不是我们实际要打印的元素,而是element,
// 所以要把它修正,让其值是以真实的打印元素顶部节点为准
const newPages = pages.map((item) => item - pages[0]);

4、核心代码

import html2canvas from 'html2canvas';
import jsPDF, { RGBAData } from 'jspdf';

/** a4纸的尺寸[595.28,841.89], 单位毫米 */
const [PAGE_WIDTH, PAGE_HEIGHT] = [595.28, 841.89];

const PAPER_CONFIG = {
  /** 竖向 */
  portrait: {
    height: PAGE_HEIGHT,
    width: PAGE_WIDTH,
    contentWidth: 560
  },
  /** 横向 */
  landscape: {
    height: PAGE_WIDTH,
    width: PAGE_HEIGHT,
    contentWidth: 800
  }
};

// 将元素转化为canvas元素
// 通过 放大 提高清晰度
// width为内容宽度
async function toCanvas(element: HTMLElement, width: number) {
  if (!element) return { width, height: 0 };

  // canvas元素
  const canvas = await html2canvas(element, {
    // allowTaint: true, // 允许渲染跨域图片
    scale: window.devicePixelRatio * 2, // 增加清晰度
    useCORS: true // 允许跨域
  });

  // 获取canvas转化后的宽高
  const { width: canvasWidth, height: canvasHeight } = canvas;

  // html页面生成的canvas在pdf中的高度
  const height = (width / canvasWidth) * canvasHeight;

  // 转化成图片Data
  const canvasData = canvas.toDataURL('image/jpeg', 1.0);

  return { width, height, data: canvasData };
}

/**
 * 生成pdf(A4多页pdf截断问题, 包括页眉、页脚 和 上下左右留空的护理)
 * @param param0
 * @returns
 */
export async function outputPDF({
  /** pdf内容的dom元素 */
  element,

  /** 页脚dom元素 */
  footer,

  /** 页眉dom元素 */
  header,

  /** pdf文件名 */
  filename,

  /** a4值的方向: portrait or landscape */
  orientation = 'portrait' as 'portrait' | 'landscape'
}) {
  if (!(element instanceof HTMLElement)) {
    return;
  }

  if (!['portrait', 'landscape'].includes(orientation)) {
    return Promise.reject(
      new Error(
        `Invalid Parameters: the parameter {orientation} is assigned wrong value, you can only assign it with {portrait} or {landscape}`
      )
    );
  }
  const [A4_WIDTH, A4_HEIGHT] = [
    PAPER_CONFIG[orientation].width,
    PAPER_CONFIG[orientation].height
  ];

  /** 一页pdf的内容宽度, 左右预设留白 */
  const { contentWidth } = PAPER_CONFIG[orientation];

  // eslint-disable-next-line new-cap
  const pdf = new jsPDF({
    unit: 'pt',
    format: 'a4',
    orientation
  });

  // 一页的高度, 转换宽度为一页元素的宽度
  const { width, height, data } = await toCanvas(element, contentWidth);

  // 添加
  function addImage(
    _x: number,
    _y: number,
    pdfInstance: jsPDF,
    base_data:
      | string
      | HTMLImageElement
      | HTMLCanvasElement
      | Uint8Array
      | RGBAData,
    _width: number,
    _height: number
  ) {
    pdfInstance.addImage(base_data, 'JPEG', _x, _y, _width, _height);
  }

  // 增加空白遮挡
  function addBlank(x: number, y: number, _width: number, _height: number) {
    pdf.setFillColor(255, 255, 255);
    pdf.rect(x, y, Math.ceil(_width), Math.ceil(_height), 'F');
  }

  // 页脚元素 经过转换后在PDF页面的高度
  const { height: tFooterHeight, data: headerData } = footer
    ? await toCanvas(footer, contentWidth)
    : { height: 0, data: undefined };

  // 页眉元素 经过转换后在PDF的高度
  const { height: tHeaderHeight, data: footerData } = header
    ? await toCanvas(header, contentWidth)
    : { height: 0, data: undefined };

  // 添加页脚
  async function addHeader(headerElement: HTMLElement) {
    headerData &&
      pdf.addImage(headerData, 'JPEG', 0, 0, contentWidth, tHeaderHeight);
  }

  // 添加页眉
  async function addFooter(
    pageNum: number,
    now: number,
    footerElement: HTMLElement
  ) {
    if (footerData) {
      pdf.addImage(
        footerData,
        'JPEG',
        0,
        A4_HEIGHT - tFooterHeight,
        contentWidth,
        tFooterHeight
      );
    }
  }

  // 距离PDF左边的距离,/ 2 表示居中
  const baseX = (A4_WIDTH - contentWidth) / 2; // 预留空间给左边
  // 距离PDF 页眉和页脚的间距, 留白留空
  const baseY = 15;

  // 除去页头、页眉、还有内容与两者之间的间距后 每页内容的实际高度
  const originalPageHeight =
    A4_HEIGHT - tFooterHeight - tHeaderHeight - 2 * baseY;

  // 元素在网页页面的宽度
  const elementWidth = element.offsetWidth;

  // PDF内容宽度 和 在HTML中宽度 的比, 用于将 元素在网页的高度 转化为 PDF内容内的高度, 将 元素距离网页顶部的高度  转化为 距离Canvas顶部的高度
  const rate = contentWidth / elementWidth;

  // 每一页的分页坐标, PDF高度, 初始值为根元素距离顶部的距离
  const pages = [rate * getElementTop(element)];

  // 获取该元素到页面顶部的高度(注意滑动scroll会影响高度)
  function getElementTop(contentElement) {
    if (contentElement.getBoundingClientRect) {
      const rect = contentElement.getBoundingClientRect() || {};
      const topDistance = rect.top;

      return topDistance;
    }
  }

  // 遍历正常的元素节点
  function traversingNodes(nodes) {
    for (const element of nodes) {
      const one = element;

      /** */
      /** 注意: 可以根据业务需求,判断其他场景的分页,本代码只判断表格的分页场景 */
      /** */

      // table的每一行元素也是深度终点
      const isTableRow =
        one.classList && one.classList.contains('ant4-table-row');

      // 对需要处理分页的元素,计算是否跨界,若跨界,则直接将顶部位置作为分页位置,进行分页,且子元素不需要再进行判断
      const { offsetHeight } = one;
      // 计算出最终高度
      const offsetTop = getElementTop(one);

      // dom转换后距离顶部的高度
      // 转换成canvas高度
      const top = rate * offsetTop;
      const rateOffsetHeight = rate * offsetHeight;

      // 对于深度终点元素进行处理
      if (isTableRow) {
        // dom高度转换成生成pdf的实际高度
        // 代码不考虑dom定位、边距、边框等因素,需在dom里自行考虑,如将box-sizing设置为border-box
        updateTablePos(rateOffsetHeight, top);
      }
      // 对于普通元素,则判断是否高度超过分页值,并且深入
      else {
        // 执行位置更新操作
        updateNormalElPos(top);
        // 遍历子节点
        traversingNodes(one.childNodes);
      }
      updatePos();
    }
  }

  // 普通元素更新位置的方法
  // 普通元素只需要考虑到是否到达了分页点,即当前距离顶部高度 - 上一个分页点的高度 大于 正常一页的高度,则需要载入分页点
  function updateNormalElPos(top) {
    if (
      top - (pages.length > 0 ? pages[pages.length - 1] : 0) >=
      originalPageHeight
    ) {
      pages.push(
        (pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight
      );
    }
  }

  // 可能跨页元素位置更新的方法
  // 需要考虑分页元素,则需要考虑两种情况
  // 1. 普通达顶情况,如上
  // 2. 当前距离顶部高度加上元素自身高度 大于 整页高度,则需要载入一个分页点
  function updateTablePos(eHeight: number, top: number) {
    // 如果高度已经超过当前页,则证明可以分页了
    if (
      top - (pages.length > 0 ? pages[pages.length - 1] : 0) >=
      originalPageHeight
    ) {
      pages.push(
        (pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight
      );
    }
    // 若 距离当前页顶部的高度 加上元素自身的高度 大于 一页内容的高度, 则证明元素跨页,将当前高度作为分页位置
    else if (
      top + eHeight - (pages.length > 0 ? pages[pages.length - 1] : 0) >
        originalPageHeight &&
      top !== (pages.length > 0 ? pages[pages.length - 1] : 0)
    ) {
      pages.push(top);
    }
  }

  // 深度遍历节点的方法
  traversingNodes(element.childNodes);

  function updatePos() {
    while (pages[pages.length - 1] + originalPageHeight < height) {
      pages.push(pages[pages.length - 1] + originalPageHeight);
    }
  }

  // 对pages进行一个值的修正,因为pages生成是根据根元素来的,根元素并不是我们实际要打印的元素,而是element,
  // 所以要把它修正,让其值是以真实的打印元素顶部节点为准
  const newPages = pages.map((item) => item - pages[0]);

  // 根据分页位置 开始分页
  for (let i = 0; i < newPages.length; ++i) {
    // 根据分页位置新增图片
    addImage(
      baseX,
      baseY + tHeaderHeight - newPages[i],
      pdf,
      data!,
      width,
      height
    );
    // 将 内容 与 页眉之间留空留白的部分进行遮白处理
    addBlank(0, tHeaderHeight, A4_WIDTH, baseY);
    // 将 内容 与 页脚之间留空留白的部分进行遮白处理
    addBlank(0, A4_HEIGHT - baseY - tFooterHeight, A4_WIDTH, baseY);
    // 对于除最后一页外,对 内容 的多余部分进行遮白处理
    if (i < newPages.length - 1) {
      // 获取当前页面需要的内容部分高度
      const imageHeight = newPages[i + 1] - newPages[i];
      // 对多余的内容部分进行遮白
      addBlank(
        0,
        baseY + imageHeight + tHeaderHeight,
        A4_WIDTH,
        A4_HEIGHT - imageHeight
      );
    }

    // 添加页眉
    if (header) {
      await addHeader(header);
    }

    // 添加页脚
    if (footer) {
      await addFooter(newPages.length, i + 1, footer);
    }

    // 若不是最后一页,则分页
    if (i !== newPages.length - 1) {
      // 增加分页
      pdf.addPage();
    }
  }
  return pdf.save(filename);
}

5、代码地址

https://gitee.com/wlmProject/pdf-pagination-truncation-demo

本文章已经生成可运行项目
在 Vue 中使用 `html2canvas` 和 `jspdf` 生成 PDF 时,可能会遇到内容分页截断问题。这是因为 `html2canvas` 是将 HTML 元素渲染为 Canvas 图像,而 `jspdf` 则是将图像插入到 PDF 文档中。如果页面内容较长,PDF 会将图像截断并延续到下一页,而不是自动处理 HTML 内容分页逻辑[^1]。 解决问题的一种常见方法是手动控制分页,具体思路是: - 将 HTML 内容拆分为多个部分(例如按高度分割),每个部分分别渲染为独立的 Canvas 图像。 - 使用 `jspdf.jsPDF()` 创建新的 PDF 页面,并逐页添加对应的图像。 - 调整 `html2canvas` 的 `scrollY` 或 `windowHeight` 参数,以确保滚动内容可以正确截图。 以下是一个基于 Vue 的解决方案示例代码: ```javascript import html2canvas from 'html2canvas'; import jsPDF from 'jspdf'; export default { methods: { generatePDF() { const element = document.getElementById('content-to-export'); const pageHeight = 842; // A4 纸张高度(单位:px) const padding = 20; const scale = 2; html2canvas(element, { scale, useCORS: true, scrollY: -window.scrollY, windowHeight: document.body.scrollHeight, }).then(canvas => { const imgData = canvas.toDataURL('image/png'); const imgWidth = 595; // A4 宽度(单位:px) const imgHeight = (canvas.height * imgWidth) / canvas.width; let heightLeft = imgHeight; const doc = new jsPDF('p', 'px', 'a4'); let position = 0; while (heightLeft > 0) { doc.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); heightLeft -= pageHeight; position -= pageHeight; if (heightLeft > 0) { doc.addPage(); } } doc.save('document.pdf'); }); }, }, }; ``` 此外,也可以考虑使用专门针对长文档导出优化的库,例如 `html2pdf.js`,它内部已经整合了 `html2canvas` 和 `jsPDF`,并且支持更高级的配置,如自动分页PDF 格式设置等[^2]。 如果仍然希望使用 `vue-html2pdf` 这样的封装组件来简化开发流程,则可以直接利用其内置的 `paginate-elements-by-height` 配置项实现一定程度上的自动分页功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

太阳与星辰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值