EasyExcel - 行合并策略(二级列表)

😼前言:博主在工作中又遇到了新的excel导出挑战:需要导出多条文章及其下联合作者的信息,简单的来说是一个二级列表的数据结构。
🕵️‍♂️思路:excel导出实际上是一行一行的记录,再根据条件对其进行合并。

目录

  • 最终效果图📌
  • 一、数据格式及处理📚
    • 1.文章对象(处理前)
    • 2.文章及作者对象(处理后)
    • 3.未合并的效果图
  • 二、通用行合并策略🔍
    • 1.源码学习
    • 2.通用行合并后的效果图
  • 三、二级通用行合并策略✍
    • 1.源码改造
    • 2.设置excel输出策略
    • 3.延伸
  • 四、问题☔
    • 1.问题描述
    • 2.问题原因
    • 3.解决办法
  • 参考文章📒

最终效果图📌

最终导出格式

一、数据格式及处理📚

首先,需要先将一条文章按联合作者数量,拆分为指定数量的文章及作者导出记录的集合。

以文章《牧区歌与马》为例,一篇文章有三名联合作者,生成三条导出记录。

1.文章对象(处理前)

// 文章记录对象 Acticle.class
[{
	"contentId": "1",
	"contentTitle": "牧区歌与马",
	"contentCount": 940,
	"releaseTime": "2025-01-09 11:21:16",
	"readNum": 1,
	"auditor": "小李",
	"orgName": "办公室",
	"authorList": [{
			"userName": "小A ",
			"orgName": "单位A"
		},
		{
			"userName": "小B ",
			"orgName": "/"
		},
		{
			"userName": "小C",
			"orgName": "单位C"
		}
	]
}]

2.文章及作者对象(处理后)

可以看到记录由一条变为三条,除了作者名称和单位,其余字段内容均一致。

// 文章处理后记录对象 ActicleAuthor.class
[{
	"contentId": "1",
	"contentTitle": "牧区歌与马",
	"contentCount": 940,
	"releaseTime": "2025-01-09 11:21:16",
	"readNum": 1,
	"auditor": "小李",
	"orgName": "办公室",
    "author":"小A""authorUnit":"单位A"
},{
	"contentId": "1",
	"contentTitle": "牧区歌与马",
	"contentCount": 940,
	"releaseTime": "2025-01-09 11:21:16",
	"readNum": 1,
	"auditor": "小李",
	"orgName": "办公室",
    "author":"小B""authorUnit":"/"
},{
	"contentId": "1",
	"contentTitle": "牧区歌与马",
	"contentCount": 940,
	"releaseTime": "2025-01-09 11:21:16",
	"readNum": 1,
	"auditor": "小李",
	"orgName": "办公室",
    "author":"小C""authorUnit":"单位C"
}]

3.未合并的效果图

未设置行合并策略直接导出时,表格的格式内容如下:
未合并前的导出格式
👆图中的E、F列示例错误,应分别为6行记录

二、通用行合并策略🔍

此处学习了csdn博主xiao谢同学分享的通用行合并策略源码
链接:EasyExcel 通用行合并策略实现

1.源码学习

🐱‍👓该策略以列的行数作为主键,每次遍历记录列的最新合并区域信息。将同列且相邻行的单元格内容进行比较:
(1)一致:则仅更新endRow和endCell,继续遍历;
(2)不一致:则将已有区域进行合并,再将MergeRange所有字段进行更新。

MergeRange.class

  • startRow :合并开始行
  • endRow:合并结束行
  • startCell:合并开始单元格
  • endCell:合并结束单元格
  • lastValue:列最新单元格内容
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import org.apache.commons.collections.map.HashedMap;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;

import java.util.*;


public class MergeStrategy extends AbstractMergeStrategy {

    // 合并的列编号,从0开始,指定的index或自己按字段顺序数
    private Set<Integer> mergeCellIndex = new HashSet<>();

    // 数据集大小,用于区别结束行位置
    private Integer maxRow = 0;

    // 禁止无参声明
    private MergeStrategy() {
    }

    public MergeStrategy(Integer maxRow, int... mergeCellIndex) {
        Arrays.stream(mergeCellIndex).forEach(item -> {
            this.mergeCellIndex.add(item);
        });
        this.maxRow = maxRow;
    }

