主要后端使用Java实现,前端可随意搭配http请求

添加依赖:

        <!-- OFD解析与转换库 --><dependency><groupId>org.ofdrw</groupId><artifactId>ofdrw-converter</artifactId><version>1.17.9</version></dependency><!-- PDFBox用于PDF生成 --><dependency><groupId>org.apache.pdfbox</groupId><artifactId>pdfbox</artifactId><version>2.0.29</version></dependency>

控制层代码实现:

@CrossOrigin
@RestController
@RequestMapping("/tool")
public class ToolsController {@Autowiredprivate ToolsService toolsService;/*** 批量转换OFD文件为PDF并打包下载*/@PostMapping("/batchofd2pdf")public ResponseEntity<byte[]> batchConvert(@RequestParam("files") MultipartFile[] ofdFiles) {if (ofdFiles == null || ofdFiles.length == 0) {return new ResponseEntity<>(HttpStatus.BAD_REQUEST);}try {// 构建文件名到输入流的映射Map<String, InputStream> fileMap = new HashMap<>();for (MultipartFile file : ofdFiles) {if (!file.isEmpty() && file.getOriginalFilename().toLowerCase().endsWith(".ofd")) {fileMap.put(file.getOriginalFilename(), file.getInputStream());}}// 执行批量转换Map<String, byte[]> pdfFiles = toolsService.batchConvert(fileMap);// 将所有PDF文件打包成ZIPtry (ByteArrayOutputStream byteOut = new ByteArrayOutputStream();ZipOutputStream zipOut = new ZipOutputStream(byteOut)) {for (Map.Entry<String, byte[]> entry : pdfFiles.entrySet()) {zipOut.putNextEntry(new ZipEntry(entry.getKey()));zipOut.write(entry.getValue());zipOut.closeEntry();}zipOut.finish();// 设置响应头,返回ZIP文件HttpHeaders headers = new HttpHeaders();headers.setContentDispositionFormData("attachment", "ofd_converted_pdfs.zip");headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);return new ResponseEntity<>(byteOut.toByteArray(), headers, HttpStatus.OK);}} catch (Exception e) {e.printStackTrace();return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);}}
}

service层代码实现

package com.tool.service;import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.Map;
import java.util.concurrent.ExecutionException;public interface ToolsService {Map<String,byte[]> batchConvert(Map<String, InputStream> fileMap) throws InterruptedException, ExecutionException;
}
@Service
public class ToolsServiceImpl implements ToolsService {// 线程池用于并行处理转换任务private final ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1);/*** 单个文件转换:输入流到输出流*/public void convertOfdToPdf(InputStream ofdInputStream, OutputStream pdfOutputStream) throws Exception {ConvertHelper.toPdf(ofdInputStream, pdfOutputStream);}/*** 批量转换多个OFD文件* @param fileMap 文件名到输入流的映射* @return 文件名到PDF字节数组的映射*/@Overridepublic Map<String, byte[]> batchConvert(Map<String, InputStream> fileMap) throws InterruptedException, ExecutionException {Map<String, Future<byte[]>> futures = new HashMap<>();// 提交所有转换任务到线程池for (Map.Entry<String, InputStream> entry : fileMap.entrySet()) {String fileName = entry.getKey();InputStream inputStream = entry.getValue();futures.put(fileName, executorService.submit(() -> {try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {convertOfdToPdf(inputStream, outputStream);return outputStream.toByteArray();} finally {inputStream.close();}}));}// 收集转换结果Map<String, byte[]> results = new HashMap<>();for (Map.Entry<String, Future<byte[]>> entry : futures.entrySet()) {String fileName = entry.getKey().replace(".ofd", ".pdf");results.put(fileName, entry.getValue().get());}return results;}/*** 应用关闭时关闭线程池*/public void shutdownExecutor() {executorService.shutdown();}
}

前端实现:

<template><div class="container mx-auto px-4 py-8 max-w-6xl"><!-- 页面标题 --><div class="text-center mb-8"><h1 class="text-[clamp(1.8rem,4vw,2.5rem)] font-bold text-gray-800 mb-2">OFD转PDF批量转换</h1><p class="text-gray-500">支持多文件上传,一键批量转换OFD文件为PDF格式</p></div><!-- 上传区域 --><div class="p-6" style="margin: 10px 10px 10px 10px;"><el-uploadref="upload"class="upload-area"action="#":http-request="handleUpload":on-change="handleFileChange":on-remove="handleFileRemove":before-upload="beforeUpload":file-list="fileList":auto-upload="false"multipleaccept=".ofd"><el-button type="primary" :icon="Upload">选择文件</el-button><template #tip><div class="el-upload__tip text-sm text-gray-500">支持上传多个文件</div></template></el-upload></div><!-- 文件列表和进度 --><el-card v-if="fileList.length > 0" class="mb-6 transition-all duration-300 hover:shadow-md"><div class="p-4 border-b"><h2 class="font-semibold text-gray-800">文件列表</h2></div><el-table:data="fileList"bordersize="small"class="mb-0"><el-table-column prop="name" label="文件名" width="350"></el-table-column><el-table-column prop="size" label="大小" width="120"><template #default="scope">{{ formatFileSize(scope.row.size) }}</template></el-table-column><el-table-column prop="status" label="状态" width="150"><template #default="scope"><el-tag:type="scope.row.status === 'ready' ? 'info' :scope.row.status === 'waiting' ? 'info' :scope.row.status === 'converting' ? 'warning' :scope.row.status === 'success' ? 'success' : 'danger'"size="small"><el-icon v-if="scope.row.status === 'converting'" class="mr-1"><Loading /></el-icon>{{ statusMap[scope.row.status] }}</el-tag></template></el-table-column><el-table-column label="进度" width="200"><template #default="scope"><el-progressv-if="scope.row.status === 'converting'":percentage="scope.row.progress"stroke-width="6"size="small"></el-progress><span v-else-if="scope.row.status === 'success'">100%</span><span v-else>-</span></template></el-table-column><el-table-column label="操作" width="120"><template #default="scope"><el-buttonv-if="scope.row.status === 'success'"type="text"size="small"text-color="#165DFF"@click="downloadFile(scope.row)"><el-icon class="mr-1"><Download /></el-icon>下载</el-button><el-buttonv-else-if="scope.row.status === 'waiting' || scope.row.status === 'error'"type="text"size="small"text-color="#F53F3F"@click="handleFileRemove(scope.row)"><el-icon class="mr-1"><Delete /></el-icon>删除</el-button><span v-else>-</span></template></el-table-column></el-table></el-card><!-- 转换进度弹窗 --><el-dialogtitle="转换进度"v-model="showProgressDialog":close-on-click-modal="false":show-close="false"width="500px"><div class="mb-4"><p class="text-gray-600 mb-2">总进度:{{ totalProgress }}%</p><el-progress :percentage="totalProgress" stroke-width="8"></el-progress></div><div v-for="file in fileList" :key="file.uid" class="mb-2"><div class="flex justify-between text-sm mb-1"><span>{{ file.name }}</span><span>{{ file.progress }}%</span></div><el-progress :percentage="file.progress" stroke-width="4" size="small"></el-progress></div><template #footer><el-buttontype="default"@click="cancelConversion":disabled="!isCancellable">取消转换</el-button></template></el-dialog><!-- 转换完成提示 --><el-dialogtitle="转换完成"v-model="showCompleteDialog"width="400px"><div class="text-center py-4">
<!--        <el-icon class="text-5xl text-success mb-4"><CheckCircle /></el-icon>--><p>所有文件转换已完成</p><p class="text-gray-500 mt-2">成功:{{ successCount }} 个,失败:{{ errorCount }} 个</p></div><template #footer><div class="text-center"><el-buttontype="primary"@click="downloadAllFiles":disabled="successCount === 0"><el-icon class="mr-1"><Download /></el-icon>下载全部</el-button><el-buttontype="default"@click="showCompleteDialog = false">关闭</el-button></div></template></el-dialog></div>
</template><script setup>
import { ref, computed, onBeforeUnmount } from 'vue';
import {DocumentAdd, Upload, Loading, Delete, Download
} from '@element-plus/icons-vue';
import {ElButton, ElMessage, ElNotification} from 'element-plus';
import axios from 'axios';// 文件列表
const fileList = ref([]);
// 上传状态
const isUploading = ref(false);
// 转换状态
const isConverting = ref(false);
// 进度弹窗显示
const showProgressDialog = ref(false);
// 完成弹窗显示
const showCompleteDialog = ref(false);
// 上传组件引用
const upload = ref(null);
// 转换请求取消令牌
const cancelTokenSource = ref(null);// 状态映射
const statusMap = {ready: '等待转换',waiting: '等待转换',converting: '转换中',success: '转换成功',error: '转换失败'
};// 修复:转换按钮是否禁用的计算属性
const isConvertDisabled = computed(() => {// 当没有文件、正在上传或正在转换时禁用return fileList.value.length === 0 || isUploading.value || isConverting.value;
});// 计算属性:总进度
const totalProgress = computed(() => {if (fileList.value.length === 0) return 0;const sum = fileList.value.reduce((acc, file) => acc + file.progress, 0);return Math.round(sum / fileList.value.length);
});// 计算属性:成功和失败数量
const successCount = computed(() => {return fileList.value.filter(file => file.status === 'success').length;
});const errorCount = computed(() => {return fileList.value.filter(file => file.status === 'error').length;
});// 计算属性:是否可取消
const isCancellable = computed(() => {return isConverting.value && totalProgress.value < 100;
});// 文件大小格式化
const formatFileSize = (bytes) => {if (bytes === 0) return '0 B';const k = 1024;const sizes = ['B', 'KB', 'MB', 'GB'];const i = Math.floor(Math.log(bytes) / Math.log(k));return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};// 上传前检查
const beforeUpload = (file) => {// 检查文件类型if (file.type !== '' && !file.name.toLowerCase().endsWith('.ofd')) {ElMessage.error('请上传OFD格式的文件');return false;}// 检查文件大小(限制50MB)const maxSize = 50 * 1024 * 1024;if (file.size > maxSize) {ElMessage.error('文件大小不能超过50MB');return false;}return true;
};// 文件变化处理 - 修复:确保文件正确添加到列表
const handleFileChange = (file, newFileList) => {// 同步更新文件列表fileList.value = newFileList;// 为新添加的文件设置初始状态if (!file.status) {file.status = 'waiting';file.progress = 0;file.pdfUrl = null;}
};// 移除文件
const handleFileRemove = (file) => {fileList.value = fileList.value.filter(item => item.uid !== file.uid);
};// 清空文件列表
const clearFiles = () => {fileList.value = [];if (upload.value) {upload.value.clearFiles();}
};// 处理上传(覆盖默认上传行为)
const handleUpload = () => {// 实际上传由submitUpload处理,这里只是为了满足组件要求
};// 提交转换 - 修复:状态管理更清晰
const submitUpload = async () => {console.log(1)if (fileList.value.length === 0) {ElMessage.warning('请先选择文件');return;}console.log(2)// 重置文件状态fileList.value.forEach(file => {file.status = 'converting';file.progress = 0;});console.log(3)// 更新状态变量isUploading.value = true;isConverting.value = true;showProgressDialog.value = true;console.log(4)try {// 创建FormDataconst formData = new FormData();fileList.value.forEach(file => {formData.append('files', file.raw);});console.log(5)// 创建取消令牌cancelTokenSource.value = axios.CancelToken.source();console.log(6)// 模拟进度更新(实际项目中可以通过WebSocket或轮询实现)const progressInterval = setInterval(() => {fileList.value.forEach(file => {if (file.status === 'converting' && file.progress < 100) {// 随机增加进度,模拟真实场景const increment = Math.floor(Math.random() * 5) + 1;file.progress = Math.min(file.progress + increment, 100);}});}, 300);console.log(7)const postUrl = `http://10.60.128.250:8080/tool/batchofd2pdf`// 发送请求const response = await axios.post(postUrl, formData, {responseType: 'blob',cancelToken: cancelTokenSource.value.token,headers: {'Content-Type': 'multipart/form-data'}});console.log(8)// 清除进度模拟clearInterval(progressInterval);console.log(9)// 更新所有文件状态为成功fileList.value.forEach(file => {file.status = 'success';file.progress = 100;// 创建下载URLfile.pdfUrl = URL.createObjectURL(response.data);});console.log(10)// 显示完成弹窗showProgressDialog.value = false;showCompleteDialog.value = true;console.log(11)ElNotification.success({title: '转换成功',message: `已成功转换 ${fileList.value.length} 个文件`,duration: 3000});console.log(12)} catch (error) {if (axios.isCancel(error)) {// 取消操作fileList.value.forEach(file => {if (file.status === 'converting') {file.status = 'waiting';}});ElMessage.info('已取消转换');} else {// 错误处理fileList.value.forEach(file => {if (file.status === 'converting') {file.status = 'error';}});ElMessage.error('转换失败:' + (error.response?.data?.message || error.message));}} finally {// 重置状态变量console.log(14)isUploading.value = false;isConverting.value = false;showProgressDialog.value = false;console.log(15)}
};// 取消转换
const cancelConversion = () => {if (cancelTokenSource.value) {cancelTokenSource.value.cancel('用户取消了转换');}
};// 下载单个文件
const downloadFile = (file) => {if (!file.pdfUrl) {ElMessage.warning('文件下载地址不存在');return;}// 创建a标签下载const link = document.createElement('a');link.href = file.pdfUrl;link.download = file.name.replace('.ofd', '.pdf');document.body.appendChild(link);link.click();document.body.removeChild(link);
};// 下载所有文件
const downloadAllFiles = () => {// 这里应该下载ZIP包const firstPdfFile = fileList.value.find(file => file.status === 'success');if (firstPdfFile?.pdfUrl) {const link = document.createElement('a');link.href = firstPdfFile.pdfUrl;link.download = 'ofd_converted_pdfs.zip';document.body.appendChild(link);link.click();document.body.removeChild(link);showCompleteDialog.value = false;}
};// 组件卸载前清理
onBeforeUnmount(() => {// 释放URL对象fileList.value.forEach(file => {if (file.pdfUrl) {URL.revokeObjectURL(file.pdfUrl);}});// 取消请求if (cancelTokenSource.value) {cancelTokenSource.value.cancel('组件已卸载');}
});
defineExpose({submitUpload,clearFiles
});
</script><style scoped>
.upload-area {border: 1px dashed #ccc;border-radius: 4px;padding: 20px;text-align: center;transition: border-color 0.3s;
}.upload-area:hover {border-color: #409eff;
}.upload-dropzone {transition: all 0.3s ease;
}::v-deep .el-progress__text {font-size: 12px !important;
}::v-deep .el-table__row:hover {background-color: #f5f7fa !important;
}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/916594.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/916594.shtml
英文地址,请注明出处:http://en.pswp.cn/news/916594.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

4.应用层自定义协议与序列化

1.应用层程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层1.1再谈“协议”协议是一种 "约定". socket api 的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们要传输一些 "结构化的数据" 怎么办呢…

【QT搭建opencv环境】

本文参考以下文章&#xff1a; https://blog.csdn.net/weixin_43763292/article/details/112975207 https://blog.csdn.net/qq_44743171/article/details/124335100 使用软件 QT 5.14.2下载地址&#xff1a;download.qt.io 选择版本&#xff1a;Qt 5.14.2 Qt 5.14.2百度网盘链接…

golang--函数栈

一、函数栈的组成结构&#xff08;栈帧&#xff09; 每个函数调用对应一个栈帧&#xff0c;包含以下核心部分&#xff1a; 1. 参数区 (Arguments) 位置&#xff1a;栈帧顶部&#xff08;高地址端&#xff09;内容&#xff1a; 函数调用时传入的参数按从右向左顺序压栈&#xff…

【FAQ】创建Dynamics 365 Sales环境

参考文章&#xff1a;5 分钟内安装 Dynamics 365 Sales 步骤 1&#xff1a;访问 Power Platform 管理中心 导航到make.powerapps.com&#xff0c;然后点击右上角的齿轮图标。选择管理中心&#xff0c;或者访问aka.ms/ppac访问 Power Platform 管理中心。 第 2 步&#xff1a…

【数据库】使用Sql Server将分组后指定字段的行数据转为一个字段显示,并且以逗号隔开每个值,收藏不迷路

大家好&#xff0c;我是全栈小5&#xff0c;欢迎来到《小5讲堂》。 这是《Sql Server》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解。 温馨提示&#xff1a;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01; 目录前言示例数据集数…

7.项目起步(1)

1&#xff0c;项目起步-初始化项目并使用git管理创建项目并精细化配置src目录调整git 管理项目2项目起步-配置别名路径联想提示什么是别名路径联想提示如何进行配置 &#xff08;自动配置了&#xff09;{"compilerOptions" : {"baseUrl" : "./",…

【C++详解】深入解析继承 类模板继承、赋值兼容转换、派生类默认成员函数、多继承与菱形继承

文章目录一、继承概念二、继承定义定义格式继承后基类成员访问方式的变化类模板的继承三、基类和派⽣类间的转换(赋值兼容转换)四、继承中的作用域隐藏规则两道笔试常考题五、派生类的默认成员函数四个常见默认成员函数实现⼀个不能被继承的类六、继承与友元七、继承与静态成员…

加法器 以及ALU(逻辑算术单元)

加法器框架&#xff0c;首先介绍原理&#xff0c;然后引入一位加法器最后再引入多位加法器最后引入带符号的加法器这一节涉及到的硬件电路的知识理解就好&#xff0c;实在看不懂就跳过&#xff0c;但是封装以后的功能必须看懂。这是一个一般的加法过程涉及到的必要元素图中已经…

设计模式实战:自定义SpringIOC(亲手实践)

上一篇&#xff1a;设计模式实战&#xff1a;自定义SpringIOC&#xff08;理论分析&#xff09; 自定义SpringIOC&#xff08;亲手实践&#xff09; 上一篇文章&#xff0c;我们介绍了SpringIOC容器的核心组件及其作用&#xff0c;下面我们来动手仿写一个SpringIOC容器&#…

力扣面试150(42/150)

7.28 20. 有效的括号 给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 s &#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。每个右括号都有一…

基于黑马教程——微服务架构解析(二):雪崩防护+分布式事务

之前的两篇文章我们介绍了微服务的基础概念及其服务间通信机制。本篇将深入探讨微服务的核心保障&#xff1a;服务保护与分布式事务。一、微服务保护问题描述&#xff1a; 在一个购物车的微服务中&#xff0c;倘若某一项服务&#xff08;服务A&#xff09;同一时刻访问的数据十…

LeetCode: 429 N叉树的层序遍历

题目描述给定一个 N 叉树&#xff0c;返回其节点值的层序遍历&#xff08;即从左到右&#xff0c;逐层访问每一层的所有节点&#xff09;。示例输入格式&#xff08;层序序列化&#xff09;&#xff1a;输入示意&#xff1a;1/ | \3 2 4/ \5 6输出&#xff1a;[[1], [3,2,4…

使用phpstudy极简快速安装mysql

使用 phpStudy 极简快速安装 MySQL 的完整指南&#xff1a; 一、phpStudy 简介 phpStudy 是一款 Windows 平台下的 PHP 环境集成包&#xff0c;包含&#xff1a; Apache/Nginx PHP 5.x-7.x MySQL 5.5-8.0 phpMyAdmin 二、安装步骤 1. 下载安装包 访问官网下载&#xf…

git lfs使用

apt install git lfs 或者下载二进制文件加到环境变量 https://github.com/git-lfs/git-lfs/releases git lfs install git lfs clone huggingface文件路径 如果访问不了hugggingface.co用hf-mirror.com替代&#xff0c;国内下载速度还是挺快的 先按照pip install modelscope m…

6、CentOS 9 安装 Docker

&#x1f433; CentOS 9 安装 Docker 最全图文教程&#xff08;含镜像源优化与常见问题解决&#xff09;标签&#xff1a;CentOS 9、Docker、容器技术、开发环境、国内镜像源 适合读者&#xff1a;后端开发、运维工程师、Linux 初学者&#x1f4cc; 前言 在 CentOS 9 上安装 Do…

SystemV消息队列揭秘:原理与实战

目录 一、消息队列的基本原理 1、基本概念 2、基本原理 3、消息类型的关键作用 4、重要特性总结 5、生命周期管理 6、典型应用场景 二、System V 消息队列的内核数据结构 1、消息队列的管理结构 msqid_ds&#xff08;消息队列标识符结构&#xff09; 关键字段解析 2…

5 分钟上手 Firecrawl

文章目录Firecrawl 是什么&#xff1f;本地部署验证mcp安装palyground&#x1f525; 5 分钟上手 FirecrawlFirecrawl 是什么&#xff1f; 一句话&#xff1a; 开源版的 “最强网页爬虫 清洗引擎” • 自动把任意网页 → 结构化 Markdown / JSON • 支持递归整站抓取、JS 渲染…

算法训练营day31 贪心算法⑤56. 合并区间、738.单调递增的数字 、968.监控二叉树

贪心算法的最后一篇博客&#xff01;前面两道题都是比较简单的思路&#xff0c;重点理解一下最后一道题即可。有一说一&#xff0c;进入到贪心算法这一章节之后&#xff0c;我的博客里和代码注释里的内容明显少了很多&#xff0c;因为很多贪心的题目我觉得不需要很复杂的文字说…

Jenkins流水线部署+webhook2.0

文章目录1. 环境2. 用到的插件3. 流水线部署脚本1. 环境 Centos7Jenkins2.5.0JDKopen17阿里云仓库 注意&#xff1a;这个版本兼容需要特别注意&#xff0c;要不然会很麻烦 2. 用到的插件 Generic Webhook Trigger 3. 流水线部署脚本 兼容钩子部署&#xff08;webhook&…

IDM下载失败排查

网络连接问题排查检查网络连接是否稳定&#xff0c;确保能够正常访问互联网 测试其他下载工具或浏览器是否能够正常下载 尝试关闭防火墙或杀毒软件&#xff0c;排除安全软件拦截的可能性代理和VPN设置检查确认IDM的代理设置是否正确&#xff0c;是否与系统代理一致 检查是否使用…