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>
相关推荐
程序员良辰1 小时前
Spring与SpringBoot:从手动挡到自动挡的Java开发进化论
java·spring boot·spring
鹦鹉0072 小时前
SpringAOP实现
java·服务器·前端·spring
练习时长两年半的程序员小胡2 小时前
JVM 性能调优实战:让系统性能 “飞” 起来的核心策略
java·jvm·性能调优·jvm调优
崎岖Qiu3 小时前
【JVM篇11】:分代回收与GC回收范围的分类详解
java·jvm·后端·面试
27669582924 小时前
东方航空 m端 wasm req res分析
java·python·node·wasm·东方航空·东航·东方航空m端
许苑向上4 小时前
Spring Boot 自动装配底层源码实现详解
java·spring boot·后端
喵叔哟5 小时前
31.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--单体转微服务--财务服务--收支分类
java·微服务·.net
codu4u13145 小时前
Maven中的bom和父依赖
java·linux·maven
呦呦鹿鸣Rzh5 小时前
微服务快速入门
java·微服务·架构
今天也好累6 小时前
C 语言基础第16天:指针补充
java·c语言·数据结构·笔记·学习·算法