    // 记录上一次合并的信息
    private Map<Integer, MergeRange> lastRow = new HashedMap();

    // 每行每列都会进入,绝对不要在这写循环
    @Override
    protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
        int currentCellIndex = cell.getColumnIndex();
        // 判断该列是否需要合并
        if (mergeCellIndex.contains(currentCellIndex)) {
            String currentCellValue = cell.getStringCellValue();
            int currentRowIndex = cell.getRowIndex();
            if (!lastRow.containsKey(currentCellIndex)) {
                // 记录首行起始位置
                lastRow.put(currentCellIndex, new MergeRange(currentCellValue, currentRowIndex, currentRowIndex, currentCellIndex, currentCellIndex));
                return;
            }
            //有上行这列的值了,拿来对比.
            MergeRange mergeRange = lastRow.get(currentCellIndex);
            if (!(mergeRange.lastValue != null && mergeRange.lastValue.equals(currentCellValue))) {
                // 结束的位置触发下合并.
                // 同行同列不能合并,会抛异常
                if (mergeRange.startRow != mergeRange.endRow || mergeRange.startCell != mergeRange.endCell) {
                    sheet.addMergedRegionUnsafe(new CellRangeAddress(mergeRange.startRow, mergeRange.endRow, mergeRange.startCell, mergeRange.endCell));
                }
                // 更新当前列起始位置
                lastRow.put(currentCellIndex, new MergeRange(currentCellValue, currentRowIndex, currentRowIndex, currentCellIndex, currentCellIndex));
            }
            // 合并行 + 1
            mergeRange.endRow += 1;
            // 结束的位置触发下最后一次没完成的合并
            if (relativeRowIndex.equals(maxRow - 1)) {
                MergeRange lastMergeRange = lastRow.get(currentCellIndex);
                // 同行同列不能合并,会抛异常
                if (lastMergeRange.startRow != lastMergeRange.endRow || lastMergeRange.startCell != lastMergeRange.endCell) {
                    sheet.addMergedRegionUnsafe(new CellRangeAddress(lastMergeRange.startRow, lastMergeRange.endRow, lastMergeRange.startCell, lastMergeRange.endCell));
                }
            }
        }
    }
}

class MergeRange {
    public int startRow;
    public int endRow;
    public int startCell;
    public int endCell;
    public String lastValue;

    public MergeRange(String lastValue, int startRow, int endRow, int startCell, int endCell) {
        this.startRow = startRow;
        this.endRow = endRow;
        this.startCell = startCell;
        this.endCell = endCell;
        this.lastValue = lastValue;
    }
}


2.通用行合并后的效果图

可以发现,这种仅根据相邻行单元格内容进行合并的方式,还未能完全满足博主想要二级列表的效果。
简单行合并的导出格式
👆黄色代表非理想合并的区域

三、二级通用行合并策略✍

🐱‍💻改造思路:列A是文章标题,以列A的内容作为第一层级的标识(tip:不如contentId准确)。即使相邻行单元格内容相等,对应行的A列内容不相等也不能合并。

处理步骤:
(1)在合并区域对象类MergeRange中,增加A列内容的值字段lastValueRowa
(2)遍历单元格构造合并区域对象时,记录A列内容值以此来作为附加的合并条件。
(3)当同一列字段相邻行内容相等且A列内容值相等时,再进行合并。

1.源码改造

import cn.hutool.json.JSONUtil;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.map.HashedMap;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;

import java.util.*;

/**
 * 行合并策略
 */
@Slf4j
public class MergeStrategy extends AbstractMergeStrategy {

    // 合并的列编号,从0开始,指定的index或自己按字段顺序数
    private Set<Integer> mergeCellIndex = new HashSet<>();

    // 数据集大小,用于区别结束行位置
    private Integer maxRow = 0;

    // 禁止无参声明
    private MergeStrategy() {
    }

    public MergeStrategy(Integer maxRow, int... mergeCellIndex) {
        Arrays.stream(mergeCellIndex).forEach(item -> {
            this.mergeCellIndex.add(item);
        });
        this.maxRow = maxRow;
    }

    // 记录上一次合并的信息
    private Map<Integer, MergeRange> lastRow = new HashedMap();

