admin 管理员组文章数量: 887017
最近接到一个任务,写两个导出工具:1.word文档导出,将数据和图片放入到word中,将多个word合并为一个导出。2.Excel文档导出,将二维码信息和图片按照模板放入到Excel中,按9个一页排版数据导出
这里word数据插入我选择使用占位符替换内容
word文档占位符文字和图片的替换
先看效果。如下图所示:
一、引入依赖
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
</dependency>
二、word导出工具
1.定义一个用来导出数据的实体类TicketProject
这里偷懒了,用@Builder方便造数据
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
@Builder
@Data
public class TicketProject implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 序列码 VvjiN63o9m
*/
private String evaluationCode;
/**
* 二维码文件地址 https://www.hjhrcloud/e/
*/
private String evaluationQrcode;
/**
* 项目名称
*/
private String evaluationProjectName;
/**
* 票种角色(领导班子、外部董事、中层)
*/
private String ticketName;
/**
* 描述 请扫一扫二维码或在手机和电脑浏览器上输入网址进行打分
*/
private String describe;
/**
* 简介(一页一码使用:欢迎参加2023年×××民主测评。。。)
*/
private String information;
/**
* 指导语
*/
private String guide;
/**
* 注意事项
*/
private String attention;
/**
* 日期
*/
private String createdDate;
}
2.word导出工具类 ExportUtils
import com.nnmzkj.common.utils.UUIDUtils;
import com.nnmzkj.evaluation.model.TicketProject;
import lombok.SneakyThrows;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.Document;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlOptions;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.*;
/**
* 测评文件导出工具类
*/
public class ExportUtils {
//占位符数组
private static String[] PLACEHOLDER = {"${evaluationProjectName}", "${ticketName}", "${guide}", "${attention}", "${evaluationCode}", "${createdDate}", "${quickMark}"};
//图片占位符单独处理
private static String[] PICTURE_PLACEHOLDER = {"${quickMark}"};
private static int WIDTH = 100; //100%
private static int HEIGHT = 100; //100%
/**
* 一页一码word导出
* @param dtos 二维码数据列表
* @param response
*/
@SneakyThrows
public static void UniqueCodeWordExport(List<TicketProject> dtos, HttpServletResponse response) {
//生成文档的名称列表
List<String> fileNameList = new ArrayList<>();
for (TicketProject dto : dtos) {
String filePath = "template/UniqueCodeWordTemplate.docx"; //模板地址
InputStream fis = Thread.currentThread().getContextClassLoader().getResourceAsStream(filePath);
XWPFDocument doc = new XWPFDocument(fis);
Iterator<XWPFParagraph> itPara = doc.getParagraphsIterator();
while (itPara.hasNext()) {
XWPFParagraph paragraph = (XWPFParagraph) itPara.next();
List<String> placeholderList = Arrays.asList(PLACEHOLDER);
placeholderList.forEach(s->{
List<XWPFRun> runs=paragraph.getRuns();
//文字替换
for (int i = 0; i < runs.size(); i++) {
//获取字符
String text = runs.get(i).getText(runs.get(i).getTextPosition());
if (text != null && text.contains("$")) {
//包含占位符的字符缓存
StringBuilder cache = new StringBuilder(text);
//记录run结束的角标,开始的角标为i
int endIndex = 0;
boolean contains = text.contains("}");
//同一个run中是否包含完成占位符
if (!contains) {
int j = i + 1 ;
for (; j < runs.size(); j++) {
String text1 = runs.get(j).getText(runs.get(j).getTextPosition());
if (text1 == null) {
continue;
}
cache.append(text1);
if (text1.contains("}")) {
endIndex = j;
break;
}
}
}
if (contains || endIndex != 0) {
//处理替换
String key = cache.toString(); //这里是完整的占位符
if (key.contains(s)) {
if (key.contains(PICTURE_PLACEHOLDER[0])) { //替换图片
InputStream in = null;
String evaluationQrcode = dto.getEvaluationQrcode();
String type = evaluationQrcode.substring(evaluationQrcode.lastIndexOf(".") + 1); //二维码图片后缀
//创建Random类对象
Random random = new Random();
//产生随机数
int number = random.nextInt(999) + 1;
try {
if (endIndex == 0) {
paragraph.removeRun(endIndex); //endIndex为0时,直接删掉run
}else {
for (int j = endIndex; j > i; j--) {
//角标移除后,runs会同步变动,直接继续处理i就可以
paragraph.removeRun(j);
}
}
//重新创建一个run,用来放二维码图片
XWPFRun run = paragraph.createRun();
in = new FileInputStream(evaluationQrcode);//设置图片路径
run.addPicture(in, getPictureType(type), "quickMark" + number,
Units.toEMU(WIDTH), Units.toEMU(HEIGHT)); //图片写入
break;
} catch (InvalidFormatException | IOException e) {
e.printStackTrace();
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}else { //文字替换
/**
* 参数0表示生成的文字是要从哪一个地方开始放置,设置文字从位置0开始
* 就可以把原来的文字全部替换掉了
*/
runs.get(i).setText(getValue(dto, s), 0);
for (int j = endIndex; j > i; j--) {
//角标移除后,runs会同步变动,直接继续处理i就可以
paragraph.removeRun(j);
}
break;
}
}
}
}
}
});
}
String strUUID = UUIDUtils.getStrUUID();
String fileName = strUUID+".docx";
fileNameList.add(fileName);
FileOutputStream outStream = null;
outStream = new FileOutputStream("D:/file/evaluation/word/"+fileName); //单个文件保存路径
doc.write(outStream);
outStream.close();
}
//合并word
if (fileNameList.size() > 0) {
String mainFileName = fileNameList.get(0);
FileInputStream fis = new FileInputStream("D:/file/evaluation/word/"+mainFileName);
XWPFDocument doc = new XWPFDocument(fis);
for (int i = 1; i < fileNameList.size(); i++) { //循环合并
FileInputStream fisTail = new FileInputStream("D:/file/evaluation/word/"+fileNameList.get(i));
XWPFDocument docTail = new XWPFDocument(fisTail);
doc = mergeWord(doc, docTail);
}
//TODO 以下写文件改为使用response下载文件
FileOutputStream outStream = null;
outStream = new FileOutputStream("D:/file/evaluation/word/合并文件.docx");
doc.write(outStream);
outStream.close();
//TODO 删除fileNameList列表的文件
}
}
private static String getValue(TicketProject dto, String s) {
if (s.equals("${evaluationProjectName}")) {
return dto.getEvaluationProjectName();
}else if (s.equals("${ticketName}")) {
return dto.getTicketName();
}else if (s.equals("${guide}")) {
return dto.getGuide();
}else if (s.equals("${attention}")) { //注意事项
return dto.getAttention();
}else if (s.equals("${evaluationCode}")) {
return dto.getEvaluationCode();
}else if (s.equals("${createdDate}")) {
return dto.getCreatedDate();
}else return null;
}
/**
* 根据图片类型,取得对应的图片类型代码
* @param picType
* @return int
*/
private static int getPictureType(String picType){
int res = Document.PICTURE_TYPE_PICT;
if(picType != null){
if(picType.equalsIgnoreCase("png")){
res = Document.PICTURE_TYPE_PNG;
}else if(picType.equalsIgnoreCase("dib")){
res = Document.PICTURE_TYPE_DIB;
}else if(picType.equalsIgnoreCase("emf")){
res = Document.PICTURE_TYPE_EMF;
}else if(picType.equalsIgnoreCase("jpg") || picType.equalsIgnoreCase("jpeg")){
res = Document.PICTURE_TYPE_JPEG;
}else if(picType.equalsIgnoreCase("wmf")){
res = Document.PICTURE_TYPE_WMF;
}
}
return res;
}
/**
* 将两个word文档合并
* @param document
* @param doucDocument2
* @return 合并后的文档
*/
public static XWPFDocument mergeWord(XWPFDocument document,XWPFDocument doucDocument2) {
XWPFParagraph p = document.createParagraph();
//设置分页符
p.setPageBreak(true);
CTBody src1Body = document.getDocument().getBody();
CTBody src2Body = doucDocument2.getDocument().getBody();
// XWPFParagraph p2 = src2Document.createParagraph();
XmlOptions optionsOuter = new XmlOptions();
optionsOuter.setSaveOuter();
String appendString = src2Body.xmlText(optionsOuter);
String srcString = src1Body.xmlText();
String prefix = srcString.substring(0,srcString.indexOf(">")+1);
String mainPart = srcString.substring(srcString.indexOf(">")+1,srcString.lastIndexOf("<"));
String sufix = srcString.substring( srcString.lastIndexOf("<") );
String addPart = appendString.substring(appendString.indexOf(">") + 1, appendString.lastIndexOf("<"));
CTBody makeBody = null;
try {
makeBody = CTBody.Factory.parse(prefix+mainPart+addPart+sufix);
} catch (XmlException e) {
e.printStackTrace();
}
src1Body.set(makeBody);
return document;
}
}
main方法调用代码如下:
public static void main(String[] args) throws Exception {
String line = System.getProperty("line.separator");
List<TicketProject> entitys = new ArrayList<>();
entitys.add(TicketProject
.builder()
.evaluationCode("VvjiN63o9m")
.evaluationProjectName("2023年×××民主测评")
.ticketName("领导班子")
.describe("请扫一扫二维码或在手机和电脑浏览器上输入网址进行打分")
.guide("欢迎参加2023年×××民主测评,本次测评采用匿名方式,不收集任何个人信息,目的在于合理、真实评价有关人员的素质能力和实际工作情况。请结合现场述职及书面述职的情况,根据您日常工作中的观察、了解和所掌握的情况,做出客观评价。")
.attention("1.请务必在限定的时间内完成评价并提交,评价通道将于2022年12月31日关闭;" +line+
" 2.中途因故退出,可用同一手机再次扫码进入考评系统;" +line+
" 3.评价一经提交无法进行修改;" +line+
" 4.如果您遇到任何问题,请与党委组织部联系,非常感谢您的支持!")
.evaluationQrcode("D:/file/evaluation/1682057427848.jpg") //图片地址
.createdDate("2023年03月09日")
.build());
entitys.add(TicketProject
.builder()
.evaluationCode("VvjiN6678o")
.evaluationProjectName("2023年×××民主测评")
.ticketName("领导班子")
.describe("请扫一扫二维码或在手机和电脑浏览器上输入网址进行打分")
.guide("欢迎参加2023年×××民主测评,本次测评采用匿名方式,不收集任何个人信息,目的在于合理、真实评价有关人员的素质能力和实际工作情况。请结合现场述职及书面述职的情况,根据您日常工作中的观察、了解和所掌握的情况,做出客观评价。")
.attention("1.请务必在限定的时间内完成评价并提交,评价通道将于2023年3月01日关闭;" +line+
" 2.中途因故退出,可用同一手机再次扫码进入考评系统;" +line+
" 3.评价一经提交无法进行修改;" +line+
" 4.如果您遇到任何问题,请与党委组织部联系,非常感谢您的支持!")
.evaluationQrcode("D:/file/evaluation/1682057427848.jpg")
.createdDate("2023年03月11日")
.build());
//合并导出word文档
UniqueCodeWordExport(entitys, null);
}
3.导出模板
来看下模板中的站位符,如下图所示:
在模板提前设置好格式和样式,数据替换占位符之后就不需要再设置样式了。
图片替换占位符比较特殊,不能像文字那样,直接替换掉占位符就可以。需要先清掉图片占位符,重新创建一个run再将图片放进去。
Excel图片插入到对应单元格
同样,先看导出效果图,如下图所示:
这里没有在程序里面设置单元格格式,可以根据自己的需求进行调整。
1.Excel导出处理方法
private static String[] VOTE_ROLE = {"领导班子", "外部董事", "中层"};
/**
* 一页一码excel导出
* @param dtos 二维码数据列表
* @param response
*/
public static void UniqueCodeExcelExport(List<TicketProject> dtos, HttpServletResponse response) {
String filePath = "template/UniqueCodeExcelTemplate.xls"; //模板地址
InputStream fis = Thread.currentThread().getContextClassLoader().getResourceAsStream(filePath);
try {
if (dtos == null || dtos.size() < 1) {
throw new RuntimeException("没有可导出的数据");
}
//根据票种角色进行分组
Map<String, List<TicketProject>> TicketNameGroupList = dtos.stream().filter(item-> StringUtils.isNotBlank(item.getTicketName())).collect(Collectors.groupingBy(TicketProject::getTicketName));
HSSFWorkbook workbook = new HSSFWorkbook(fis);
HSSFSheet sheet = workbook.getSheetAt(0);
int excelPage = 0;
for (String s : VOTE_ROLE) {
excelPage++; //第一页,遍历到下一个类型时,换下一页
List<TicketProject> ticketProjects = TicketNameGroupList.get(s); //获取某一个类型的所有数据
if (ticketProjects != null && ticketProjects.size() > 0) {
int currentPage;
//手工分页,一行3条,一行数据为一页
int totalRecord = ticketProjects.size();
int totalPage = totalRecord % 3;
if (totalPage > 0) {
totalPage = totalRecord / 3 + 1;
} else {
totalPage = totalRecord / 3;
}
int currentRow = 0; //每一行二维码的起始位置
for (int i=1; i<=totalPage; i++) { //页数(list的页数,每3条一页)
currentPage = i;
int fromIndex = 3 * (currentPage - 1);
int toIndex = Math.min(3 * currentPage, totalRecord);
//9条数据换下一页
int Cumulative = currentPage % 3;
if (Cumulative == 0) {
excelPage++;
}
if(currentPage != 1 && ((currentPage-1) % 3)==0){ //第四行数据开始+1
currentRow++; //加上分类那一行,纠正行数
}
int rowNum = (excelPage-1)*22;//每一页开始的行数
List<TicketProject> page = ticketProjects.subList(fromIndex, toIndex);
//首行为分类
if (currentPage == 1 || Cumulative == 0) {
Row row1 = sheet.getRow(rowNum);
Cell row1cell = row1.createCell(0);
row1cell.setCellValue(page.get(0).getTicketName());
}
//计算行数
if (currentPage == 1){
currentRow = rowNum;
}
if (currentPage != 1) {
currentRow += 7;
}
for (int j=0; j<page.size(); j++) {
TicketProject ticketProject = page.get(j);
//计算cellNum 方法比较笨,但是实用
int cellNum;
if (j==0) {
cellNum = 1;
}else if (j==1) {
cellNum = 4;
}else {
cellNum = 7;
}
//第二行二维码
String url = ticketProject.getEvaluationQrcode(); //图片路径
String type = url.substring(url.lastIndexOf(".") + 1); //二维码图片后缀
InputStream in = new FileInputStream(url);
byte[] btImg = null;
try {
btImg = readInputStream(in);// 得到图片的二进制数据
} catch (Exception e) {
e.printStackTrace();
}
HSSFPatriarch patriarch = sheet.createDrawingPatriarch();
/**
* dx1:左边偏移量
* dy1:顶部偏移量
*/
HSSFClientAnchor anchor = new HSSFClientAnchor(50, 10, 0, 0, (short) cellNum, currentRow+1, (short) (cellNum+1), currentRow+2);
assert btImg != null;
Picture picture = patriarch.createPicture(anchor, workbook.addPicture(btImg, getPictureType(type)));
picture.resize(0.95,0.95); //设置了图片大小,就不能跟随单元格变化了(不设置这个设置不了偏移量),没有找到解决方案
//第三行
Row row3 = sheet.getRow(currentRow+2);
Cell row3cell = row3.createCell(cellNum);
row3cell.setCellValue(ticketProject.getTicketName());
//第四行
Row row4 = sheet.getRow(currentRow+3);
Cell row4cell = row4.createCell(cellNum);
row4cell.setCellValue(ticketProject.getEvaluationProjectName());
//第五行
Row row5 = sheet.getRow(currentRow+4);
Cell row5cell = row5.createCell(cellNum);
row5cell.setCellValue("序列码:"+ticketProject.getEvaluationCode());
//第六行
Row row6 = sheet.getRow(currentRow+5);
Cell row6cell = row6.createCell(cellNum);
row6cell.setCellValue(ticketProject.getDescribe());
}
}
}
}
String strUUID = UUIDUtils.getStrUUID();
String fileName = strUUID+".xls";
FileOutputStream outStream = null;
outStream = new FileOutputStream("D:/file/evaluation/excel/"+fileName); //单个文件保存路径
workbook.write(outStream);
outStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 从输入流中获取字节流数据
*
* @param inStream 输入流
* @return
* @throws Exception
*/
public static byte[] readInputStream(InputStream inStream) throws Exception {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
byte[] buffer = new byte[10240];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outStream.write(buffer, 0, len);
}
inStream.close();
return outStream.toByteArray();
}
2.main方法调用
public static void main(String[] args) throws Exception {
String line = System.getProperty("line.separator");
List<TicketProject> entitys = new ArrayList<>();
entitys.add(TicketProject
.builder()
.evaluationCode("VvjiN63o9m")
.evaluationProjectName("2023年×××民主测评")
.ticketName("领导班子")
.describe("请扫一扫二维码或在手机和电脑浏览器上输入网址进行打分")
.guide("欢迎参加2023年×××民主测评,本次测评采用匿名方式,不收集任何个人信息,目的在于合理、真实评价有关人员的素质能力和实际工作情况。请结合现场述职及书面述职的情况,根据您日常工作中的观察、了解和所掌握的情况,做出客观评价。")
.attention("1.请务必在限定的时间内完成评价并提交,评价通道将于2022年12月31日关闭;" +line+
" 2.中途因故退出,可用同一手机再次扫码进入考评系统;" +line+
" 3.评价一经提交无法进行修改;" +line+
" 4.如果您遇到任何问题,请与党委组织部联系,非常感谢您的支持!")
.evaluationQrcode("D:/file/evaluation/1682057427848.jpg")
.createdDate("2023年03月09日")
.build());
entitys.add(TicketProject
.builder()
.evaluationCode("VvjiN6678o")
.evaluationProjectName("2023年×××民主测评")
.ticketName("领导班子")
.describe("请扫一扫二维码或在手机和电脑浏览器上输入网址进行打分")
.guide("欢迎参加2023年×××民主测评,本次测评采用匿名方式,不收集任何个人信息,目的在于合理、真实评价有关人员的素质能力和实际工作情况。请结合现场述职及书面述职的情况,根据您日常工作中的观察、了解和所掌握的情况,做出客观评价。")
.attention("1.请务必在限定的时间内完成评价并提交,评价通道将于2023年3月01日关闭;" +line+
" 2.中途因故退出,可用同一手机再次扫码进入考评系统;" +line+
" 3.评价一经提交无法进行修改;" +line+
" 4.如果您遇到任何问题,请与党委组织部联系,非常感谢您的支持!")
.evaluationQrcode("D:/file/evaluation/1682057427848.jpg")
.createdDate("2023年03月11日")
.build());
entitys.add(TicketProject
.builder()
.evaluationCode("VvjiN25n11")
.evaluationProjectName("2023年×××民主测评")
.ticketName("领导班子")
.describe("请扫一扫二维码或在手机和电脑浏览器上输入网址进行打分")
.guide("欢迎参加2023年×××民主测评,本次测评采用匿名方式,不收集任何个人信息,目的在于合理、真实评价有关人员的素质能力和实际工作情况。请结合现场述职及书面述职的情况,根据您日常工作中的观察、了解和所掌握的情况,做出客观评价。")
.attention("1.请务必在限定的时间内完成评价并提交,评价通道将于2023年3月01日关闭;" +line+
" 2.中途因故退出,可用同一手机再次扫码进入考评系统;" +line+
" 3.评价一经提交无法进行修改;" +line+
" 4.如果您遇到任何问题,请与党委组织部联系,非常感谢您的支持!")
.evaluationQrcode("D:/file/evaluation/1682238606868.jpg")
.createdDate("2023年03月11日")
.build());
entitys.add(TicketProject
.builder()
.evaluationCode("Vbime1877a")
.evaluationProjectName("2023年×××民主测评")
.ticketName("领导班子")
.describe("请扫一扫二维码或在手机和电脑浏览器上输入网址进行打分")
.guide("欢迎参加2023年×××民主测评,本次测评采用匿名方式,不收集任何个人信息,目的在于合理、真实评价有关人员的素质能力和实际工作情况。请结合现场述职及书面述职的情况,根据您日常工作中的观察、了解和所掌握的情况,做出客观评价。")
.attention("1.请务必在限定的时间内完成评价并提交,评价通道将于2023年3月01日关闭;" +line+
" 2.中途因故退出,可用同一手机再次扫码进入考评系统;" +line+
" 3.评价一经提交无法进行修改;" +line+
" 4.如果您遇到任何问题,请与党委组织部联系,非常感谢您的支持!")
.evaluationQrcode("D:/file/evaluation/1682238606868.jpg")
.createdDate("2023年03月11日")
.build());
entitys.add(TicketProject
.builder()
.evaluationCode("Vbime18N2a")
.evaluationProjectName("2023年×××民主测评")
.ticketName("外部董事")
.describe("请扫一扫二维码或在手机和电脑浏览器上输入网址进行打分")
.guide("欢迎参加2023年×××民主测评,本次测评采用匿名方式,不收集任何个人信息,目的在于合理、真实评价有关人员的素质能力和实际工作情况。请结合现场述职及书面述职的情况,根据您日常工作中的观察、了解和所掌握的情况,做出客观评价。")
.attention("1.请务必在限定的时间内完成评价并提交,评价通道将于2023年3月01日关闭;" +line+
" 2.中途因故退出,可用同一手机再次扫码进入考评系统;" +line+
" 3.评价一经提交无法进行修改;" +line+
" 4.如果您遇到任何问题,请与党委组织部联系,非常感谢您的支持!")
.evaluationQrcode("D:/file/evaluation/1682238606868.jpg")
.createdDate("2023年03月11日")
.build());
entitys.add(TicketProject
.builder()
.evaluationCode("Vbime18N2a")
.evaluationProjectName("2023年×××民主测评")
.ticketName("外部董事")
.describe("请扫一扫二维码或在手机和电脑浏览器上输入网址进行打分")
.guide("欢迎参加2023年×××民主测评,本次测评采用匿名方式,不收集任何个人信息,目的在于合理、真实评价有关人员的素质能力和实际工作情况。请结合现场述职及书面述职的情况,根据您日常工作中的观察、了解和所掌握的情况,做出客观评价。")
.attention("1.请务必在限定的时间内完成评价并提交,评价通道将于2023年3月01日关闭;" +line+
" 2.中途因故退出,可用同一手机再次扫码进入考评系统;" +line+
" 3.评价一经提交无法进行修改;" +line+
" 4.如果您遇到任何问题,请与党委组织部联系,非常感谢您的支持!")
.evaluationQrcode("D:/file/evaluation/1682238606868.jpg")
.createdDate("2023年03月11日")
.build());
//加测试数据
List<TicketProject> list = new ArrayList<>(entitys);
entitys.addAll(list);
entitys.addAll(list);
entitys.addAll(list);
entitys.addAll(list);
entitys.addAll(list);
//excel导出
UniqueCodeExcelExport(entitys, null);
}
懒人造数据,不要吐槽我!
Excel导出有要求,9条数据为一页,一行三条数据。同一个分类的数据,分页之后要在前面加上分类名称,所以中间计算开始行数的时候,要加上这一行。
3.Excel模板
不想在代码里面设置格式样式,提前画好直接取模板用就好
因为在做这两个工具的时候找了很多网上的代码,感觉都不完整,而且都没有能达到我想要的效果。
在这里发出来记录一下,也分享给大家,希望对你有用!
版权声明:本文标题:【java spring boot使用easypoi实现word文档占位符替换文字和图片、Excel图片贴到对应单元格】 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/jishu/1726311116h934435.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论