@anohanafes/offline-document-viewer
Version:
🔒 完全离线的多格式文档预览器 - 支持PDF、DOCX、PPTX、XLSX、CSV,按需加载,支持URL预览
541 lines (478 loc) • 22.1 kB
HTML
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>URL文档预览器 - 支持PDF/DOCX/PPTX</title>
<link rel="stylesheet" href="css/styles.css">
<link rel="stylesheet" href="css/pptx-styles.css">
<style>
/* URL输入框特定样式 */
.url-input-section {
background: #f8f9fa;
border-radius: 12px;
padding: 2rem;
margin-bottom: 2rem;
border: 2px dashed #e1dfdd;
transition: all 0.3s ease;
}
.url-input-section:hover {
border-color: #2196F3;
background: #f5f9ff;
}
.url-input-container {
display: flex;
gap: 1rem;
align-items: center;
margin-bottom: 1rem;
}
.url-input {
flex: 1;
padding: 0.8rem 1rem;
border: 2px solid #e1dfdd;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
.url-input:focus {
outline: none;
border-color: #2196F3;
box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.1);
}
.load-btn {
background: #2196F3;
color: white;
border: none;
padding: 0.8rem 1.5rem;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background 0.3s ease;
white-space: nowrap;
}
.load-btn:hover:not(:disabled) {
background: #1976D2;
}
.load-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.url-examples {
font-size: 0.9rem;
color: #666;
margin-top: 1rem;
}
.url-examples strong {
color: #333;
}
.url-examples code {
background: #f0f0f0;
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-family: monospace;
font-size: 0.85rem;
}
.download-progress {
margin-top: 1rem;
display: none;
}
.progress-bar {
width: 100%;
height: 8px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #2196F3;
transition: width 0.3s ease;
width: 0%;
}
.progress-text {
margin-top: 0.5rem;
font-size: 0.9rem;
color: #666;
text-align: center;
}
.error-message {
text-align: center;
padding: 2rem;
background: #fff5f5;
border: 1px solid #fed7d7;
border-radius: 8px;
color: #e53e3e;
margin: 2rem auto;
max-width: 500px;
}
.error-message h3 {
margin-top: 0;
margin-bottom: 1rem;
}
.error-message code {
background: #f7fafc;
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-family: monospace;
color: #2d3748;
}
</style>
</head>
<body>
<div class="app-container">
<!-- 头部 -->
<header class="app-header">
<div class="header-content">
<h1>🌐 URL文档预览器</h1>
<p>输入文档URL • 在线预览 • 支持多种格式</p>
<div style="margin-top: 0.5rem;">
<a href="index.html" style="color: white; text-decoration: none; opacity: 0.8;">
← 返回本地文件预览
</a>
</div>
</div>
</header>
<!-- 主要内容区域 -->
<main class="main-content">
<!-- URL输入区域 -->
<section class="url-input-section">
<div class="upload-icon" style="text-align: center; font-size: 3rem; margin-bottom: 1rem;">🔗</div>
<h3 style="text-align: center; margin-bottom: 1.5rem;">输入文档URL地址</h3>
<div class="url-input-container">
<input type="url"
id="urlInput"
class="url-input"
placeholder="请输入文档URL,如:https://example.com/document.pptx"
value="">
<button id="loadBtn" class="load-btn">📥 加载预览</button>
</div>
<!-- 隐藏的文件输入元素(供document-viewer.js使用) -->
<div id="uploadArea" style="display: none;">
<input type="file" id="fileInput" style="display: none;">
</div>
<div class="download-progress" id="downloadProgress">
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="progress-text" id="progressText">准备下载...</div>
</div>
<div class="supported-formats" style="text-align: center; margin-bottom: 1rem;">
<span class="format-badge pdf">PDF</span>
<span class="format-badge docx">DOCX</span>
<span class="format-badge pptx">PPTX</span>
<span class="format-badge" style="background: #28a745;">XLSX</span>
<span class="format-badge" style="background: #6f42c1;">CSV</span>
</div>
<div class="url-examples">
<strong>支持的URL示例:</strong><br>
• 直接文件链接:<code>https://example.com/demo.pptx</code><br>
• CDN文件:<code>https://cdn.example.com/files/report.pdf</code><br>
• 文件分享服务:<code>https://drive.google.com/uc?id=...</code><br>
<br>
<strong>注意:</strong>目标服务器必须支持CORS跨域访问
</div>
</section>
<!-- 文档信息栏(复用原有结构) -->
<section class="document-info" id="documentInfo" style="display: none;">
<div class="info-item">
<span class="info-label">文件名:</span>
<span class="info-value" id="fileName"></span>
</div>
<div class="info-item">
<span class="info-label">文件大小:</span>
<span class="info-value" id="fileSize"></span>
</div>
<div class="info-item">
<span class="info-label">文件类型:</span>
<span class="info-value" id="fileType"></span>
</div>
<div class="info-item">
<span class="info-label">文件来源:</span>
<span class="info-value" id="fileSource"></span>
</div>
</section>
<!-- 预览控制栏(复用原有结构) -->
<section class="viewer-controls" id="viewerControls" style="display: none;">
<!-- PDF控制 -->
<div class="pdf-controls" id="pdfControls" style="display: none;">
<button class="btn btn-secondary" id="prevPage" disabled>
<span>◀</span> 上一页
</button>
<span class="page-info" id="pageInfo">第 1 页</span>
<button class="btn btn-secondary" id="nextPage">
下一页 <span>▶</span>
</button>
<div class="zoom-controls">
<button class="btn btn-sm" id="zoomOut">🔍-</button>
<span id="zoomLevel">100%</span>
<button class="btn btn-sm" id="zoomIn">🔍+</button>
<button class="btn btn-sm" id="fitWidth">适应宽度</button>
</div>
</div>
<!-- DOCX控制 -->
<div class="docx-controls" id="docxControls" style="display: none;">
<button class="btn btn-secondary" id="docxZoomOut">🔍-</button>
<span id="docxZoomLevel">100%</span>
<button class="btn btn-secondary" id="docxZoomIn">🔍+</button>
<button class="btn btn-secondary" id="docxFitWidth">适应宽度</button>
</div>
</section>
<!-- 预览区域(复用原有结构) -->
<section class="viewer-area">
<div class="viewer-container" id="viewerContainer">
<div class="welcome-message">
<div class="welcome-icon">🔗</div>
<h2>欢迎使用URL文档预览器</h2>
<p>请输入一个文档URL开始预览</p>
<div class="features">
<div class="feature">
<span class="feature-icon">🌐</span>
<span>在线预览</span>
</div>
<div class="feature">
<span class="feature-icon">⚡</span>
<span>快速加载</span>
</div>
<div class="feature">
<span class="feature-icon">🎯</span>
<span>多种格式</span>
</div>
</div>
</div>
</div>
<!-- 加载状态 -->
<div class="loading-overlay" id="loadingOverlay" style="display: none;">
<div class="loading-spinner"></div>
<div class="loading-text">正在处理文档...</div>
</div>
</section>
</main>
<!-- 底部信息 -->
<footer class="app-footer">
<p>🌐 在线文档预览 • 🔗 支持URL链接 • ⚡ 快速加载</p>
</footer>
</div>
<!-- 基础依赖(必需) -->
<script src="lib/jquery.min.js"></script>
<!-- 动态资源管理器 -->
<script src="js/resource-manager.js"></script>
<script src="js/document-processor-enhancer.js"></script>
<!-- 主应用脚本 -->
<script src="js/document-viewer.js"></script>
<!-- URL处理专用脚本 -->
<script>
class URLDocumentViewer {
constructor() {
this.documentViewer = null;
this.init();
}
async init() {
try {
// 等待原有的文档查看器初始化完成
await this.waitForDocumentViewer();
// 获取已经初始化的文档查看器实例
this.documentViewer = window.documentViewer;
if (!this.documentViewer) {
throw new Error('文档查看器实例不可用');
}
// 绑定URL处理事件
this.bindURLEvents();
console.log('✅ URL文档预览器初始化成功');
} catch (error) {
console.error('❌ URL文档预览器初始化失败:', error);
this.showInitializationError(error.message);
}
}
async waitForDocumentViewer() {
// 等待document-viewer.js完成初始化
return new Promise((resolve, reject) => {
let attempts = 0;
const maxAttempts = 50; // 最多等待5秒
const checkViewer = () => {
attempts++;
if (window.documentViewer && window.documentViewer.fileHandler) {
// 文档查看器已准备就绪
resolve();
} else if (attempts >= maxAttempts) {
// 超时
reject(new Error('文档查看器初始化超时'));
} else {
// 继续等待
setTimeout(checkViewer, 100);
}
};
checkViewer();
});
}
showInitializationError(errorMessage) {
const container = document.getElementById('viewerContainer');
if (container) {
container.innerHTML = `
<div class="error-message">
<h3>❌ URL预览器初始化失败</h3>
<p>${errorMessage}</p>
<p>请刷新页面重试,或检查浏览器控制台获取详细错误信息</p>
</div>
`;
}
}
bindURLEvents() {
const urlInput = document.getElementById('urlInput');
const loadBtn = document.getElementById('loadBtn');
// 回车键加载
urlInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && urlInput.value.trim()) {
this.loadFromURL(urlInput.value.trim());
}
});
// 按钮点击加载
loadBtn.addEventListener('click', () => {
const url = urlInput.value.trim();
if (url) {
this.loadFromURL(url);
}
});
// 实时验证URL格式
urlInput.addEventListener('input', () => {
const url = urlInput.value.trim();
loadBtn.disabled = !url || !this.isValidURL(url);
});
}
isValidURL(string) {
try {
const url = new URL(string);
return url.protocol === 'http:' || url.protocol === 'https:';
} catch (_) {
return false;
}
}
getFileNameFromURL(url) {
try {
const urlObj = new URL(url);
const pathname = urlObj.pathname;
const filename = pathname.split('/').pop();
return filename && filename.includes('.') ? filename : 'document';
} catch (e) {
return 'document';
}
}
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
showProgress() {
const progressDiv = document.getElementById('downloadProgress');
progressDiv.style.display = 'block';
}
hideProgress() {
const progressDiv = document.getElementById('downloadProgress');
progressDiv.style.display = 'none';
}
updateProgress(loaded, total) {
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
if (total > 0) {
const percent = (loaded / total) * 100;
progressFill.style.width = percent + '%';
progressText.textContent = `下载中... ${this.formatFileSize(loaded)} / ${this.formatFileSize(total)} (${Math.round(percent)}%)`;
} else {
progressFill.style.width = '50%';
progressText.textContent = `下载中... ${this.formatFileSize(loaded)}`;
}
}
async loadFromURL(url) {
const loadBtn = document.getElementById('loadBtn');
const urlInput = document.getElementById('urlInput');
try {
// 禁用按钮,显示进度
loadBtn.disabled = true;
loadBtn.textContent = '📥 加载中...';
this.showProgress();
// 开始下载
console.log('🌐 开始从URL下载文件:', url);
const response = await fetch(url, {
method: 'GET',
mode: 'cors' // 明确指定CORS模式
});
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
}
// 获取文件信息
const contentLength = response.headers.get('content-length');
const totalSize = contentLength ? parseInt(contentLength, 10) : 0;
const filename = this.getFileNameFromURL(url);
// 创建Reader来跟踪下载进度
const reader = response.body.getReader();
const chunks = [];
let receivedLength = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedLength += value.length;
// 更新进度
this.updateProgress(receivedLength, totalSize);
}
// 合并所有chunks
const arrayBuffer = new Uint8Array(receivedLength);
let position = 0;
for (let chunk of chunks) {
arrayBuffer.set(chunk, position);
position += chunk.length;
}
console.log('✅ 文件下载完成,大小:', this.formatFileSize(receivedLength));
// 创建File对象(这是关键步骤!)
const file = new File([arrayBuffer], filename, {
type: this.getMimeTypeFromExtension(filename)
});
// 隐藏进度条
this.hideProgress();
// 更新文件来源显示
const fileSource = document.getElementById('fileSource');
if (fileSource) {
fileSource.textContent = url;
}
// 调用现有的文件处理逻辑!
await this.documentViewer.fileHandler.handleFile(file);
console.log('🎉 URL文档预览成功!');
} catch (error) {
console.error('❌ URL加载失败:', error);
this.hideProgress();
let errorMessage = '文档加载失败: ';
if (error.name === 'TypeError' && error.message.includes('CORS')) {
errorMessage += '目标服务器不支持跨域访问(CORS)';
} else if (error.message.includes('HTTP错误')) {
errorMessage += error.message;
} else {
errorMessage += error.message;
}
// 使用现有的错误显示功能
this.documentViewer.uiController.showError(errorMessage);
} finally {
// 恢复按钮状态
loadBtn.disabled = false;
loadBtn.textContent = '📥 加载预览';
}
}
getMimeTypeFromExtension(filename) {
const ext = filename.toLowerCase().split('.').pop();
const mimeTypes = {
'pdf': 'application/pdf',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'csv': 'text/csv'
};
return mimeTypes[ext] || 'application/octet-stream';
}
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
new URLDocumentViewer();
});
</script>
</body>
</html>