This commit is contained in:
liang.chao 2025-11-29 14:48:04 +08:00
parent 06fbca491f
commit e4497785fd
1 changed files with 103 additions and 378 deletions

View File

@ -1,5 +1,4 @@
<template> <template>
<!-- 预览文件 -->
<el-dialog <el-dialog
v-model="dialogVisible" v-model="dialogVisible"
:title="title" :title="title"
@ -9,88 +8,53 @@
:append-to-body="true" :append-to-body="true"
:width="width > 500 ? '700px' : '500px'" :width="width > 500 ? '700px' : '500px'"
> >
<div style="text-align:center"> <div style="text-align: center; height: 70vh; display: flex; align-items: center; justify-content: center;">
<!-- 图片预览 --> <!-- Loading 状态 -->
<template v-if="isImage"> <div v-if="loading" class="loading-state">
<div class="image-toolbar"> <el-icon class="is-loading"><Loading /></el-icon>
<!-- <el-button size="small" @click="downloadFile"><el-icon><Download /></el-icon> </el-button> --> <p>加载中...</p>
</div> </div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-state">
<el-icon><Warning /></el-icon>
<p>文件加载失败</p>
<el-button size="small" @click="retryLoad">重试</el-button>
</div>
<!-- 图片预览 -->
<template v-else-if="isImage">
<el-image <el-image
:src="processedFileUrl" :src="processedFileUrl"
:preview-src-list="previewList" :preview-src-list="[processedFileUrl]"
fit="contain" fit="contain"
style="max-width:100%;max-height:70vh" style="max-width: 100%; max-height: 70vh;"
> >
<template #error> <template #error>
<div class="image-slot"> <div class="image-error">
<el-icon><Picture /></el-icon> <el-icon><Picture /></el-icon>
<p>图片加载失败</p>
</div> </div>
</template> </template>
</el-image> </el-image>
</template> </template>
<!-- PDF 预览 --> <!-- PDF 预览 -->
<template v-else> <template v-else-if="isPdf">
<div class="pdf-container"> <div class="pdf-viewer">
<!-- PDF加载状态 --> <iframe
<div v-if="pdfLoading" class="pdf-loading">
<el-icon class="is-loading"><Loading /></el-icon>
<p>PDF加载中...</p>
</div>
<!-- PDF错误状态 -->
<div v-else-if="pdfError" class="pdf-error">
<el-icon><Warning /></el-icon>
<p>PDF加载失败</p>
<el-button size="small" @click="retryLoadPdf">重试</el-button>
</div>
<!-- PDF内容 -->
<div v-else class="pdf-content">
<!-- PDF工具栏 -->
<div class="pdf-toolbar">
<div class="pdf-controls">
<el-button
size="small"
:disabled="currentPage <= 1"
@click="prevPage"
>
<el-icon><ArrowLeft /></el-icon>
</el-button>
<span class="page-info">
{{ currentPage }} / {{ numPages }}
</span>
<el-button
size="small"
:disabled="currentPage >= numPages"
@click="nextPage"
>
下一页 <el-icon><ArrowRight /></el-icon>
</el-button>
</div>
<div class="pdf-actions">
<!-- <el-button size="small" @click="downloadFile"><el-icon><Download /></el-icon> </el-button> -->
</div>
</div>
<!-- PDF显示区域 -->
<div class="pdf-viewer" @wheel="handleWheel" ref="pdfViewer">
<!-- 注意vue-pdf 需要安装兼容 Vue3 的版本 -->
<!-- <pdf
:src="processedFileUrl" :src="processedFileUrl"
:page="currentPage" style="width: 100%; height: 100%; border: none;"
@num-pages="numPages = $event" frameborder="0"
@loaded="onPdfLoaded" ></iframe>
@error="onPdfError" </div>
style="width:100%;height:70vh" </template>
/> -->
<div class="pdf-placeholder"> <!-- 不支持的类型 -->
<template v-else>
<div class="unsupported">
<el-icon><Document /></el-icon> <el-icon><Document /></el-icon>
<p>PDF预览功能</p> <p>不支持的文件类型</p>
<p>需要安装 vue-pdf Vue3 兼容版本</p>
</div>
</div>
</div>
</div> </div>
</template> </template>
</div> </div>
@ -98,294 +62,115 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, computed, onMounted, watch, nextTick } from 'vue' import { ref, computed, onMounted } from 'vue'
import { import {
Document,
Picture, Picture,
Loading, Loading,
Warning, Warning,
ArrowLeft, Document
ArrowRight,
Download
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
// Vue3 pdf
// import pdf from 'vue-pdf'
import { getFileAsBase64Api } from '@/api/archivesManagement/fileManager/fileManager' import { getFileAsBase64Api } from '@/api/archivesManagement/fileManager/fileManager'
const props = defineProps({ const props = defineProps({
width: { width: { type: [Number, String], default: 600 },
type: [Number, String], title: { type: String, default: '文件预览' },
default: 600 rowData: { type: Object, default: () => ({}) }
},
hight: {
type: [Number, String],
default: 400
},
dataForm: {
type: Object,
default: () => ({})
},
title: {
type: String,
default: '文件预览'
},
disabled: {
type: Boolean,
default: false
},
isAdd: {
type: String,
default: ''
},
rowData: {
type: Object,
default: () => ({})
},
projectId: {
type: String,
default: ''
}
}) })
const emit = defineEmits(['closeDialog', 'showColose']) const emit = defineEmits(['closeDialog'])
// //
const dialogVisible = ref(true) const dialogVisible = ref(true)
const fileUrl = ref('') const fileUrl = ref('')
const fileName = ref('') const fileName = ref('')
const previewList = ref([]) const loading = ref(false)
const pdfLoading = ref(false) const error = ref(false)
const pdfError = ref(false)
const currentPage = ref(1)
const numPages = ref(0)
const wheelTimeout = ref(null)
const isWheelScrolling = ref(false)
const pdfViewer = ref()
// //
const isImage = computed(() => { const isImage = computed(() => {
const exts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'] const exts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
const url = (fileUrl.value || '')
const name = (fileName.value || '').toLowerCase() const name = (fileName.value || '').toLowerCase()
return exts.some(ext => url.endsWith(ext) || name.endsWith(ext)) return exts.some(ext => name.endsWith(ext))
}) })
// PDF
const isPdf = computed(() => { const isPdf = computed(() => {
const exts = ['.pdf']
const url = (fileUrl.value || '')
const name = (fileName.value || '').toLowerCase() const name = (fileName.value || '').toLowerCase()
return exts.some(ext => url.endsWith(ext) || name.endsWith(ext)) return name.endsWith('.pdf')
}) })
// MIME
function getImageMimeType() {
const name = (fileName.value || '').toLowerCase()
if (name.endsWith('.png')) return 'image/png'
if (name.endsWith('.gif')) return 'image/gif'
if (name.endsWith('.bmp')) return 'image/bmp'
if (name.endsWith('.webp')) return 'image/webp'
return 'image/jpeg'
}
// URL data:
const processedFileUrl = computed(() => { const processedFileUrl = computed(() => {
if (fileUrl.value && !fileUrl.value.startsWith('data:')) { const url = fileUrl.value
if (!url) return ''
if (url.startsWith('data:')) return url
if (isPdf.value) { if (isPdf.value) {
return `data:application/pdf;base64,${fileUrl.value}` return `data:application/pdf;base64,${url}`
} else if (isImage.value) { } else if (isImage.value) {
const imageType = getImageMimeType() const mime = getImageMimeType()
return `data:${imageType};base64,${fileUrl.value}` return `data:${mime};base64,${url}`
} }
} return url
return fileUrl.value
}) })
// //
const handleClose = () => { const handleClose = () => {
dialogVisible.value = false dialogVisible.value = false
emit('closeDialog') emit('closeDialog')
} }
/* 获取文件的base64 */ // base64
const getFileAsBase64 = async () => { const loadFile = async () => {
if (isPdf.value) { loading.value = true
pdfLoading.value = true error.value = false
pdfError.value = false
}
try { try {
const res = await getFileAsBase64Api({ id: props.rowData.fileId }) const res = await getFileAsBase64Api({ id: props.rowData.fileId })
const obj = res.data const obj = res.data.data
fileUrl.value = obj?.fileBase64 || ''
fileName.value = obj?.fileName || ''
if (isImage.value && fileUrl.value) { if (!obj?.fileBase64) {
previewList.value = [processedFileUrl.value] throw new Error('文件数据为空')
} }
} catch (error) {
if (isPdf.value) { fileName.value = obj.fileName || ''
pdfError.value = true fileUrl.value = obj.fileBase64
pdfLoading.value = false
} loading.value = false
console.error('获取文件失败:', error) } catch (err) {
console.error('获取文件失败:', err)
error.value = true
loading.value = false
} }
} }
// PDF //
const resetPdfState = () => { const retryLoad = () => {
currentPage.value = 1 loadFile()
numPages.value = 0
pdfLoading.value = false
pdfError.value = false
isWheelScrolling.value = false
if (wheelTimeout.value) {
clearTimeout(wheelTimeout.value)
wheelTimeout.value = null
} }
}
const onPdfLoaded = () => {
pdfLoading.value = false
pdfError.value = false
}
const onPdfError = (error) => {
pdfLoading.value = false
pdfError.value = true
console.error('PDF加载失败:', error)
}
const retryLoadPdf = () => {
resetPdfState()
getFileAsBase64()
}
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--
}
}
const nextPage = () => {
if (currentPage.value < numPages.value) {
currentPage.value++
}
}
//
const downloadFile = () => {
if (!fileUrl.value) return
const filename = fileName.value || '文件'
let mime = isPdf.value ? 'application/pdf' : getImageMimeType()
let base64Data = fileUrl.value
if (typeof base64Data === 'string' && base64Data.startsWith('data:')) {
try {
const parts = base64Data.split(',')
const header = parts[0]
const dataPart = parts[1]
const match = header.match(/^data:(.*?);base64$/)
if (match && match[1]) mime = match[1]
base64Data = dataPart
} catch (e) {
//
}
}
const blob = base64ToBlob(base64Data, mime)
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.style.display = 'none'
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
// base64Blob
const base64ToBlob = (base64, mime) => {
const byteChars = atob(base64)
const sliceSize = 1024
const byteArrays = []
for (let offset = 0; offset < byteChars.length; offset += sliceSize) {
const slice = byteChars.slice(offset, offset + sliceSize)
const byteNumbers = new Array(slice.length)
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers)
byteArrays.push(byteArray)
}
return new Blob(byteArrays, { type: mime || 'application/octet-stream' })
}
// MIME
const getImageMimeType = () => {
const name = (fileName.value || '').toLowerCase()
if (name.endsWith('.jpg') || name.endsWith('.jpeg')) {
return 'image/jpeg'
} else if (name.endsWith('.png')) {
return 'image/png'
} else if (name.endsWith('.gif')) {
return 'image/gif'
} else if (name.endsWith('.bmp')) {
return 'image/bmp'
} else if (name.endsWith('.webp')) {
return 'image/webp'
}
return 'image/jpeg'
}
//
const handleWheel = (event) => {
event.preventDefault()
if (isWheelScrolling.value) {
return
}
isWheelScrolling.value = true
if (wheelTimeout.value) {
clearTimeout(wheelTimeout.value)
}
if (event.deltaY > 0) {
nextPage()
} else if (event.deltaY < 0) {
prevPage()
}
wheelTimeout.value = setTimeout(() => {
isWheelScrolling.value = false
}, 300)
}
//
watch(isPdf, (newVal) => {
if (newVal) {
resetPdfState()
}
})
// //
onMounted(() => { onMounted(() => {
getFileAsBase64() loadFile()
}) })
</script> </script>
<style scoped> <style scoped>
.image-slot { .loading-state,
width: 100%; .error-state,
height: 240px; .image-error,
display: flex; .unsupported {
align-items: center;
justify-content: center;
color: #909399;
font-size: 24px;
}
/* PDF预览样式 */
.pdf-container {
width: 100%;
height: 70vh;
position: relative;
}
.pdf-loading,
.pdf-error {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@ -395,89 +180,29 @@ onMounted(() => {
font-size: 16px; font-size: 16px;
} }
.pdf-loading .el-icon { .loading-state .el-icon,
.error-state .el-icon,
.unsupported .el-icon {
font-size: 32px; font-size: 32px;
margin-bottom: 16px; margin-bottom: 16px;
animation: rotating 2s linear infinite;
} }
.pdf-error .el-icon { .error-state .el-icon {
font-size: 32px; color: #f56c6c;
margin-bottom: 16px;
color: #F56C6C;
} }
.pdf-content { .unsupported .el-icon {
height: 100%; font-size: 48px;
display: flex;
flex-direction: column;
}
.pdf-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #f5f5f5;
border-bottom: 1px solid #e4e7ed;
flex-shrink: 0;
}
.pdf-controls {
display: flex;
align-items: center;
gap: 8px;
}
.image-toolbar {
display: flex;
justify-content: flex-end;
padding: 4px 0 8px 0;
}
.pdf-actions {
display: flex;
align-items: center;
gap: 8px;
}
.page-info {
font-size: 14px;
color: #606266;
min-width: 60px;
text-align: center;
} }
.pdf-viewer { .pdf-viewer {
flex: 1; width: 100%;
overflow: hidden; height: 70vh;
display: flex;
justify-content: center;
background: #f8f9fa; background: #f8f9fa;
cursor: pointer;
}
.pdf-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #909399;
font-size: 16px;
}
.pdf-placeholder .el-icon {
font-size: 48px;
margin-bottom: 16px;
} }
@keyframes rotating { @keyframes rotating {
0% { 0% { transform: rotate(0deg); }
transform: rotate(0deg); 100% { transform: rotate(360deg); }
}
100% {
transform: rotate(360deg);
}
} }
</style> </style>