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

  1. /**
  2. * 文件预览工具类
  3. * 支持图片视频音频PDFTXTExcelWordPPT等格式的预览
  4. */
  5. import XLSX from 'xlsx'
  6. // 文件类型分类
  7. const FILE_TYPES = {
  8. image: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'],
  9. video: ['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv', 'ogg'],
  10. audio: ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma'],
  11. pdf: ['pdf'],
  12. txt: ['txt', 'log', 'json', 'xml', 'csv', 'html', 'htm', 'css', 'js'],
  13. excel: ['xls', 'xlsx'],
  14. word: ['doc', 'docx'],
  15. ppt: ['ppt', 'pptx']
  16. }
  17. /**
  18. * 获取文件后缀
  19. */
  20. export function getFileSuffix(fileName) {
  21. if (!fileName || !fileName.includes('.')) return ''
  22. return fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
  23. }
  24. /**
  25. * 获取文件类型
  26. */
  27. export function getFileType(fileName) {
  28. const suffix = getFileSuffix(fileName)
  29. for (const [type, extensions] of Object.entries(FILE_TYPES)) {
  30. if (extensions.includes(suffix)) return type
  31. }
  32. return 'unknown'
  33. }
  34. /**
  35. * 获取MIME类型
  36. */
  37. export function getMimeType(suffix) {
  38. const mimeMap = {
  39. // 图片
  40. 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',
  41. // 视频
  42. 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',
  43. // 音频
  44. mp3: 'audio/mpeg', wav: 'audio/wav', flac: 'audio/flac', aac: 'audio/aac', m4a: 'audio/mp4', wma: 'audio/x-ms-wma',
  45. // 文档
  46. pdf: 'application/pdf',
  47. 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',
  48. // Office
  49. xls: 'application/vnd.ms-excel',
  50. xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  51. doc: 'application/msword',
  52. docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  53. ppt: 'application/vnd.ms-powerpoint',
  54. pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
  55. }
  56. return mimeMap[suffix] || 'application/octet-stream'
  57. }
  58. /**
  59. * 将Blob转换为ArrayBuffer
  60. * @param {Blob} blob - Blob对象
  61. * @returns {Promise<ArrayBuffer>}
  62. */
  63. export function blobToArrayBuffer(blob) {
  64. return new Promise((resolve, reject) => {
  65. const reader = new FileReader()
  66. reader.onload = () => resolve(reader.result)
  67. reader.onerror = () => reject(new Error('Blob转换失败'))
  68. reader.readAsArrayBuffer(blob)
  69. })
  70. }
  71. /**
  72. * 预览图片文件
  73. * @param {Blob} blobData - 文件数据
  74. * @param {String} fileName - 文件名
  75. */
  76. export function previewImage(blobData, fileName) {
  77. try {
  78. const fileURL = URL.createObjectURL(blobData)
  79. const previewWindow = window.open('', '_blank')
  80. if (!previewWindow) {
  81. // 如果弹窗被阻止,尝试直接打开
  82. window.open(fileURL, '_blank')
  83. return true
  84. }
  85. previewWindow.document.write(`
  86. <!DOCTYPE html>
  87. <html>
  88. <head>
  89. <meta charset="UTF-8">
  90. <title>图片预览 - ${fileName}</title>
  91. <style>
  92. * { margin: 0; padding: 0; box-sizing: border-box; }
  93. body {
  94. font-family: Arial, sans-serif;
  95. background: #1a1a1a;
  96. min-height: 100vh;
  97. display: flex;
  98. flex-direction: column;
  99. }
  100. .header {
  101. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  102. color: white;
  103. padding: 12px 20px;
  104. display: flex;
  105. justify-content: space-between;
  106. align-items: center;
  107. }
  108. .header h2 { font-size: 16px; font-weight: 500; }
  109. .toolbar { display: flex; gap: 10px; }
  110. .toolbar button {
  111. background: rgba(255,255,255,0.2);
  112. border: none;
  113. color: white;
  114. padding: 6px 12px;
  115. border-radius: 4px;
  116. cursor: pointer;
  117. font-size: 12px;
  118. }
  119. .toolbar button:hover { background: rgba(255,255,255,0.3); }
  120. .image-container {
  121. flex: 1;
  122. display: flex;
  123. justify-content: center;
  124. align-items: center;
  125. padding: 20px;
  126. overflow: auto;
  127. }
  128. .image-container img {
  129. max-width: 100%;
  130. max-height: calc(100vh - 80px);
  131. object-fit: contain;
  132. box-shadow: 0 4px 20px rgba(0,0,0,0.5);
  133. transition: transform 0.3s;
  134. }
  135. </style>
  136. </head>
  137. <body>
  138. <div class="header">
  139. <h2>📷 ${fileName}</h2>
  140. <div class="toolbar">
  141. <button onclick="zoomIn()">放大 +</button>
  142. <button onclick="zoomOut()">缩小 -</button>
  143. <button onclick="resetZoom()">重置</button>
  144. <button onclick="downloadImage()">下载</button>
  145. </div>
  146. </div>
  147. <div class="image-container">
  148. <img id="previewImg" src="${fileURL}" alt="${fileName}">
  149. </div>
  150. <script>
  151. var scale = 1;
  152. var img = document.getElementById('previewImg');
  153. function zoomIn() { scale = Math.min(scale + 0.25, 5); img.style.transform = 'scale(' + scale + ')'; }
  154. function zoomOut() { scale = Math.max(scale - 0.25, 0.25); img.style.transform = 'scale(' + scale + ')'; }
  155. function resetZoom() { scale = 1; img.style.transform = 'scale(1)'; }
  156. function downloadImage() {
  157. var a = document.createElement('a');
  158. a.href = '${fileURL}';
  159. a.download = '${fileName}';
  160. a.click();
  161. }
  162. <\/script>
  163. </body>
  164. </html>
  165. `)
  166. previewWindow.document.close()
  167. return true
  168. } catch (error) {
  169. console.error('图片预览失败:', error)
  170. return false
  171. }
  172. }
  173. /**
  174. * 预览视频文件
  175. * @param {Blob} blobData - 文件数据
  176. * @param {String} fileName - 文件名
  177. * @param {String} mimeType - MIME类型
  178. */
  179. export function previewVideo(blobData, fileName, mimeType) {
  180. try {
  181. const fileURL = URL.createObjectURL(blobData)
  182. const previewWindow = window.open('', '_blank')
  183. if (!previewWindow) {
  184. window.open(fileURL, '_blank')
  185. return true
  186. }
  187. previewWindow.document.write(`
  188. <!DOCTYPE html>
  189. <html>
  190. <head>
  191. <meta charset="UTF-8">
  192. <title>视频播放 - ${fileName}</title>
  193. <style>
  194. * { margin: 0; padding: 0; box-sizing: border-box; }
  195. body {
  196. font-family: Arial, sans-serif;
  197. background: #1a1a1a;
  198. min-height: 100vh;
  199. display: flex;
  200. flex-direction: column;
  201. }
  202. .header {
  203. background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
  204. color: white;
  205. padding: 12px 20px;
  206. }
  207. .header h2 { font-size: 16px; font-weight: 500; }
  208. .video-container {
  209. flex: 1;
  210. display: flex;
  211. justify-content: center;
  212. align-items: center;
  213. padding: 20px;
  214. }
  215. video {
  216. max-width: 100%;
  217. max-height: calc(100vh - 80px);
  218. box-shadow: 0 4px 20px rgba(0,0,0,0.5);
  219. }
  220. </style>
  221. </head>
  222. <body>
  223. <div class="header">
  224. <h2>🎬 ${fileName}</h2>
  225. </div>
  226. <div class="video-container">
  227. <video controls autoplay>
  228. <source src="${fileURL}" type="${mimeType}">
  229. 您的浏览器不支持视频播放
  230. </video>
  231. </div>
  232. </body>
  233. </html>
  234. `)
  235. previewWindow.document.close()
  236. return true
  237. } catch (error) {
  238. console.error('视频预览失败:', error)
  239. return false
  240. }
  241. }
  242. /**
  243. * 预览音频文件
  244. * @param {Blob} blobData - 文件数据
  245. * @param {String} fileName - 文件名
  246. * @param {String} mimeType - MIME类型
  247. */
  248. export function previewAudio(blobData, fileName, mimeType) {
  249. try {
  250. const fileURL = URL.createObjectURL(blobData)
  251. const previewWindow = window.open('', '_blank')
  252. if (!previewWindow) {
  253. window.open(fileURL, '_blank')
  254. return true
  255. }
  256. previewWindow.document.write(`
  257. <!DOCTYPE html>
  258. <html>
  259. <head>
  260. <meta charset="UTF-8">
  261. <title>音频播放 - ${fileName}</title>
  262. <style>
  263. * { margin: 0; padding: 0; box-sizing: border-box; }
  264. body {
  265. font-family: Arial, sans-serif;
  266. background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
  267. min-height: 100vh;
  268. display: flex;
  269. flex-direction: column;
  270. justify-content: center;
  271. align-items: center;
  272. }
  273. .audio-card {
  274. background: rgba(255,255,255,0.1);
  275. backdrop-filter: blur(10px);
  276. border-radius: 16px;
  277. padding: 40px;
  278. text-align: center;
  279. box-shadow: 0 8px 32px rgba(0,0,0,0.3);
  280. }
  281. .icon { font-size: 64px; margin-bottom: 20px; }
  282. .title { color: white; font-size: 18px; margin-bottom: 30px; word-break: break-all; max-width: 400px; }
  283. audio { width: 350px; }
  284. </style>
  285. </head>
  286. <body>
  287. <div class="audio-card">
  288. <div class="icon">🎵</div>
  289. <div class="title">${fileName}</div>
  290. <audio controls autoplay>
  291. <source src="${fileURL}" type="${mimeType}">
  292. 您的浏览器不支持音频播放
  293. </audio>
  294. </div>
  295. </body>
  296. </html>
  297. `)
  298. previewWindow.document.close()
  299. return true
  300. } catch (error) {
  301. console.error('音频预览失败:', error)
  302. return false
  303. }
  304. }
  305. /**
  306. * 预览PDF文件
  307. * @param {Blob} blobData - 文件数据
  308. * @param {String} fileName - 文件名
  309. */
  310. export function previewPDF(blobData, fileName) {
  311. try {
  312. const fileURL = URL.createObjectURL(blobData)
  313. const previewWindow = window.open('', '_blank')
  314. if (!previewWindow) {
  315. window.open(fileURL, '_blank')
  316. return true
  317. }
  318. previewWindow.document.write(`
  319. <!DOCTYPE html>
  320. <html>
  321. <head>
  322. <meta charset="UTF-8">
  323. <title>PDF预览 - ${fileName}</title>
  324. <style>
  325. * { margin: 0; padding: 0; }
  326. body { height: 100vh; }
  327. iframe { width: 100%; height: 100%; border: none; }
  328. </style>
  329. </head>
  330. <body>
  331. <iframe src="${fileURL}"></iframe>
  332. </body>
  333. </html>
  334. `)
  335. previewWindow.document.close()
  336. return true
  337. } catch (error) {
  338. console.error('PDF预览失败:', error)
  339. return false
  340. }
  341. }
  342. /**
  343. * 预览文本文件
  344. * @param {Blob} blobData - 文件数据
  345. * @param {String} fileName - 文件名
  346. */
  347. export async function previewText(blobData, fileName) {
  348. try {
  349. const text = await blobData.text()
  350. const previewWindow = window.open('', '_blank')
  351. if (!previewWindow) {
  352. throw new Error('无法打开预览窗口')
  353. }
  354. // 转义HTML特殊字符
  355. const escapedText = text
  356. .replace(/&/g, '&amp;')
  357. .replace(/</g, '&lt;')
  358. .replace(/>/g, '&gt;')
  359. .replace(/"/g, '&quot;')
  360. previewWindow.document.write(`
  361. <!DOCTYPE html>
  362. <html>
  363. <head>
  364. <meta charset="UTF-8">
  365. <title>文本预览 - ${fileName}</title>
  366. <style>
  367. body {
  368. font-family: 'Consolas', 'Monaco', monospace;
  369. padding: 20px;
  370. background: #1e1e1e;
  371. color: #d4d4d4;
  372. line-height: 1.6;
  373. }
  374. .header {
  375. background: #333;
  376. margin: -20px -20px 20px -20px;
  377. padding: 12px 20px;
  378. color: #fff;
  379. }
  380. pre {
  381. white-space: pre-wrap;
  382. word-wrap: break-word;
  383. }
  384. </style>
  385. </head>
  386. <body>
  387. <div class="header">📄 ${fileName}</div>
  388. <pre>${escapedText}</pre>
  389. </body>
  390. </html>
  391. `)
  392. previewWindow.document.close()
  393. return true
  394. } catch (error) {
  395. console.error('文本预览失败:', error)
  396. return false
  397. }
  398. }
  399. /**
  400. * 预览Excel文件
  401. * @param {ArrayBuffer} arrayBuffer - 文件数据ArrayBuffer格式
  402. * @param {String} fileName - 文件名
  403. */
  404. export function previewExcel(arrayBuffer, fileName) {
  405. try {
  406. const workbook = XLSX.read(arrayBuffer, { type: 'array' })
  407. const sheetName = workbook.SheetNames[0]
  408. const worksheet = workbook.Sheets[sheetName]
  409. const htmlString = XLSX.utils.sheet_to_html(worksheet, { editable: false })
  410. // 预先生成所有sheet的HTML
  411. const sheetsHtml = workbook.SheetNames.map(name => {
  412. return XLSX.utils.sheet_to_html(workbook.Sheets[name], { editable: false })
  413. })
  414. // 创建预览窗口
  415. const previewWindow = window.open('', '_blank')
  416. if (!previewWindow) {
  417. throw new Error('无法打开预览窗口,请检查浏览器是否阻止了弹出窗口')
  418. }
  419. previewWindow.document.write(`
  420. <!DOCTYPE html>
  421. <html>
  422. <head>
  423. <meta charset="UTF-8">
  424. <title>Excel预览 - ${fileName}</title>
  425. <style>
  426. body { font-family: Arial, sans-serif; padding: 20px; background: #f5f5f5; }
  427. .header { background: #409EFF; color: white; padding: 15px 20px; margin: -20px -20px 20px -20px; }
  428. .header h2 { margin: 0; font-size: 18px; }
  429. .sheet-tabs { background: #fff; padding: 10px; margin-bottom: 10px; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
  430. .sheet-tab { display: inline-block; padding: 8px 16px; margin-right: 5px; background: #f0f0f0; border-radius: 4px; cursor: pointer; }
  431. .sheet-tab.active { background: #409EFF; color: white; }
  432. .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); }
  433. table { border-collapse: collapse; width: 100%; }
  434. th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; white-space: nowrap; }
  435. th { background: #f5f7fa; font-weight: 600; position: sticky; top: 0; }
  436. tr:nth-child(even) { background: #fafafa; }
  437. tr:hover { background: #f0f7ff; }
  438. </style>
  439. </head>
  440. <body>
  441. <div class="header">
  442. <h2>Excel预览 - ${fileName}</h2>
  443. </div>
  444. <div class="sheet-tabs">
  445. ${workbook.SheetNames.map((name, index) =>
  446. `<span class="sheet-tab ${index === 0 ? 'active' : ''}" onclick="showSheet(${index})">${name}</span>`
  447. ).join('')}
  448. </div>
  449. <div class="table-container" id="tableContainer">
  450. ${htmlString}
  451. </div>
  452. <script>
  453. var sheets = ${JSON.stringify(sheetsHtml)};
  454. function showSheet(index) {
  455. document.getElementById('tableContainer').innerHTML = sheets[index];
  456. var tabs = document.querySelectorAll('.sheet-tab');
  457. tabs.forEach(function(tab, i) {
  458. tab.className = 'sheet-tab' + (i === index ? ' active' : '');
  459. });
  460. }
  461. <\/script>
  462. </body>
  463. </html>
  464. `)
  465. previewWindow.document.close()
  466. return true
  467. } catch (error) {
  468. console.error('Excel预览失败:', error)
  469. return false
  470. }
  471. }
  472. /**
  473. * 预览Word文件下载后打开
  474. * @param {Blob} blobData - 文件数据Blob格式
  475. * @param {String} fileName - 文件名
  476. */
  477. export function previewWord(blobData, fileName) {
  478. // 对于Word文件,直接下载让用户用本地软件打开
  479. const url = URL.createObjectURL(blobData)
  480. // 创建下载链接
  481. const link = document.createElement('a')
  482. link.href = url
  483. link.download = fileName
  484. link.click()
  485. URL.revokeObjectURL(url)
  486. return true
  487. }
  488. /**
  489. * 预览PPT文件下载后打开
  490. * @param {Blob} blobData - 文件数据Blob格式
  491. * @param {String} fileName - 文件名
  492. */
  493. export function previewPPT(blobData, fileName) {
  494. // 对于PPT文件,直接下载让用户用本地软件打开
  495. const url = URL.createObjectURL(blobData)
  496. // 创建下载链接
  497. const link = document.createElement('a')
  498. link.href = url
  499. link.download = fileName
  500. link.click()
  501. URL.revokeObjectURL(url)
  502. return true
  503. }
  504. /**
  505. * 通用文件预览方法
  506. * @param {ArrayBuffer|Blob} data - 文件数据可能是Blob或ArrayBuffer
  507. * @param {String} fileName - 文件名
  508. * @returns {Promise} 预览结果
  509. */
  510. export async function previewFile(data, fileName) {
  511. const suffix = getFileSuffix(fileName)
  512. const fileType = getFileType(fileName)
  513. const mimeType = getMimeType(suffix)
  514. try {
  515. // 确保data是正确的格式
  516. let blobData = data
  517. let arrayBufferData = null
  518. // 如果data是Blob,保存引用;如果不是,创建Blob
  519. if (!(data instanceof Blob)) {
  520. blobData = new Blob([data], { type: mimeType })
  521. }
  522. switch (fileType) {
  523. case 'image':
  524. // 图片预览 - 在新窗口中显示带工具栏的预览
  525. if (previewImage(blobData, fileName)) {
  526. return { success: true, type: 'image' }
  527. } else {
  528. throw new Error('图片预览失败')
  529. }
  530. case 'video':
  531. // 视频预览 - 在新窗口中播放
  532. if (previewVideo(blobData, fileName, mimeType)) {
  533. return { success: true, type: 'video' }
  534. } else {
  535. throw new Error('视频预览失败')
  536. }
  537. case 'audio':
  538. // 音频预览 - 在新窗口中播放
  539. if (previewAudio(blobData, fileName, mimeType)) {
  540. return { success: true, type: 'audio' }
  541. } else {
  542. throw new Error('音频预览失败')
  543. }
  544. case 'pdf':
  545. // PDF预览
  546. if (previewPDF(blobData, fileName)) {
  547. return { success: true, type: 'pdf' }
  548. } else {
  549. throw new Error('PDF预览失败')
  550. }
  551. case 'txt':
  552. // 文本文件预览
  553. if (await previewText(blobData, fileName)) {
  554. return { success: true, type: 'txt' }
  555. } else {
  556. throw new Error('文本预览失败')
  557. }
  558. case 'excel':
  559. // Excel文件需要ArrayBuffer格式
  560. arrayBufferData = await blobToArrayBuffer(blobData)
  561. if (previewExcel(arrayBufferData, fileName)) {
  562. return { success: true, type: 'excel' }
  563. } else {
  564. throw new Error('Excel预览失败')
  565. }
  566. case 'word':
  567. // Word文件下载后打开
  568. previewWord(blobData, fileName)
  569. return { success: true, type: 'word', message: 'Word文件已下载,请使用本地软件打开' }
  570. case 'ppt':
  571. // PPT文件下载后打开
  572. previewPPT(blobData, fileName)
  573. return { success: true, type: 'ppt', message: 'PPT文件已下载,请使用本地软件打开' }
  574. default:
  575. // 未知类型,下载处理
  576. const downloadUrl = URL.createObjectURL(blobData)
  577. const link = document.createElement('a')
  578. link.href = downloadUrl
  579. link.download = fileName
  580. link.click()
  581. URL.revokeObjectURL(downloadUrl)
  582. return { success: true, type: 'download', message: '文件已下载' }
  583. }
  584. } catch (error) {
  585. console.error('文件预览错误:', error)
  586. throw error
  587. }
  588. }
  589. export default {
  590. getFileSuffix,
  591. getFileType,
  592. getMimeType,
  593. previewImage,
  594. previewVideo,
  595. previewAudio,
  596. previewPDF,
  597. previewText,
  598. previewExcel,
  599. previewWord,
  600. previewPPT,
  601. previewFile
  602. }