This commit is contained in:
parent
06fbca491f
commit
e4497785fd
|
|
@ -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">
|
||||
<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-if="isImage">
|
||||
<div class="image-toolbar">
|
||||
<!-- <el-button size="small" @click="downloadFile"><el-icon><Download /></el-icon> 下载</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-if="isPdf">
|
||||
<div class="pdf-viewer">
|
||||
<iframe
|
||||
:src="processedFileUrl"
|
||||
style="width: 100%; height: 100%; border: none;"
|
||||
frameborder="0"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 不支持的类型 -->
|
||||
<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
|
||||
:src="processedFileUrl"
|
||||
:page="currentPage"
|
||||
@num-pages="numPages = $event"
|
||||
@loaded="onPdfLoaded"
|
||||
@error="onPdfError"
|
||||
style="width:100%;height:70vh"
|
||||
/> -->
|
||||
<div class="pdf-placeholder">
|
||||
<el-icon><Document /></el-icon>
|
||||
<p>PDF预览功能</p>
|
||||
<p>需要安装 vue-pdf 的 Vue3 兼容版本</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="unsupported">
|
||||
<el-icon><Document /></el-icon>
|
||||
<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:')) {
|
||||
if (isPdf.value) {
|
||||
return `data:application/pdf;base64,${fileUrl.value}`
|
||||
} else if (isImage.value) {
|
||||
const imageType = getImageMimeType()
|
||||
return `data:${imageType};base64,${fileUrl.value}`
|
||||
}
|
||||
const url = fileUrl.value
|
||||
if (!url) return ''
|
||||
|
||||
if (url.startsWith('data:')) return url
|
||||
|
||||
if (isPdf.value) {
|
||||
return `data:application/pdf;base64,${url}`
|
||||
} else if (isImage.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)
|
||||
}
|
||||
|
||||
// base64转Blob
|
||||
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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue