Files
aue/文档阅读去器基于vue框架/document-reader.html
2026-06-03 13:16:30 +08:00

904 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>在线文档阅读器</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 24px 32px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 16px;
}
.header h1 {
font-size: 24px;
font-weight: 600;
}
.file-info {
display: flex;
align-items: center;
gap: 12px;
background: rgba(255, 255, 255, 0.2);
padding: 8px 16px;
border-radius: 8px;
}
.file-name {
font-size: 14px;
font-weight: 500;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: white;
color: #667eea;
}
.btn-primary:hover {
background: #f0f0f0;
transform: translateY(-2px);
}
.btn-secondary {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.3);
}
.view-mode-selector {
display: flex;
gap: 8px;
background: rgba(255, 255, 255, 0.1);
padding: 4px;
border-radius: 8px;
}
.view-mode-btn {
padding: 8px 16px;
border: none;
background: transparent;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
transition: all 0.3s ease;
}
.view-mode-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.view-mode-btn.active {
background: white;
color: #667eea;
}
.upload-area {
padding: 60px 40px;
text-align: center;
background: #f8f9fa;
}
.upload-box {
border: 3px dashed #667eea;
border-radius: 16px;
padding: 60px 40px;
background: white;
transition: all 0.3s ease;
}
.upload-box:hover {
border-color: #764ba2;
background: #f0f4ff;
}
.upload-box.dragover {
border-color: #764ba2;
background: #e8ecff;
transform: scale(1.02);
}
.upload-icon {
font-size: 64px;
margin-bottom: 16px;
}
.upload-text {
font-size: 18px;
color: #333;
margin-bottom: 8px;
}
.upload-hint {
font-size: 14px;
color: #666;
margin-bottom: 24px;
}
.supported-formats {
display: flex;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
margin-top: 20px;
}
.format-tag {
padding: 6px 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
}
.viewer-area {
display: none;
min-height: 70vh;
}
.viewer-area.active {
display: block;
}
.pdf-viewer {
width: 100%;
height: 80vh;
border: none;
}
.office-viewer {
width: 100%;
height: 80vh;
border: none;
}
.doc-viewer {
padding: 40px;
max-width: 900px;
margin: 0 auto;
line-height: 1.8;
}
.doc-viewer h1,
.doc-viewer h2,
.doc-viewer h3,
.doc-viewer h4,
.doc-viewer h5,
.doc-viewer h6 {
margin: 24px 0 12px 0;
color: #333;
font-weight: 600;
}
.doc-viewer h1 { font-size: 2em; }
.doc-viewer h2 { font-size: 1.5em; }
.doc-viewer h3 { font-size: 1.25em; }
.doc-viewer h4 { font-size: 1.1em; }
.doc-viewer h5 { font-size: 1em; }
.doc-viewer h6 { font-size: 0.875em; }
.doc-viewer p {
margin: 16px 0;
color: #333;
text-align: justify;
}
.doc-viewer img {
max-width: 100%;
height: auto;
margin: 16px 0;
display: block;
border-radius: 4px;
}
.doc-viewer ul,
.doc-viewer ol {
margin: 16px 0;
padding-left: 24px;
}
.doc-viewer li {
margin: 8px 0;
color: #333;
}
.doc-viewer table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
}
.doc-viewer th,
.doc-viewer td {
border: 1px solid #ddd;
padding: 12px;
text-align: left;
}
.doc-viewer th {
background: #f5f5f5;
font-weight: 600;
}
.doc-viewer blockquote {
border-left: 4px solid #667eea;
padding-left: 16px;
margin: 16px 0;
color: #666;
font-style: italic;
}
.doc-viewer pre {
background: #f5f5f5;
padding: 16px;
border-radius: 8px;
overflow-x: auto;
margin: 16px 0;
}
.doc-viewer code {
background: #f0f0f0;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
}
.doc-viewer a {
color: #667eea;
text-decoration: none;
}
.doc-viewer a:hover {
text-decoration: underline;
}
.excel-viewer {
padding: 20px;
overflow-x: auto;
}
.excel-sheet-tabs {
display: flex;
gap: 4px;
margin-bottom: 16px;
padding: 0 20px;
border-bottom: 2px solid #667eea;
}
.sheet-tab {
padding: 10px 20px;
background: #f0f0f0;
border: none;
border-radius: 8px 8px 0 0;
cursor: pointer;
font-weight: 500;
transition: all 0.3s ease;
}
.sheet-tab:hover {
background: #e0e0e0;
}
.sheet-tab.active {
background: #667eea;
color: white;
}
.excel-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.excel-table th,
.excel-table td {
border: 1px solid #ddd;
padding: 8px 12px;
text-align: left;
white-space: nowrap;
}
.excel-table th {
background: #667eea;
color: white;
font-weight: 600;
position: sticky;
top: 0;
z-index: 10;
}
.excel-table tr:nth-child(even) {
background: #f9f9f9;
}
.excel-table tr:hover {
background: #f0f4ff;
}
.loading {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
z-index: 1000;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 20px;
}
.loading.active {
display: flex;
}
.spinner {
width: 60px;
height: 60px;
border: 4px solid #f0f0f0;
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
font-size: 16px;
color: #667eea;
font-weight: 500;
}
.error {
display: none;
padding: 40px;
text-align: center;
background: #fff5f5;
color: #c53030;
}
.error.active {
display: block;
}
.error-icon {
font-size: 48px;
margin-bottom: 16px;
}
.error-message {
font-size: 16px;
margin-bottom: 24px;
}
.hidden-input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
@media (max-width: 768px) {
.header {
flex-direction: column;
text-align: center;
}
.upload-box {
padding: 40px 20px;
}
.upload-icon {
font-size: 48px;
}
.doc-viewer {
padding: 20px;
}
.excel-viewer {
padding: 10px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📄 在线文档阅读器</h1>
<div class="file-info" id="fileInfo" style="display: none;">
<span class="file-name" id="fileName"></span>
<div class="view-mode-selector" id="viewModeSelector" style="display: none;">
<button class="view-mode-btn active" data-mode="online" onclick="switchViewMode('online')">在线预览</button>
<button class="view-mode-btn" data-mode="local" onclick="switchViewMode('local')">本地解析</button>
</div>
<button class="btn btn-secondary" onclick="clearViewer()">
🗑️ 清空预览
</button>
</div>
<div class="header-buttons">
<button class="btn btn-primary" onclick="document.getElementById('fileInput').click()">
📁 选择文件
</button>
</div>
</div>
<div class="upload-area" id="uploadArea">
<div class="upload-box" id="uploadBox">
<div class="upload-icon">📤</div>
<div class="upload-text">拖拽文件到此处或点击选择文件</div>
<div class="upload-hint">支持 .doc, .docx, .xls, .xlsx, .ppt, .pptx, .pdf</div>
<button class="btn btn-primary" onclick="document.getElementById('fileInput').click()">
选择文件
</button>
<div class="supported-formats">
<span class="format-tag">.doc</span>
<span class="format-tag">.docx</span>
<span class="format-tag">.xls</span>
<span class="format-tag">.xlsx</span>
<span class="format-tag">.ppt</span>
<span class="format-tag">.pptx</span>
<span class="format-tag">.pdf</span>
</div>
</div>
</div>
<div class="viewer-area" id="viewerArea">
<div id="pdfViewerContainer" style="display: none;">
<iframe class="pdf-viewer" id="pdfViewer"></iframe>
</div>
<div id="officeViewerContainer" style="display: none;">
<iframe class="office-viewer" id="officeViewer"></iframe>
</div>
<div id="docViewerContainer" style="display: none;">
<div class="doc-viewer" id="docViewer"></div>
</div>
<div id="excelViewerContainer" style="display: none;">
<div id="excelSheetTabs" class="excel-sheet-tabs"></div>
<div class="excel-viewer">
<table class="excel-table" id="excelViewer"></table>
</div>
</div>
</div>
<div class="error" id="errorArea">
<div class="error-icon"></div>
<div class="error-message" id="errorMessage">文件加载失败</div>
<div style="display: flex; gap: 12px; justify-content: center;">
<button class="btn btn-primary" onclick="clearViewer()">
重新选择文件
</button>
<button class="btn btn-secondary" id="tryLocalBtn" style="display: none;" onclick="tryLocalMode()">
尝试本地解析
</button>
</div>
</div>
</div>
<div class="loading" id="loadingArea">
<div class="spinner"></div>
<div class="loading-text" id="loadingText">正在加载文件...</div>
</div>
<input type="file" class="hidden-input" id="fileInput" accept=".doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf">
<script src="https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.6.0/mammoth.browser.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script>
let currentFile = null;
let excelData = null;
let currentViewMode = 'online';
let currentFileExt = '';
let currentFileUrl = null;
const fileInput = document.getElementById('fileInput');
const uploadBox = document.getElementById('uploadBox');
const uploadArea = document.getElementById('uploadArea');
const viewerArea = document.getElementById('viewerArea');
const errorArea = document.getElementById('errorArea');
const loadingArea = document.getElementById('loadingArea');
const loadingText = document.getElementById('loadingText');
const fileInfo = document.getElementById('fileInfo');
const fileName = document.getElementById('fileName');
const viewModeSelector = document.getElementById('viewModeSelector');
const tryLocalBtn = document.getElementById('tryLocalBtn');
fileInput.addEventListener('change', handleFileSelect);
uploadBox.addEventListener('dragover', (e) => {
e.preventDefault();
uploadBox.classList.add('dragover');
});
uploadBox.addEventListener('dragleave', () => {
uploadBox.classList.remove('dragover');
});
uploadBox.addEventListener('drop', (e) => {
e.preventDefault();
uploadBox.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
processFile(files[0]);
}
});
function handleFileSelect(e) {
const files = e.target.files;
if (files.length > 0) {
processFile(files[0]);
}
}
function processFile(file) {
currentFile = file;
currentFileExt = getFileExtension(file.name);
showLoading();
hideAllViews();
fileName.textContent = file.name;
fileInfo.style.display = 'flex';
const isOfficeFile = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(currentFileExt);
viewModeSelector.style.display = isOfficeFile ? 'flex' : 'none';
setTimeout(() => {
try {
switch (currentFileExt) {
case 'pdf':
handlePDF(file);
break;
case 'docx':
case 'doc':
case 'xlsx':
case 'xls':
case 'pptx':
case 'ppt':
handleOfficeFile(file);
break;
default:
showError('不支持的文件格式');
}
} catch (error) {
console.error('Error processing file:', error);
showError('文件处理失败: ' + error.message);
}
}, 100);
}
function getFileExtension(filename) {
return filename.split('.').pop().toLowerCase();
}
function handlePDF(file) {
currentFileUrl = URL.createObjectURL(file);
const pdfViewer = document.getElementById('pdfViewer');
pdfViewer.src = currentFileUrl;
showViewer('pdf');
hideLoading();
}
function handleOfficeFile(file) {
if (currentViewMode === 'online') {
handleOfficeOnline(file);
} else {
handleOfficeLocal(file);
}
}
function handleOfficeOnline(file) {
loadingText.textContent = '正在使用Microsoft Office Online加载...';
const formData = new FormData();
formData.append('file', file);
currentFileUrl = URL.createObjectURL(file);
const officeViewerUrl = 'https://view.officeapps.live.com/op/view.aspx?src=' + encodeURIComponent(currentFileUrl);
const officeViewer = document.getElementById('officeViewer');
officeViewer.src = officeViewerUrl;
officeViewer.onload = function() {
showViewer('office');
hideLoading();
};
officeViewer.onerror = function() {
console.log('Office Online Viewer failed, trying local mode');
tryLocalMode();
};
setTimeout(() => {
if (loadingArea.classList.contains('active')) {
tryLocalMode();
}
}, 10000);
}
function handleOfficeLocal(file) {
loadingText.textContent = '正在本地解析文件...';
switch (currentFileExt) {
case 'docx':
case 'doc':
handleDocxLocal(file);
break;
case 'xlsx':
case 'xls':
handleExcelLocal(file);
break;
case 'pptx':
case 'ppt':
showError('PPT/PPTX文件本地解析不支持请尝试在线预览模式');
break;
}
}
function handleDocxLocal(file) {
const reader = new FileReader();
reader.onload = function(e) {
const arrayBuffer = e.target.result;
const options = {
includeDefaultStyleMap: true,
styleMap: [
"p[style-name='Heading 1'] => h1:fresh",
"p[style-name='Heading 2'] => h2:fresh",
"p[style-name='Heading 3'] => h3:fresh",
"p[style-name='Heading 4'] => h4:fresh",
"p[style-name='Heading 5'] => h5:fresh",
"p[style-name='Heading 6'] => h6:fresh",
"p[style-name='Title'] => h1:fresh",
"p[style-name='Subtitle'] => h2:fresh",
"p[style-name='Quote'] => blockquote:fresh",
"p[style-name='Intense Quote'] => blockquote:fresh",
"r[style-name='Strong'] => strong",
"r[style-name='Emphasis'] => em",
"r[style-name='Intense Emphasis'] => em",
"r[style-name='Subtle Emphasis'] => em"
]
};
mammoth.convertToHtml({ arrayBuffer: arrayBuffer }, options)
.then(function(result) {
const docViewer = document.getElementById('docViewer');
let html = result.value;
html = html
.replace(/<p>\s*<\/p>/g, '<p>&nbsp;</p>')
.replace(/\n/g, '')
.replace(/\t/g, ' ');
docViewer.innerHTML = html;
showViewer('doc');
hideLoading();
if (result.messages.length > 0) {
console.log('Conversion messages:', result.messages);
}
})
.catch(function(error) {
console.error('Error converting docx:', error);
showError('DOCX文件转换失败: ' + error.message);
});
};
reader.onerror = function() {
showError('文件读取失败');
};
reader.readAsArrayBuffer(file);
}
function handleExcelLocal(file) {
const reader = new FileReader();
reader.onload = function(e) {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
excelData = workbook;
renderSheetTabs();
renderSheet(workbook.SheetNames[0]);
showViewer('excel');
hideLoading();
};
reader.onerror = function() {
showError('文件读取失败');
};
reader.readAsArrayBuffer(file);
}
function renderSheetTabs() {
const tabsContainer = document.getElementById('excelSheetTabs');
tabsContainer.innerHTML = '';
excelData.SheetNames.forEach((sheetName, index) => {
const tab = document.createElement('button');
tab.className = 'sheet-tab' + (index === 0 ? ' active' : '');
tab.textContent = sheetName;
tab.onclick = function() {
document.querySelectorAll('.sheet-tab').forEach(t => t.classList.remove('active'));
this.classList.add('active');
renderSheet(sheetName);
};
tabsContainer.appendChild(tab);
});
}
function renderSheet(sheetName) {
const sheet = excelData.Sheets[sheetName];
const html = XLSX.utils.sheet_to_html(sheet, {
id: 'excelViewer',
editable: false,
header: '',
footer: ''
});
const container = document.getElementById('excelViewerContainer');
const tableContainer = container.querySelector('.excel-viewer');
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
const table = tempDiv.querySelector('table');
if (table) {
table.className = 'excel-table';
tableContainer.innerHTML = '';
tableContainer.appendChild(table);
}
}
function switchViewMode(mode) {
currentViewMode = mode;
document.querySelectorAll('.view-mode-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === mode);
});
if (currentFile) {
hideAllViews();
showLoading();
handleOfficeFile(currentFile);
}
}
function tryLocalMode() {
currentViewMode = 'local';
document.querySelectorAll('.view-mode-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === 'local');
});
hideAllViews();
errorArea.classList.remove('active');
showLoading();
handleOfficeLocal(currentFile);
}
function showViewer(type) {
uploadArea.style.display = 'none';
errorArea.classList.remove('active');
viewerArea.classList.add('active');
document.getElementById('pdfViewerContainer').style.display = 'none';
document.getElementById('officeViewerContainer').style.display = 'none';
document.getElementById('docViewerContainer').style.display = 'none';
document.getElementById('excelViewerContainer').style.display = 'none';
switch (type) {
case 'pdf':
document.getElementById('pdfViewerContainer').style.display = 'block';
break;
case 'office':
document.getElementById('officeViewerContainer').style.display = 'block';
break;
case 'doc':
document.getElementById('docViewerContainer').style.display = 'block';
break;
case 'excel':
document.getElementById('excelViewerContainer').style.display = 'block';
break;
}
}
function hideAllViews() {
viewerArea.classList.remove('active');
uploadArea.style.display = 'none';
errorArea.classList.remove('active');
}
function showLoading() {
loadingArea.classList.add('active');
loadingText.textContent = '正在加载文件...';
}
function hideLoading() {
loadingArea.classList.remove('active');
}
function showError(message) {
hideLoading();
hideAllViews();
errorArea.classList.add('active');
document.getElementById('errorMessage').textContent = message;
const isOfficeFile = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(currentFileExt);
tryLocalBtn.style.display = (isOfficeFile && currentViewMode === 'online') ? 'inline-flex' : 'none';
}
function clearViewer() {
if (currentFileUrl) {
URL.revokeObjectURL(currentFileUrl);
}
currentFile = null;
currentFileUrl = null;
excelData = null;
currentViewMode = 'online';
fileInput.value = '';
document.getElementById('pdfViewer').src = '';
document.getElementById('officeViewer').src = '';
document.getElementById('docViewer').innerHTML = '';
document.getElementById('excelViewer').innerHTML = '';
fileInfo.style.display = 'none';
viewModeSelector.style.display = 'none';
viewerArea.classList.remove('active');
errorArea.classList.remove('active');
uploadArea.style.display = 'block';
}
</script>
</body>
</html>