feat:修改存在的问题

This commit is contained in:
leilei
2025-11-03 09:03:32 +08:00
parent 528170fe2f
commit df359d01cc
37 changed files with 7182 additions and 913 deletions

View File

@@ -0,0 +1,846 @@
<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>
<!-- <p v-if="convertTaskId" class="task-id">任务ID: {{ convertTaskId }}</p> -->
</div>
<!-- 转换状态 -->
<div v-else-if="converting" class="loading-container">
<el-icon class="is-loading" size="48">
<Loading />
</el-icon>
<p>{{ conversionMessage }}</p>
<!-- <p class="task-id">任务ID: {{ convertTaskId }}</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>
</div>
</div>
</div>
<!-- 底部操作栏 -->
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="close">
关闭
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, getCurrentInstance, onUnmounted, nextTick, onMounted } from 'vue'
import { convertFileApi, getConvertStatusApi } from '@/api/conferencingRoom'
import { ElMessage ,ElMessageBox} from 'element-plus'
import {
Loading,
CircleClose,
Document,
ArrowLeft,
ArrowRight,
} from '@element-plus/icons-vue'
import VuePdfEmbed from 'vue-pdf-embed'
import { mqttClient } from "@/utils/mqtt.js";
import { emitter } from "@/utils/bus.js";
// 定义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 converting = ref(false)
const downloading = ref(false)
const downloadProgress = ref(0)
const loadingText = ref('正在加载...')
const conversionMessage = ref('正在转换文件...')
const error = ref(false)
const errorMessage = ref('')
const previewUrl = ref('')
const fileName = ref('')
const textContent = ref('')
const currentFileData = ref(null)
const convertTaskId = ref('')
// PDF相关状态
const pdfSource = ref('')
const currentPage = ref(1)
const pageCount = ref(0)
const scale = ref(1.0)
const pdfDocument = ref(null)
// MQTT相关
const isMqttConnected = ref(false)
// 计算属性
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
})
// 组件挂载时初始化MQTT连接
onMounted(async () => {
})
emitter.on('subscribeToFileConversionStatusTopic',subscribeToFileConversionStatusTopic)
emitter.on('fileUploadStatus',fileUploadStatus)
emitter.on('subscribeToFilePreviewTopic',subscribeToFilePreviewTopic)
function subscribeToFileConversionStatusTopic(data){
try {
const userId = JSON.parse(sessionStorage.getItem('userData'))?.uid
if (!userId) {
console.error('用户ID不存在')
return
}
const topic = `xsynergy/room/${data.roomId}/file/${userId}/conversion_status`
mqttClient.subscribe(topic, handlePdfMessage)
} catch (error) {
console.error('订阅pdf转换事件失败:', error)
}
}
function subscribeToFilePreviewTopic(data){
try {
const topic = `xsynergy/room/${data.roomId}/file/preview`
mqttClient.subscribe(topic, handleFileUploadMessage)
} catch (error) {
console.error('订阅文件上传事件失败:', error)
}
}
function fileUploadStatus(data){
if(!dialogFormVisible.value){
}
}
// 初始化MQTT连接 pdf转换成功
async function initMqttConnection() {
try {
if (isMqttConnected.value) return
const clientId = `PdfConversion_${Date.now()}`
await mqttClient.connect(clientId)
isMqttConnected.value = true
// 订阅主题
subscribeToPdfConversionTopic()
} catch (error) {
console.error('MQTT连接失败:', error)
ElMessage.error('文件转换服务连接失败')
}
}
function subscribeToPdfConversionTopic() {
try {
const userId = JSON.parse(sessionStorage.getItem('userData'))?.uid
if (!userId) {
console.error('用户ID不存在')
return
}
const topic = `xsynergy/room/${props.roomId}/file/${userId}/conversion_status`
mqttClient.subscribe(topic, handlePdfMessage)
} catch (error) {
console.error('订阅pdf转换事件失败:', error)
}
}
function handleFileUploadMessage(payload, topic){
try {
const messageStr = payload.toString()
const data = JSON.parse(messageStr)
emitter.emit('fileUploadStatus')
if(dialogFormVisible.value){
// 显示确认对话框
ElMessageBox.confirm(
`用户${data.user_uid}上传了${data.file_name}文件,是否立即预览?`,
'文件更新提示',
{
confirmButtonText: '预览',
cancelButtonText: '取消',
type: 'warning',
closeOnClickModal: false,
closeOnPressEscape: false,
showClose: false
}
).then(() => {
// 用户点击"预览"
resetPreviewState()
getPreviewFileUrl(data)
ElMessage({
message: '已切换到新文件预览',
type: 'success'
})
}).catch(() => {
// 用户点击"取消"
ElMessage({
message: '已取消预览新文件,继续查看当前文件',
type: 'info'
})
})
// resetPreviewState()
// getPreviewFileUrl(data)
} else {
ElMessage({
message: `用户${data.user_uid}上传了${data.file_name}文件`,
type: 'info',
})
showEdit(data)
}
} catch (error) {
console.error('处理转换状态消息失败:', error)
handleError('处理转换状态失败')
}
}
// 新增重置预览状态的方法
function resetPreviewState() {
loading.value = true
converting.value = false
downloading.value = false
downloadProgress.value = 0
error.value = false
previewUrl.value = ''
textContent.value = ''
resetPdfState()
// 强制重新渲染
nextTick(() => {
// 确保DOM更新
})
}
function handlePdfMessage(payload, topic){
try {
const messageStr = payload.toString()
const data = JSON.parse(messageStr)
switch (data.status) {
case 'converting':
break
case 'completed':
getConvertedFile(data.task_id)
break
case 'failed':
break
default:
console.warn('未知的转换状态:', data.status)
}
} catch (error) {
console.error('处理转换状态消息失败:', error)
handleError('处理转换状态失败')
}
}
// 获取转换后的文件
const getConvertedFile = async (taskId) => {
try {
if (!taskId) {
throw new Error('任务ID不存在')
}
const fileRes = await getConvertStatusApi(taskId,props.roomId)
} catch (err) {
console.error('获取转换文件失败:', err)
handleError('获取转换文件失败', err)
}
}
// 修改 getPreviewFileUrl 方法,确保状态正确更新
async function getPreviewFileUrl(file){
fileName.value = file.file_name || file.source_url.split('/').pop()
const fileExt = fileName.value.split('.').pop().toLowerCase()
try {
// 根据文件类型处理转换后的文件
if (fileExt === 'pdf' || enumType.includes(fileExt)) {
// PDF和Office文档使用PDF预览器
await loadPdfFile(file.preview_url || file.file_url)
} else if (fileExt === 'txt') {
// 文本文件
await loadTextFile(file.preview_url || file.file_url)
} else if (['png', 'jpg', 'jpeg', 'gif'].includes(fileExt)) {
// 图片文件直接预览
previewUrl.value = file.preview_url || file.file_url
// 确保图片重新加载
nextTick(() => {
loading.value = false
})
} else if (fileExt === 'mp4') {
// 视频文件
previewUrl.value = file.preview_url || file.file_url
nextTick(() => {
loading.value = false
})
} else if (fileExt === 'mp3') {
// 音频文件
previewUrl.value = file.preview_url || file.file_url
nextTick(() => {
loading.value = false
})
} else {
// 其他文件类型
previewUrl.value = file.preview_url || file.file_url
nextTick(() => {
loading.value = false
})
}
} catch (error) {
console.error('加载预览文件失败:', error)
handleError('加载预览文件失败', error)
}
}
// 显示弹框
const showEdit = async (data) => {
// 重置状态
resetPreviewState()
title.value = '文件预览'
dialogFormVisible.value = true
currentFileData.value = data
// fileName.value = data.file_name || data.source_url.split('/').pop()
await getPreviewFileUrl(data)
}
// 加载PDF文件
const loadPdfFile = async (fileUrl) => {
try {
loading.value = false
downloading.value = true
downloadProgress.value = 0
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)
}
}
const blob = new Blob(chunks)
const blobUrl = URL.createObjectURL(blob)
// 先清理之前的URL
if (pdfSource.value) {
URL.revokeObjectURL(pdfSource.value)
}
previewUrl.value = fileUrl
pdfSource.value = blobUrl
// 重置PDF状态
currentPage.value = 1
pageCount.value = 0
downloading.value = false
downloadProgress.value = 0
} 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 },props.roomId)
if (res.meta.code !== 200) {
throw new Error(res.meta.msg || '文件转换失败')
}
convertTaskId.value = res.data.task_id
// 等待MQTT消息不进行轮询
loading.value = false
converting.value = true
conversionMessage.value = '已提交转换任务,等待处理...'
} catch (err) {
handleError('文件转换失败', err)
}
}
// PDF相关方法
const handlePdfLoaded = (data) => {
pageCount.value = data.numPages
pdfDocument.value = data
loading.value = false
converting.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 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) => {
error.value = true
errorMessage.value = message
loading.value = false
converting.value = false
downloading.value = false
downloadProgress.value = 0
ElMessage.error(message)
}
// 重试预览
const retryPreview = () => {
if (currentFileData.value) {
showEdit(currentFileData.value)
}
}
// 关闭按钮点击事件
const close = () => {
dialogFormVisible.value = false
resetState()
}
// 处理对话框关闭
const handleClose = (done) => {
close()
}
// 重置状态
const resetState = () => {
loading.value = false
converting.value = false
downloading.value = false
downloadProgress.value = 0
error.value = false
previewUrl.value = ''
textContent.value = ''
currentFileData.value = null
convertTaskId.value = ''
resetPdfState()
}
// 组件卸载时清理
onUnmounted(() => {
// 可以在这里取消特定主题的订阅,但不断开连接
// const userId = JSON.parse(sessionStorage.getItem('userData'))?.uid
// if (userId) {
// const topic = `xsynergy/room/${props.roomId}/file/${userId}/conversion_status`
// mqttClient.unsubscribe(topic, handlePdfMessage)
// }
})
// 暴露方法给父组件
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>