    // 每行每列都会进入,绝对不要在这写循环
    @Override
    protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
        // 获取单元格当前列
        int currentCellIndex = cell.getColumnIndex();

        log.info("遍历单元格:{}行,{}列 >>>>>>>>>>>>>>>>>", cell.getRowIndex(), currentCellIndex);
        // 判断该列是否需要合并
        if (mergeCellIndex.contains(currentCellIndex)) {
            // 获取当前单元格内容值
            String currentCellValue = new DataFormatter().formatCellValue(cell);
            // 获取当前单元格的行row
            int currentRowIndex = cell.getRowIndex();
            // 获取当前行A列单元格内容 new
            String currentValueRowa = "";
            // 如果最后合并行的map,不包括当前列
            if (!lastRow.containsKey(currentCellIndex)) {
                log.info("lastRow添加第{}列【前】,当前lastRow={}", currentCellIndex, JSONUtil.toJsonStr(lastRow));
                // 获取当前行的列A内容 new
                if (currentCellIndex == 0) {
                    currentValueRowa = currentCellValue;
                } else {
                    currentValueRowa = getRowaValue();
                }

                // 记录首行起始位置? 记录当前列及合并范围
                lastRow.put(currentCellIndex, new MergeRange(currentCellValue, currentRowIndex, currentRowIndex, currentCellIndex, currentCellIndex, currentValueRowa));
                log.info("lastRow添加第{}列【后】,当前行列A内容={},新lastRow={}", currentCellIndex, currentValueRowa, JSONUtil.toJsonStr(lastRow));
                return;
            } else {
            	// 该列已存在lastRow中,则取最新的列A内容值 new
                currentValueRowa = getRowaValue();
            }

            //有上行这列的值了,拿来对比.
            MergeRange mergeRange = lastRow.get(currentCellIndex);
            // 判断条件:增加A列内容判断 new
            log.info("合并比对1>>>>>>>>>:第{}列最新内容lastValue = {},当前内容currentCellValue={},"
                    , currentCellIndex, mergeRange.lastValue, currentCellValue);
            log.info("合并比对2>>>>>>>>>第{}列最新列A内容lastValueRowa={}, 当前列A内容currentValueRowa={}"
                    , currentCellIndex, mergeRange.lastValueRowa, currentValueRowa);
            if (!(mergeRange.lastValue != null
                    && mergeRange.lastValue.equals(currentCellValue) && mergeRange.lastValueRowa.equals(currentValueRowa))) {
                // 结束的位置触发下合并.
                // 同行同列不能合并,会抛异常
                if (mergeRange.startRow != mergeRange.endRow || mergeRange.startCell != mergeRange.endCell) {
                    sheet.addMergedRegionUnsafe(new CellRangeAddress(mergeRange.startRow, mergeRange.endRow, mergeRange.startCell, mergeRange.endCell));
                }
                // 更新当前列起始位置
                lastRow.put(currentCellIndex, new MergeRange(currentCellValue, currentRowIndex, currentRowIndex, currentCellIndex, currentCellIndex, currentValueRowa));
                log.info("比对不一致,确认合并!!>>>>>>>>>:第{}列最新列A内容={},最新lastRow = {}"
                        , currentCellIndex, currentValueRowa, JSONUtil.toJsonStr(lastRow));
            }
            // 合并行 + 1
            mergeRange.endRow += 1;
            // 结束的位置触发下最后一次没完成的合并
            if (relativeRowIndex.equals(maxRow - 1)) {
                MergeRange lastMergeRange = lastRow.get(currentCellIndex);
                // 同行同列不能合并,会抛异常
                if (lastMergeRange.startRow != lastMergeRange.endRow || lastMergeRange.startCell != lastMergeRange.endCell) {
                    sheet.addMergedRegionUnsafe(new CellRangeAddress(lastMergeRange.startRow, lastMergeRange.endRow, lastMergeRange.startCell, lastMergeRange.endCell));
                }
            }
        }
    }

	/**
	* 获取列A最新一行的内容值 new
	*/
    private String getRowaValue() {
        // 获取当前行A列单元格内容
        String currentValueRowa = "";
        if (lastRow.get(0) != null) {
            currentValueRowa = lastRow.get(0).lastValue;
        }
        return currentValueRowa;
    }
}

