|
|
@@ -0,0 +1,616 @@
|
|
|
+<template>
|
|
|
+ <van-popup v-model="showPopup" class="ibps-fullscreen-popup">
|
|
|
+ <div class="file-preview-container">
|
|
|
+ <!-- 加载状态 -->
|
|
|
+ <div v-if="loading" class="loading-wrapper">
|
|
|
+ <div class="loading-spinner"></div>
|
|
|
+ <p class="loading-text">{{ loadingText }}</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 错误状态 -->
|
|
|
+ <div v-else-if="error" class="error-wrapper">
|
|
|
+ <div class="error-icon">⚠️</div>
|
|
|
+ <p class="error-text">{{ error }}</p>
|
|
|
+ <button class="retry-btn" @click="loadFile">重试</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- PDF 预览 -->
|
|
|
+ <div v-else-if="fileType === 'pdf'" class="pdf-preview-wrapper">
|
|
|
+ <div class="preview-header">
|
|
|
+ <van-icon name="arrow-left" class="left-icon" @click="agree"/>
|
|
|
+ <span class="file-name">《{{ fileInfo.fileName }}》</span>
|
|
|
+ </div>
|
|
|
+ <div class="pdf-viewer">
|
|
|
+ <iframe
|
|
|
+ v-if="previewUrl"
|
|
|
+ :src="previewUrl"
|
|
|
+ class="pdf-iframe"
|
|
|
+ frameborder="0"
|
|
|
+ ></iframe>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Word 预览 -->
|
|
|
+ <div v-else-if="fileType === 'word'" class="word-preview-wrapper">
|
|
|
+ <div class="preview-header">
|
|
|
+ <van-icon name="arrow-left" class="left-icon" @click="agree"/>
|
|
|
+ <span class="file-name">《{{ fileInfo.fileName }}》</span>
|
|
|
+ </div>
|
|
|
+ <div ref="wordViewer" class="word-viewer">
|
|
|
+ <div class="word-content" v-html="wordContent"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 不支持的文件类型 -->
|
|
|
+ <div v-else class="unsupported-wrapper">
|
|
|
+ <div class="unsupported-icon">📄</div>
|
|
|
+ <p class="unsupported-text">不支持的文件类型</p>
|
|
|
+ <p class="unsupported-hint">当前仅支持 PDF 和 Word (.docx) 文件</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </van-popup>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import axios from 'axios'
|
|
|
+import { SYSTEM_URL, BASE_API } from '@/api/baseUrl'
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'FilePreview',
|
|
|
+ props: {
|
|
|
+ value: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ },
|
|
|
+ // 文件信息
|
|
|
+ fileInfo: {
|
|
|
+ type: Object,
|
|
|
+ required: true
|
|
|
+ },
|
|
|
+ // 文件名(可选)
|
|
|
+ fileName: {
|
|
|
+ type: String,
|
|
|
+ default: ''
|
|
|
+ },
|
|
|
+ // 请求配置(用于传递 headers、params 等)
|
|
|
+ requestConfig: {
|
|
|
+ type: Object,
|
|
|
+ default: () => ({})
|
|
|
+ },
|
|
|
+ // 加载提示文本
|
|
|
+ loadingText: {
|
|
|
+ type: String,
|
|
|
+ default: '正在加载文件...'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ loading: false,
|
|
|
+ error: null,
|
|
|
+ previewUrl: null,
|
|
|
+ wordContent: '',
|
|
|
+ detectedFileType: null,
|
|
|
+ blobUrl: null,
|
|
|
+ showPopup: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ // 实际使用的文件类型
|
|
|
+ fileType() {
|
|
|
+ return this.detectFileTypeFromUrl()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ value: {
|
|
|
+ handler(val) {
|
|
|
+ console.log(val)
|
|
|
+ if (val) {
|
|
|
+ this.cleanup()
|
|
|
+ this.loadFile()
|
|
|
+ }
|
|
|
+ this.showPopup = val
|
|
|
+ },
|
|
|
+ immediate: true
|
|
|
+ }
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ this.cleanup()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ agree() {
|
|
|
+ this.$emit('close')
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 从 URL 检测文件类型
|
|
|
+ */
|
|
|
+ detectFileTypeFromUrl() {
|
|
|
+ const url = '.' + this.fileInfo.ext
|
|
|
+ if (url.includes('.pdf') || url.includes('pdf')) {
|
|
|
+ return 'pdf'
|
|
|
+ }
|
|
|
+ if (url.includes('.docx') || url.includes('.doc') || url.includes('word')) {
|
|
|
+ return 'word'
|
|
|
+ }
|
|
|
+ return null
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 加载文件
|
|
|
+ */
|
|
|
+ async loadFile() {
|
|
|
+ this.loading = true
|
|
|
+ this.error = null
|
|
|
+ this.previewUrl = null
|
|
|
+ this.wordContent = ''
|
|
|
+ console.log(1111)
|
|
|
+ try {
|
|
|
+ // 获取文件流
|
|
|
+ const response = await this.fetchFileStream()
|
|
|
+ console.log('response', response)
|
|
|
+ // 检测文件类型
|
|
|
+ const blob = new Blob([response.data], { type: `application/${this.detectFileTypeFromUrl()}` })
|
|
|
+ this.detectedFileType = this.detectFileTypeFromUrl()
|
|
|
+ // 创建 Blob URL
|
|
|
+ this.blobUrl = URL.createObjectURL(blob)
|
|
|
+
|
|
|
+ // 根据文件类型处理预览
|
|
|
+ if (this.detectedFileType === 'pdf') {
|
|
|
+ this.previewUrl = this.blobUrl
|
|
|
+ console.log(this.blobUrl)
|
|
|
+ } else if (this.detectedFileType === 'word') {
|
|
|
+ await this.renderWord(blob)
|
|
|
+ } else {
|
|
|
+ this.error = '不支持的文件类型,仅支持 PDF 和 Word (.docx) 文件'
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.error('文件加载失败:', err)
|
|
|
+ this.error = err.message || '文件加载失败,请稍后重试'
|
|
|
+ } finally {
|
|
|
+ this.loading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取文件流
|
|
|
+ */
|
|
|
+ async fetchFileStream() {
|
|
|
+ const url =
|
|
|
+ BASE_API() +
|
|
|
+ SYSTEM_URL() +
|
|
|
+ '/file/downloadFileStream?attachmentId=' +
|
|
|
+ this.fileInfo.id +
|
|
|
+ '&fileType=' +
|
|
|
+ this.fileInfo.ext
|
|
|
+ const config = {
|
|
|
+ method: 'GET',
|
|
|
+ url: url,
|
|
|
+ responseType: 'blob', // 重要:设置为 blob 以接收流数据
|
|
|
+ ...this.requestConfig
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await axios(config)
|
|
|
+ return response
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检测文件类型
|
|
|
+ */
|
|
|
+ async detectFileType(contentType, data) {
|
|
|
+ // 从 Content-Type 检测
|
|
|
+ if (contentType.includes('application/pdf') || contentType.includes('pdf')) {
|
|
|
+ return 'pdf'
|
|
|
+ }
|
|
|
+ if (contentType.includes('application/vnd.openxmlformats-officedocument.wordprocessingml.document') ||
|
|
|
+ contentType.includes('application/msword') ||
|
|
|
+ contentType.includes('word')) {
|
|
|
+ return 'word'
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从文件内容检测(检查文件头)
|
|
|
+ if (data instanceof Blob) {
|
|
|
+ try {
|
|
|
+ // 读取文件前几个字节来检测文件类型
|
|
|
+ const arrayBuffer = await data.slice(0, 8).arrayBuffer()
|
|
|
+ const uint8Array = new Uint8Array(arrayBuffer)
|
|
|
+
|
|
|
+ // PDF 文件头:%PDF
|
|
|
+ if (uint8Array[0] === 0x25 && uint8Array[1] === 0x50 && uint8Array[2] === 0x44 && uint8Array[3] === 0x46) {
|
|
|
+ return 'pdf'
|
|
|
+ }
|
|
|
+
|
|
|
+ // DOCX 文件头:PK (ZIP 格式,因为 DOCX 是基于 ZIP 的)
|
|
|
+ // DOCX 文件是 ZIP 格式,前两个字节是 0x50 0x4B (PK)
|
|
|
+ if (uint8Array[0] === 0x50 && uint8Array[1] === 0x4B) {
|
|
|
+ // 进一步检查是否是 DOCX(检查 ZIP 中的 [Content_Types].xml)
|
|
|
+ // 这里简化处理,如果 URL 包含 docx 或 word,则认为是 word
|
|
|
+ const urlType = this.detectFileTypeFromUrl()
|
|
|
+ if (urlType === 'word') {
|
|
|
+ return 'word'
|
|
|
+ }
|
|
|
+ // 否则可能是其他 ZIP 格式文件,默认返回 null
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.warn('文件类型检测失败:', err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果无法检测,使用 URL 检测
|
|
|
+ const urlType = this.detectFileTypeFromUrl()
|
|
|
+ if (urlType) {
|
|
|
+ return urlType
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果仍然无法检测,根据文件大小和 Content-Type 做最后判断
|
|
|
+ // 默认返回 null,让调用方处理
|
|
|
+ return null
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 渲染 Word 文档
|
|
|
+ */
|
|
|
+ async renderWord(blob) {
|
|
|
+ try {
|
|
|
+ // 动态加载 mammoth.js
|
|
|
+ if (!window.mammoth) {
|
|
|
+ await this.loadMammoth()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 将 Blob 转换为 ArrayBuffer
|
|
|
+ const arrayBuffer = await blob.arrayBuffer()
|
|
|
+
|
|
|
+ // 使用 mammoth 转换为 HTML
|
|
|
+ const result = await window.mammoth.convertToHtml(
|
|
|
+ { arrayBuffer },
|
|
|
+ {
|
|
|
+ styleMap: [
|
|
|
+ "p[style-name='Heading 1'] => h1:fresh",
|
|
|
+ "p[style-name='Heading 2'] => h2:fresh",
|
|
|
+ "p[style-name='Heading 3'] => h3:fresh"
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ this.wordContent = result.value
|
|
|
+
|
|
|
+ // 如果有警告,可以在这里处理
|
|
|
+ if (result.messages && result.messages.length > 0) {
|
|
|
+ console.warn('Word 转换警告:', result.messages)
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Word 渲染失败:', err)
|
|
|
+ throw new Error('Word 文件解析失败,请确保文件格式正确')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 动态加载 mammoth.js
|
|
|
+ */
|
|
|
+ loadMammoth() {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ if (window.mammoth) {
|
|
|
+ resolve()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const script = document.createElement('script')
|
|
|
+ script.src = 'https://cdn.jsdelivr.net/npm/mammoth@1.6.0/mammoth.browser.min.js'
|
|
|
+ script.onload = () => {
|
|
|
+ if (window.mammoth) {
|
|
|
+ resolve()
|
|
|
+ } else {
|
|
|
+ reject(new Error('mammoth.js 加载失败'))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ script.onerror = () => {
|
|
|
+ reject(new Error('无法加载 mammoth.js,请检查网络连接'))
|
|
|
+ }
|
|
|
+ document.head.appendChild(script)
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 下载文件
|
|
|
+ */
|
|
|
+ downloadFile() {
|
|
|
+ if (!this.blobUrl) return
|
|
|
+
|
|
|
+ const link = document.createElement('a')
|
|
|
+ link.href = this.blobUrl
|
|
|
+ link.download = this.fileName || 'download'
|
|
|
+ document.body.appendChild(link)
|
|
|
+ link.click()
|
|
|
+ document.body.removeChild(link)
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 在新窗口打开
|
|
|
+ */
|
|
|
+ openInNewTab() {
|
|
|
+ if (!this.blobUrl) return
|
|
|
+ window.open(this.blobUrl, '_blank')
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 清理资源
|
|
|
+ */
|
|
|
+ cleanup() {
|
|
|
+ if (this.blobUrl) {
|
|
|
+ URL.revokeObjectURL(this.blobUrl)
|
|
|
+ this.blobUrl = null
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.left-icon {
|
|
|
+ position: absolute;
|
|
|
+ left: 8px;
|
|
|
+}
|
|
|
+.file-preview-container {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ background: #f5f5f5;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+/* 加载状态 */
|
|
|
+.loading-wrapper {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 40px;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-spinner {
|
|
|
+ width: 50px;
|
|
|
+ height: 50px;
|
|
|
+ border: 4px solid #e0e0e0;
|
|
|
+ border-top-color: #2196f3;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes spin {
|
|
|
+ to {
|
|
|
+ transform: rotate(360deg);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.loading-text {
|
|
|
+ margin-top: 20px;
|
|
|
+ color: #666;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 错误状态 */
|
|
|
+.error-wrapper {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 40px;
|
|
|
+}
|
|
|
+
|
|
|
+.error-icon {
|
|
|
+ font-size: 48px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.error-text {
|
|
|
+ color: #f44336;
|
|
|
+ font-size: 16px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.retry-btn {
|
|
|
+ padding: 10px 24px;
|
|
|
+ background: #2196f3;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+ transition: background 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.retry-btn:hover {
|
|
|
+ background: #1976d2;
|
|
|
+}
|
|
|
+
|
|
|
+/* 预览头部 */
|
|
|
+.preview-header {
|
|
|
+ text-align: center;
|
|
|
+ padding: 12px 16px;
|
|
|
+ background: white;
|
|
|
+ border-bottom: 1px solid #e0e0e0;
|
|
|
+}
|
|
|
+
|
|
|
+.file-name {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #333;
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.header-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.action-btn {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ padding: 6px 12px;
|
|
|
+ background: #f5f5f5;
|
|
|
+ border: 1px solid #e0e0e0;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #666;
|
|
|
+ transition: all 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.action-btn:hover {
|
|
|
+ background: #e0e0e0;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.action-btn span {
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+/* PDF 预览 */
|
|
|
+.pdf-preview-wrapper {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.pdf-viewer {
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #525252;
|
|
|
+}
|
|
|
+
|
|
|
+.pdf-iframe {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+/* Word 预览 */
|
|
|
+.word-preview-wrapper {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.word-viewer {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ background: white;
|
|
|
+ padding: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.word-content {
|
|
|
+ max-width: 800px;
|
|
|
+ margin: 0 auto;
|
|
|
+ line-height: 1.6;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.word-content >>> h1,
|
|
|
+.word-content >>> h2,
|
|
|
+.word-content >>> h3 {
|
|
|
+ margin-top: 24px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.word-content >>> h1 {
|
|
|
+ font-size: 32px;
|
|
|
+ border-bottom: 2px solid #e0e0e0;
|
|
|
+ padding-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.word-content >>> h2 {
|
|
|
+ font-size: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.word-content >>> h3 {
|
|
|
+ font-size: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.word-content >>> p {
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.word-content >>> ul,
|
|
|
+.word-content >>> ol {
|
|
|
+ margin-left: 24px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.word-content >>> table {
|
|
|
+ width: 100%;
|
|
|
+ border-collapse: collapse;
|
|
|
+ margin: 16px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.word-content >>> table td,
|
|
|
+.word-content >>> table th {
|
|
|
+ border: 1px solid #e0e0e0;
|
|
|
+ padding: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.word-content >>> table th {
|
|
|
+ background: #f5f5f5;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+/* 不支持的文件类型 */
|
|
|
+.unsupported-wrapper {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 40px;
|
|
|
+}
|
|
|
+
|
|
|
+.unsupported-icon {
|
|
|
+ font-size: 64px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.unsupported-text {
|
|
|
+ font-size: 18px;
|
|
|
+ color: #666;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.unsupported-hint {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+
|
|
|
+/* 移动端适配 */
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .preview-header {
|
|
|
+ padding: 10px 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-name {
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .action-btn {
|
|
|
+ padding: 6px 10px;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .word-viewer {
|
|
|
+ padding: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .word-content {
|
|
|
+ max-width: 100%;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|