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>
<!-- 预览文件 -->
<el-dialog
v-model="dialogVisible"
:title="title"
@ -9,88 +8,53 @@
:append-to-body="true"
:width="width > 500 ? '700px' : '500px'"
>
<div style="text-align:center">
<!-- 图片预览 -->
<template v-if="isImage">
<div class="image-toolbar">
<!-- <el-button size="small" @click="downloadFile"><el-icon><Download /></el-icon> </el-button> -->
<div style="text-align: center; height: 70vh; display: flex; align-items: center; justify-content: center;">
<!-- Loading 状态 -->
<div v-if="loading" class="loading-state">
<el-icon class="is-loading"><Loading /></el-icon>
<p>加载中...</p>
</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
:src="processedFileUrl"
:preview-src-list="previewList"
:preview-src-list="[processedFileUrl]"
fit="contain"
style="max-width:100%;max-height:70vh"
style="max-width: 100%; max-height: 70vh;"
>
<template #error>
<div class="image-slot">
<div class="image-error">
<el-icon><Picture /></el-icon>
<p>图片加载失败</p>
</div>
</template>
</el-image>
</template>
<!-- PDF 预览 -->
<template v-else>
<div class="pdf-container">
<!-- PDF加载状态 -->
<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
<template v-else-if="isPdf">
<div class="pdf-viewer">
<iframe
:src="processedFileUrl"
:page="currentPage"
@num-pages="numPages = $event"
@loaded="onPdfLoaded"
@error="onPdfError"
style="width:100%;height:70vh"
/> -->
<div class="pdf-placeholder">
style="width: 100%; height: 100%; border: none;"
frameborder="0"
></iframe>
</div>
</template>
<!-- 不支持的类型 -->
<template v-else>
<div class="unsupported">
<el-icon><Document /></el-icon>
<p>PDF预览功能</p>
<p>需要安装 vue-pdf Vue3 兼容版本</p>
</div>
</div>
</div>
<p>不支持的文件类型</p>
</div>
</template>
</div>
@ -98,294 +62,115 @@
</template>
<script setup>
import { ref, reactive, computed, onMounted, watch, nextTick } from 'vue'
import { ref, computed, onMounted } from 'vue'
import {
Document,
Picture,
Loading,
Warning,
ArrowLeft,
ArrowRight,
Download
Document
} from '@element-plus/icons-vue'
// Vue3 pdf
// import pdf from 'vue-pdf'
import { getFileAsBase64Api } from '@/api/archivesManagement/fileManager/fileManager'
const props = defineProps({
width: {
type: [Number, String],
default: 600
},
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: ''
}
width: { type: [Number, String], default: 600 },
title: { type: String, default: '文件预览' },
rowData: { type: Object, default: () => ({}) }
})
const emit = defineEmits(['closeDialog', 'showColose'])
const emit = defineEmits(['closeDialog'])
//
const dialogVisible = ref(true)
const fileUrl = ref('')
const fileName = ref('')
const previewList = ref([])
const pdfLoading = 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 loading = ref(false)
const error = ref(false)
//
//
const isImage = computed(() => {
const exts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
const url = (fileUrl.value || '')
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 exts = ['.pdf']
const url = (fileUrl.value || '')
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(() => {
if (fileUrl.value && !fileUrl.value.startsWith('data:')) {
const url = fileUrl.value
if (!url) return ''
if (url.startsWith('data:')) return url
if (isPdf.value) {
return `data:application/pdf;base64,${fileUrl.value}`
return `data:application/pdf;base64,${url}`
} else if (isImage.value) {
const imageType = getImageMimeType()
return `data:${imageType};base64,${fileUrl.value}`
const mime = getImageMimeType()
return `data:${mime};base64,${url}`
}
}
return fileUrl.value
return url
})
//
//
const handleClose = () => {
dialogVisible.value = false
emit('closeDialog')
}
/* 获取文件的base64 */
const getFileAsBase64 = async () => {
if (isPdf.value) {
pdfLoading.value = true
pdfError.value = false
}
// base64
const loadFile = async () => {
loading.value = true
error.value = false
try {
const res = await getFileAsBase64Api({ id: props.rowData.fileId })
const obj = res.data
fileUrl.value = obj?.fileBase64 || ''
fileName.value = obj?.fileName || ''
const obj = res.data.data
if (isImage.value && fileUrl.value) {
previewList.value = [processedFileUrl.value]
if (!obj?.fileBase64) {
throw new Error('文件数据为空')
}
} catch (error) {
if (isPdf.value) {
pdfError.value = true
pdfLoading.value = false
}
console.error('获取文件失败:', error)
fileName.value = obj.fileName || ''
fileUrl.value = obj.fileBase64
loading.value = false
} catch (err) {
console.error('获取文件失败:', err)
error.value = true
loading.value = false
}
}
// PDF
const resetPdfState = () => {
currentPage.value = 1
numPages.value = 0
pdfLoading.value = false
pdfError.value = false
isWheelScrolling.value = false
if (wheelTimeout.value) {
clearTimeout(wheelTimeout.value)
wheelTimeout.value = null
//
const retryLoad = () => {
loadFile()
}
}
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(() => {
getFileAsBase64()
loadFile()
})
</script>
<style scoped>
.image-slot {
width: 100%;
height: 240px;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
font-size: 24px;
}
/* PDF预览样式 */
.pdf-container {
width: 100%;
height: 70vh;
position: relative;
}
.pdf-loading,
.pdf-error {
.loading-state,
.error-state,
.image-error,
.unsupported {
display: flex;
flex-direction: column;
align-items: center;
@ -395,89 +180,29 @@ onMounted(() => {
font-size: 16px;
}
.pdf-loading .el-icon {
.loading-state .el-icon,
.error-state .el-icon,
.unsupported .el-icon {
font-size: 32px;
margin-bottom: 16px;
animation: rotating 2s linear infinite;
}
.pdf-error .el-icon {
font-size: 32px;
margin-bottom: 16px;
color: #F56C6C;
.error-state .el-icon {
color: #f56c6c;
}
.pdf-content {
height: 100%;
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;
.unsupported .el-icon {
font-size: 48px;
}
.pdf-viewer {
flex: 1;
overflow: hidden;
display: flex;
justify-content: center;
width: 100%;
height: 70vh;
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 {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>