class MergeRange {
    public int startRow;
    public int endRow;
    public int startCell;
    public int endCell;
    public String lastValue;
    // 最后一个合并值得A列值 new
    public String lastValueRowa;

    public MergeRange(String lastValue, int startRow, int endRow, int startCell, int endCell, String lastValueRowa) {
        this.startRow = startRow;
        this.endRow = endRow;
        this.startCell = startCell;
        this.endCell = endCell;
        this.lastValue = lastValue;
        this.lastValueRowa = lastValueRowa;
    }
}

2.设置excel输出策略

  // 设置excel输出策略
 EasyExcel.write(fileName, ActicleAuthor.class)
 			// 0,1 表示 对1,2列启用合并策略
          .registerWriteHandler(new MergeStrategy(dataList.size(),0,1)) 
          .sheet(0)
          .doWrite(dataList);

3.延伸

🐱‍🚀如果需要做三级、四级等列表,可以将指定多个字段的拼接值当作列A来处理。可修改getRowaValue()方法实现逻辑。

四、问题☔

在开发的过程中,不可避免地碰到了一些问题……

1.问题描述

💁‍♀️在获取列A内容值时,曾尝试从Sheet对象中获取。
因为存在Sheet.getRow(0)获取第一行的row对象是null的问题,所以用从lastRow中获取列A内容的方法进行替代。

/**
	* 从sheet.getRow(0)中获取列A内容值
	*/
    private String getRowaValue(Sheet sheet, int rowId) {
        // 获取当前行A列单元格内容
        String currentValueRowa = "";
        Row row = sheet.getRow(rowId);
        if (row != null) {
            Cell cell = row.getCell(0);
            if (cell != null) {
                currentValueRowa = cell.getStringCellValue();
            }
        }
        return currentValueRowa;
    }

2.问题原因

👩‍💻经过面向百度查询,从csdn博主吾乃南华老仙分享的文章sheet.getRow(0)获取的row为null?中得知:

new SXSSFWorkbook(new XSSFWorkbook(inputStream)) 创建Workbook的时候,
SXSSFWorkbook对象内部会维护一个HashMap(反编译后的名称为_xFromSxHash)。

而当使用workBook.getSheetAt(0)的时候,其实是从_xFromSxHash中获取新创建的Sheet对象,从而导致sheet.getRow(0)获取的row为null。

😸文中提供的解决方法:

将获取首行代码:
Workbook workBook = new SXSSFWorkbook(new XSSFWorkbook(inputStream));
Sheet sheet = workBook.getSheetAt(0);
Row row = sheet.getRow(0);

修改为👇:

Workbook workBook = new SXSSFWorkbook(new XSSFWorkbook(inputStream));
Sheet sheet;
if (workBook instanceof SXSSFWorkbook) {
   SXSSFWorkbook sxssfWorkbook = (SXSSFWorkbook) workBook;
   sheet = sxssfWorkbook.getXSSFWorkbook().getSheetAt(sheetIndex);
} else {
   sheet = workBook.getSheetAt(sheetIndex);
}
Row row = sheet.getRow(0);

😧而我们使用的导出是基于EasyExcel的,并没有单独的使用流去创建对象,应该怎么办呢?

👇可以看到debug过程中,显示sheet对象类型是SXSSFSheet。
在这里插入图片描述

3.解决办法

😾再次经过一番查询,在EasyExcel语雀文档的QA:EasyExcel 我想在导出excel文件的时候添加水印,要怎么做,请给出代码示例和解释中找到了解释:

  • inMemory(true):
    EasyExcel默认使用SXSSFWorkbook以减少内存消耗,但它不支持复杂的样式设置(如水印)。通过设置inMemory(true),我们改用XSSFWorkbook,它提供了更全面的样式支持。

在设置excel输出策略时加上inMemory(true)设置:

  // 设置excel输出策略
 EasyExcel.write(fileName, ActicleAuthor.class)
 		 // 必须设置,以便使用XSSFWorkbook而非SXSSFWorkbook new
           .inMemory(true)
 		 // 0,1 表示 对1,2列启用合并策略
          .registerWriteHandler(new MergeStrategy(dataList.size(),0,1)) 
          .sheet(0)
          .doWrite(dataList);

