726 lines
19 KiB
Vue
726 lines
19 KiB
Vue
<template>
|
||
<div>
|
||
<el-dialog
|
||
v-model="dialogFormVisible"
|
||
:title="title"
|
||
width="80%"
|
||
:before-close="handleClose"
|
||
class="file-preview-dialog"
|
||
>
|
||
<div class="preview-container">
|
||
<!-- 加载状态 -->
|
||
<div v-if="loading" class="loading-container">
|
||
<el-icon class="is-loading" size="48">
|
||
<Loading />
|
||
</el-icon>
|
||
<p>{{ loadingText }}</p>
|
||
</div>
|
||
|
||
<!-- 下载状态 -->
|
||
<div v-else-if="downloading" class="loading-container">
|
||
<el-icon class="is-loading" size="48">
|
||
<Loading />
|
||
</el-icon>
|
||
<p>正在下载文件,请稍候...</p>
|
||
<p class="download-progress" v-if="downloadProgress > 0">
|
||
下载进度: {{ downloadProgress }}%
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 错误状态 -->
|
||
<div v-else-if="error" class="error-container">
|
||
<el-icon size="48" color="#F56C6C">
|
||
<CircleClose />
|
||
</el-icon>
|
||
<p>文件预览失败</p>
|
||
<p class="error-message">{{ errorMessage }}</p>
|
||
<el-button type="primary" @click="retryPreview">重试</el-button>
|
||
</div>
|
||
|
||
<!-- 文件预览内容 -->
|
||
<div v-else class="file-content">
|
||
<!-- 图片预览 -->
|
||
<div v-if="isImage" class="image-preview">
|
||
<img :src="previewUrl" :alt="fileName" @load="handleImageLoad" />
|
||
</div>
|
||
|
||
<!-- PDF预览 - 使用 vue-pdf-embed -->
|
||
<div v-else-if="isPdf" class="pdf-preview">
|
||
<div class="pdf-controls" v-if="pageCount > 0">
|
||
<el-button-group>
|
||
<el-button :disabled="currentPage <= 1" @click="previousPage">
|
||
<el-icon><ArrowLeft /></el-icon>
|
||
上一页
|
||
</el-button>
|
||
<el-button>
|
||
{{ currentPage }} / {{ pageCount }}
|
||
</el-button>
|
||
<el-button :disabled="currentPage >= pageCount" @click="nextPage">
|
||
下一页
|
||
<el-icon><ArrowRight /></el-icon>
|
||
</el-button>
|
||
</el-button-group>
|
||
</div>
|
||
|
||
<div class="pdf-viewer-container">
|
||
<VuePdfEmbed
|
||
:source="pdfSource"
|
||
:page="currentPage"
|
||
:scale="scale"
|
||
@loaded="handlePdfLoaded"
|
||
@rendered="handlePdfRendered"
|
||
@error="handlePdfError"
|
||
class="pdf-viewer"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 视频预览 -->
|
||
<div v-else-if="isVideo" class="video-preview">
|
||
<video controls :src="previewUrl" class="video-player">
|
||
您的浏览器不支持视频播放
|
||
</video>
|
||
</div>
|
||
|
||
<!-- 音频预览 -->
|
||
<div v-else-if="isAudio" class="audio-preview">
|
||
<audio controls :src="previewUrl" class="audio-player">
|
||
您的浏览器不支持音频播放
|
||
</audio>
|
||
</div>
|
||
|
||
<!-- 文本预览 -->
|
||
<div v-else-if="isText" class="text-preview">
|
||
<pre>{{ textContent }}</pre>
|
||
</div>
|
||
|
||
<!-- Office文档预览(转换后) -->
|
||
<div v-else-if="isConvertedOffice" class="office-preview">
|
||
<!-- 转换后的Office文件也是PDF,使用相同的PDF预览器 -->
|
||
<div class="pdf-controls" v-if="pageCount > 0">
|
||
<el-button-group>
|
||
<el-button :disabled="currentPage <= 1" @click="previousPage">
|
||
<el-icon><ArrowLeft /></el-icon>
|
||
上一页
|
||
</el-button>
|
||
<el-button>
|
||
{{ currentPage }} / {{ pageCount }}
|
||
</el-button>
|
||
<el-button :disabled="currentPage >= pageCount" @click="nextPage">
|
||
下一页
|
||
<el-icon><ArrowRight /></el-icon>
|
||
</el-button>
|
||
</el-button-group>
|
||
</div>
|
||
|
||
<div class="pdf-viewer-container">
|
||
<VuePdfEmbed
|
||
:source="pdfSource"
|
||
:page="currentPage"
|
||
:scale="scale"
|
||
@loaded="handlePdfLoaded"
|
||
@rendered="handlePdfRendered"
|
||
@error="handlePdfError"
|
||
class="pdf-viewer"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 不支持预览的文件类型 -->
|
||
<div v-else class="unsupported-preview">
|
||
<el-icon size="64" color="#909399">
|
||
<Document />
|
||
</el-icon>
|
||
<p>不支持在线预览此文件类型</p>
|
||
<p class="file-name">{{ fileName }}</p>
|
||
<!-- <el-button type="primary" @click="downloadFile">下载文件</el-button> -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部操作栏 -->
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<!-- <el-button @click="downloadFile" :disabled="loading">
|
||
<el-icon><Download /></el-icon>
|
||
下载
|
||
</el-button> -->
|
||
<el-button type="primary" @click="close">
|
||
关闭
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, getCurrentInstance, onUnmounted, nextTick } from 'vue'
|
||
import { convertFileApi, getConvertStatusApi } from '@/api/conferencingRoom'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import {
|
||
Loading,
|
||
CircleClose,
|
||
Document,
|
||
Download,
|
||
ArrowLeft,
|
||
ArrowRight,
|
||
Plus,
|
||
Minus,
|
||
Refresh
|
||
} from '@element-plus/icons-vue'
|
||
import VuePdfEmbed from 'vue-pdf-embed'
|
||
|
||
// 定义props
|
||
const props = defineProps({
|
||
fileType: {
|
||
type: Array,
|
||
default: () => ["pdf", "png", "jpg", "jpeg", "gif", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "mp4", "mp3"],
|
||
},
|
||
roomId: {
|
||
type: String,
|
||
default: '',
|
||
},
|
||
})
|
||
|
||
// 定义emits
|
||
const emit = defineEmits(['fetch-data'])
|
||
|
||
const { proxy } = getCurrentInstance()
|
||
const enumType = ["doc", "docx", "xls", "xlsx", "ppt", "pptx"]
|
||
|
||
// 响应式数据
|
||
const dialogFormVisible = ref(false)
|
||
const title = ref('')
|
||
const loading = ref(false)
|
||
const downloading = ref(false) // 新增:下载状态
|
||
const downloadProgress = ref(0) // 新增:下载进度
|
||
const loadingText = ref('正在加载...')
|
||
const error = ref(false)
|
||
const errorMessage = ref('')
|
||
const previewUrl = ref('')
|
||
const fileName = ref('')
|
||
const textContent = ref('')
|
||
const currentFileData = ref(null)
|
||
const convertTaskId = ref('')
|
||
const statusCheckInterval = ref(null)
|
||
|
||
// PDF相关状态
|
||
const pdfSource = ref('')
|
||
const currentPage = ref(1)
|
||
const pageCount = ref(0)
|
||
const scale = ref(1.0)
|
||
const pdfDocument = ref(null)
|
||
|
||
// 计算属性
|
||
const isImage = computed(() => {
|
||
const ext = fileName.value.split('.').pop().toLowerCase()
|
||
return ['png', 'jpg', 'jpeg', 'gif'].includes(ext)
|
||
})
|
||
|
||
const isPdf = computed(() => {
|
||
const ext = fileName.value.split('.').pop().toLowerCase()
|
||
return ext === 'pdf'
|
||
})
|
||
|
||
const isVideo = computed(() => {
|
||
const ext = fileName.value.split('.').pop().toLowerCase()
|
||
return ['mp4'].includes(ext)
|
||
})
|
||
|
||
const isAudio = computed(() => {
|
||
const ext = fileName.value.split('.').pop().toLowerCase()
|
||
return ['mp3'].includes(ext)
|
||
})
|
||
|
||
const isText = computed(() => {
|
||
const ext = fileName.value.split('.').pop().toLowerCase()
|
||
return ext === 'txt'
|
||
})
|
||
|
||
const isOffice = computed(() => {
|
||
const ext = fileName.value.split('.').pop().toLowerCase()
|
||
return enumType.includes(ext)
|
||
})
|
||
|
||
const isConvertedOffice = computed(() => {
|
||
return previewUrl.value && previewUrl.value.endsWith('.pdf') && isOffice.value
|
||
})
|
||
|
||
// 显示弹框
|
||
const showEdit = async (data) => {
|
||
// 重置状态
|
||
loading.value = true
|
||
downloading.value = false
|
||
downloadProgress.value = 0
|
||
title.value = '文件预览'
|
||
dialogFormVisible.value = true
|
||
currentFileData.value = data
|
||
fileName.value = data.fileName || data.fileUrl.split('/').pop()
|
||
|
||
error.value = false
|
||
previewUrl.value = ''
|
||
textContent.value = ''
|
||
resetPdfState()
|
||
const fileExt = fileName.value.split('.').pop().toLowerCase()
|
||
try {
|
||
if (enumType.includes(fileExt)) {
|
||
// Office文档需要转换
|
||
await getFilePdf(data.fileUrl)
|
||
} else if (fileExt === 'txt') {
|
||
// 文本文件需要特殊处理
|
||
await loadTextFile(data.fileUrl)
|
||
} else if (fileExt === 'pdf') {
|
||
// PDF文件使用vue-pdf-embed预览
|
||
await loadPdfFile(data.fileUrl)
|
||
} else {
|
||
// 其他文件直接预览
|
||
previewUrl.value = data.fileUrl
|
||
loading.value = false
|
||
}
|
||
} catch (err) {
|
||
handleError('文件加载失败', err)
|
||
}
|
||
}
|
||
|
||
// 加载PDF文件
|
||
const loadPdfFile = async (fileUrl) => {
|
||
try {
|
||
// 先显示下载状态
|
||
loading.value = false
|
||
downloading.value = true
|
||
downloadProgress.value = 0
|
||
|
||
// 使用fetch下载文件并跟踪进度
|
||
const response = await fetch(fileUrl)
|
||
if (!response.ok) {
|
||
throw new Error('文件下载失败')
|
||
}
|
||
|
||
const contentLength = response.headers.get('content-length')
|
||
const total = parseInt(contentLength, 10)
|
||
let loaded = 0
|
||
|
||
// 创建读取器
|
||
const reader = response.body.getReader()
|
||
const chunks = []
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read()
|
||
|
||
if (done) break
|
||
|
||
chunks.push(value)
|
||
loaded += value.length
|
||
|
||
// 更新下载进度
|
||
if (total) {
|
||
downloadProgress.value = Math.round((loaded / total) * 100)
|
||
}
|
||
}
|
||
|
||
// 创建Blob URL
|
||
const blob = new Blob(chunks)
|
||
const blobUrl = URL.createObjectURL(blob)
|
||
|
||
// 设置PDF源
|
||
previewUrl.value = fileUrl
|
||
pdfSource.value = blobUrl
|
||
|
||
// 重置状态
|
||
downloading.value = false
|
||
downloadProgress.value = 0
|
||
|
||
// 使用nextTick确保DOM更新
|
||
nextTick(() => {
|
||
// PDF组件会自动开始加载,loading状态会在handlePdfLoaded中设置为false
|
||
})
|
||
|
||
} catch (err) {
|
||
downloading.value = false
|
||
downloadProgress.value = 0
|
||
handleError('PDF文件下载失败', err)
|
||
}
|
||
}
|
||
|
||
// 加载文本文件
|
||
const loadTextFile = async (fileUrl) => {
|
||
try {
|
||
const response = await fetch(fileUrl)
|
||
if (!response.ok) throw new Error('无法加载文本文件')
|
||
const text = await response.text()
|
||
textContent.value = text
|
||
loading.value = false
|
||
} catch (err) {
|
||
handleError('文本文件加载失败', err)
|
||
}
|
||
}
|
||
|
||
// 上传文件获取office获取pdf文件
|
||
async function getFilePdf(fileUrl) {
|
||
try {
|
||
loadingText.value = '正在转换文件...'
|
||
const res = await convertFileApi({ file_url: fileUrl })
|
||
|
||
if (res.meta.code !== 200) {
|
||
throw new Error(res.meta.msg || '文件转换失败')
|
||
}
|
||
|
||
convertTaskId.value = res.data.task_id
|
||
loadingText.value = '正在等待转换完成...'
|
||
|
||
// 开始轮询转换状态
|
||
startStatusPolling()
|
||
} catch (err) {
|
||
handleError('文件转换失败', err)
|
||
}
|
||
}
|
||
|
||
// 开始轮询转换状态
|
||
const startStatusPolling = () => {
|
||
// 清除之前的轮询
|
||
if (statusCheckInterval.value) {
|
||
clearInterval(statusCheckInterval.value)
|
||
}
|
||
|
||
// 设置新的轮询
|
||
statusCheckInterval.value = setInterval(async () => {
|
||
try {
|
||
const fileRes = await getConvertStatusApi(convertTaskId.value)
|
||
|
||
if (fileRes.meta.code === 200) {
|
||
if (fileRes.data.status === 'completed') {
|
||
// 转换完成
|
||
clearInterval(statusCheckInterval.value)
|
||
previewUrl.value = fileRes.data.output_file
|
||
pdfSource.value = fileRes.data.output_file
|
||
loading.value = false
|
||
} else if (fileRes.data.status === 'failed') {
|
||
// 转换失败
|
||
clearInterval(statusCheckInterval.value)
|
||
throw new Error('文件转换失败')
|
||
}
|
||
// 其他状态继续等待
|
||
} else {
|
||
throw new Error(fileRes.meta.msg || '获取转换状态失败')
|
||
}
|
||
} catch (err) {
|
||
clearInterval(statusCheckInterval.value)
|
||
handleError('获取转换状态失败', err)
|
||
}
|
||
}, 2000) // 每2秒检查一次
|
||
}
|
||
|
||
// PDF相关方法
|
||
const handlePdfLoaded = (data) => {
|
||
pageCount.value = data.numPages
|
||
pdfDocument.value = data
|
||
loading.value = false
|
||
}
|
||
|
||
const handlePdfRendered = () => {
|
||
console.log('PDF页面渲染完成')
|
||
}
|
||
|
||
const handlePdfError = (error) => {
|
||
console.error('PDF加载错误:', error)
|
||
handleError('PDF文件加载失败', error)
|
||
}
|
||
|
||
const previousPage = () => {
|
||
if (currentPage.value > 1) {
|
||
currentPage.value--
|
||
}
|
||
}
|
||
|
||
const nextPage = () => {
|
||
if (currentPage.value < pageCount.value) {
|
||
currentPage.value++
|
||
}
|
||
}
|
||
|
||
const zoomIn = () => {
|
||
if (scale.value < 2) {
|
||
scale.value = Math.round((scale.value + 0.1) * 10) / 10
|
||
}
|
||
}
|
||
|
||
const zoomOut = () => {
|
||
if (scale.value > 0.5) {
|
||
scale.value = Math.round((scale.value - 0.1) * 10) / 10
|
||
}
|
||
}
|
||
|
||
const resetZoom = () => {
|
||
scale.value = 1.0
|
||
}
|
||
|
||
const resetPdfState = () => {
|
||
pdfSource.value = ''
|
||
currentPage.value = 1
|
||
pageCount.value = 0
|
||
scale.value = 1.0
|
||
pdfDocument.value = null
|
||
}
|
||
|
||
// 处理图片加载完成
|
||
const handleImageLoad = () => {
|
||
loading.value = false
|
||
}
|
||
|
||
// 处理错误
|
||
const handleError = (message, error) => {
|
||
console.error('File preview error:', error)
|
||
error.value = true
|
||
errorMessage.value = message
|
||
loading.value = false
|
||
downloading.value = false
|
||
downloadProgress.value = 0
|
||
ElMessage.error(message)
|
||
}
|
||
|
||
// 重试预览
|
||
const retryPreview = () => {
|
||
if (currentFileData.value) {
|
||
showEdit(currentFileData.value)
|
||
}
|
||
}
|
||
|
||
// 下载文件
|
||
const downloadFile = () => {
|
||
if (currentFileData.value && currentFileData.value.fileUrl) {
|
||
const link = document.createElement('a')
|
||
link.href = currentFileData.value.fileUrl
|
||
link.download = fileName.value
|
||
document.body.appendChild(link)
|
||
link.click()
|
||
document.body.removeChild(link)
|
||
} else {
|
||
ElMessage.warning('无法下载文件')
|
||
}
|
||
}
|
||
|
||
// 关闭按钮点击事件
|
||
const close = () => {
|
||
dialogFormVisible.value = false
|
||
resetState()
|
||
}
|
||
|
||
// 处理对话框关闭
|
||
const handleClose = (done) => {
|
||
close()
|
||
}
|
||
|
||
// 重置状态
|
||
const resetState = () => {
|
||
// 清除轮询
|
||
if (statusCheckInterval.value) {
|
||
clearInterval(statusCheckInterval.value)
|
||
statusCheckInterval.value = null
|
||
}
|
||
|
||
// 重置其他状态
|
||
loading.value = false
|
||
downloading.value = false
|
||
downloadProgress.value = 0
|
||
error.value = false
|
||
previewUrl.value = ''
|
||
textContent.value = ''
|
||
currentFileData.value = null
|
||
convertTaskId.value = ''
|
||
resetPdfState()
|
||
}
|
||
|
||
// 组件卸载时清理
|
||
onUnmounted(() => {
|
||
if (statusCheckInterval.value) {
|
||
clearInterval(statusCheckInterval.value)
|
||
}
|
||
})
|
||
|
||
// 暴露方法给父组件
|
||
defineExpose({
|
||
showEdit,
|
||
close,
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.file-preview-dialog {
|
||
.preview-container {
|
||
min-height: 400px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
.loading-container, .error-container {
|
||
text-align: center;
|
||
padding: 40px 0;
|
||
|
||
p {
|
||
margin-top: 16px;
|
||
color: #606266;
|
||
}
|
||
|
||
.error-message {
|
||
font-size: 14px;
|
||
color: #F56C6C;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.download-progress {
|
||
font-size: 14px;
|
||
color: #409EFF;
|
||
margin-top: 8px;
|
||
}
|
||
}
|
||
|
||
.file-content {
|
||
width: 100%;
|
||
height: 100%;
|
||
|
||
.image-preview {
|
||
text-align: center;
|
||
|
||
img {
|
||
max-width: 100%;
|
||
max-height: 70vh;
|
||
object-fit: contain;
|
||
}
|
||
}
|
||
|
||
.pdf-preview, .office-preview {
|
||
.pdf-controls {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
padding: 8px;
|
||
background: #f5f5f5;
|
||
border-radius: 4px;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.pdf-viewer-container {
|
||
height: 70vh;
|
||
overflow: auto;
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 4px;
|
||
background: #f9f9f9;
|
||
|
||
.pdf-viewer {
|
||
width: 100%;
|
||
min-height: 100%;
|
||
display: flex;
|
||
justify-content: center;
|
||
|
||
:deep(.vue-pdf-embed) {
|
||
text-align: center;
|
||
}
|
||
|
||
:deep(canvas) {
|
||
max-width: 100%;
|
||
height: auto;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.video-preview {
|
||
text-align: center;
|
||
|
||
.video-player {
|
||
max-width: 100%;
|
||
max-height: 70vh;
|
||
}
|
||
}
|
||
|
||
.audio-preview {
|
||
text-align: center;
|
||
padding: 40px 0;
|
||
|
||
.audio-player {
|
||
width: 80%;
|
||
}
|
||
}
|
||
|
||
.text-preview {
|
||
height: 70vh;
|
||
overflow: auto;
|
||
background: #f5f5f5;
|
||
padding: 16px;
|
||
border-radius: 4px;
|
||
|
||
pre {
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
margin: 0;
|
||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
}
|
||
}
|
||
|
||
.unsupported-preview {
|
||
text-align: center;
|
||
padding: 40px 0;
|
||
|
||
p {
|
||
margin-top: 16px;
|
||
color: #606266;
|
||
}
|
||
|
||
.file-name {
|
||
font-size: 14px;
|
||
color: #909399;
|
||
margin-top: 8px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.dialog-footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.file-preview-dialog {
|
||
width: 95% !important;
|
||
|
||
.preview-container {
|
||
min-height: 300px;
|
||
|
||
.file-content {
|
||
.pdf-preview, .office-preview {
|
||
.pdf-viewer-container {
|
||
height: 50vh;
|
||
}
|
||
}
|
||
|
||
.text-preview {
|
||
height: 50vh;
|
||
}
|
||
}
|
||
}
|
||
|
||
.pdf-controls {
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
|
||
.el-button-group {
|
||
width: 100%;
|
||
display: flex;
|
||
|
||
.el-button {
|
||
flex: 1;
|
||
font-size: 12px;
|
||
padding: 8px 4px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style> |