ofd文件转pdf

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

添加依赖:

XML 复制代码
        <!-- 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>

控制层代码实现:

java 复制代码
@CrossOrigin
@RestController
@RequestMapping("/tool")
public class ToolsController {

    @Autowired
    private 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文件打包成ZIP
            try (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层代码实现

java 复制代码
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;
}
java 复制代码
@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字节数组的映射
     */
    @Override
    public 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();
    }
}

前端实现:

TypeScript 复制代码
<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-upload
          ref="upload"
          class="upload-area"
          action="#"
          :http-request="handleUpload"
          :on-change="handleFileChange"
          :on-remove="handleFileRemove"
          :before-upload="beforeUpload"
          :file-list="fileList"
          :auto-upload="false"
          multiple
          accept=".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"
        border
        size="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-progress
              v-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-button
              v-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-button
              v-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-dialog
      title="转换进度"
      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-button
          type="default"
          @click="cancelConversion"
          :disabled="!isCancellable"
        >
          取消转换
        </el-button>
      </template>
    </el-dialog>

    <!-- 转换完成提示 -->
    <el-dialog
      title="转换完成"
      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-button
            type="primary"
            @click="downloadAllFiles"
            :disabled="successCount === 0"
          >
            <el-icon class="mr-1"><Download /></el-icon>下载全部
          </el-button>
          <el-button
            type="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 {
    // 创建FormData
    const 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;
      // 创建下载URL
      file.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>
相关推荐
敖正炀3 分钟前
ReentrantReadWriteLock、ReentrantLock、synchronized 对比
java
cike_y14 分钟前
Java反序列化漏洞-Shiro721流程分析
java·反序列化·shiro框架
极创信息36 分钟前
信创系统认证服务怎么做?从适配到验收全流程指南
java·大数据·运维·tomcat·健康医疗
格鸰爱童话42 分钟前
向AI学习项目技能(六)
java·人工智能·spring boot·python·学习
白宇横流学长1 小时前
停车场管理系统的设计与实现
java
Flittly1 小时前
【SpringAIAlibaba新手村系列】(18)Agent 智能体与今日菜单应用
java·spring boot·agent
木井巳1 小时前
【递归算法】目标和
java·算法·leetcode·决策树·深度优先
亦暖筑序1 小时前
手写 Spring AI Agent:让大模型自主规划任务,ReAct 模式全流程拆解
java·人工智能·spring
敖正炀1 小时前
ReentrantLock 与 synchronized对比
java
XiYang-DING1 小时前
【Java】二叉搜索树(BST)
java·开发语言·python