feat:修改存在的问题
This commit is contained in:
3235
package-lock.json
generated
3235
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,8 @@
|
|||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-svg-icons": "^2.0.1",
|
"vite-plugin-svg-icons": "^2.0.1",
|
||||||
"vue": "^3.5.12",
|
"vue": "^3.5.12",
|
||||||
|
"vue-pdf": "^4.3.0",
|
||||||
|
"vue-pdf-embed": "^2.1.3",
|
||||||
"vue-router": "^4.4.5"
|
"vue-router": "^4.4.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -72,3 +72,52 @@ export function getRoomInfoApi(room_uid) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//获取文件列表
|
||||||
|
export function getFileListApi(room_uid) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/conversion/${room_uid}/files`,
|
||||||
|
method: 'get',
|
||||||
|
// params:{
|
||||||
|
// service: room_uid
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//上传文件获取token
|
||||||
|
export function getUploadTokenApi(data) {
|
||||||
|
return request({
|
||||||
|
url: `/api/file/token`,
|
||||||
|
method: 'post',
|
||||||
|
data: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//上传文件
|
||||||
|
export function uploadFileApi(token,data) {
|
||||||
|
return request({
|
||||||
|
url: `/api/file/${token}/upload`,
|
||||||
|
method: 'post',
|
||||||
|
data: data,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//提交一个文档转换任务,将指定的 Office 文档转换为 PDF 格式
|
||||||
|
export function convertFileApi(data,roomId) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/conversion/${ roomId }/convert`,
|
||||||
|
method: 'post',
|
||||||
|
data: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取文档转换任务状态结构
|
||||||
|
export function getConvertStatusApi(taskId,roomId) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/conversion/${ roomId }/status/${taskId}`,
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
src/assets/images/Gc_114_line-Level-action.png
Normal file
BIN
src/assets/images/Gc_114_line-Level-action.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/images/Gc_114_line-Level.png
Normal file
BIN
src/assets/images/Gc_114_line-Level.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/images/book-read-fill-action.png
Normal file
BIN
src/assets/images/book-read-fill-action.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/assets/images/book-read-fill.png
Normal file
BIN
src/assets/images/book-read-fill.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/assets/images/file-logo.png
Normal file
BIN
src/assets/images/file-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 861 B |
BIN
src/assets/images/file-logoW.png
Normal file
BIN
src/assets/images/file-logoW.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 744 B |
@@ -43,10 +43,10 @@ function isDashboard(route) {
|
|||||||
}
|
}
|
||||||
function handleLink(item) {
|
function handleLink(item) {
|
||||||
const { redirect, path } = item
|
const { redirect, path } = item
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
router.push(redirect)
|
router.push(redirect)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
router.push(path)
|
router.push(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ onMounted(() => {
|
|||||||
transition: background 0.3s ease, transform 0.2s ease;
|
transition: background 0.3s ease, transform 0.2s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
// background: linear-gradient(135deg, #000, #000);
|
||||||
background: linear-gradient(135deg, #00a5a1, #008f8b);
|
background: linear-gradient(135deg, #00a5a1, #008f8b);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,9 +124,9 @@ function handleSelect(key, keyPath) {
|
|||||||
// 没有子路由路径内部打开
|
// 没有子路由路径内部打开
|
||||||
const routeMenu = childrenMenus.value.find(item => item.path === key);
|
const routeMenu = childrenMenus.value.find(item => item.path === key);
|
||||||
if (routeMenu && routeMenu.query) {
|
if (routeMenu && routeMenu.query) {
|
||||||
let query = JSON.parse(routeMenu.query);
|
let query = JSON.parse(routeMenu.query);
|
||||||
router.push({ path: key, query: query });
|
router.push({ path: key, query: query });
|
||||||
} else {
|
} else {
|
||||||
router.push({ path: key });
|
router.push({ path: key });
|
||||||
}
|
}
|
||||||
appStore.toggleSideBarHide(true);
|
appStore.toggleSideBarHide(true);
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ function closeLeftTags() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
function closeOthersTags() {
|
function closeOthersTags() {
|
||||||
router.push(selectedTag.value).catch(() => { });
|
router.push(selectedTag.value).catch(() => { });
|
||||||
proxy.$tab.closeOtherPage(selectedTag.value).then(() => {
|
proxy.$tab.closeOtherPage(selectedTag.value).then(() => {
|
||||||
moveToCurrentTag()
|
moveToCurrentTag()
|
||||||
@@ -201,15 +201,15 @@ function closeAllTags(view) {
|
|||||||
}
|
}
|
||||||
function toLastView(visitedViews, view) {
|
function toLastView(visitedViews, view) {
|
||||||
const latestView = visitedViews.slice(-1)[0]
|
const latestView = visitedViews.slice(-1)[0]
|
||||||
if (latestView) {
|
if (latestView) {
|
||||||
router.push(latestView.fullPath)
|
router.push(latestView.fullPath)
|
||||||
} else {
|
} else {
|
||||||
// now the default is to redirect to the home page if there is no tags-view,
|
// now the default is to redirect to the home page if there is no tags-view,
|
||||||
// you can adjust it according to your needs.
|
// you can adjust it according to your needs.
|
||||||
if (view.name === 'Dashboard') {
|
if (view.name === 'Dashboard') {
|
||||||
// to reload home page
|
// to reload home page
|
||||||
router.replace({ path: '/redirect' + view.fullPath })
|
router.replace({ path: '/redirect' + view.fullPath })
|
||||||
} else {
|
} else {
|
||||||
router.push('/')
|
router.push('/')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ function handleClickOutside() {
|
|||||||
.main-container {
|
.main-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: calc(100vh - 50px);
|
height: calc(100vh - 50px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -163,7 +163,7 @@ function handleClickOutside() {
|
|||||||
width: 200px;
|
width: 200px;
|
||||||
// box-shadow: 0 0 10px 4px #f6f6f6;
|
// box-shadow: 0 0 10px 4px #f6f6f6;
|
||||||
}
|
}
|
||||||
.sidebar-right {
|
.sidebar-right {
|
||||||
width: calc(100% - 200px);
|
width: calc(100% - 200px);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -2,6 +2,17 @@ import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router
|
|||||||
import Layout from "@/layout/index.vue";
|
import Layout from "@/layout/index.vue";
|
||||||
|
|
||||||
export const constantRoutes = [
|
export const constantRoutes = [
|
||||||
|
{
|
||||||
|
path: '/redirect',
|
||||||
|
component: Layout,
|
||||||
|
hidden: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/redirect/:path(.*)',
|
||||||
|
component: () => import('@/views/redirect/index.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
redirect: '/login', // 这里做重定向
|
redirect: '/login', // 这里做重定向
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ export const useAppStore = defineStore(
|
|||||||
this.sidebar.opened = !this.sidebar.opened
|
this.sidebar.opened = !this.sidebar.opened
|
||||||
this.sidebar.withoutAnimation = withoutAnimation
|
this.sidebar.withoutAnimation = withoutAnimation
|
||||||
if (this.sidebar.opened) {
|
if (this.sidebar.opened) {
|
||||||
Cookies.set('sidebarStatus', 1)
|
// Cookies.set('sidebarStatus', 1)
|
||||||
} else {
|
} else {
|
||||||
Cookies.set('sidebarStatus', 0)
|
// Cookies.set('sidebarStatus', 0)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
closeSideBar({ withoutAnimation }) {
|
closeSideBar({ withoutAnimation }) {
|
||||||
Cookies.set('sidebarStatus', 0)
|
// Cookies.set('sidebarStatus', 0)
|
||||||
this.sidebar.opened = false
|
this.sidebar.opened = false
|
||||||
this.sidebar.withoutAnimation = withoutAnimation
|
this.sidebar.withoutAnimation = withoutAnimation
|
||||||
},
|
},
|
||||||
|
|||||||
2
src/utils/bus.js
Normal file
2
src/utils/bus.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import mitt from 'mitt'
|
||||||
|
export const emitter = mitt()
|
||||||
@@ -16,7 +16,7 @@ const service = axios.create({
|
|||||||
// axios中请求配置有baseURL选项,表示请求URL公共部分
|
// axios中请求配置有baseURL选项,表示请求URL公共部分
|
||||||
baseURL: import.meta.env.VITE_APP_BASE_API,
|
baseURL: import.meta.env.VITE_APP_BASE_API,
|
||||||
// 超时
|
// 超时
|
||||||
timeout: 10000,
|
// timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// request拦截器
|
// request拦截器
|
||||||
@@ -134,11 +134,15 @@ service.interceptors.response.use(
|
|||||||
case 201:
|
case 201:
|
||||||
return Promise.resolve(responseData);
|
return Promise.resolve(responseData);
|
||||||
case 401:
|
case 401:
|
||||||
return Promise.resolve(responseData);
|
// return Promise.resolve(responseData);
|
||||||
// return handleUnauthorized().then(() => {
|
const currentPath = router.currentRoute.value.name;
|
||||||
// return Promise.reject({ code: 401, message: '未授权' });
|
if(currentPath == 'ConferencingRoom'){
|
||||||
// });
|
return Promise.resolve(responseData);
|
||||||
|
}else{
|
||||||
|
return handleUnauthorized().then(() => {
|
||||||
|
return Promise.reject({ code: 401, message: '未授权' });
|
||||||
|
});
|
||||||
|
}
|
||||||
case 500:
|
case 500:
|
||||||
const serverErrorMsg = responseData.meta?.message || '服务器内部错误';
|
const serverErrorMsg = responseData.meta?.message || '服务器内部错误';
|
||||||
ElMessage({ message: serverErrorMsg, type: 'error' });
|
ElMessage({ message: serverErrorMsg, type: 'error' });
|
||||||
@@ -173,31 +177,54 @@ service.interceptors.response.use(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 单独处理401未授权
|
// 单独处理401未授权
|
||||||
|
// function handleUnauthorized() {
|
||||||
|
// return ElMessageBox.confirm(
|
||||||
|
// '认证信息已失效,您可以继续留在该页面,或者重新登录',
|
||||||
|
// '系统提示',
|
||||||
|
// {
|
||||||
|
// confirmButtonText: '重新登录',
|
||||||
|
// cancelButtonText: '取消',
|
||||||
|
// type: 'warning',
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// .then(() => {
|
||||||
|
// removeToken()
|
||||||
|
// if (router.currentRoute.path !== '/login') {
|
||||||
|
// router.push({
|
||||||
|
// path: '/login',
|
||||||
|
// query: { redirect: router.currentRoute.fullPath }
|
||||||
|
// });
|
||||||
|
// } else {
|
||||||
|
// // 如果在登录页,强制刷新以清除残留状态
|
||||||
|
// window.location.reload();
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .catch(() => {
|
||||||
|
// return Promise.reject('用户取消操作');
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
function handleUnauthorized() {
|
function handleUnauthorized() {
|
||||||
return ElMessageBox.confirm(
|
removeToken();
|
||||||
'认证信息已失效,您可以继续留在该页面,或者重新登录',
|
|
||||||
'系统提示',
|
// 使用 nextTick 确保路由状态已更新
|
||||||
{
|
import('vue').then(({ nextTick }) => {
|
||||||
confirmButtonText: '重新登录',
|
nextTick(() => {
|
||||||
cancelButtonText: '取消',
|
const currentPath = router.currentRoute.value.fullPath;
|
||||||
type: 'warning',
|
console.log('当前路由:', currentPath);
|
||||||
}
|
|
||||||
)
|
if (router.currentRoute.value.path !== '/login') {
|
||||||
.then(() => {
|
|
||||||
removeToken()
|
|
||||||
if (router.currentRoute.path !== '/login') {
|
|
||||||
router.push({
|
router.push({
|
||||||
path: '/login',
|
path: '/login',
|
||||||
query: { redirect: router.currentRoute.fullPath }
|
query: {
|
||||||
|
redirect: currentPath !== '/login' ? currentPath : undefined
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 如果在登录页,强制刷新以清除残留状态
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
return Promise.reject('用户取消操作');
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default service;
|
export default service;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getWhiteboardShapes, getWhiteboardHistory } from "@/views/custom/api";
|
|||||||
import { useMeterStore } from '@/stores/modules/meter';
|
import { useMeterStore } from '@/stores/modules/meter';
|
||||||
import { encode, decode } from '@msgpack/msgpack'
|
import { encode, decode } from '@msgpack/msgpack'
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { emitter } from "@/utils/bus.js";
|
||||||
const meterStore = useMeterStore();
|
const meterStore = useMeterStore();
|
||||||
meterStore.initUdid();
|
meterStore.initUdid();
|
||||||
|
|
||||||
@@ -39,6 +39,8 @@ export const WhiteboardSync = {
|
|||||||
const res = await getWhiteboardHistory({ after_timestamp: 0 }, roomUid);
|
const res = await getWhiteboardHistory({ after_timestamp: 0 }, roomUid);
|
||||||
if (res.meta.code === 200 && res.data.shapes.length > 0) {
|
if (res.meta.code === 200 && res.data.shapes.length > 0) {
|
||||||
canvasInstance.addShape(res.data.shapes);
|
canvasInstance.addShape(res.data.shapes);
|
||||||
|
}else if(res.meta.code === 401){
|
||||||
|
emitter.emit('whiteboardFailed',true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 订阅当前房间
|
// 订阅当前房间
|
||||||
|
|||||||
@@ -85,4 +85,18 @@ export function drawSmoothCurve(ctx, path) {
|
|||||||
|
|
||||||
// 连接最后两个点
|
// 连接最后两个点
|
||||||
ctx.lineTo(path[path.length - 1].x, path[path.length - 1].y);
|
ctx.lineTo(path[path.length - 1].x, path[path.length - 1].y);
|
||||||
|
}
|
||||||
|
|
||||||
|
//计算文件的SHA1值
|
||||||
|
export async function calculateFileSHA1(file) {
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-1', arrayBuffer);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
return hashHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用于比较两个对象是否相等
|
||||||
|
export function simpleDeepEqual(obj1, obj2) {
|
||||||
|
return JSON.stringify(obj1) === JSON.stringify(obj2);
|
||||||
}
|
}
|
||||||
@@ -156,11 +156,8 @@ import {
|
|||||||
try {
|
try {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
let res = await getDirectories({ level: 1 });
|
let res = await getDirectories({ level: 1 });
|
||||||
if (res.meta.code == 401) {
|
if (res.meta.code == 200) {
|
||||||
emit('showLogin', true);
|
resolve(res.data);
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
resolve(res.data);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let res = await getDirectoriesUsers(id, { directory_uuid: id });
|
let res = await getDirectoriesUsers(id, { directory_uuid: id });
|
||||||
|
|||||||
846
src/views/conferencingRoom/components/fileUpload/browseFile.vue
Normal file
846
src/views/conferencingRoom/components/fileUpload/browseFile.vue
Normal 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>
|
||||||
269
src/views/conferencingRoom/components/fileUpload/fileList.vue
Normal file
269
src/views/conferencingRoom/components/fileUpload/fileList.vue
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-drawer v-model="drawerVisible" direction="rtl" title="文件列表" size="40%">
|
||||||
|
<template #header>
|
||||||
|
<h4>文件列表</h4>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="drawer-content">
|
||||||
|
<!-- 上传按钮 -->
|
||||||
|
<div class="upload-section">
|
||||||
|
<el-button type="primary" size="small" @click="handleUpload">
|
||||||
|
<el-icon><Upload /></el-icon>
|
||||||
|
上传文件
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文件列表 -->
|
||||||
|
<div class="file-list" v-loading="loading">
|
||||||
|
<!-- 文件列表内容 -->
|
||||||
|
<div v-for="item in fileList" :key="item.id || item.fileKey" class="file-item">
|
||||||
|
<div class="file-info">
|
||||||
|
<div class="file-icon">
|
||||||
|
<img :src="getFileIcon(item.file_name)" alt="文件图标" class="file-icon-img">
|
||||||
|
</div>
|
||||||
|
<div class="file-details">
|
||||||
|
<div class="file-name" :title="item.file_name">{{ item.file_name }}</div>
|
||||||
|
<!-- <div class="file-meta">
|
||||||
|
<span class="file-size">{{ formatFileSize(item.fileSize) }}</span>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="file-actions">
|
||||||
|
<el-button type="primary" size="small" :disabled="!item.preview_url" @click="handlePreview(item)">预览</el-button>
|
||||||
|
<el-button type="success" size="small" @click="handleDownload(item)">下载</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-if="fileList.length === 0" class="empty-state">
|
||||||
|
<el-empty description="暂无文件" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-drawer>
|
||||||
|
<!-- 文件上传 -->
|
||||||
|
<UpLoadFile
|
||||||
|
ref="uploadRef"
|
||||||
|
:fileType='["pdf", "png", "jpg", "jpeg","gif","doc","docx","xls","xlsx","ppt","pptx","txt","mp4","mp3"]'
|
||||||
|
:roomId="roomId"
|
||||||
|
@upload-success="handleUploadSuccess"
|
||||||
|
/>
|
||||||
|
<!-- 文件预览 -->
|
||||||
|
<BrowseFile ref="browseFileRef" :roomId="roomId"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
getFileListApi,
|
||||||
|
} from '@/api/conferencingRoom.js'
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { Upload } from '@element-plus/icons-vue'
|
||||||
|
import UpLoadFile from './upLoadFile.vue'
|
||||||
|
import BrowseFile from './browseFile.vue'
|
||||||
|
import fileLogo from '@/assets/images/file-logo.png';
|
||||||
|
import { emitter } from "@/utils/bus.js";
|
||||||
|
// 定义 emit
|
||||||
|
const emit = defineEmits([""]);
|
||||||
|
|
||||||
|
// 接收 props
|
||||||
|
const props = defineProps({
|
||||||
|
roomId: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('fileUploadStatus',async ()=>{
|
||||||
|
if(drawerVisible.value){
|
||||||
|
console.log('更新啦')
|
||||||
|
await getFileList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const drawerVisible = ref(false);
|
||||||
|
const fileList = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const uploadRef = ref(null);
|
||||||
|
|
||||||
|
//文件预览
|
||||||
|
const browseFileRef = ref(null);
|
||||||
|
|
||||||
|
// 根据文件扩展名获取文件图标
|
||||||
|
function getFileIcon(fileName) {
|
||||||
|
return fileLogo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
function formatFileSize(size) {
|
||||||
|
if (!size) return '未知大小';
|
||||||
|
|
||||||
|
if (size < 1024) {
|
||||||
|
return size + ' B';
|
||||||
|
} else if (size < 1024 * 1024) {
|
||||||
|
return (size / 1024).toFixed(2) + ' KB';
|
||||||
|
} else {
|
||||||
|
return (size / (1024 * 1024)).toFixed(2) + ' MB';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件下载功能
|
||||||
|
function handleDownload(file) {
|
||||||
|
try {
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.style.display = 'none';
|
||||||
|
iframe.src = file.source_url;
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(iframe);
|
||||||
|
}, 5000); // 5秒后清理iframe
|
||||||
|
|
||||||
|
ElMessage.success('开始下载文件');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('iframe下载失败:', error);
|
||||||
|
ElMessage.error('下载失败,请检查浏览器设置');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件预览功能
|
||||||
|
function handlePreview(file) {
|
||||||
|
if (!file.preview_url) {
|
||||||
|
ElMessage.error('文件链接无效');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
browseFileRef.value.showEdit(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传文件
|
||||||
|
function handleUpload() {
|
||||||
|
uploadRef.value.showEdit()
|
||||||
|
// 这里可以添加文件上传逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示抽屉
|
||||||
|
async function show() {
|
||||||
|
drawerVisible.value = true;
|
||||||
|
await getFileList()
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取文件列表
|
||||||
|
async function getFileList(){
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
// xsy
|
||||||
|
const res = await getFileListApi(props.roomId);
|
||||||
|
if (res.meta.code !== 200) {
|
||||||
|
ElMessage.error(res.meta.msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fileList.value = res.data.files || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取文件列表失败:', error);
|
||||||
|
ElMessage.error('获取文件列表失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//文件上传成功
|
||||||
|
async function handleUploadSuccess(){
|
||||||
|
if(drawerVisible.value){
|
||||||
|
await getFileList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
show,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.drawer-content {
|
||||||
|
padding: 0 10px;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-section {
|
||||||
|
padding: 15px 0;
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0; /* 防止内容溢出 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
margin-right: 12px;
|
||||||
|
flex-shrink: 0; /* 防止图标被压缩 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon-img {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-details {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0; /* 防止文本溢出 */
|
||||||
|
overflow: hidden; /* 隐藏溢出内容 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0; /* 防止按钮被压缩 */
|
||||||
|
margin-left: 10px; /* 添加左边距,与文件信息保持距离 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="file-upload-container">
|
|
||||||
<input type="file" ref="fileInput" @change="handleFileChange" />
|
|
||||||
<button @click="uploadFile">上传文件</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -0,0 +1,726 @@
|
|||||||
|
<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>
|
||||||
291
src/views/conferencingRoom/components/fileUpload/upLoadFile.vue
Normal file
291
src/views/conferencingRoom/components/fileUpload/upLoadFile.vue
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogFormVisible"
|
||||||
|
:title="title"
|
||||||
|
width="403px"
|
||||||
|
@close="close"
|
||||||
|
>
|
||||||
|
<el-upload
|
||||||
|
ref="uploadRef"
|
||||||
|
:accept="acceptString"
|
||||||
|
:show-file-list="false"
|
||||||
|
:limit="999"
|
||||||
|
style="width: 100%; text-align: center"
|
||||||
|
:before-upload="handleBeforeUpload"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
class="el-button-custom-css blue-css"
|
||||||
|
:loading="uploadLoading"
|
||||||
|
:disabled="uploadLoading"
|
||||||
|
>
|
||||||
|
{{ uploadLoading ? '上传中...' : '上传文件' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
<div style="margin-top: 20px">
|
||||||
|
请上传格式为:
|
||||||
|
<b style="color: #f56c6c">
|
||||||
|
{{ acceptString }}
|
||||||
|
</b>
|
||||||
|
的文件
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, getCurrentInstance } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { getUploadTokenApi, uploadFileApi,convertFileApi } from '@/api/conferencingRoom'
|
||||||
|
import { calculateFileSHA1 } from '@/views/conferencingRoom/business/index.js'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
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"],
|
||||||
|
},
|
||||||
|
roomId: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义emits
|
||||||
|
const emit = defineEmits(['upload-success'])
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const dialogFormVisible = ref(false)
|
||||||
|
const title = ref('')
|
||||||
|
const fileList = ref([])
|
||||||
|
const fileIds = ref([])
|
||||||
|
const uploadRef = ref(null)
|
||||||
|
const showList = ref(true)
|
||||||
|
const saveLoading = ref(false)
|
||||||
|
|
||||||
|
const uploadToken = ref('')
|
||||||
|
const fileUrl = ref('')
|
||||||
|
const currentUploadFile = ref(null) // 存储当前要上传的文件
|
||||||
|
const uploadLoading = ref(false) // 上传loading状态
|
||||||
|
|
||||||
|
const roomId = ref()
|
||||||
|
const uploaderInfo = ref('')
|
||||||
|
|
||||||
|
// 计算属性:将文件类型数组转换为accept字符串
|
||||||
|
const acceptString = computed(() => {
|
||||||
|
return props.fileType.map(type => `.${type}`).join(', ')
|
||||||
|
})
|
||||||
|
|
||||||
|
emitter.on('subscribeToFileUploadTopic',subscribeToFileUploadTopic)
|
||||||
|
|
||||||
|
|
||||||
|
function subscribeToFileUploadTopic(data){
|
||||||
|
try {
|
||||||
|
// 订阅文件上传状态主题
|
||||||
|
roomId.value = data.roomId
|
||||||
|
const topic = `xsynergy/room/${data.roomId}/file/upload`
|
||||||
|
mqttClient.subscribe(topic, handleFileUploadMessage)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('订阅文件上传事件失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//订阅文件上传状态主题
|
||||||
|
function handleFileUploadMessage(payload, topic){
|
||||||
|
try {
|
||||||
|
const messageStr = payload.toString()
|
||||||
|
const data = JSON.parse(messageStr)
|
||||||
|
emitter.emit('fileUploadStatus',data)
|
||||||
|
emit('upload-success')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('文件长传状态消息失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传前校检格式和大小
|
||||||
|
const handleBeforeUpload = async (file) => {
|
||||||
|
// 如果正在上传中,阻止新文件上传
|
||||||
|
if (uploadLoading.value) {
|
||||||
|
ElMessage.warning('文件正在上传中,请稍候...')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件扩展名
|
||||||
|
const fileExtension = file.name.toLowerCase().slice(((file.name.lastIndexOf(".") - 1) >>> 0) + 2)
|
||||||
|
// 校验文件格式
|
||||||
|
if (!props.fileType.includes(fileExtension)) {
|
||||||
|
ElMessage.error(`文件格式不支持,请上传 ${acceptString.value} 格式的文件`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校检文件大小
|
||||||
|
const isLt = file.size / 1024 / 1024 < 50
|
||||||
|
if (!isLt) {
|
||||||
|
ElMessage.error(`上传文件大小不能超过 50 MB!`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始上传,设置loading状态
|
||||||
|
uploadLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 保存当前文件引用
|
||||||
|
currentUploadFile.value = file
|
||||||
|
|
||||||
|
// 计算文件SHA1
|
||||||
|
const sha1 = await calculateFileSHA1(file)
|
||||||
|
|
||||||
|
// 获取上传token
|
||||||
|
const res = await getUploadTokenApi({
|
||||||
|
service: props.roomId,
|
||||||
|
hash: sha1,
|
||||||
|
ext: fileExtension,
|
||||||
|
})
|
||||||
|
|
||||||
|
if(res.meta.code != 200){
|
||||||
|
ElMessage.error(res.meta.msg)
|
||||||
|
uploadLoading.value = false // 出错时取消loading
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if(res.data.exists){
|
||||||
|
// 文件已存在,直接获取文件URL
|
||||||
|
fileUrl.value = res.data.fileUrl
|
||||||
|
ElMessage.info('文件已存在,无需重复上传')
|
||||||
|
// dialogFormVisible.value = false
|
||||||
|
uploadLoading.value = false
|
||||||
|
} else {
|
||||||
|
// 文件不存在,获取token并执行上传
|
||||||
|
uploadToken.value = res.data.token
|
||||||
|
// 执行上传操作
|
||||||
|
await handleHttpRequest(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传过程出错:', error)
|
||||||
|
ElMessage.error('上传过程出错,请重试')
|
||||||
|
uploadLoading.value = false // 出错时取消loading
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return false // 阻止默认上传行为,因为我们使用自定义上传
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义上传
|
||||||
|
const handleHttpRequest = async (file) => {
|
||||||
|
if (!uploadToken.value) {
|
||||||
|
ElMessage.error('上传凭证不存在')
|
||||||
|
uploadLoading.value = false // 取消loading
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let params = new FormData()
|
||||||
|
params.append('file', file)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await uploadFileApi(uploadToken.value, params)
|
||||||
|
if(res.meta.code != 200){
|
||||||
|
ElMessage.error(res.meta.msg)
|
||||||
|
uploadLoading.value = false // 出错时取消loading
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
fileUrl.value = res.data.fileUrl
|
||||||
|
getFileTaskId(res.data.fileUrl)
|
||||||
|
ElMessage.success('文件上传成功')
|
||||||
|
// publishFileUploadData(file);
|
||||||
|
|
||||||
|
// 上传成功,取消loading
|
||||||
|
uploadLoading.value = false
|
||||||
|
dialogFormVisible.value = false
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传文件失败:', error)
|
||||||
|
ElMessage.error('上传文件失败')
|
||||||
|
uploadLoading.value = false // 出错时取消loading
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function getFileTaskId(fileUrl) {
|
||||||
|
try {
|
||||||
|
const res = await convertFileApi({ file_url: fileUrl },props.roomId)
|
||||||
|
if (res.meta.code !== 200) {
|
||||||
|
throw new Error(res.meta.msg || '文件转换失败')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error('文件转换失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function publishFileUploadData(fileData) {
|
||||||
|
try {
|
||||||
|
const message = {
|
||||||
|
uploaderName: uploaderInfo.value?.name || '',
|
||||||
|
uploaderUid: uploaderInfo.value?.uid || '',
|
||||||
|
fileName: fileData.name,
|
||||||
|
fileUrl: fileUrl.value,
|
||||||
|
roomId: roomId.value,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
mqttClient.publish(`xSynergy/File/Upload/${roomId.value}`, message);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发布激光笔数据失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传前校检格式和大小
|
||||||
|
const beforeUploadser = (file) => {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示弹框
|
||||||
|
const showEdit = () => {
|
||||||
|
title.value = '上传文件'
|
||||||
|
dialogFormVisible.value = true
|
||||||
|
uploaderInfo.value = JSON.parse(sessionStorage.getItem('userData'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭按钮点击事件
|
||||||
|
const close = () => {
|
||||||
|
fileList.value = []
|
||||||
|
currentUploadFile.value = null
|
||||||
|
uploadToken.value = ''
|
||||||
|
fileUrl.value = ''
|
||||||
|
uploadLoading.value = false // 关闭时重置loading状态
|
||||||
|
dialogFormVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存按钮点击事件
|
||||||
|
const save = () => {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
showEdit,
|
||||||
|
close,
|
||||||
|
save
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.avatar-uploader {
|
||||||
|
.el-upload {
|
||||||
|
width: 345px;
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
.el-upload-list__item-thumbnail {
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 765px) {
|
||||||
|
.el-dialog {
|
||||||
|
width: 80% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,10 @@
|
|||||||
<div class="video-overlay">
|
<div class="video-overlay">
|
||||||
<span class="participant-name">{{ screenSharingUser }}</span>
|
<span class="participant-name">{{ screenSharingUser }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 添加canvas容器 -->
|
||||||
|
<div class="canvas-container" ref="canvasContainerRef">
|
||||||
|
<canvas ref="screenShareCanvasRef" class="screen-share-canvas"></canvas>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,12 +136,67 @@
|
|||||||
<!-- 固定在底部的控制按钮 -->
|
<!-- 固定在底部的控制按钮 -->
|
||||||
<div class="fixed-controls">
|
<div class="fixed-controls">
|
||||||
<div class="controls-container">
|
<div class="controls-container">
|
||||||
<el-button @click="toggleCamera" :type="cameraEnabled ? 'danger' : 'info'" class="control-btn" size="large">
|
|
||||||
{{ cameraEnabled ? '关闭摄像头' : '开启摄像头' }}
|
<div class="microphone-control-group">
|
||||||
</el-button>
|
<el-button @click="toggleCamera" :type="cameraEnabled ? 'danger' : 'info'" class="control-btn microphone-btn" size="large">
|
||||||
<el-button @click="toggleMicrophone" :type="microphoneEnabled ? 'danger' : 'info'" class="control-btn" size="large">
|
{{ cameraEnabled ? '关闭摄像头' : '开启摄像头' }}
|
||||||
{{ microphoneEnabled ? '关闭麦克风' : '开启麦克风' }}
|
</el-button>
|
||||||
</el-button>
|
<!-- 摄像头选择下拉菜单 -->
|
||||||
|
<el-dropdown trigger="click" @command="handleCameraCommand" @visible-change="handleCameraVisibleChange" class="control-dropdown microphone-dropdown">
|
||||||
|
<el-button :type="cameraEnabled ? 'danger' : 'info'" class="control-btn dropdown-btn" size="large">
|
||||||
|
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item
|
||||||
|
v-for="device in cameraDevices"
|
||||||
|
:key="device.deviceId"
|
||||||
|
:command="device.deviceId"
|
||||||
|
:class="{ 'selected-device': selectedCameraId === device.deviceId }"
|
||||||
|
>
|
||||||
|
<i class="el-icon-video-camera"></i>
|
||||||
|
{{ device.label || `摄像头 ${cameraDevices.indexOf(device) + 1}` }}
|
||||||
|
<el-icon v-if="selectedCameraId === device.deviceId" class="check-icon"><check /></el-icon>
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item divided command="refresh">
|
||||||
|
<el-icon><refresh /></el-icon>
|
||||||
|
刷新设备列表
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
<div class="microphone-control-group">
|
||||||
|
<el-button @click="toggleMicrophone" :type="microphoneEnabled ? 'danger' : 'info'" class="control-btn microphone-btn" size="large">
|
||||||
|
{{ microphoneEnabled ? '关闭麦克风' : '开启麦克风' }}
|
||||||
|
</el-button>
|
||||||
|
<!-- 麦克风选择下拉菜单 -->
|
||||||
|
<el-dropdown trigger="click" @command="handleMicrophoneCommand" @visible-change="handleMicrophoneVisibleChange" class="control-dropdown microphone-dropdown">
|
||||||
|
<el-button :type="microphoneEnabled ? 'danger' : 'info'" class="control-btn dropdown-btn" size="large">
|
||||||
|
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item
|
||||||
|
v-for="device in microphoneDevices"
|
||||||
|
:key="device.deviceId"
|
||||||
|
:command="device.deviceId"
|
||||||
|
:class="{ 'selected-device': selectedMicrophoneId === device.deviceId }"
|
||||||
|
>
|
||||||
|
<i class="el-icon-microphone"></i>
|
||||||
|
{{ device.label || `麦克风 ${microphoneDevices.indexOf(device) + 1}` }}
|
||||||
|
<el-icon v-if="selectedMicrophoneId === device.deviceId" class="check-icon"><check /></el-icon>
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item divided command="refresh">
|
||||||
|
<el-icon><refresh /></el-icon>
|
||||||
|
刷新设备列表
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<el-button
|
<el-button
|
||||||
@click="toggleScreenShare"
|
@click="toggleScreenShare"
|
||||||
:type="isScreenSharing ? 'danger' : (isGlobalScreenSharing ? 'primary' : 'info')"
|
:type="isScreenSharing ? 'danger' : (isGlobalScreenSharing ? 'primary' : 'info')"
|
||||||
@@ -145,7 +204,6 @@
|
|||||||
class="control-btn"
|
class="control-btn"
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
<!-- :type="isScreenSharing ? 'danger' : (isGlobalScreenSharing ? 'info' : 'primary')" -->
|
|
||||||
<span v-if="isScreenSharing">停止共享</span>
|
<span v-if="isScreenSharing">停止共享</span>
|
||||||
<span v-else-if="isGlobalScreenSharing">他人共享中</span>
|
<span v-else-if="isGlobalScreenSharing">他人共享中</span>
|
||||||
<span v-else>共享屏幕</span>
|
<span v-else>共享屏幕</span>
|
||||||
@@ -153,6 +211,12 @@
|
|||||||
<el-button @click="toggleWhiteboard" :type="isWhiteboardActive ? 'danger' : 'info'" class="control-btn" size="large">
|
<el-button @click="toggleWhiteboard" :type="isWhiteboardActive ? 'danger' : 'info'" class="control-btn" size="large">
|
||||||
{{ isWhiteboardActive ? '退出白板' : '共享白板' }}
|
{{ isWhiteboardActive ? '退出白板' : '共享白板' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button @click="laserPointer" type="info" class="control-btn" size="large">
|
||||||
|
激光笔
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="fileUploadHandle" type="info" class="control-btn" size="large">
|
||||||
|
文件上传
|
||||||
|
</el-button>
|
||||||
<el-button @click="inviterJoinRoom" type="info" class="control-btn" size="large">
|
<el-button @click="inviterJoinRoom" type="info" class="control-btn" size="large">
|
||||||
邀请人员
|
邀请人员
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -162,14 +226,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!-- 邀请人员组件 -->
|
||||||
<!-- 邀请人员组件 -->
|
<InviterJoinRoom ref="inviterJoinRoomRef" @confirmSelection="handleConfirmSelection" />
|
||||||
<InviterJoinRoom ref="inviterJoinRoomRef" @confirmSelection="handleConfirmSelection" />
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref, onMounted, onUnmounted, nextTick, computed, watch } from "vue";
|
import { reactive, ref, onMounted, onUnmounted, nextTick, computed, watch } from "vue";
|
||||||
import { ElMessage ,ElMessageBox} from 'element-plus';
|
import { ElMessage ,ElMessageBox} from 'element-plus';
|
||||||
|
import { ArrowDown, Refresh, Check } from '@element-plus/icons-vue';
|
||||||
import { getRoomToken, getRoomList ,getInvite,getTokenApi,exitRoomApi,getRoomInfoApi} from "@/api/conferencingRoom.js"
|
import { getRoomToken, getRoomList ,getInvite,getTokenApi,exitRoomApi,getRoomInfoApi} from "@/api/conferencingRoom.js"
|
||||||
import { Room, RoomEvent, ParticipantEvent, Track } from "livekit-client";
|
import { Room, RoomEvent, ParticipantEvent, Track } from "livekit-client";
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
@@ -178,20 +243,16 @@ import { useUserStore } from '@/stores/modules/user.js'
|
|||||||
import tabulaRase from '@/views/custom/tabulaRase/index.vue'
|
import tabulaRase from '@/views/custom/tabulaRase/index.vue'
|
||||||
import { mqttClient } from "@/utils/mqtt.js";
|
import { mqttClient } from "@/utils/mqtt.js";
|
||||||
import InviterJoinRoom from "@/views/conferencingRoom/components/InviterJoinRoom/index.vue"
|
import InviterJoinRoom from "@/views/conferencingRoom/components/InviterJoinRoom/index.vue"
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const roomStore = useRoomStore()
|
const roomStore = useRoomStore()
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// LiveKit 服务器配置
|
// LiveKit 服务器配置
|
||||||
const wsURL = "wss://meeting.cnsdt.com:443";
|
const wsURL = "wss://meeting.cnsdt.com:443";
|
||||||
|
|
||||||
const hostUid = ref('')
|
const hostUid = ref('')
|
||||||
const status = ref(true);
|
const status = ref(true);
|
||||||
const roomName = ref('')
|
const roomName = ref('')
|
||||||
const roomId = ref('')
|
const roomId = ref('')
|
||||||
|
|
||||||
// 音视频相关引用和数据
|
// 音视频相关引用和数据
|
||||||
const localVideo = ref(null);
|
const localVideo = ref(null);
|
||||||
const localAudio = ref(null);
|
const localAudio = ref(null);
|
||||||
@@ -199,30 +260,38 @@ const cameraEnabled = ref(false);
|
|||||||
const microphoneEnabled = ref(false);
|
const microphoneEnabled = ref(false);
|
||||||
const isScreenSharing = ref(false);
|
const isScreenSharing = ref(false);
|
||||||
const isLocalSpeaking = ref(false);
|
const isLocalSpeaking = ref(false);
|
||||||
|
// 设备列表
|
||||||
|
const cameraDevices = ref([]);
|
||||||
|
const microphoneDevices = ref([]);
|
||||||
|
const selectedCameraId = ref('');
|
||||||
|
const selectedMicrophoneId = ref('');
|
||||||
// 远程参与者管理
|
// 远程参与者管理
|
||||||
const remoteParticipants = ref(new Map());
|
const remoteParticipants = ref(new Map());
|
||||||
const videoElementsMap = ref(new Map());
|
const videoElementsMap = ref(new Map());
|
||||||
const audioElementsMap = ref(new Map());
|
const audioElementsMap = ref(new Map());
|
||||||
|
|
||||||
// 屏幕共享相关
|
// 屏幕共享相关
|
||||||
const screenSharingUser = ref('');
|
const screenSharingUser = ref('');
|
||||||
const activeScreenShareTrack = ref(null);
|
const activeScreenShareTrack = ref(null);
|
||||||
const screenShareVideo = ref(null);
|
const screenShareVideo = ref(null);//屏幕共享视频元素
|
||||||
const globalScreenSharingUser = ref(''); // 当前正在共享屏幕的用户
|
const globalScreenSharingUser = ref(''); // 当前正在共享屏幕的用户
|
||||||
const isGlobalScreenSharing = ref(false); // 是否有用户正在共享屏幕
|
const isGlobalScreenSharing = ref(false); // 是否有用户正在共享屏幕
|
||||||
|
|
||||||
//共享白板
|
//共享白板
|
||||||
const isWhiteboardActive = ref(false);
|
const isWhiteboardActive = ref(false);
|
||||||
const whiteboardRef = ref(null);
|
const whiteboardRef = ref(null);
|
||||||
const isGlobalWhiteboardSharing = ref(false); // 是否有用户正在共享白板
|
const isGlobalWhiteboardSharing = ref(false); // 是否有用户正在共享白板
|
||||||
|
|
||||||
|
|
||||||
//当前房间信息
|
//当前房间信息
|
||||||
const roomInfo = ref('')
|
const roomInfo = ref('')
|
||||||
|
|
||||||
//邀请参会人员
|
//邀请参会人员
|
||||||
const inviterJoinRoomRef = ref()
|
const inviterJoinRoomRef = ref()
|
||||||
|
// 添加canvas相关引用
|
||||||
|
const screenShareCanvasRef = ref(null);
|
||||||
|
const canvasContainerRef = ref(null);
|
||||||
|
const canvasContext = ref(null);
|
||||||
|
const isCanvasActive = ref(false);
|
||||||
|
const canvas = ref(null)
|
||||||
|
const ctx = ref(null)
|
||||||
|
const isDrawing = ref(false)
|
||||||
|
const startCoords = ref(null)
|
||||||
|
|
||||||
// 白板消息类型
|
// 白板消息类型
|
||||||
const WHITEBOARD_MESSAGE_TYPES = {
|
const WHITEBOARD_MESSAGE_TYPES = {
|
||||||
@@ -267,7 +336,7 @@ const room = new Room({
|
|||||||
autoGainControl: true,
|
autoGainControl: true,
|
||||||
},
|
},
|
||||||
videoCaptureDefaults: {
|
videoCaptureDefaults: {
|
||||||
resolution: { width: 1280, height: 720 }
|
resolution: { width: 1280, height: 720 },
|
||||||
},
|
},
|
||||||
publishDefaults: {
|
publishDefaults: {
|
||||||
screenShareEncoding: {
|
screenShareEncoding: {
|
||||||
@@ -280,13 +349,220 @@ const room = new Room({
|
|||||||
},
|
},
|
||||||
// 音频发布配置
|
// 音频发布配置
|
||||||
audioEncoding: {
|
audioEncoding: {
|
||||||
maxBitrate: 64000, // 64kbps for audio
|
maxBitrate: 64000,
|
||||||
},
|
},
|
||||||
// dtx: true, // 不连续传输,节省带宽
|
// dtx: true, // 不连续传输,节省带宽
|
||||||
red: true, // 冗余编码,提高抗丢包能力
|
red: true, // 冗余编码,提高抗丢包能力
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//文件上传
|
||||||
|
async function fileUploadHandle(){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//摄像头打开下拉框触发
|
||||||
|
async function handleCameraVisibleChange(e){
|
||||||
|
try {
|
||||||
|
if(e){
|
||||||
|
// 请求摄像头权限
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
||||||
|
|
||||||
|
// 获取设备列表
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
|
||||||
|
// 过滤摄像头设备
|
||||||
|
cameraDevices.value = devices.filter(device => device.kind === 'videoinput');
|
||||||
|
// 重要:立即停止临时媒体流,避免占用摄像头
|
||||||
|
stream.getTracks().forEach(track => {
|
||||||
|
track.stop();
|
||||||
|
console.log('临时摄像头轨道已停止');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('摄像头访问失败:', error);
|
||||||
|
// 使用更友好的方式提示用户
|
||||||
|
errorHandling(error,'摄像头');
|
||||||
|
// 清空设备列表
|
||||||
|
cameraDevices.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理摄像头设备选择
|
||||||
|
async function handleCameraCommand(deviceId) {
|
||||||
|
if (deviceId === 'refresh') {
|
||||||
|
await handleCameraVisibleChange(true);
|
||||||
|
ElMessage.success('设备列表已刷新');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedCameraId.value = deviceId;
|
||||||
|
|
||||||
|
// 如果摄像头已经开启,重新开启以应用新设备
|
||||||
|
if (cameraEnabled.value) {
|
||||||
|
await switchCameraDevice(deviceId);
|
||||||
|
} else {
|
||||||
|
// 如果摄像头未开启,直接开启选中的设备
|
||||||
|
await enableCameraWithDevice(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success(`已切换到摄像头: ${getDeviceName(cameraDevices.value, deviceId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
//麦克风打开下拉框触发
|
||||||
|
async function handleMicrophoneVisibleChange(e){
|
||||||
|
try {
|
||||||
|
if(e){
|
||||||
|
// 请求麦克风权限
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
|
||||||
|
// 获取设备列表
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
|
||||||
|
// 过滤麦克风设备
|
||||||
|
microphoneDevices.value = devices.filter(device => device.kind === 'audioinput');
|
||||||
|
|
||||||
|
// 停止所有轨道来关闭临时的麦克风访问
|
||||||
|
stream.getTracks().forEach(track => track.stop());
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('麦克风访问失败:', error);
|
||||||
|
// 使用更友好的方式提示用户
|
||||||
|
errorHandling(error,'麦克风');
|
||||||
|
// 清空设备列表
|
||||||
|
microphoneDevices.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理麦克风设备选择
|
||||||
|
async function handleMicrophoneCommand(deviceId) {
|
||||||
|
if (deviceId === 'refresh') {
|
||||||
|
await handleMicrophoneVisibleChange();
|
||||||
|
ElMessage.success('设备列表已刷新');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedMicrophoneId.value = deviceId;
|
||||||
|
|
||||||
|
// 如果麦克风已经开启,重新开启以应用新设备
|
||||||
|
if (microphoneEnabled.value) {
|
||||||
|
await switchMicrophoneDevice(deviceId);
|
||||||
|
} else {
|
||||||
|
// 如果麦克风未开启,直接开启选中的设备
|
||||||
|
await enableMicrophoneWithDevice(deviceId);
|
||||||
|
}
|
||||||
|
ElMessage.success(`已切换到麦克风: ${getDeviceName(microphoneDevices.value, deviceId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用指定设备开启摄像头
|
||||||
|
async function enableCameraWithDevice(deviceId) {
|
||||||
|
try {
|
||||||
|
// 更新设备配置
|
||||||
|
room.options.videoCaptureDefaults.deviceId = deviceId;
|
||||||
|
|
||||||
|
// 开启摄像头
|
||||||
|
await room.localParticipant.setCameraEnabled(true);
|
||||||
|
cameraEnabled.value = true;
|
||||||
|
|
||||||
|
// 手动获取并附加视频轨道
|
||||||
|
setTimeout(() => {
|
||||||
|
attachLocalCameraTrack();
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(`使用指定设备开启摄像头失败`);
|
||||||
|
try {
|
||||||
|
if (cameraEnabled.value) {
|
||||||
|
await room.localParticipant.setCameraEnabled(true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
cameraEnabled.value = false;
|
||||||
|
selectedCameraId.value = '';
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用指定设备开启麦克风
|
||||||
|
async function enableMicrophoneWithDevice(deviceId) {
|
||||||
|
try {
|
||||||
|
// 更新设备配置
|
||||||
|
room.options.audioCaptureDefaults.deviceId = deviceId;
|
||||||
|
|
||||||
|
// 开启麦克风
|
||||||
|
await room.localParticipant.setMicrophoneEnabled(true);
|
||||||
|
microphoneEnabled.value = true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('使用指定设备开启麦克风失败:', error);
|
||||||
|
microphoneEnabled.value = false;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换摄像头设备
|
||||||
|
async function switchCameraDevice(deviceId) {
|
||||||
|
try {
|
||||||
|
// 先关闭当前摄像头
|
||||||
|
await room.localParticipant.setCameraEnabled(false);
|
||||||
|
|
||||||
|
// 更新设备配置
|
||||||
|
room.options.videoCaptureDefaults.deviceId = deviceId;
|
||||||
|
|
||||||
|
// 重新开启摄像头
|
||||||
|
await room.localParticipant.setCameraEnabled(true);
|
||||||
|
|
||||||
|
// 手动获取并附加视频轨道
|
||||||
|
setTimeout(() => {
|
||||||
|
attachLocalCameraTrack();
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('切换摄像头设备失败:', error);
|
||||||
|
// 如果切换失败,尝试重新开启之前的设备
|
||||||
|
try {
|
||||||
|
await room.localParticipant.setCameraEnabled(true);
|
||||||
|
} catch (e) {
|
||||||
|
cameraEnabled.value = false;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换麦克风设备
|
||||||
|
async function switchMicrophoneDevice(deviceId) {
|
||||||
|
try {
|
||||||
|
// 先关闭当前麦克风
|
||||||
|
await room.localParticipant.setMicrophoneEnabled(false);
|
||||||
|
|
||||||
|
// 更新设备配置
|
||||||
|
room.options.audioCaptureDefaults.deviceId = deviceId;
|
||||||
|
|
||||||
|
// 重新开启麦克风
|
||||||
|
await room.localParticipant.setMicrophoneEnabled(true);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('切换麦克风设备失败:', error);
|
||||||
|
// 如果切换失败,尝试重新开启之前的设备
|
||||||
|
try {
|
||||||
|
await room.localParticipant.setMicrophoneEnabled(true);
|
||||||
|
} catch (e) {
|
||||||
|
microphoneEnabled.value = false;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取设备名称
|
||||||
|
function getDeviceName(devices, deviceId) {
|
||||||
|
const device = devices.find(d => d.deviceId === deviceId);
|
||||||
|
return device ? (device.label || '未知设备') : '未知设备';
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化MQTT连接
|
// 初始化MQTT连接
|
||||||
async function initMqtt() {
|
async function initMqtt() {
|
||||||
try {
|
try {
|
||||||
@@ -493,6 +769,7 @@ async function exitWhiteboard() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置视频元素引用
|
// 设置视频元素引用
|
||||||
function setParticipantVideoRef(el, identity, type) {
|
function setParticipantVideoRef(el, identity, type) {
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@@ -516,6 +793,7 @@ function setParticipantVideoRef(el, identity, type) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置音频元素引用
|
// 设置音频元素引用
|
||||||
function setParticipantAudioRef(el, identity) {
|
function setParticipantAudioRef(el, identity) {
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@@ -528,16 +806,17 @@ function setParticipantAudioRef(el, identity) {
|
|||||||
attachTrackToAudio(el, participantData.audioTrack);
|
attachTrackToAudio(el, participantData.audioTrack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置屏幕共享视频引用
|
// 设置屏幕共享视频引用
|
||||||
function setScreenShareVideoRef(el) {
|
function setScreenShareVideoRef(el) {
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
screenShareVideo.value = el;
|
screenShareVideo.value = el;
|
||||||
|
|
||||||
// 如果已经有屏幕共享轨道,立即附加
|
// 如果已经有屏幕共享轨道,立即附加
|
||||||
if (activeScreenShareTrack.value) {
|
if (activeScreenShareTrack.value) {
|
||||||
attachTrackToVideo(el, activeScreenShareTrack.value);
|
attachTrackToVideo(el, activeScreenShareTrack.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置事件监听器
|
// 设置事件监听器
|
||||||
function setupRoomListeners() {
|
function setupRoomListeners() {
|
||||||
room.removeAllListeners();
|
room.removeAllListeners();
|
||||||
@@ -557,11 +836,13 @@ function setupRoomListeners() {
|
|||||||
.on(RoomEvent.DataReceived, handleDataReceived)
|
.on(RoomEvent.DataReceived, handleDataReceived)
|
||||||
.on(RoomEvent.ConnectionStateChanged, handleConnectionStateChanged);
|
.on(RoomEvent.ConnectionStateChanged, handleConnectionStateChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 事件处理函数
|
// 事件处理函数
|
||||||
async function handleConnected() {
|
async function handleConnected() {
|
||||||
await initMqtt();
|
await initMqtt();
|
||||||
roomId.value = room.name
|
roomId.value = room.name
|
||||||
status.value = false;
|
status.value = false;
|
||||||
|
|
||||||
ElMessage.success('已成功连接到房间');
|
ElMessage.success('已成功连接到房间');
|
||||||
// 初始化现有远程参与者
|
// 初始化现有远程参与者
|
||||||
room.remoteParticipants.forEach(participant => {
|
room.remoteParticipants.forEach(participant => {
|
||||||
@@ -570,14 +851,6 @@ async function handleConnected() {
|
|||||||
// 立即检查并更新参与者的轨道状态
|
// 立即检查并更新参与者的轨道状态
|
||||||
updateParticipantTracks(participant);
|
updateParticipantTracks(participant);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 自动开启麦克风
|
|
||||||
// try {
|
|
||||||
// await enableMicrophone();
|
|
||||||
// ElMessage.success('麦克风已自动开启');
|
|
||||||
// } catch (error) {
|
|
||||||
// console.warn('自动开启麦克风失败:', error);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDisconnected(reason) {
|
function handleDisconnected(reason) {
|
||||||
@@ -598,6 +871,7 @@ function handleDisconnected(reason) {
|
|||||||
function handleReconnected() {
|
function handleReconnected() {
|
||||||
ElMessage.success('已重新连接到房间');
|
ElMessage.success('已重新连接到房间');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理轨道订阅事件
|
// 处理轨道订阅事件
|
||||||
function handleTrackSubscribed(track, publication, participant) {
|
function handleTrackSubscribed(track, publication, participant) {
|
||||||
|
|
||||||
@@ -733,10 +1007,12 @@ function handleLocalTrackUnpublished(publication) {
|
|||||||
function handleTrackMuted(publication, participant) {
|
function handleTrackMuted(publication, participant) {
|
||||||
updateParticipantTracks(participant);
|
updateParticipantTracks(participant);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理轨道取消静音事件
|
// 处理轨道取消静音事件
|
||||||
function handleTrackUnmuted(publication, participant) {
|
function handleTrackUnmuted(publication, participant) {
|
||||||
updateParticipantTracks(participant);
|
updateParticipantTracks(participant);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置参与者事件监听器
|
// 设置参与者事件监听器
|
||||||
function setupParticipantListeners(participant) {
|
function setupParticipantListeners(participant) {
|
||||||
participant
|
participant
|
||||||
@@ -756,6 +1032,7 @@ function setupParticipantListeners(participant) {
|
|||||||
updateParticipantSpeaking(participant, speaking);
|
updateParticipantSpeaking(participant, speaking);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理活动说话者变化事件
|
// 处理活动说话者变化事件
|
||||||
function handleActiveSpeakersChanged(speakers) {
|
function handleActiveSpeakersChanged(speakers) {
|
||||||
// 更新本地说话状态
|
// 更新本地说话状态
|
||||||
@@ -771,6 +1048,7 @@ function handleActiveSpeakersChanged(speakers) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理数据接收事件
|
// 处理数据接收事件
|
||||||
function handleDataReceived(payload, participant, kind) {
|
function handleDataReceived(payload, participant, kind) {
|
||||||
try {
|
try {
|
||||||
@@ -785,6 +1063,7 @@ function handleDataReceived(payload, participant, kind) {
|
|||||||
function handleConnectionStateChanged(state) {
|
function handleConnectionStateChanged(state) {
|
||||||
console.log('连接状态改变:', state);
|
console.log('连接状态改变:', state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新屏幕共享状态
|
// 更新屏幕共享状态
|
||||||
function updateScreenShareState(participant, track) {
|
function updateScreenShareState(participant, track) {
|
||||||
if (track) {
|
if (track) {
|
||||||
@@ -807,6 +1086,7 @@ function updateScreenShareState(participant, track) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 参与者管理函数
|
// 参与者管理函数
|
||||||
function addRemoteParticipant(participant) {
|
function addRemoteParticipant(participant) {
|
||||||
if (!participant || participant.identity === room.localParticipant?.identity) {
|
if (!participant || participant.identity === room.localParticipant?.identity) {
|
||||||
@@ -839,6 +1119,7 @@ function removeRemoteParticipant(participant) {
|
|||||||
audioElementsMap.value.delete(identity);
|
audioElementsMap.value.delete(identity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新参与者轨道信息
|
// 更新参与者轨道信息
|
||||||
function updateParticipantTrack(participant, source, track) {
|
function updateParticipantTrack(participant, source, track) {
|
||||||
const data = remoteParticipants.value.get(participant.identity);
|
const data = remoteParticipants.value.get(participant.identity);
|
||||||
@@ -865,6 +1146,7 @@ function removeParticipantTrack(participant, source) {
|
|||||||
}
|
}
|
||||||
remoteParticipants.value.set(participant.identity, { ...data });
|
remoteParticipants.value.set(participant.identity, { ...data });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新参与者音频轨道信息
|
// 更新参与者音频轨道信息
|
||||||
function updateParticipantAudioTrack(participant, track) {
|
function updateParticipantAudioTrack(participant, track) {
|
||||||
const data = remoteParticipants.value.get(participant.identity);
|
const data = remoteParticipants.value.get(participant.identity);
|
||||||
@@ -881,6 +1163,7 @@ function removeParticipantAudioTrack(participant) {
|
|||||||
data.audioEnabled = false;
|
data.audioEnabled = false;
|
||||||
remoteParticipants.value.set(participant.identity, { ...data });
|
remoteParticipants.value.set(participant.identity, { ...data });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 附加轨道到视频元素
|
// 附加轨道到视频元素
|
||||||
function attachTrackToVideo(videoElement, track) {
|
function attachTrackToVideo(videoElement, track) {
|
||||||
if (!videoElement || !track) return;
|
if (!videoElement || !track) return;
|
||||||
@@ -893,6 +1176,7 @@ function attachTrackToVideo(videoElement, track) {
|
|||||||
console.error('附加轨道到视频元素失败:', error);
|
console.error('附加轨道到视频元素失败:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 附加轨道到音频元素
|
// 附加轨道到音频元素
|
||||||
function attachTrackToAudio(audioElement, track) {
|
function attachTrackToAudio(audioElement, track) {
|
||||||
if (!audioElement || !track) return;
|
if (!audioElement || !track) return;
|
||||||
@@ -908,6 +1192,7 @@ function attachTrackToAudio(audioElement, track) {
|
|||||||
console.error('附加轨道到音频元素失败:', error);
|
console.error('附加轨道到音频元素失败:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 附加轨道到参与者的视频元素
|
// 附加轨道到参与者的视频元素
|
||||||
function attachTrackToParticipantVideo(identity, source, track) {
|
function attachTrackToParticipantVideo(identity, source, track) {
|
||||||
const videoElements = videoElementsMap.value.get(identity);
|
const videoElements = videoElementsMap.value.get(identity);
|
||||||
@@ -918,6 +1203,7 @@ function attachTrackToParticipantVideo(identity, source, track) {
|
|||||||
attachTrackToVideo(videoElement, track);
|
attachTrackToVideo(videoElement, track);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 附加轨道到参与者的音频元素
|
// 附加轨道到参与者的音频元素
|
||||||
function attachTrackToParticipantAudio(identity, track) {
|
function attachTrackToParticipantAudio(identity, track) {
|
||||||
const audioElement = audioElementsMap.value.get(identity);
|
const audioElement = audioElementsMap.value.get(identity);
|
||||||
@@ -925,6 +1211,7 @@ function attachTrackToParticipantAudio(identity, track) {
|
|||||||
attachTrackToAudio(audioElement, track);
|
attachTrackToAudio(audioElement, track);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从参与者的视频元素分离轨道
|
// 从参与者的视频元素分离轨道
|
||||||
function detachTrackFromParticipantVideo(identity, source) {
|
function detachTrackFromParticipantVideo(identity, source) {
|
||||||
const videoElements = videoElementsMap.value.get(identity);
|
const videoElements = videoElementsMap.value.get(identity);
|
||||||
@@ -935,6 +1222,7 @@ function detachTrackFromParticipantVideo(identity, source) {
|
|||||||
videoElement.srcObject = null;
|
videoElement.srcObject = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从参与者的音频元素分离轨道
|
// 从参与者的音频元素分离轨道
|
||||||
function detachTrackFromParticipantAudio(identity) {
|
function detachTrackFromParticipantAudio(identity) {
|
||||||
const audioElement = audioElementsMap.value.get(identity);
|
const audioElement = audioElementsMap.value.get(identity);
|
||||||
@@ -1001,6 +1289,7 @@ function handleVideoLoaded(identity, type) {
|
|||||||
function handleScreenShareLoaded() {
|
function handleScreenShareLoaded() {
|
||||||
console.log('屏幕共享视频加载完成');
|
console.log('屏幕共享视频加载完成');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 视频轨道处理函数
|
// 视频轨道处理函数
|
||||||
function attachLocalVideoTrack(track) {
|
function attachLocalVideoTrack(track) {
|
||||||
if (localVideo.value && track) {
|
if (localVideo.value && track) {
|
||||||
@@ -1014,6 +1303,7 @@ function attachLocalVideoTrack(track) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 媒体控制函数
|
// 媒体控制函数
|
||||||
async function enableCamera() {
|
async function enableCamera() {
|
||||||
try {
|
try {
|
||||||
@@ -1050,21 +1340,50 @@ async function toggleCamera() {
|
|||||||
localVideo.value.srcObject.getTracks().forEach(track => track.stop());
|
localVideo.value.srcObject.getTracks().forEach(track => track.stop());
|
||||||
localVideo.value.srcObject = null;
|
localVideo.value.srcObject = null;
|
||||||
}
|
}
|
||||||
|
// 清空选中的视屏设备ID
|
||||||
|
selectedCameraId.value = '';
|
||||||
ElMessage.info('摄像头已关闭');
|
ElMessage.info('摄像头已关闭');
|
||||||
} else {
|
} else {
|
||||||
// 确保视频元素存在
|
// 确保视频元素存在
|
||||||
if (!localVideo.value) {
|
if (!localVideo.value) {
|
||||||
console.warn('本地视频元素未找到,等待DOM更新');
|
console.warn('本地视频元素未找到,等待DOM更新');
|
||||||
await nextTick();
|
await nextTick();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开启摄像头
|
// 开启摄像头
|
||||||
await room.localParticipant.setCameraEnabled(true);
|
// 确保有视屏输入设备权限和设备列表
|
||||||
cameraEnabled.value = true;
|
if (cameraDevices.value.length === 0) {
|
||||||
|
// 如果没有设备列表,先获取
|
||||||
|
await handleCameraVisibleChange(true);
|
||||||
|
}
|
||||||
|
if (cameraDevices.value.length === 0) {
|
||||||
|
ElMessage.error('未找到可用的摄像头设备');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动选择第一个可用设备(如果当前没有选中设备)
|
||||||
|
let deviceToUse = selectedCameraId.value;
|
||||||
|
if (!deviceToUse && cameraDevices.value.length > 0) {
|
||||||
|
deviceToUse = cameraDevices.value[0].deviceId;
|
||||||
|
selectedCameraId.value = deviceToUse;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceToUse) {
|
||||||
|
await enableCameraWithDevice(deviceToUse);
|
||||||
|
ElMessage.success(`摄像头已开启 - ${getDeviceName(cameraDevices.value, deviceToUse)}`);
|
||||||
|
} else {
|
||||||
|
// 使用默认方式开启
|
||||||
|
await room.localParticipant.setMicrophoneEnabled(true);
|
||||||
|
microphoneEnabled.value = true;
|
||||||
|
// 手动获取并附加视频轨道
|
||||||
|
setTimeout(() => {
|
||||||
|
attachLocalCameraTrack();
|
||||||
|
}, 200);
|
||||||
|
ElMessage.success('麦克风已开启');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
ElMessage.success('摄像头已开启');
|
ElMessage.success('摄像头已开启');
|
||||||
// 手动获取并附加视频轨道,增加延迟
|
|
||||||
setTimeout(() => {
|
|
||||||
attachLocalCameraTrack();
|
|
||||||
}, 200);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorHandling(error,'摄像头');
|
errorHandling(error,'摄像头');
|
||||||
@@ -1105,54 +1424,75 @@ async function toggleMicrophone() {
|
|||||||
try {
|
try {
|
||||||
if (microphoneEnabled.value) {
|
if (microphoneEnabled.value) {
|
||||||
await room.localParticipant.setMicrophoneEnabled(false);
|
await room.localParticipant.setMicrophoneEnabled(false);
|
||||||
microphoneEnabled.value = false;
|
microphoneEnabled.value = false;
|
||||||
// 停止所有音频轨道
|
// 停止所有音频轨道
|
||||||
const audioPublications = Array.from(room.localParticipant.audioTrackPublications.values());
|
const audioPublications = Array.from(room.localParticipant.audioTrackPublications.values());
|
||||||
for (const publication of audioPublications) {
|
for (const publication of audioPublications) {
|
||||||
if (publication.track) {
|
if (publication.track) {
|
||||||
publication.track.stop(); // 停止轨道
|
publication.track.stop(); // 停止轨道
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 清空选中的麦克风设备ID
|
||||||
|
selectedMicrophoneId.value = '';
|
||||||
ElMessage.info('麦克风已关闭');
|
ElMessage.info('麦克风已关闭');
|
||||||
} else {
|
} else {
|
||||||
// 确保有音频输入设备权限
|
// 开启麦克风
|
||||||
try {
|
// 确保有音频输入设备权限和设备列表
|
||||||
// 先检查麦克风权限
|
if (microphoneDevices.value.length === 0) {
|
||||||
await navigator.mediaDevices.getUserMedia({ audio: true });
|
// 如果没有设备列表,先获取
|
||||||
} catch (error) {
|
await handleMicrophoneVisibleChange(true);
|
||||||
ElMessage.error('无法访问麦克风,请检查权限设置');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await room.localParticipant.setMicrophoneEnabled(true);
|
if (microphoneDevices.value.length === 0) {
|
||||||
microphoneEnabled.value = true;
|
ElMessage.error('未找到可用的麦克风设备');
|
||||||
ElMessage.success('麦克风已开启');
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 等待音频轨道发布
|
// 自动选择第一个可用设备(如果当前没有选中设备)
|
||||||
setTimeout(() => {
|
let deviceToUse = selectedMicrophoneId.value;
|
||||||
const audioPublications = Array.from(room.localParticipant.audioTrackPublications.values());
|
if (!deviceToUse && microphoneDevices.value.length > 0) {
|
||||||
const audioPublication = audioPublications.find(pub => pub.track);
|
deviceToUse = microphoneDevices.value[0].deviceId;
|
||||||
if (audioPublication && audioPublication.track) {
|
selectedMicrophoneId.value = deviceToUse;
|
||||||
console.log('本地音频轨道已发布:', audioPublication.track);
|
}
|
||||||
}
|
|
||||||
}, 500);
|
if (deviceToUse) {
|
||||||
|
await enableMicrophoneWithDevice(deviceToUse);
|
||||||
|
ElMessage.success(`麦克风已开启 - ${getDeviceName(microphoneDevices.value, deviceToUse)}`);
|
||||||
|
} else {
|
||||||
|
// 使用默认方式开启
|
||||||
|
await room.localParticipant.setMicrophoneEnabled(true);
|
||||||
|
microphoneEnabled.value = true;
|
||||||
|
ElMessage.success('麦克风已开启');
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success('麦克风已开启');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorHandling(error,'麦克风');
|
errorHandling(error, '麦克风');
|
||||||
|
// 如果开启失败,确保状态正确
|
||||||
|
if (!microphoneEnabled.value) {
|
||||||
|
selectedMicrophoneId.value = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function errorHandling(error,type) {
|
function errorHandling(error,type) {
|
||||||
switch (error.name) {
|
switch (error.name) {
|
||||||
case 'NotAllowedError':
|
case 'NotAllowedError':
|
||||||
ElMessage.error('用户拒绝了权限请求');
|
ElMessage.error('用户拒绝了权限请求,请允许此网站使用摄像头');
|
||||||
break;
|
break;
|
||||||
case 'NotFoundError':
|
case 'NotFoundError':
|
||||||
ElMessage.error(`未找到${type}设备`);
|
ElMessage.error(`未检测到可用的${type}设备,请检查${type}是否已正确连接`);
|
||||||
break;
|
break;
|
||||||
case 'NotSupportedError':
|
case 'NotSupportedError':
|
||||||
ElMessage.error('当前浏览器不支持此功能,请更换其他浏览器');
|
ElMessage.error(`当前浏览器不支持${type}功能,请使用现代浏览器如Chrome、Firefox或Edge`);
|
||||||
|
break;
|
||||||
|
case 'NotReadableError':
|
||||||
|
ElMessage.error(`${type}设备正被其他应用程序占用,请关闭其他使用${type}的应用后重试`);
|
||||||
|
break;
|
||||||
|
case 'OverconstrainedError':
|
||||||
|
ElMessage.error(`${type}配置不兼容,请尝试调整${type}设置`);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
ElMessage.error('服务错误,请刷新重试');
|
ElMessage.error('服务错误,请刷新重试');
|
||||||
@@ -1224,6 +1564,7 @@ async function joinRoomBtn() {
|
|||||||
status.value = true;
|
status.value = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 离开会议函数
|
// 离开会议函数
|
||||||
async function leaveRoom() {
|
async function leaveRoom() {
|
||||||
try {
|
try {
|
||||||
@@ -1238,28 +1579,26 @@ async function leaveRoom() {
|
|||||||
if (whiteboardRef.value && whiteboardRef.value.cleanup) {
|
if (whiteboardRef.value && whiteboardRef.value.cleanup) {
|
||||||
whiteboardRef.value.cleanup();
|
whiteboardRef.value.cleanup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 断开MQTT连接
|
// 断开MQTT连接
|
||||||
mqttClient.disconnect();
|
mqttClient.disconnect();
|
||||||
|
|
||||||
// const res = await exitRoomApi(room.name)
|
// const res = await exitRoomApi(room.name)
|
||||||
// 停止屏幕共享(如果正在共享)
|
// 停止屏幕共享(如果正在共享)
|
||||||
if (isScreenSharing.value) {
|
if (isScreenSharing.value) {
|
||||||
await room.localParticipant.setScreenShareEnabled(false);
|
await room.localParticipant.setScreenShareEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭摄像头和麦克风
|
// 关闭摄像头和麦克风
|
||||||
await room.localParticipant.setCameraEnabled(false);
|
await room.localParticipant.setCameraEnabled(false);
|
||||||
await room.localParticipant.setMicrophoneEnabled(false);
|
await room.localParticipant.setMicrophoneEnabled(false);
|
||||||
|
microphoneEnabled.value = false;
|
||||||
|
cameraEnabled.value = false;
|
||||||
|
selectedMicrophoneId.value = '';
|
||||||
|
selectedCameraId.value = ''
|
||||||
// 断开与房间的连接
|
// 断开与房间的连接
|
||||||
await room.disconnect();
|
await room.disconnect();
|
||||||
|
|
||||||
// 重置所有状态
|
// 重置所有状态
|
||||||
resetRoomState();
|
resetRoomState();
|
||||||
|
ElMessage.success('已离开会议');
|
||||||
ElMessage.success('已离开会议');
|
|
||||||
router.push({
|
router.push({
|
||||||
path: '/coordinate',
|
path: '/coordinate',
|
||||||
})
|
})
|
||||||
@@ -1281,7 +1620,11 @@ function resetRoomState() {
|
|||||||
isScreenSharing.value = false;
|
isScreenSharing.value = false;
|
||||||
isLocalSpeaking.value = false;
|
isLocalSpeaking.value = false;
|
||||||
status.value = true;
|
status.value = true;
|
||||||
hostUid.value = '';
|
hostUid.value = '';
|
||||||
|
selectedMicrophoneId.value = '';
|
||||||
|
selectedCameraId.value = ''
|
||||||
|
microphoneDevices.value = [];
|
||||||
|
cameraDevices.value = [];
|
||||||
|
|
||||||
// 断开MQTT连接
|
// 断开MQTT连接
|
||||||
mqttClient.disconnect();
|
mqttClient.disconnect();
|
||||||
@@ -1345,16 +1688,6 @@ onUnmounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 确保在连接前请求音频权限
|
|
||||||
// try {
|
|
||||||
// // 预请求音频权限
|
|
||||||
// await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
||||||
// console.log('音频权限已获取');
|
|
||||||
// } catch (error) {
|
|
||||||
// console.warn('音频权限获取失败:', error);
|
|
||||||
// ElMessage.warning('请允许麦克风权限以使用音频功能');
|
|
||||||
// }
|
|
||||||
|
|
||||||
if(route.query.type == '1'){
|
if(route.query.type == '1'){
|
||||||
await joinRoomBtn()
|
await joinRoomBtn()
|
||||||
hostUid.value = roomStore.userUid
|
hostUid.value = roomStore.userUid
|
||||||
@@ -1379,7 +1712,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss" scoped>
|
||||||
/* 白板容器样式 */
|
/* 白板容器样式 */
|
||||||
.whiteboard-container {
|
.whiteboard-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -1430,6 +1763,37 @@ onMounted(async () => {
|
|||||||
.audio-element, .participant-audio {
|
.audio-element, .participant-audio {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 控制下拉菜单样式 */
|
||||||
|
.control-dropdown {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-dropdown .el-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中的设备样式 */
|
||||||
|
.selected-device {
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-icon {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 设备列表项样式 */
|
||||||
|
.el-dropdown-menu__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 全局样式 */
|
/* 全局样式 */
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -1621,7 +1985,7 @@ body {
|
|||||||
|
|
||||||
.screen-share-wrapper {
|
.screen-share-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: calc(100% - 36px) ;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1739,7 +2103,7 @@ body {
|
|||||||
.video-wrapper {
|
.video-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%; /* 固定高度 */
|
height:calc(100% - 36px) ; /* 固定高度 */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #000; /* 统一背景色 */
|
background: #000; /* 统一背景色 */
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -1858,8 +2222,86 @@ body {
|
|||||||
background: linear-gradient(135deg, #e64a4a 0%, #d63030 100%);
|
background: linear-gradient(135deg, #e64a4a 0%, #d63030 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 开启摄像头和麦克风下拉框 */
|
||||||
|
.control-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.dropdown-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.selected-device {
|
||||||
|
background-color: #f0f7ff;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
.check-icon {
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .microphone-btn {
|
||||||
|
border-radius: 12px 0 0 12px;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .microphone-dropdown {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .dropdown-btn {
|
||||||
|
border-radius: 0 12px 12px 0;
|
||||||
|
border: none;
|
||||||
|
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: none;
|
||||||
|
width: 44px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 悬停效果 */
|
||||||
|
.microphone-control-group:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .control-btn:hover {
|
||||||
|
transform: none; /* 取消单独的悬停位移 */
|
||||||
|
}
|
||||||
|
|
||||||
/* 响应式调整 */
|
/* 响应式调整 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.microphone-control-group {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .microphone-btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .dropdown-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
.whiteboard-container {
|
.whiteboard-container {
|
||||||
height: 300px; /* 在移动端限制白板高度 */
|
height: 300px; /* 在移动端限制白板高度 */
|
||||||
}
|
}
|
||||||
@@ -1920,6 +2362,19 @@ body {
|
|||||||
|
|
||||||
/* 超小屏幕调整 */
|
/* 超小屏幕调整 */
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
|
.microphone-control-group {
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .microphone-btn {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .dropdown-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
.controls-container {
|
.controls-container {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -116,7 +116,7 @@ service.interceptors.response.use(
|
|||||||
return Promise.resolve(responseData);
|
return Promise.resolve(responseData);
|
||||||
|
|
||||||
case 401:
|
case 401:
|
||||||
console.log('未授权', responseData)
|
console.log('未授权', responseData)
|
||||||
return Promise.resolve(responseData);
|
return Promise.resolve(responseData);
|
||||||
// return handleUnauthorized().then(() => {
|
// return handleUnauthorized().then(() => {
|
||||||
// return Promise.reject({ code: 401, message: '未授权' });
|
// return Promise.reject({ code: 401, message: '未授权' });
|
||||||
@@ -168,12 +168,12 @@ function handleUnauthorized() {
|
|||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
removeToken()
|
removeToken()
|
||||||
if (router.currentRoute.path !== '/login') {
|
if (router.currentRoute.path !== '/login') {
|
||||||
router.push({
|
router.push({
|
||||||
path: '/login',
|
path: '/login',
|
||||||
query: { redirect: router.currentRoute.fullPath }
|
query: { redirect: router.currentRoute.fullPath }
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 如果在登录页,强制刷新以清除残留状态
|
// 如果在登录页,强制刷新以清除残留状态
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,15 @@
|
|||||||
:class="'list-tab-item ' + (leftTab == 1 ? 'pitch-on' : '')"
|
:class="'list-tab-item ' + (leftTab == 1 ? 'pitch-on' : '')"
|
||||||
@click="() => (leftTab = 1)"
|
@click="() => (leftTab = 1)"
|
||||||
>
|
>
|
||||||
<img src="@/assets/images/cooponents-tab3.png" v-if="leftTab == 1" />
|
<img src="@/assets/images/Gc_114_line-Level-action.png" v-if="leftTab == 1" />
|
||||||
<img src="@/assets/images/cooponents-tab4.png" v-else />
|
<img src="@/assets/images/Gc_114_line-Level.png" v-else />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
:class="'list-tab-item ' + (leftTab == 2 ? 'pitch-on' : '')"
|
:class="'list-tab-item ' + (leftTab == 2 ? 'pitch-on' : '')"
|
||||||
@click="() => (leftTab = 2)"
|
@click="() => (leftTab = 2)"
|
||||||
>
|
>
|
||||||
<img src="@/assets/images/cooponents-tab2.png" v-if="leftTab == 2" />
|
<img src="@/assets/images/book-read-fill-action.png" v-if="leftTab == 2" />
|
||||||
<img src="@/assets/images/cooponents-tab1.png" v-else />
|
<img src="@/assets/images/book-read-fill.png" v-else />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-content">
|
<div class="list-content">
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
@@ -317,13 +317,9 @@ const loadNode = async(resolve,id)=>{
|
|||||||
state.leftListLoading = true
|
state.leftListLoading = true
|
||||||
if(!id){
|
if(!id){
|
||||||
let res = await getDirectories({level:1})
|
let res = await getDirectories({level:1})
|
||||||
if(res.meta.code == 401){
|
if(res.meta.code == 200){
|
||||||
emit('showLogin', true)
|
|
||||||
return
|
|
||||||
}else{
|
|
||||||
resolve(res.data)
|
resolve(res.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
}else{
|
}else{
|
||||||
let res = await getDirectoriesUsers(id,{directory_uuid:id})
|
let res = await getDirectoriesUsers(id,{directory_uuid:id})
|
||||||
resolve(res.data)
|
resolve(res.data)
|
||||||
@@ -436,8 +432,8 @@ const {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 50px;
|
width: 40px;
|
||||||
height: 50px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -453,7 +449,7 @@ const {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
padding: 6px 20px;
|
padding: 6px 20px;
|
||||||
background: #167bff;
|
background: #666666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-datapicker {
|
.content-datapicker {
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="showLogin" style="height:100%">
|
<div>
|
||||||
<!-- 登录界面 -->
|
|
||||||
<Login @loginSuccess="handleLoginSuccess" />
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<div class="app-container" v-loading="load" :element-loading-text="loadText">
|
<div class="app-container" v-loading="load" :element-loading-text="loadText">
|
||||||
<el-row :gutter="6" style="padding: 0 10px; background: #fefefe">
|
<el-row :gutter="6" style="padding: 0 10px; background: #fefefe">
|
||||||
<el-col :xs="24" :sm="24" :md="8" :lg="6">
|
<el-col :xs="24" :sm="24" :md="8" :lg="6">
|
||||||
<leftTab
|
<leftTab
|
||||||
@updateDetail="updateDetail"
|
@updateDetail="updateDetail"
|
||||||
@updateTab="updateTab"
|
@updateTab="updateTab"
|
||||||
@showLogin="showLoginHandle"
|
|
||||||
:loading="!detail?.appId && !detail?.userId && isShow"
|
:loading="!detail?.appId && !detail?.userId && isShow"
|
||||||
/>
|
/>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -212,7 +207,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-card-btn">
|
<div class="user-card-btn">
|
||||||
<el-button type="primary" @click="clickInitiate">
|
<el-button type="info" @click="clickInitiate">
|
||||||
发起协作
|
发起协作
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -265,7 +260,7 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onActivated, onMounted, reactive, toRefs, watch, getCurrentInstance ,ref} from 'vue'
|
import { onActivated, onMounted, reactive, toRefs, watch, getCurrentInstance ,ref} from 'vue'
|
||||||
@@ -277,8 +272,7 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import { useRoomStore } from '@/stores/modules/room'
|
import { useRoomStore } from '@/stores/modules/room'
|
||||||
import { useUserStore } from '@/stores/modules/user.js'
|
import { useUserStore } from '@/stores/modules/user.js'
|
||||||
import { mqttClient } from "@/utils/mqtt.js";
|
import { mqttClient } from "@/utils/mqtt.js";
|
||||||
import { getToken } from '@/utils/auth.js'
|
import { getToken } from '@/utils/auth.js'
|
||||||
import Login from "@/components/Login/index.vue";
|
|
||||||
const roomStore = useRoomStore()
|
const roomStore = useRoomStore()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -294,21 +288,9 @@ const state = reactive({
|
|||||||
socketInformation: null,
|
socketInformation: null,
|
||||||
inviteDialog: false,
|
inviteDialog: false,
|
||||||
cooperation: import.meta.env.VITE_APP_COOPERATION_TYPE,
|
cooperation: import.meta.env.VITE_APP_COOPERATION_TYPE,
|
||||||
})
|
})
|
||||||
const showLogin = ref(false); // 是否显示登录页面
|
|
||||||
const userLoading = ref(false); // 用户信息加载状态
|
const userLoading = ref(false); // 用户信息加载状态
|
||||||
|
|
||||||
|
|
||||||
function showLoginHandle(e){
|
|
||||||
showLogin.value = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 登录成功回调 */
|
|
||||||
function handleLoginSuccess() {
|
|
||||||
showLogin.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const isEmptyObject = (obj) => {
|
const isEmptyObject = (obj) => {
|
||||||
return !obj || Object.keys(obj).length === 0
|
return !obj || Object.keys(obj).length === 0
|
||||||
}
|
}
|
||||||
@@ -460,12 +442,7 @@ const showNotification = (data) => {
|
|||||||
|
|
||||||
// 暴露给模板
|
// 暴露给模板
|
||||||
const { detail, weekName, tabValue, isShow, load, loadText, isLinkKnow, socketInformation, inviteDialog, cooperation } = toRefs(state)
|
const { detail, weekName, tabValue, isShow, load, loadText, isLinkKnow, socketInformation, inviteDialog, cooperation } = toRefs(state)
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if(!getToken()){
|
|
||||||
showLogin.value = true;
|
|
||||||
}else{
|
|
||||||
showLogin.value = false;
|
|
||||||
}
|
|
||||||
await mqttClient.connect(`room${Math.random().toString(16).substr(2, 8)}`);
|
await mqttClient.connect(`room${Math.random().toString(16).substr(2, 8)}`);
|
||||||
const res = await userStore.getInfo()
|
const res = await userStore.getInfo()
|
||||||
const topic = `xSynergy/ROOM/+/rooms/${res.uid}`;
|
const topic = `xSynergy/ROOM/+/rooms/${res.uid}`;
|
||||||
@@ -487,7 +464,8 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.app-container {
|
.app-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
// margin: 0 17px;
|
// margin: 0 17px;
|
||||||
|
// height: calc(100vh - 50px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-null {
|
.message-null {
|
||||||
@@ -505,7 +483,7 @@ onMounted(async () => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
background: #167bff;
|
background: #666666;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
@@ -613,8 +591,8 @@ onMounted(async () => {
|
|||||||
height: 40%;
|
height: 40%;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
rgba(13, 116, 255, 0.22) 0%,
|
rgba(153, 153, 153, 0.22) 0%,
|
||||||
rgba(30, 173, 255, 0) 100%
|
rgba(153, 153, 153, 0) 100%
|
||||||
);
|
);
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="wrapper-content">
|
<div class="wrapper-content">
|
||||||
<div v-if="showLogin">
|
<div>
|
||||||
<!-- 登录界面 -->
|
|
||||||
<Login @loginSuccess="handleLoginSuccess" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else>
|
|
||||||
<!-- 未加入时显示按钮 -->
|
<!-- 未加入时显示按钮 -->
|
||||||
<div v-if="!hasJoined" class="login-button-container">
|
<div v-if="!hasJoined" class="login-button-container">
|
||||||
<el-button type="primary" size="large" link @click="joinWhiteboard">
|
<el-button type="primary" size="large" link @click="joinWhiteboard">
|
||||||
@@ -29,8 +24,7 @@ import { v4 as uuidv4 } from "uuid";
|
|||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { mqttClient } from "@/utils/mqtt";
|
import { mqttClient } from "@/utils/mqtt";
|
||||||
import { WhiteboardSync } from "@/utils/whiteboardSync";
|
import { WhiteboardSync } from "@/utils/whiteboardSync";
|
||||||
import ToolBox from "@/components/ToolBox/index.vue";
|
import ToolBox from "@/components/ToolBox/index.vue";
|
||||||
import Login from "@/components/Login/index.vue";
|
|
||||||
import Canvas from "@/core/index.js";
|
import Canvas from "@/core/index.js";
|
||||||
import { getInfo } from "@/api/login";
|
import { getInfo } from "@/api/login";
|
||||||
|
|
||||||
@@ -44,8 +38,7 @@ const props = defineProps({
|
|||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const showLogin = ref(false); // 是否显示登录页面
|
|
||||||
const hasJoined = ref(false); // 是否加入白板
|
const hasJoined = ref(false); // 是否加入白板
|
||||||
const canvas = ref(null);
|
const canvas = ref(null);
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -78,11 +71,6 @@ async function joinWhiteboard() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 登录成功回调 */
|
|
||||||
function handleLoginSuccess() {
|
|
||||||
showLogin.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 初始化白板 */
|
/** 初始化白板 */
|
||||||
function initWhiteboard() {
|
function initWhiteboard() {
|
||||||
const el = document.getElementById("whiteboard");
|
const el = document.getElementById("whiteboard");
|
||||||
@@ -99,18 +87,7 @@ function initWhiteboard() {
|
|||||||
WhiteboardSync.init(canvas.value, roomUid);
|
WhiteboardSync.init(canvas.value, roomUid);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
|
||||||
showLogin.value = false;
|
|
||||||
const res = await getInfo("self");
|
|
||||||
if (res.meta.code === 401) {
|
|
||||||
showLogin.value = true;
|
|
||||||
} else {
|
|
||||||
showLogin.value = false;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("⚠️ 用户信息校验失败:", err);
|
|
||||||
}
|
|
||||||
joinWhiteboard()
|
joinWhiteboard()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ let { proxy } = getCurrentInstance();
|
|||||||
const errGif = ref(errImage + "?" + +new Date());
|
const errGif = ref(errImage + "?" + +new Date());
|
||||||
|
|
||||||
function back() {
|
function back() {
|
||||||
if (proxy.$route.query.noGoBack) {
|
if (proxy.$route.query.noGoBack) {
|
||||||
proxy.$router.push({ path: "/" });
|
proxy.$router.push({ path: "/" });
|
||||||
} else {
|
} else {
|
||||||
proxy.$router.go(-1);
|
proxy.$router.go(-1);
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="loginView" v-loading="loginView">
|
<div class="loginView">
|
||||||
<div class="wrapper-content" v-if="showLogin">
|
<div class="wrapper-content" >
|
||||||
<!-- <div class="content-nav">
|
|
||||||
<div class="nav-left">
|
|
||||||
<img src="../assets/logo/logo.png" />
|
|
||||||
<div>多人互动白板</div>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
<div class="login-form">
|
<div class="login-form">
|
||||||
<div class="selected-rectangle"></div>
|
<div class="selected-rectangle"></div>
|
||||||
<el-form ref="loginRef" class="form-info" :model="loginForm" :rules="loginRules">
|
<el-form ref="loginRef" class="form-info" :model="loginForm" :rules="loginRules">
|
||||||
@@ -40,26 +34,26 @@
|
|||||||
import { useUserStore } from '@/stores/modules/user.js'
|
import { useUserStore } from '@/stores/modules/user.js'
|
||||||
import { watch, ref, getCurrentInstance, onMounted } from 'vue'
|
import { watch, ref, getCurrentInstance, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElNotification,ElMessage } from 'element-plus'
|
import { ElNotification,ElMessage } from 'element-plus'
|
||||||
import { getInfo } from "@/api/login";
|
|
||||||
import CryptoJS from 'crypto-js';
|
import CryptoJS from 'crypto-js';
|
||||||
import { useMeterStore } from '@/stores/modules/meter'
|
import { useMeterStore } from '@/stores/modules/meter'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
const showLogin = ref(false)
|
|
||||||
|
|
||||||
const meterStore = useMeterStore()
|
const meterStore = useMeterStore()
|
||||||
|
|
||||||
|
|
||||||
const redirect = ref(undefined);
|
const redirect = ref(undefined);
|
||||||
const loginView = ref(false)
|
const loginView = ref(true)
|
||||||
|
|
||||||
watch(() => router.currentRoute.value, (newRoute) => {
|
// 监听路由变化,获取重定向参数
|
||||||
redirect.value = newRoute.query && newRoute.query.redirect;
|
// watch(() => route, (newRoute) => {
|
||||||
}, { immediate: true });
|
// redirect.value = newRoute.query && newRoute.query.redirect;
|
||||||
|
// console.log('重定向路径:', redirect.value);
|
||||||
|
// }, { immediate: true });
|
||||||
|
|
||||||
const loginForm = ref({
|
const loginForm = ref({
|
||||||
username: '',
|
username: '',
|
||||||
@@ -74,7 +68,7 @@ const loginRules = {
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
|
||||||
function handleLogin() {
|
function handleLogin() {
|
||||||
proxy.$refs.loginRef.validate((valid) => {
|
proxy.$refs.loginRef.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -86,11 +80,8 @@ function handleLogin() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const secretKey = ((loginForm.value.username + localStorage?.getItem('UDID')).toLowerCase()).replaceAll('-', ''); // 用户名+UDID(32位16进制,全小写)
|
const secretKey = ((loginForm.value.username + localStorage?.getItem('UDID')).toLowerCase()).replaceAll('-', ''); // 用户名+UDID(32位16进制,全小写)
|
||||||
|
|
||||||
const randomChars = generateRandomChars(6);
|
const randomChars = generateRandomChars(6);
|
||||||
|
|
||||||
const message = `Gx${randomChars}${loginForm.value.password}`;
|
const message = `Gx${randomChars}${loginForm.value.password}`;
|
||||||
|
|
||||||
const ciphertext = CryptoJS.Blowfish.encrypt(message, secretKey).toString();
|
const ciphertext = CryptoJS.Blowfish.encrypt(message, secretKey).toString();
|
||||||
// 调用action的登录方法
|
// 调用action的登录方法
|
||||||
userStore
|
userStore
|
||||||
@@ -101,14 +92,7 @@ function handleLogin() {
|
|||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
const userInfo = JSON.parse(sessionStorage.getItem('userData'))
|
const userInfo = JSON.parse(sessionStorage.getItem('userData'))
|
||||||
router.push({
|
await handleLoginSuccess();
|
||||||
path: '/coordinate',
|
|
||||||
})
|
|
||||||
// router.push({
|
|
||||||
// path: '/whiteboard',
|
|
||||||
// query: { room_uid: 'nxst-ok4j' }
|
|
||||||
// })
|
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.log('登录失败', e)
|
console.log('登录失败', e)
|
||||||
@@ -119,6 +103,42 @@ function handleLogin() {
|
|||||||
requestNotificationPermission()
|
requestNotificationPermission()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理登录成功后的跳转逻辑
|
||||||
|
*/
|
||||||
|
async function handleLoginSuccess() {
|
||||||
|
try {
|
||||||
|
// 如果有重定向路径且不是登录页,则跳转到重定向页面
|
||||||
|
if (redirect.value && redirect.value !== '/login') {
|
||||||
|
console.log('跳转到重定向页面:', redirect.value);
|
||||||
|
|
||||||
|
// 确保路由存在,如果不存在则跳转到默认页面
|
||||||
|
try {
|
||||||
|
// 解析路径,检查是否是有效路由
|
||||||
|
const resolved = router.resolve(redirect.value);
|
||||||
|
if (resolved.matched.length > 0) {
|
||||||
|
await router.push(redirect.value);
|
||||||
|
} else {
|
||||||
|
console.warn('重定向路径无效,跳转到默认页面');
|
||||||
|
await router.push('/coordinate');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('重定向跳转失败,跳转到默认页面:', error);
|
||||||
|
await router.push('/coordinate');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 没有重定向或重定向到登录页,跳转到默认页面
|
||||||
|
await router.push('/coordinate');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录跳转异常:', error);
|
||||||
|
// 降级处理:跳转到默认页面
|
||||||
|
await router.push('/coordinate');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 生成随机字符串
|
// 生成随机字符串
|
||||||
function generateRandomChars(length) {
|
function generateRandomChars(length) {
|
||||||
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
@@ -144,27 +164,6 @@ function requestNotificationPermission() {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
meterStore.initUdid()
|
meterStore.initUdid()
|
||||||
try {
|
|
||||||
loginView.value = true
|
|
||||||
const res = await getInfo("self");
|
|
||||||
showLogin.value = false;
|
|
||||||
if (res.meta.code === 401) {
|
|
||||||
showLogin.value = true;
|
|
||||||
} else {
|
|
||||||
// router.push({
|
|
||||||
// path: '/whiteboard',
|
|
||||||
// query: { room_uid: 'nxst-ok4j' }
|
|
||||||
// })
|
|
||||||
router.push({
|
|
||||||
path: '/coordinate',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
loginView.value = false
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("⚠️ 用户信息校验失败:", err);
|
|
||||||
} finally {
|
|
||||||
loginView.value = false
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
14
src/views/redirect/index.vue
Normal file
14
src/views/redirect/index.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const { params, query } = route
|
||||||
|
const { path } = params
|
||||||
|
|
||||||
|
router.replace({ path: '/' + path, query })
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user