🙆‍♀️执行debug后,发现可以获取到sheet.getRow(0)的第一行对象了,切换类型的问题成功解决。
在这里插入图片描述

参考文章📒

EasyExcel 通用行合并策略实现-xiao谢同学
sheet.getRow(0)获取的row为null?
EasyExcel 我想在导出excel文件的时候添加水印,要怎么做,请给出代码示例和解释

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/955030.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

leetcode:205. 同构字符串(python3解法)

难度&#xff1a;简单 给定两个字符串 s 和 t &#xff0c;判断它们是否是同构的。 如果 s 中的字符可以按某种映射关系替换得到 t &#xff0c;那么这两个字符串是同构的。 每个出现的字符都应当映射到另一个字符&#xff0c;同时不改变字符的顺序。不同字符不能映射到同一个字…

【Linux】sed编辑器二

一、处理多行命令 sed编辑器有3种可用于处理多行文本的特殊命令。 N&#xff1a;加入数据流中的下一行&#xff0c;创建一个多行组进行处理&#xff1b;D&#xff1a;删除多行组中的一行&#xff1b;P&#xff1a;打印多行组中的一行。 1、next命令&#xff1a;N 单行next命…

从AI生成内容到虚拟现实:娱乐体验的新边界

引言 在快速发展的科技时代&#xff0c;娱乐行业正经历一场前所未有的变革。传统的娱乐方式正与先进技术融合&#xff0c;创造出全新的沉浸式体验。从AI生成的个性化内容&#xff0c;到虚拟现实带来的身临其境的互动场景&#xff0c;科技不仅改变了我们消费娱乐的方式&#xf…

下载文件,浏览器阻止不安全下载

背景&#xff1a; 在项目开发中&#xff0c;遇到需要下载文件的情况&#xff0c;文件类型可能是图片、excell表、pdf、zip等文件类型&#xff0c;但浏览器会阻止不安全的下载链接。 效果展示&#xff1a; 下载文件的两种方式&#xff1a; 一、根据接口的相对url&#xff0c;拼…

springMVC---常用注解

