Commit c6c27c3d by zhangxingmin

push

parent 19f3caf2
<template>
<div class="pdf-viewer-container">
<!-- 控制面板 -->
<div class="control-panel">
<button @click="loadPDF" :disabled="loading">加载PDF</button>
<button @click="downloadAndFixPDF" :disabled="loading">下载并修复PDF</button>
<button @click="testSmallChunk" :disabled="loading">测试小文件下载</button>
<button @click="resetViewer" :disabled="loading">重置</button>
<span class="page-info" v-if="totalPages > 0">{{ currentPage }} 页 / 共 {{ totalPages }}</span>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<div class="progress-info">
<div v-if="downloadProgress < 100">
下载进度: {{ downloadProgress }}%
<div class="progress-bar">
<div class="progress-fill" :style="{ width: downloadProgress + '%' }"></div>
</div>
<div>已下载: {{ loadedProgressText }}</div>
</div>
<div v-else>
PDF处理中...
</div>
</div>
</div>
<!-- 错误信息 -->
<div v-if="error" class="error-message">
{{ error }}
</div>
<!-- PDF 渲染区域 -->
<div ref="pdfContainer" class="pdf-container" :class="{ hidden: loading }">
<iframe
v-if="pdfUrl"
:src="pdfUrl"
width="100%"
height="600px"
style="border: none;"
@load="onIframeLoad"
@error="onIframeError"
></iframe>
<div v-else-if="!loading" class="no-pdf-message">
暂无PDF显示
</div>
</div>
<!-- 调试信息 -->
<div v-if="debugInfo" class="debug-info">
{{ debugInfo }}
</div>
<!-- 下载链接 -->
<div v-if="pdfUrl" style="margin-top: 10px;">
<a :href="pdfUrl" download="document.pdf" style="color: green; font-weight: bold;">
↓ 下载PDF文件
</a>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'PDFViewer',
data() {
return {
fileKey: 'pdf/2025/10/11/company-intro.pdf',
currentPage: 1,
totalPages: 0,
loading: false,
error: '',
debugInfo: '',
totalSize: 0,
loadedSize: 0,
pdfUrl: '',
downloadProgress: 0,
// 配置项
chunkSize: 2 * 1024 * 1024, // 2MB 每块
maxConcurrentDownloads: 3, // 最大并发下载数
};
},
computed: {
loadedProgressText() {
if (this.totalSize === 0) return '';
const loadedMB = (this.loadedSize / 1024 / 1024).toFixed(1);
const totalMB = (this.totalSize / 1024 / 1024).toFixed(1);
return `${loadedMB}MB / ${totalMB}MB`;
}
},
methods: {
async loadPDF() {
await this.downloadAndRenderPDF();
},
async downloadAndFixPDF() {
await this.downloadAndRenderPDF(true);
},
async downloadAndRenderPDF(attemptFix = false) {
this.loading = true;
this.error = '';
this.downloadProgress = 0;
this.pdfUrl = '';
try {
this.debugInfo = '开始加载PDF...';
// 获取文件大小
const fileSize = await this.getFileSize();
this.totalSize = fileSize;
this.debugInfo = `文件大小: ${(fileSize / 1024 / 1024).toFixed(1)}MB`;
// 分块下载完整文件
const fullPdfData = await this.downloadFileByChunks(fileSize);
// 检查并修复PDF
let processedData = fullPdfData;
if (attemptFix) {
processedData = await this.attemptFixPDF(fullPdfData);
}
// 使用 iframe 渲染
await this.renderWithIframe(processedData);
this.debugInfo = `PDF 加载完成,大小: ${(processedData.byteLength / 1024 / 1024).toFixed(1)}MB`;
} catch (error) {
this.error = `PDF 加载失败: ${error.message}`;
console.error('PDF 加载失败:', error);
} finally {
this.loading = false;
}
},
// 尝试修复PDF文件
async attemptFixPDF(pdfData) {
this.debugInfo = '尝试修复PDF文件...';
const dataView = new DataView(pdfData);
const dataArray = new Uint8Array(pdfData);
// 检查文件头
const header = String.fromCharCode(...dataArray.slice(0, 8));
console.log('PDF文件头:', header);
if (header.substring(0, 4) !== '%PDF') {
throw new Error('无效的PDF文件头');
}
// 查找文件尾的xref表和trailer
const trailerInfo = this.findTrailerAndXref(dataArray);
console.log('Trailer信息:', trailerInfo);
if (!trailerInfo.foundTrailer) {
console.warn('未找到完整的trailer,尝试手动添加EOF');
// 手动添加EOF标记
return this.addManualEOF(dataArray);
}
return pdfData;
},
// 查找trailer和xref表
findTrailerAndXref(dataArray) {
const decoder = new TextDecoder('ascii');
const dataString = decoder.decode(dataArray);
// 从文件末尾开始查找
const chunkSize = 1024;
let foundTrailer = false;
let foundStartXref = false;
let xrefOffset = -1;
// 检查最后几个chunk
for (let i = 0; i < 5; i++) {
const start = Math.max(0, dataArray.length - chunkSize * (i + 1));
const end = dataArray.length - chunkSize * i;
const chunk = dataArray.slice(start, end);
const chunkString = decoder.decode(chunk);
if (chunkString.includes('trailer')) {
foundTrailer = true;
}
if (chunkString.includes('startxref')) {
foundStartXref = true;
// 提取xref偏移量
const startxrefMatch = chunkString.match(/startxref\s+(\d+)/);
if (startxrefMatch) {
xrefOffset = parseInt(startxrefMatch[1]);
}
}
if (chunkString.includes('%%EOF')) {
console.log('找到EOF标记');
}
}
return {
foundTrailer,
foundStartXref,
xrefOffset
};
},
// 手动添加EOF标记
addManualEOF(dataArray) {
this.debugInfo = '手动添加PDF结束标记...';
// 创建新的ArrayBuffer,额外空间用于添加EOF
const newBuffer = new ArrayBuffer(dataArray.length + 50);
const newArray = new Uint8Array(newBuffer);
// 复制原始数据
newArray.set(dataArray, 0);
// 添加基本的PDF结束结构
const eofString = '\n\n%%EOF\n';
const encoder = new TextEncoder();
const eofBytes = encoder.encode(eofString);
newArray.set(eofBytes, dataArray.length);
console.log('已添加手动EOF标记');
return newArray.buffer;
},
// 测试小文件下载
async testSmallChunk() {
this.loading = true;
this.error = '';
try {
this.debugInfo = '测试小文件下载...';
// 只下载前5MB
const testSize = 5 * 1024 * 1024;
const testData = await this.downloadChunk(0, testSize - 1);
// 检查下载的数据
await this.analyzePDFChunk(testData);
this.debugInfo = '小文件测试完成';
} catch (error) {
this.error = `测试失败: ${error.message}`;
} finally {
this.loading = false;
}
},
// 分析PDF数据块
async analyzePDFChunk(pdfData) {
const dataArray = new Uint8Array(pdfData);
const decoder = new TextDecoder('ascii');
// 检查文件头
const header = String.fromCharCode(...dataArray.slice(0, 8));
console.log('测试文件头:', header);
// 检查文件内容
const sampleSize = Math.min(1000, dataArray.length);
const sampleStart = decoder.decode(dataArray.slice(0, sampleSize));
const sampleEnd = decoder.decode(dataArray.slice(-sampleSize));
console.log('文件开始样本:', sampleStart.substring(0, 200));
console.log('文件结束样本:', sampleEnd.substring(Math.max(0, sampleEnd.length - 200)));
// 检查关键标记
const hasObj = sampleStart.includes('obj');
const hasEndObj = sampleStart.includes('endobj');
const hasXref = sampleEnd.includes('xref');
const hasTrailer = sampleEnd.includes('trailer');
const hasEOF = sampleEnd.includes('%%EOF');
console.log('PDF结构检查:', {
hasObj,
hasEndObj,
hasXref,
hasTrailer,
hasEOF
});
this.debugInfo = `PDF结构: 对象${hasObj ? '✓' : '✗'} XREF${hasXref ? '✓' : '✗'} Trailer${hasTrailer ? '✓' : '✗'} EOF${hasEOF ? '✓' : '✗'}`;
},
// 分块下载文件
async downloadFileByChunks(totalSize) {
const chunks = [];
const chunkCount = Math.ceil(totalSize / this.chunkSize);
console.log(`开始分块下载,总大小: ${(totalSize / 1024 / 1024).toFixed(1)}MB, 块数: ${chunkCount}`);
// 创建所有下载任务
const chunkPromises = [];
for (let i = 0; i < chunkCount; i++) {
const start = i * this.chunkSize;
const end = Math.min(start + this.chunkSize - 1, totalSize - 1);
chunkPromises.push(() => this.downloadChunkWithProgress(start, end, i, chunkCount));
}
// 并发下载(限制并发数)
for (let i = 0; i < chunkPromises.length; i += this.maxConcurrentDownloads) {
const batch = chunkPromises.slice(i, i + this.maxConcurrentDownloads);
const batchResults = await Promise.all(batch.map(fn => fn()));
chunks.push(...batchResults);
console.log(`已完成 ${Math.min(i + this.maxConcurrentDownloads, chunkCount)}/${chunkCount} 块下载`);
}
// 合并所有块
return this.mergeChunks(chunks);
},
// 下载单个块(带进度)
async downloadChunkWithProgress(start, end, chunkIndex, totalChunks) {
try {
const response = await axios.get('http://10.0.10.215:9106/oss/api/api/file/chunk', {
params: {
fileKey: this.fileKey,
start: start,
end: end
},
responseType: 'arraybuffer',
onDownloadProgress: (progressEvent) => {
// 计算整体下载进度
const chunkProgress = progressEvent.loaded ? (progressEvent.loaded / (end - start + 1)) * 100 : 0;
const overallProgress = (chunkIndex / totalChunks) * 100 + (chunkProgress / totalChunks);
this.downloadProgress = Math.min(100, Math.round(overallProgress));
}
});
this.loadedSize = Math.max(this.loadedSize, end + 1);
return response.data;
} catch (error) {
console.error(`下载块 ${start}-${end} 失败:`, error);
throw error;
}
},
// 下载单个块(无进度)
async downloadChunk(start, end) {
try {
const response = await axios.get('http://10.0.10.215:9106/oss/api/api/file/chunk', {
params: {
fileKey: this.fileKey,
start: start,
end: end
},
responseType: 'arraybuffer'
});
return response.data;
} catch (error) {
console.error(`下载块 ${start}-${end} 失败:`, error);
throw error;
}
},
// 合并所有块
mergeChunks(chunks) {
const totalLength = chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
chunks.forEach(chunk => {
const chunkArray = new Uint8Array(chunk);
result.set(chunkArray, offset);
offset += chunkArray.length;
});
console.log('合并完成,总大小:', totalLength);
return result;
},
// 使用 iframe 渲染
async renderWithIframe(pdfData) {
try {
this.debugInfo = '使用 iframe 渲染 PDF...';
// 创建 Blob 并生成 URL
const blob = new Blob([pdfData], { type: 'application/pdf' });
this.pdfUrl = URL.createObjectURL(blob);
this.debugInfo = 'iframe PDF 渲染完成';
this.currentPage = 1;
this.totalPages = 1; // iframe 模式下我们不知道总页数
} catch (error) {
console.error('iframe 渲染失败:', error);
throw error;
}
},
// iframe 加载成功
onIframeLoad() {
console.log('iframe PDF 加载成功');
this.debugInfo += ' | iframe 加载成功';
},
// iframe 加载失败
onIframeError() {
console.error('iframe PDF 加载失败');
this.error = 'iframe 无法加载 PDF 文档';
},
// 获取文件大小
async getFileSize() {
try {
const response = await axios.get('http://10.0.10.215:9106/oss/api/api/file/chunk', {
params: {
fileKey: this.fileKey,
start: 0,
end: 1024
},
responseType: 'arraybuffer'
});
// 尝试从响应头获取文件大小
const contentRange = response.headers['content-range'];
if (contentRange) {
const match = contentRange.match(/\/(\d+)$/);
if (match) {
return parseInt(match[1]);
}
}
// 使用默认值
return 85 * 1024 * 1024;
} catch (error) {
console.error('获取文件大小失败,使用默认值');
return 85 * 1024 * 1024;
}
},
// 重置查看器
resetViewer() {
if (this.pdfUrl) {
URL.revokeObjectURL(this.pdfUrl);
}
this.currentPage = 1;
this.totalPages = 0;
this.error = '';
this.debugInfo = '';
this.loadedSize = 0;
this.totalSize = 0;
this.pdfUrl = '';
this.downloadProgress = 0;
}
},
beforeUnmount() {
this.resetViewer();
}
};
</script>
<style scoped>
.pdf-viewer-container {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.control-panel {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
padding: 15px;
background: #f5f5f5;
border-radius: 8px;
flex-wrap: wrap;
}
.control-panel button {
padding: 8px 16px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.control-panel button:hover:not(:disabled) {
background: #e9e9e9;
}
.control-panel button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.page-info {
margin-left: auto;
font-weight: bold;
color: #333;
}
.loading-container {
text-align: center;
padding: 40px;
}
.progress-info {
max-width: 400px;
margin: 0 auto;
}
.progress-bar {
width: 100%;
height: 20px;
background: #e0e0e0;
border-radius: 10px;
margin-top: 10px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #4CAF50;
transition: width 0.3s ease;
}
.error-message {
background: #ffebee;
color: #c62828;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
border-left: 4px solid #c62828;
}
.pdf-container {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
background: white;
min-height: 600px;
display: flex;
flex-direction: column;
align-items: center;
}
.pdf-container.hidden {
display: none;
}
.no-pdf-message {
color: #666;
font-style: italic;
text-align: center;
margin-top: 50px;
}
.debug-info {
margin-top: 20px;
padding: 10px;
background: #e3f2fd;
border-radius: 4px;
font-size: 12px;
color: #1565c0;
}
</style>
\ No newline at end of file
......@@ -278,7 +278,7 @@
</el-radio-group>
</el-form-item>
<el-form-item
label="项目图标"
label="项目图标"0
prop="logoUrl"
:rules="[{ required: true, message: '请上传项目图标', trigger: 'change' }]"
>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment