You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

641 lines
19 KiB

/**
* 文件预览工具类
* 支持图片、视频、音频、PDF、TXT、Excel、Word、PPT等格式的预览
*/
import XLSX from 'xlsx'
// 文件类型分类
const FILE_TYPES = {
image: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'],
video: ['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv', 'ogg'],
audio: ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma'],
pdf: ['pdf'],
txt: ['txt', 'log', 'json', 'xml', 'csv', 'html', 'htm', 'css', 'js'],
excel: ['xls', 'xlsx'],
word: ['doc', 'docx'],
ppt: ['ppt', 'pptx']
}
/**
* 获取文件后缀
*/
export function getFileSuffix(fileName) {
if (!fileName || !fileName.includes('.')) return ''
return fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
}
/**
* 获取文件类型
*/
export function getFileType(fileName) {
const suffix = getFileSuffix(fileName)
for (const [type, extensions] of Object.entries(FILE_TYPES)) {
if (extensions.includes(suffix)) return type
}
return 'unknown'
}
/**
* 获取MIME类型
*/
export function getMimeType(suffix) {
const mimeMap = {
// 图片
jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', bmp: 'image/bmp', webp: 'image/webp', svg: 'image/svg+xml', ico: 'image/x-icon',
// 视频
mp4: 'video/mp4', avi: 'video/x-msvideo', mov: 'video/quicktime', wmv: 'video/x-ms-wmv', flv: 'video/x-flv', webm: 'video/webm', mkv: 'video/x-matroska', ogg: 'video/ogg',
// 音频
mp3: 'audio/mpeg', wav: 'audio/wav', flac: 'audio/flac', aac: 'audio/aac', m4a: 'audio/mp4', wma: 'audio/x-ms-wma',
// 文档
pdf: 'application/pdf',
txt: 'text/plain', log: 'text/plain', json: 'application/json', xml: 'application/xml', csv: 'text/csv', html: 'text/html', htm: 'text/html', css: 'text/css', js: 'application/javascript',
// Office
xls: 'application/vnd.ms-excel',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
doc: 'application/msword',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
ppt: 'application/vnd.ms-powerpoint',
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
}
return mimeMap[suffix] || 'application/octet-stream'
}
/**
* 将Blob转换为ArrayBuffer
* @param {Blob} blob - Blob对象
* @returns {Promise<ArrayBuffer>}
*/
export function blobToArrayBuffer(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result)
reader.onerror = () => reject(new Error('Blob转换失败'))
reader.readAsArrayBuffer(blob)
})
}
/**
* 预览图片文件
* @param {Blob} blobData - 文件数据
* @param {String} fileName - 文件名
*/
export function previewImage(blobData, fileName) {
try {
const fileURL = URL.createObjectURL(blobData)
const previewWindow = window.open('', '_blank')
if (!previewWindow) {
// 如果弹窗被阻止,尝试直接打开
window.open(fileURL, '_blank')
return true
}
previewWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>图片预览 - ${fileName}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
background: #1a1a1a;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h2 { font-size: 16px; font-weight: 500; }
.toolbar { display: flex; gap: 10px; }
.toolbar button {
background: rgba(255,255,255,0.2);
border: none;
color: white;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.toolbar button:hover { background: rgba(255,255,255,0.3); }
.image-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
overflow: auto;
}
.image-container img {
max-width: 100%;
max-height: calc(100vh - 80px);
object-fit: contain;
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
transition: transform 0.3s;
}
</style>
</head>
<body>
<div class="header">
<h2>📷 ${fileName}</h2>
<div class="toolbar">
<button onclick="zoomIn()">放大 +</button>
<button onclick="zoomOut()">缩小 -</button>
<button onclick="resetZoom()">重置</button>
<button onclick="downloadImage()">下载</button>
</div>
</div>
<div class="image-container">
<img id="previewImg" src="${fileURL}" alt="${fileName}">
</div>
<script>
var scale = 1;
var img = document.getElementById('previewImg');
function zoomIn() { scale = Math.min(scale + 0.25, 5); img.style.transform = 'scale(' + scale + ')'; }
function zoomOut() { scale = Math.max(scale - 0.25, 0.25); img.style.transform = 'scale(' + scale + ')'; }
function resetZoom() { scale = 1; img.style.transform = 'scale(1)'; }
function downloadImage() {
var a = document.createElement('a');
a.href = '${fileURL}';
a.download = '${fileName}';
a.click();
}
<\/script>
</body>
</html>
`)
previewWindow.document.close()
return true
} catch (error) {
console.error('图片预览失败:', error)
return false
}
}
/**
* 预览视频文件
* @param {Blob} blobData - 文件数据
* @param {String} fileName - 文件名
* @param {String} mimeType - MIME类型
*/
export function previewVideo(blobData, fileName, mimeType) {
try {
const fileURL = URL.createObjectURL(blobData)
const previewWindow = window.open('', '_blank')
if (!previewWindow) {
window.open(fileURL, '_blank')
return true
}
previewWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>视频播放 - ${fileName}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
background: #1a1a1a;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
color: white;
padding: 12px 20px;
}
.header h2 { font-size: 16px; font-weight: 500; }
.video-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
video {
max-width: 100%;
max-height: calc(100vh - 80px);
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
}
</style>
</head>
<body>
<div class="header">
<h2>🎬 ${fileName}</h2>
</div>
<div class="video-container">
<video controls autoplay>
<source src="${fileURL}" type="${mimeType}">
您的浏览器不支持视频播放
</video>
</div>
</body>
</html>
`)
previewWindow.document.close()
return true
} catch (error) {
console.error('视频预览失败:', error)
return false
}
}
/**
* 预览音频文件
* @param {Blob} blobData - 文件数据
* @param {String} fileName - 文件名
* @param {String} mimeType - MIME类型
*/
export function previewAudio(blobData, fileName, mimeType) {
try {
const fileURL = URL.createObjectURL(blobData)
const previewWindow = window.open('', '_blank')
if (!previewWindow) {
window.open(fileURL, '_blank')
return true
}
previewWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>音频播放 - ${fileName}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.audio-card {
background: rgba(255,255,255,0.1);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 40px;
text-align: center;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
}
.icon { font-size: 64px; margin-bottom: 20px; }
.title { color: white; font-size: 18px; margin-bottom: 30px; word-break: break-all; max-width: 400px; }
audio { width: 350px; }
</style>
</head>
<body>
<div class="audio-card">
<div class="icon">🎵</div>
<div class="title">${fileName}</div>
<audio controls autoplay>
<source src="${fileURL}" type="${mimeType}">
您的浏览器不支持音频播放
</audio>
</div>
</body>
</html>
`)
previewWindow.document.close()
return true
} catch (error) {
console.error('音频预览失败:', error)
return false
}
}
/**
* 预览PDF文件
* @param {Blob} blobData - 文件数据
* @param {String} fileName - 文件名
*/
export function previewPDF(blobData, fileName) {
try {
const fileURL = URL.createObjectURL(blobData)
const previewWindow = window.open('', '_blank')
if (!previewWindow) {
window.open(fileURL, '_blank')
return true
}
previewWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>PDF预览 - ${fileName}</title>
<style>
* { margin: 0; padding: 0; }
body { height: 100vh; }
iframe { width: 100%; height: 100%; border: none; }
</style>
</head>
<body>
<iframe src="${fileURL}"></iframe>
</body>
</html>
`)
previewWindow.document.close()
return true
} catch (error) {
console.error('PDF预览失败:', error)
return false
}
}
/**
* 预览文本文件
* @param {Blob} blobData - 文件数据
* @param {String} fileName - 文件名
*/
export async function previewText(blobData, fileName) {
try {
const text = await blobData.text()
const previewWindow = window.open('', '_blank')
if (!previewWindow) {
throw new Error('无法打开预览窗口')
}
// 转义HTML特殊字符
const escapedText = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
previewWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>文本预览 - ${fileName}</title>
<style>
body {
font-family: 'Consolas', 'Monaco', monospace;
padding: 20px;
background: #1e1e1e;
color: #d4d4d4;
line-height: 1.6;
}
.header {
background: #333;
margin: -20px -20px 20px -20px;
padding: 12px 20px;
color: #fff;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
</style>
</head>
<body>
<div class="header">📄 ${fileName}</div>
<pre>${escapedText}</pre>
</body>
</html>
`)
previewWindow.document.close()
return true
} catch (error) {
console.error('文本预览失败:', error)
return false
}
}
/**
* 预览Excel文件
* @param {ArrayBuffer} arrayBuffer - 文件数据(ArrayBuffer格式)
* @param {String} fileName - 文件名
*/
export function previewExcel(arrayBuffer, fileName) {
try {
const workbook = XLSX.read(arrayBuffer, { type: 'array' })
const sheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[sheetName]
const htmlString = XLSX.utils.sheet_to_html(worksheet, { editable: false })
// 预先生成所有sheet的HTML
const sheetsHtml = workbook.SheetNames.map(name => {
return XLSX.utils.sheet_to_html(workbook.Sheets[name], { editable: false })
})
// 创建预览窗口
const previewWindow = window.open('', '_blank')
if (!previewWindow) {
throw new Error('无法打开预览窗口,请检查浏览器是否阻止了弹出窗口')
}
previewWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Excel预览 - ${fileName}</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; background: #f5f5f5; }
.header { background: #409EFF; color: white; padding: 15px 20px; margin: -20px -20px 20px -20px; }
.header h2 { margin: 0; font-size: 18px; }
.sheet-tabs { background: #fff; padding: 10px; margin-bottom: 10px; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.sheet-tab { display: inline-block; padding: 8px 16px; margin-right: 5px; background: #f0f0f0; border-radius: 4px; cursor: pointer; }
.sheet-tab.active { background: #409EFF; color: white; }
.table-container { background: white; padding: 15px; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); overflow: auto; max-height: calc(100vh - 180px); }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; white-space: nowrap; }
th { background: #f5f7fa; font-weight: 600; position: sticky; top: 0; }
tr:nth-child(even) { background: #fafafa; }
tr:hover { background: #f0f7ff; }
</style>
</head>
<body>
<div class="header">
<h2>Excel预览 - ${fileName}</h2>
</div>
<div class="sheet-tabs">
${workbook.SheetNames.map((name, index) =>
`<span class="sheet-tab ${index === 0 ? 'active' : ''}" onclick="showSheet(${index})">${name}</span>`
).join('')}
</div>
<div class="table-container" id="tableContainer">
${htmlString}
</div>
<script>
var sheets = ${JSON.stringify(sheetsHtml)};
function showSheet(index) {
document.getElementById('tableContainer').innerHTML = sheets[index];
var tabs = document.querySelectorAll('.sheet-tab');
tabs.forEach(function(tab, i) {
tab.className = 'sheet-tab' + (i === index ? ' active' : '');
});
}
<\/script>
</body>
</html>
`)
previewWindow.document.close()
return true
} catch (error) {
console.error('Excel预览失败:', error)
return false
}
}
/**
* 预览Word文件(下载后打开)
* @param {Blob} blobData - 文件数据(Blob格式)
* @param {String} fileName - 文件名
*/
export function previewWord(blobData, fileName) {
// 对于Word文件,直接下载让用户用本地软件打开
const url = URL.createObjectURL(blobData)
// 创建下载链接
const link = document.createElement('a')
link.href = url
link.download = fileName
link.click()
URL.revokeObjectURL(url)
return true
}
/**
* 预览PPT文件(下载后打开)
* @param {Blob} blobData - 文件数据(Blob格式)
* @param {String} fileName - 文件名
*/
export function previewPPT(blobData, fileName) {
// 对于PPT文件,直接下载让用户用本地软件打开
const url = URL.createObjectURL(blobData)
// 创建下载链接
const link = document.createElement('a')
link.href = url
link.download = fileName
link.click()
URL.revokeObjectURL(url)
return true
}
/**
* 通用文件预览方法
* @param {ArrayBuffer|Blob} data - 文件数据(可能是Blob或ArrayBuffer)
* @param {String} fileName - 文件名
* @returns {Promise} 预览结果
*/
export async function previewFile(data, fileName) {
const suffix = getFileSuffix(fileName)
const fileType = getFileType(fileName)
const mimeType = getMimeType(suffix)
try {
// 确保data是正确的格式
let blobData = data
let arrayBufferData = null
// 如果data是Blob,保存引用;如果不是,创建Blob
if (!(data instanceof Blob)) {
blobData = new Blob([data], { type: mimeType })
}
switch (fileType) {
case 'image':
// 图片预览 - 在新窗口中显示带工具栏的预览
if (previewImage(blobData, fileName)) {
return { success: true, type: 'image' }
} else {
throw new Error('图片预览失败')
}
case 'video':
// 视频预览 - 在新窗口中播放
if (previewVideo(blobData, fileName, mimeType)) {
return { success: true, type: 'video' }
} else {
throw new Error('视频预览失败')
}
case 'audio':
// 音频预览 - 在新窗口中播放
if (previewAudio(blobData, fileName, mimeType)) {
return { success: true, type: 'audio' }
} else {
throw new Error('音频预览失败')
}
case 'pdf':
// PDF预览
if (previewPDF(blobData, fileName)) {
return { success: true, type: 'pdf' }
} else {
throw new Error('PDF预览失败')
}
case 'txt':
// 文本文件预览
if (await previewText(blobData, fileName)) {
return { success: true, type: 'txt' }
} else {
throw new Error('文本预览失败')
}
case 'excel':
// Excel文件需要ArrayBuffer格式
arrayBufferData = await blobToArrayBuffer(blobData)
if (previewExcel(arrayBufferData, fileName)) {
return { success: true, type: 'excel' }
} else {
throw new Error('Excel预览失败')
}
case 'word':
// Word文件下载后打开
previewWord(blobData, fileName)
return { success: true, type: 'word', message: 'Word文件已下载,请使用本地软件打开' }
case 'ppt':
// PPT文件下载后打开
previewPPT(blobData, fileName)
return { success: true, type: 'ppt', message: 'PPT文件已下载,请使用本地软件打开' }
default:
// 未知类型,下载处理
const downloadUrl = URL.createObjectURL(blobData)
const link = document.createElement('a')
link.href = downloadUrl
link.download = fileName
link.click()
URL.revokeObjectURL(downloadUrl)
return { success: true, type: 'download', message: '文件已下载' }
}
} catch (error) {
console.error('文件预览错误:', error)
throw error
}
}
export default {
getFileSuffix,
getFileType,
getMimeType,
previewImage,
previewVideo,
previewAudio,
previewPDF,
previewText,
previewExcel,
previewWord,
previewPPT,
previewFile
}