目录 一、创建项目 1.依赖 2.web.xml 3.spring-mvc.xml 二、RequestParam注解 1.作用 2.属性 3.代码 DeptController类 启动tomcat 三、RequestBody注解 1.作用 2.属性 3.代码 (1&#xff09;DeptController类 (2&#xff09;index.jsp (3)启动tomcat 四、P…

记录一次微信小程序使用云能力开发的过程

对于开发微信小程序云开发不知从何起的同学们&#xff0c;可以当作一次参考。虽说官方有文档&#xff0c;有模板示例&#xff0c;但是这些都是片段或者完整的结果展示。对于初学或者开发经验较少的同学们&#xff0c;可能不知先从那里入手进行第一步的开发。下面解析下构建微信…

Agent一键安装,快速上手Zabbix监控!

目录 一、Linux操作系统部署Agent环境配置1、防火墙配置2、永久关闭selinux yum方式安装1、配置zabbix仓库2、安装agent3、配置 Zabbix-Agent 指向 Zabbix-Server4、启动agent服务 二进制包安装1、下载二进制包2、创建用户和目录及更改属主&#xff08;组&#xff09;3、解压二…

人工智能实验(五)-基于神经网络的模式识别实验

一、实验目的 理解BP神经网络的结构和原理&#xff0c;掌握反向传播算法对神经元的训练过程&#xff0c;了解反向传播公式。通过构建BP网络模式识别实例&#xff0c;熟悉BP网络的原理及结构 二、实验内容 基于提供的数据集&#xff0c;训练1个BP神经网络模型&#xff1a; 1. …

开发手札:.asmdef和RuntimeInitializeOnLoadMethod

最近为了快速响应临时小demo和老项目改造开发&#xff0c;思考了几种方式。 一般我们开发都是基于一套完善的unity框架进行的&#xff0c;完全建一个new project就是干&#xff0c;这样开发不仅慢而且乱。我们组框架代码都有现成的&#xff0c;而且组员都熟练使用了&…

九 RK3568 android11 MPU6500

一 MPU6500 内核驱动 1.1 查询设备连接地址 查看原理图, MPU6500 I2C 连接在 I2C4 上, 且中断没有使用 i2c 探测设备地址为 0x68 1.2 驱动源码 drivers/input/sensors/gyro/mpu6500_gyro.c drivers/input/sensors/accel/mpu6500_acc.c 默认 .config 配置编译了 mpu6550 …

Quinlan C4.5剪枝U(0,6)U(1,16)等置信上限如何计算?

之前看到Quinlan中关于C4.5决策树算法剪枝环节中,关于错误率e置信区间估计,为啥 当E=0时,U(0,1)=0.75,U(0,6)=0.206,U(0,9)=0.143? 而当E不为0时,比如U(1,16)=0.157,如图: 关于C4.5决策树,Quinlan写了一本书,如下: J. Ross Quinlan (Auth.) - C4.5. Programs f…

机器学习第一道菜(一):线性回归的理论模型

机器学习第一道菜&#xff08;一&#xff09;&#xff1a;线性回归的理论模型 一、问题&#xff1a;千金买笑1.1 散点图1.2 机器学习能搞啥 二、模型的建立2.1 线性回归2.2 回归模型 前面讲了机器学习的“四大绝技”&#xff0c;今天&#xff0c;开始研究第一绝技“回归”&…

Edge Scdn是什么,它如何提升网站安全性与访问速度?

随着网络攻击的日益猖獗&#xff0c;尤其是分布式拒绝服务&#xff08;DDoS&#xff09;攻击的频繁发生&#xff0c;如何保护网站的安全性并确保用户的访问体验变得极为重要。Edge Scdn&#xff08;内容分发网络&#xff09;作为一种新兴的技术方案&#xff0c;逐渐被越来越多的…

深入浅出Redis持久化

文章目录 前言RDB快照原理保存时机 AOF同步策略AOF重写 混合持久化总结 前言 redis作为内存数据库&#xff0c;数据都在内存里&#xff0c;如果突然宕机&#xff0c;则数据都会丢失&#xff08;这里假设不使用非易失性内存&#xff09;&#xff0c;redis提供了持久化机制来防止…

JAVA实现五子棋小游戏(附源码)

文章目录 一、设计来源捡金币闯关小游戏讲解1.1 主界面1.2 黑棋胜利界面1.3 白棋胜利界面 二、效果和源码2.1 动态效果2.2 源代码 源码下载更多优质源码分享 作者&#xff1a;xcLeigh 文章地址&#xff1a;https://blog.csdn.net/weixin_43151418/article/details/145161039 JA…

无公网IP 实现外网访问本地 Docker 部署 Navidrome

Navidrome 是一款可以在 macOS、Linux、Windows以及 Docker 等平台上运行的跨平台开源音乐服务器应用&#xff0c;它支持传输常见的 MP3、FLAC、WAV等音频格式。允许用户通过 Web 界面或 API 进行音乐库的管理和访问。本文就介绍如何快速在 Linux 系统使用 Docker 进行本地部署…

JS后盾人--JS数组挖掘(少年篇)

数组引用类型分析 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> </he…

VirtualBox环境中vscode报错:提取扩展时出错。Failed to fetch

问题现象&#xff1a; VSCode切换到扩展并筛选特色时(为了触发联网)&#xff0c;提示提取扩展时出错。Failed to fetch 拔网线复现不了&#xff0c;完全没有网络VSCode只会提示离线 虚拟机内Edge浏览器访问https://marketplace.visualstudio.com/直接报ERR_CONNECTION_REFUSED…

HTML文章翻页功能

效果展示&#xff1a; 效果原理&#xff1a; 1、引入CDN 2、绘制文章翻页样式&#xff0c;以及自动分段 3、获取窗口宽高&#xff0c;计算出当前文章总分段&#xff0c;并实现分页 4、完整代码 <!DOCTYPE html> <html><head><meta charset"utf-8&qu…

IDEA运行测试函数@Test注解旁边没有运行按钮

如图我想直接运行 testSaveShop 函数进行测试&#xff0c;但是旁边没有运行按钮。 解决办法&#xff1a;测试类需要加上public&#xff0c;系统才能识别到并调用运行。 位置如下&#xff0c;在类和方法都加上 public &#xff0c;然后就会出现运行按钮&#xff1a; 但是如果点…