Compare commits
10 Commits
3ac556d7a5
...
a894367dcc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a894367dcc | ||
|
|
ca93e91326 | ||
|
|
6fbe53009c | ||
|
|
df359d01cc | ||
|
|
528170fe2f | ||
|
|
f19ab86ada | ||
|
|
2b8e7349e6 | ||
|
|
e0001ba430 | ||
|
|
db72ea9f33 | ||
|
|
e429e4286a |
@@ -6,7 +6,8 @@ VITE_APP_PORT = 80
|
|||||||
VITE_BASE_PATH = '/'
|
VITE_BASE_PATH = '/'
|
||||||
|
|
||||||
# 应用模板管理后台/生产环境
|
# 应用模板管理后台/生产环境
|
||||||
VITE_APP_BASE_API = 'https://xsynergy.gxtech.ltd'
|
# VITE_APP_BASE_API = 'https://xsynergy.gxtech.ltd'
|
||||||
|
VITE_APP_BASE_API = ''
|
||||||
|
|
||||||
# 公网为“web” 私有化为不跳转为“private” 私有化跳转为“skip”
|
# 公网为“web” 私有化为不跳转为“private” 私有化跳转为“skip”
|
||||||
VITE_APP_COOPERATION_TYPE = 'skip'
|
VITE_APP_COOPERATION_TYPE = 'skip'
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>互动白板</title>
|
<title>xSynergy远程协作系统</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
6966
package-lock.json
generated
11
package.json
@@ -11,9 +11,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.0.10",
|
"@element-plus/icons-vue": "^2.0.10",
|
||||||
"@msgpack/msgpack": "^3.1.2",
|
"@msgpack/msgpack": "^3.1.2",
|
||||||
|
"@sentry/vue": "^10.20.0",
|
||||||
|
"@vueuse/core": "^9.5.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"code-inspector-plugin": "^0.20.12",
|
"code-inspector-plugin": "^0.20.12",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"element-plus": "^2.2.27",
|
"element-plus": "^2.2.27",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"livekit-client": "^2.7.5",
|
"livekit-client": "^2.7.5",
|
||||||
@@ -23,14 +26,20 @@
|
|||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
|
"vite-plugin-compression": "^0.5.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": {
|
||||||
"@vitejs/plugin-vue": "^5.1.4",
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
|
"@vue/compiler-sfc": "^3.2.45",
|
||||||
"sass": "^1.56.1",
|
"sass": "^1.56.1",
|
||||||
"unplugin-auto-import": "^20.1.0",
|
"unplugin-auto-import": "^0.11.4",
|
||||||
"unplugin-vue-components": "^29.0.0",
|
"unplugin-vue-components": "^29.0.0",
|
||||||
|
"unplugin-vue-setup-extend-plus": "^0.4.9",
|
||||||
"vite": "^5.4.10"
|
"vite": "^5.4.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/App.vue
@@ -1,6 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { RouterLink, RouterView } from 'vue-router'
|
import { RouterLink, RouterView } from 'vue-router'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import { useUserStore } from '@/stores/modules/user.js'
|
||||||
|
// 在根组件预先初始化用户 store
|
||||||
|
onMounted(() => {
|
||||||
|
try {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('App.vue: Pinia 初始化中...', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -64,3 +64,77 @@ export function exitRoomApi(room_uid) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//获取当前房间信息
|
||||||
|
export function getRoomInfoApi(room_uid) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/meeting/${room_uid}/info`,
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取文件列表
|
||||||
|
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',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取当前会议中所有参与者信息
|
||||||
|
export function getParticipantsApi(roomId) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/meeting/${ roomId }/participants`,
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 获取当前房间中视频
|
||||||
|
export function getvideoUrlApi(room_uid) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/room/${ room_uid }/recordings`,
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,3 +17,22 @@ export function getDirectoriesUsers(directory_uuid,data) {
|
|||||||
params:data
|
params:data
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//获取参与者历史参会记录
|
||||||
|
export function getParticipantsHistoryApi(userId,data) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/rooms/${ userId }/participants/history`,
|
||||||
|
method: 'get',
|
||||||
|
params:data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户详细信息
|
||||||
|
export function getInfo(userUid,type) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/auth/users/${userUid}`,
|
||||||
|
method: 'get',
|
||||||
|
params:type
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,3 +34,28 @@ export function logout() {
|
|||||||
method: 'post'
|
method: 'post'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//修改密码
|
||||||
|
export function changePwd(oldPassword, newPassword) {
|
||||||
|
const params = {
|
||||||
|
old_password: oldPassword,
|
||||||
|
new_password: newPassword
|
||||||
|
}
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/auth/change-password',
|
||||||
|
method: 'post',
|
||||||
|
data: params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//验证密码强度
|
||||||
|
export function checkPwdStrength(password) {
|
||||||
|
const params = {
|
||||||
|
password: password
|
||||||
|
}
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/auth/check-password-strength',
|
||||||
|
method: 'post',
|
||||||
|
data: params
|
||||||
|
})
|
||||||
|
}
|
||||||
9
src/api/menu.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 获取路由
|
||||||
|
export const getRouters = () => {
|
||||||
|
return request({
|
||||||
|
url: '/system/menu/getRouters',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
BIN
src/assets/images/Gc_114_line-Level-action.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/images/Gc_114_line-Level.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/images/amplify.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
src/assets/images/book-read-fill-action.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/assets/images/book-read-fill.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
39
src/assets/images/dark.svg
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<defs>
|
||||||
|
<filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
|
||||||
|
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||||
|
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||||
|
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||||
|
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
<rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
|
||||||
|
<filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
|
||||||
|
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||||
|
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||||
|
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<g id="配置面板" width="48" height="40" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="setting-copy-2" width="48" height="40" transform="translate(-1190.000000, -136.000000)">
|
||||||
|
<g id="Group-8" width="48" height="40" transform="translate(1167.000000, 0.000000)">
|
||||||
|
<g id="Group-5-Copy-5" filter="url(#filter-1)" transform="translate(25.000000, 137.000000)">
|
||||||
|
<mask id="mask-3" fill="white">
|
||||||
|
<use xlink:href="#path-2"></use>
|
||||||
|
</mask>
|
||||||
|
<g id="Rectangle-18">
|
||||||
|
<use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
|
||||||
|
<use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
|
||||||
|
</g>
|
||||||
|
<rect id="Rectangle-11" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
|
||||||
|
<rect id="Rectangle-18" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/assets/images/file-logo.png
Normal file
|
After Width: | Height: | Size: 861 B |
BIN
src/assets/images/file-logoW.png
Normal file
|
After Width: | Height: | Size: 744 B |
39
src/assets/images/light.svg
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<defs>
|
||||||
|
<filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
|
||||||
|
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||||
|
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||||
|
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
||||||
|
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
<rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
|
||||||
|
<filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
|
||||||
|
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||||
|
<feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||||
|
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<g id="配置面板" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="setting-copy-2" transform="translate(-1254.000000, -136.000000)">
|
||||||
|
<g id="Group-8" transform="translate(1167.000000, 0.000000)">
|
||||||
|
<g id="Group-5" filter="url(#filter-1)" transform="translate(89.000000, 137.000000)">
|
||||||
|
<mask id="mask-3" fill="white">
|
||||||
|
<use xlink:href="#path-2"></use>
|
||||||
|
</mask>
|
||||||
|
<g id="Rectangle-18">
|
||||||
|
<use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
|
||||||
|
<use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
|
||||||
|
</g>
|
||||||
|
<rect id="Rectangle-18" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
|
||||||
|
<rect id="Rectangle-11" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/assets/images/shrink.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
@@ -1,4 +1,5 @@
|
|||||||
$menu-bg-color: #8290f0;
|
// $menu-bg-color: #8290f0;
|
||||||
|
$menu-bg-color: #434343;
|
||||||
#app {
|
#app {
|
||||||
|
|
||||||
.main-container {
|
.main-container {
|
||||||
@@ -92,12 +93,12 @@ $menu-bg-color: #8290f0;
|
|||||||
.sub-menu-title-noDropdown,
|
.sub-menu-title-noDropdown,
|
||||||
.el-sub-menu__title {
|
.el-sub-menu__title {
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #c4cbf3 !important;
|
background-color: #e5e5e5 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-item:hover {
|
.el-menu-item:hover {
|
||||||
background-color: #c4cbf3 !important;
|
background-color: #e5e5e5 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .theme-dark .is-active>.el-sub-menu__title {
|
& .theme-dark .is-active>.el-sub-menu__title {
|
||||||
@@ -109,7 +110,7 @@ $menu-bg-color: #8290f0;
|
|||||||
min-width: $base-sidebar-width !important;
|
min-width: $base-sidebar-width !important;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #c4cbf3 !important;
|
background-color: #e5e5e5 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
81
src/components/Breadcrumb/index.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<el-breadcrumb class="app-breadcrumb" separator="/">
|
||||||
|
<transition-group name="breadcrumb">
|
||||||
|
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
|
||||||
|
<span
|
||||||
|
v-if="item.redirect === 'noRedirect' || index == levelList.length - 1"
|
||||||
|
class="no-redirect"
|
||||||
|
>
|
||||||
|
{{ item.meta.title }}
|
||||||
|
</span>
|
||||||
|
<a v-else @click.prevent="handleLink(item)">
|
||||||
|
{{ item.meta.title }}
|
||||||
|
</a>
|
||||||
|
</el-breadcrumb-item>
|
||||||
|
</transition-group>
|
||||||
|
</el-breadcrumb>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const levelList = ref([])
|
||||||
|
|
||||||
|
function getBreadcrumb() {
|
||||||
|
// only show routes with meta.title
|
||||||
|
let matched = route.matched.filter((item) => item.meta && item.meta.title)
|
||||||
|
const first = matched[0]
|
||||||
|
// 判断是否为首页
|
||||||
|
if (!isDashboard(first)) {
|
||||||
|
matched = matched
|
||||||
|
}
|
||||||
|
|
||||||
|
levelList.value = matched.filter(
|
||||||
|
(item) => item.meta && item.meta.title && item.meta.breadcrumb !== false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
function isDashboard(route) {
|
||||||
|
const name = route && route.name
|
||||||
|
if (!name) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return name.trim() === 'Index'
|
||||||
|
}
|
||||||
|
function handleLink(item) {
|
||||||
|
const { redirect, path } = item
|
||||||
|
if (redirect) {
|
||||||
|
router.push(redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
router.push(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
// if you go to the redirect page, do not update the breadcrumbs
|
||||||
|
if (route.path.startsWith('/redirect/')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getBreadcrumb()
|
||||||
|
})
|
||||||
|
getBreadcrumb()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.app-breadcrumb.el-breadcrumb {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 56px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
margin-left: 8px;
|
||||||
|
|
||||||
|
.no-redirect {
|
||||||
|
color: #333333;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
::v-deep .el-breadcrumb__separator {
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
41
src/components/Hamburger/index.vue
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<div style="padding: 0 15px;" @click="toggleClick">
|
||||||
|
<svg
|
||||||
|
:class="{'is-active':isActive}"
|
||||||
|
class="hamburger"
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
>
|
||||||
|
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits()
|
||||||
|
const toggleClick = () => {
|
||||||
|
emit('toggleClick');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hamburger {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger.is-active {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -26,15 +26,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import useUserStore from '@/stores/modules/user'
|
import { useUserStore } from '@/stores/modules/user.js'
|
||||||
import { watch, ref, getCurrentInstance } from 'vue'
|
import { watch, ref, getCurrentInstance,onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useMeterStore } from '@/stores/modules/meter'
|
||||||
|
import CryptoJS from 'crypto-js';
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
const redirect = ref(undefined);
|
const redirect = ref(undefined);
|
||||||
const emit = defineEmits(['loginSuccess'])
|
const emit = defineEmits(['loginSuccess'])
|
||||||
|
const meterStore = useMeterStore()
|
||||||
|
|
||||||
const loginForm = ref({
|
const loginForm = ref({
|
||||||
username: '',
|
username: '',
|
||||||
@@ -52,14 +55,31 @@ function handleLogin() {
|
|||||||
proxy.$refs.loginRef.validate((valid) => {
|
proxy.$refs.loginRef.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
if(!localStorage?.getItem('UDID')){
|
||||||
|
ElMessage({
|
||||||
|
message: '服务错误,请刷新页面',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const secretKey = ((loginForm.value.username + localStorage?.getItem('UDID')).toLowerCase()).replaceAll('-', ''); // 用户名+UDID(32位16进制,全小写)
|
||||||
|
|
||||||
|
const randomChars = generateRandomChars(6);
|
||||||
|
|
||||||
|
const message = `Gx${randomChars}${loginForm.value.password}`;
|
||||||
|
|
||||||
|
const ciphertext = CryptoJS.Blowfish.encrypt(message, secretKey).toString();
|
||||||
|
|
||||||
|
|
||||||
// 调用action的登录方法
|
// 调用action的登录方法
|
||||||
userStore
|
userStore
|
||||||
.login({
|
.login({
|
||||||
password: loginForm.value.password,
|
password: ciphertext,
|
||||||
|
// password: loginForm.value.password,
|
||||||
username: loginForm.value.username,
|
username: loginForm.value.username,
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
const userInfo = JSON.parse(localStorage.getItem('userData'))
|
const userInfo = JSON.parse(sessionStorage.getItem('userData'))
|
||||||
emit('loginSuccess', userInfo)
|
emit('loginSuccess', userInfo)
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@@ -71,6 +91,16 @@ function handleLogin() {
|
|||||||
requestNotificationPermission()
|
requestNotificationPermission()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 生成随机字符串
|
||||||
|
function generateRandomChars(length) {
|
||||||
|
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 请求浏览器的通知权限
|
* @description 请求浏览器的通知权限
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
@@ -83,13 +113,17 @@ function requestNotificationPermission() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
meterStore.initUdid()
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.wrapper-content {
|
.wrapper-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
height: calc(100vh - 60px);
|
||||||
padding: 0 40px;
|
padding: 0 40px;
|
||||||
background: linear-gradient(135deg, #f5f7fa 0%, #eef1f6 100%);
|
background: linear-gradient(135deg, #f5f7fa 0%, #eef1f6 100%);
|
||||||
}
|
}
|
||||||
@@ -160,6 +194,7 @@ function requestNotificationPermission() {
|
|||||||
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);
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/components/ParentView/index.vue
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<template >
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
210
src/components/TopNav/index.vue
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
<template>
|
||||||
|
<el-menu
|
||||||
|
:default-active="activeMenu"
|
||||||
|
mode="horizontal"
|
||||||
|
@select="handleSelect"
|
||||||
|
:ellipsis="false"
|
||||||
|
>
|
||||||
|
<template v-for="(item, index) in topMenus">
|
||||||
|
<el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber"
|
||||||
|
><svg-icon
|
||||||
|
v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
|
||||||
|
:icon-class="item.meta.icon"
|
||||||
|
/>
|
||||||
|
{{ item.meta.title }}</el-menu-item
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 顶部菜单超出数量折叠 -->
|
||||||
|
<el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
|
||||||
|
<template #title>更多菜单</template>
|
||||||
|
<template v-for="(item, index) in topMenus">
|
||||||
|
<el-menu-item
|
||||||
|
:index="item.path"
|
||||||
|
:key="index"
|
||||||
|
v-if="index >= visibleNumber"
|
||||||
|
><svg-icon :icon-class="item.meta.icon" />
|
||||||
|
{{ item.meta.title }}</el-menu-item
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</el-sub-menu>
|
||||||
|
</el-menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { constantRoutes } from "@/router/index.js"
|
||||||
|
import { isHttp } from '@/utils/validate.js'
|
||||||
|
import { useAppStore } from '@/stores/modules/app.js'
|
||||||
|
import { useSettingsStore } from '@/stores/modules/settings.js'
|
||||||
|
|
||||||
|
|
||||||
|
// 顶部栏初始数
|
||||||
|
const visibleNumber = ref(null);
|
||||||
|
// 当前激活菜单的 index
|
||||||
|
const currentIndex = ref(null);
|
||||||
|
// 隐藏侧边栏路由
|
||||||
|
const hideList = ['/index', '/user/profile'];
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 主题颜色
|
||||||
|
const theme = computed(() => settingsStore.theme);
|
||||||
|
|
||||||
|
|
||||||
|
// 顶部显示菜单
|
||||||
|
const topMenus = computed(() => {
|
||||||
|
let topMenus = [];
|
||||||
|
routers.value.map((menu) => {
|
||||||
|
if (menu.hidden !== true) {
|
||||||
|
// 兼容顶部栏一级菜单内部跳转
|
||||||
|
if (menu.path === "/") {
|
||||||
|
topMenus.push(menu.children[0]);
|
||||||
|
} else {
|
||||||
|
topMenus.push(menu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return topMenus;
|
||||||
|
})
|
||||||
|
|
||||||
|
// 设置子路由
|
||||||
|
const childrenMenus = computed(() => {
|
||||||
|
let childrenMenus = [];
|
||||||
|
routers.value.map((router) => {
|
||||||
|
for (let item in router.children) {
|
||||||
|
if (router.children[item].parentPath === undefined) {
|
||||||
|
if(router.path === "/") {
|
||||||
|
router.children[item].path = "/" + router.children[item].path;
|
||||||
|
} else {
|
||||||
|
if(!isHttp(router.children[item].path)) {
|
||||||
|
router.children[item].path = router.path + "/" + router.children[item].path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
router.children[item].parentPath = router.path;
|
||||||
|
}
|
||||||
|
childrenMenus.push(router.children[item]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return constantRoutes.concat(childrenMenus);
|
||||||
|
})
|
||||||
|
|
||||||
|
// 默认激活的菜单
|
||||||
|
const activeMenu = computed(() => {
|
||||||
|
const path = route.path;
|
||||||
|
let activePath = path;
|
||||||
|
if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
|
||||||
|
const tmpPath = path.substring(1, path.length);
|
||||||
|
activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"));
|
||||||
|
if (!route.meta.link) {
|
||||||
|
appStore.toggleSideBarHide(false);
|
||||||
|
}
|
||||||
|
} else if(!route.children) {
|
||||||
|
activePath = path;
|
||||||
|
appStore.toggleSideBarHide(true);
|
||||||
|
}
|
||||||
|
activeRoutes(activePath);
|
||||||
|
return activePath;
|
||||||
|
})
|
||||||
|
|
||||||
|
function setVisibleNumber() {
|
||||||
|
const width = document.body.getBoundingClientRect().width / 3;
|
||||||
|
visibleNumber.value = parseInt(width / 85);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelect(key, keyPath) {
|
||||||
|
currentIndex.value = key;
|
||||||
|
const route = routers.value.find(item => item.path === key);
|
||||||
|
if (isHttp(key)) {
|
||||||
|
// http(s):// 路径新窗口打开
|
||||||
|
window.open(key, "_blank");
|
||||||
|
} else if (!route || !route.children) {
|
||||||
|
// 没有子路由路径内部打开
|
||||||
|
const routeMenu = childrenMenus.value.find(item => item.path === key);
|
||||||
|
if (routeMenu && routeMenu.query) {
|
||||||
|
let query = JSON.parse(routeMenu.query);
|
||||||
|
router.push({ path: key, query: query });
|
||||||
|
} else {
|
||||||
|
router.push({ path: key });
|
||||||
|
}
|
||||||
|
appStore.toggleSideBarHide(true);
|
||||||
|
} else {
|
||||||
|
// 显示左侧联动菜单
|
||||||
|
activeRoutes(key);
|
||||||
|
appStore.toggleSideBarHide(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeRoutes(key) {
|
||||||
|
let routes = [];
|
||||||
|
if (childrenMenus.value && childrenMenus.value.length > 0) {
|
||||||
|
childrenMenus.value.map((item) => {
|
||||||
|
if (key == item.parentPath || (key == "index" && "" == item.path)) {
|
||||||
|
routes.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(routes.length > 0) {
|
||||||
|
} else {
|
||||||
|
appStore.toggleSideBarHide(true);
|
||||||
|
}
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('resize', setVisibleNumber)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', setVisibleNumber)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setVisibleNumber()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.topmenu-container.el-menu--horizontal > .el-menu-item {
|
||||||
|
float: left;
|
||||||
|
height: 50px !important;
|
||||||
|
line-height: 50px !important;
|
||||||
|
color: #999093 !important;
|
||||||
|
padding: 0 5px !important;
|
||||||
|
margin: 0 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-sub-menu.is-active .el-submenu__title {
|
||||||
|
border-bottom: 2px solid #{'var(--theme)'} !important;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sub-menu item */
|
||||||
|
.topmenu-container.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
|
||||||
|
float: left;
|
||||||
|
height: 50px !important;
|
||||||
|
line-height: 50px !important;
|
||||||
|
color: #999093 !important;
|
||||||
|
padding: 0 5px !important;
|
||||||
|
margin: 0 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 背景色隐藏 */
|
||||||
|
.topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus, .topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover, .topmenu-container.el-menu--horizontal>.el-submenu .el-submenu__title:hover {
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图标右间距 */
|
||||||
|
.topmenu-container .svg-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* topmenu more arrow */
|
||||||
|
.topmenu-container .el-sub-menu .el-sub-menu__icon-arrow {
|
||||||
|
position: static;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -42,7 +42,7 @@ class Canvas extends EventEmitter {
|
|||||||
const parent = this.canvas.parentElement;
|
const parent = this.canvas.parentElement;
|
||||||
if (!parent) return;
|
if (!parent) return;
|
||||||
|
|
||||||
const containerWidth = parent.offsetWidth;
|
const containerWidth = parent.offsetWidth - 150;
|
||||||
const containerHeight = parent.offsetHeight;
|
const containerHeight = parent.offsetHeight;
|
||||||
|
|
||||||
let width = containerWidth;
|
let width = containerWidth;
|
||||||
@@ -63,6 +63,7 @@ class Canvas extends EventEmitter {
|
|||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setDrawingTool(tool) { this.drawingTool = tool; }
|
setDrawingTool(tool) { this.drawingTool = tool; }
|
||||||
setColor(color) { this.currentColor = color; }
|
setColor(color) { this.currentColor = color; }
|
||||||
setThickness(size) { this.currentThickness = size; }
|
setThickness(size) { this.currentThickness = size; }
|
||||||
@@ -71,11 +72,12 @@ class Canvas extends EventEmitter {
|
|||||||
getShapes() { return this.shapes; }
|
getShapes() { return this.shapes; }
|
||||||
setShapes(shapes) { this.shapes = shapes; this.render(); }
|
setShapes(shapes) { this.shapes = shapes; this.render(); }
|
||||||
|
|
||||||
|
// 获取鼠标坐标,转换为百分比坐标
|
||||||
getMouseCoordinates(e) {
|
getMouseCoordinates(e) {
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
const rect = this.canvas.getBoundingClientRect();
|
||||||
return {
|
return {
|
||||||
x: (e.clientX - rect.left) / this.canvas.width,
|
x:((e.clientX - rect.left) / this.canvas.width).toFixed(4),
|
||||||
y: (e.clientY - rect.top) / this.canvas.height
|
y:((e.clientY - rect.top) / this.canvas.height).toFixed(4)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
285
src/core/index_old.js
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
import EventEmitter from "@/utils/emitter";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canvas绘图类,支持多种图形绘制和多人同步
|
||||||
|
* 使用百分比坐标系统确保跨设备一致性
|
||||||
|
*/
|
||||||
|
class Canvas extends EventEmitter {
|
||||||
|
constructor(canvasId) {
|
||||||
|
super();
|
||||||
|
this.canvas = document.getElementById(canvasId);
|
||||||
|
if (!this.canvas) {
|
||||||
|
throw new Error(`Canvas element with id ${canvasId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ctx = this.canvas.getContext('2d');
|
||||||
|
this.shapes = []; // 所有已绘制形状
|
||||||
|
this.currentShape = null; // 当前正在绘制的形状
|
||||||
|
this.isDrawing = false;
|
||||||
|
this.drawingTool = 'pencil';
|
||||||
|
this.pathOptimizationEnabled = true;
|
||||||
|
this.optimizationThreshold = 0.005;
|
||||||
|
this.currentColor = '#ffcc00';
|
||||||
|
this.currentThickness = 2;
|
||||||
|
|
||||||
|
this.resize();
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
this.handleMouseDown = this.handleMouseDown.bind(this);
|
||||||
|
this.handleMouseMove = this.handleMouseMove.bind(this);
|
||||||
|
this.handleMouseUp = this.handleMouseUp.bind(this);
|
||||||
|
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
||||||
|
|
||||||
|
this.canvas.addEventListener('mousedown', this.handleMouseDown);
|
||||||
|
this.canvas.addEventListener('mousemove', this.handleMouseMove);
|
||||||
|
this.canvas.addEventListener('mouseup', this.handleMouseUp);
|
||||||
|
this.canvas.addEventListener('mouseleave', this.handleMouseLeave);
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => this.resize());
|
||||||
|
}
|
||||||
|
|
||||||
|
resize() {
|
||||||
|
const parent = this.canvas.parentElement;
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
const containerWidth = parent.offsetWidth;
|
||||||
|
const containerHeight = parent.offsetHeight;
|
||||||
|
|
||||||
|
let width = containerWidth;
|
||||||
|
let height = Math.floor((width * 9) / 16);
|
||||||
|
|
||||||
|
if (height > containerHeight) {
|
||||||
|
height = containerHeight;
|
||||||
|
width = Math.floor((height * 16) / 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.canvas.width === width && this.canvas.height === height) return;
|
||||||
|
|
||||||
|
this.canvas.width = width;
|
||||||
|
this.canvas.height = height;
|
||||||
|
this.canvas.style.width = width + "px";
|
||||||
|
this.canvas.style.height = height + "px";
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
setDrawingTool(tool) { this.drawingTool = tool; }
|
||||||
|
setColor(color) { this.currentColor = color; }
|
||||||
|
setThickness(size) { this.currentThickness = size; }
|
||||||
|
setPathOptimization(enabled) { this.pathOptimizationEnabled = enabled; }
|
||||||
|
setOptimizationThreshold(threshold) { this.optimizationThreshold = threshold; }
|
||||||
|
getShapes() { return this.shapes; }
|
||||||
|
setShapes(shapes) { this.shapes = shapes; this.render(); }
|
||||||
|
|
||||||
|
getMouseCoordinates(e) {
|
||||||
|
const rect = this.canvas.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: (e.clientX - rect.left) / this.canvas.width,
|
||||||
|
y: (e.clientY - rect.top) / this.canvas.height
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseDown(e) {
|
||||||
|
this.isDrawing = true;
|
||||||
|
const coords = this.getMouseCoordinates(e);
|
||||||
|
|
||||||
|
if (this.drawingTool === 'pencil') {
|
||||||
|
this.currentShape = { type: 'pencil', data: { color: this.currentColor, path: [coords], thickness: this.currentThickness } };
|
||||||
|
} else if (this.drawingTool === 'line') {
|
||||||
|
this.currentShape = { type: 'line', data: { color: this.currentColor, start: coords, end: coords, thickness: this.currentThickness } };
|
||||||
|
} else if (this.drawingTool === 'rectangle') {
|
||||||
|
this.currentShape = { type: 'rectangle', data: { color: this.currentColor, start: coords, end: coords, thickness: this.currentThickness, fill: false } };
|
||||||
|
} else if (this.drawingTool === 'circle') {
|
||||||
|
this.currentShape = { type: 'circle', data: { color: this.currentColor, start: coords, end: coords, thickness: this.currentThickness, fill: false } };
|
||||||
|
} else if (this.drawingTool === 'eraser') {
|
||||||
|
this.currentShape = { type: 'eraser', data: { color: '#ffffff', start: coords, end: coords, thickness: 3 } };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit('drawingStart', this.currentShape);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseMove(e) {
|
||||||
|
if (!this.isDrawing || !this.currentShape) return;
|
||||||
|
const coords = this.getMouseCoordinates(e);
|
||||||
|
|
||||||
|
if (this.drawingTool === 'pencil') {
|
||||||
|
this.currentShape.data.path.push(coords);
|
||||||
|
} else {
|
||||||
|
this.currentShape.data.end = coords;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
this.emit('drawingUpdate', this.currentShape);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseUp(e) {
|
||||||
|
if (!this.isDrawing || !this.currentShape) return;
|
||||||
|
this.isDrawing = false;
|
||||||
|
const coords = this.getMouseCoordinates(e);
|
||||||
|
if (this.drawingTool === 'pencil' && this.pathOptimizationEnabled && this.currentShape.data.path.length > 10) {
|
||||||
|
this.currentShape.data.path = this.optimizePath(this.currentShape.data.path);
|
||||||
|
} else {
|
||||||
|
this.currentShape.data.end = coords;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.shapes.push({ ...this.currentShape });
|
||||||
|
this.emit('drawingEnd', this.currentShape);
|
||||||
|
this.currentShape = null;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseLeave(e) {
|
||||||
|
if (this.isDrawing) this.handleMouseUp(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
optimizePath(path) {
|
||||||
|
if (path.length < 3) return path;
|
||||||
|
const optimizedPath = [path[0]];
|
||||||
|
for (let i = 1; i < path.length - 1;) {
|
||||||
|
let a = 1;
|
||||||
|
while (i + a < path.length && this.calculateDistance(path[i], path[i + a]) < this.optimizationThreshold) a++;
|
||||||
|
optimizedPath.push(path[i]);
|
||||||
|
i += a;
|
||||||
|
}
|
||||||
|
optimizedPath.push(path[path.length - 1]);
|
||||||
|
return optimizedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateDistance(p1, p2) {
|
||||||
|
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
addShape(shapeData) {
|
||||||
|
if (Array.isArray(shapeData)) this.shapes.push(...shapeData);
|
||||||
|
else this.shapes.push(shapeData);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCanvas() {
|
||||||
|
this.shapes = [];
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
this.emit('clear');
|
||||||
|
}
|
||||||
|
|
||||||
|
exportToDataURL(type = 'image/png', quality = 1) {
|
||||||
|
return this.canvas.toDataURL(type, quality);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
// 绘制历史形状
|
||||||
|
this.shapes.forEach(shape => this.drawShape(shape));
|
||||||
|
|
||||||
|
// 绘制当前正在绘制的形状
|
||||||
|
if (this.currentShape) this.drawShape(this.currentShape);
|
||||||
|
|
||||||
|
// 画画布边框
|
||||||
|
this.ctx.save();
|
||||||
|
this.ctx.strokeStyle = "#000";
|
||||||
|
this.ctx.lineWidth = 2;
|
||||||
|
this.ctx.strokeRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
this.ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawShape(shape) {
|
||||||
|
switch (shape.type) {
|
||||||
|
case 'pencil': this.drawPencil(shape.data); break;
|
||||||
|
case 'line': this.drawLine(shape.data); break;
|
||||||
|
case 'rectangle': this.drawRectangle(shape.data); break;
|
||||||
|
case 'circle': this.drawCircle(shape.data); break;
|
||||||
|
case 'eraser': this.drawEraser(shape.data, shape); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawPencil(p) {
|
||||||
|
if (p.path.length < 2) return;
|
||||||
|
const path = p.path.map(pt => ({ x: pt.x * this.canvas.width, y: pt.y * this.canvas.height }));
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.strokeStyle = p.color;
|
||||||
|
this.ctx.lineWidth = p.thickness;
|
||||||
|
this.ctx.lineCap = 'round';
|
||||||
|
this.ctx.lineJoin = 'round';
|
||||||
|
this.drawSmoothCurve(this.ctx, path);
|
||||||
|
this.ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSmoothCurve(ctx, path) {
|
||||||
|
if (path.length < 3) { ctx.moveTo(path[0].x, path[0].y); for (let i = 1; i < path.length; i++) ctx.lineTo(path[i].x, path[i].y); return; }
|
||||||
|
ctx.moveTo(path[0].x, path[0].y);
|
||||||
|
const threshold = 5;
|
||||||
|
for (let i = 1; i < path.length - 2;) {
|
||||||
|
let a = 1;
|
||||||
|
while (i + a < path.length - 2 &&
|
||||||
|
Math.sqrt(Math.pow(path[i].x - path[i + a].x, 2) + Math.pow(path[i].y - path[i + a].y, 2)) < threshold) a++;
|
||||||
|
const xc = (path[i].x + path[i + a].x) / 2;
|
||||||
|
const yc = (path[i].y + path[i + a].y) / 2;
|
||||||
|
ctx.quadraticCurveTo(path[i].x, path[i].y, xc, yc);
|
||||||
|
i += a;
|
||||||
|
}
|
||||||
|
ctx.lineTo(path[path.length - 1].x, path[path.length - 1].y);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawLine(l) {
|
||||||
|
const sx = l.start.x * this.canvas.width, sy = l.start.y * this.canvas.height;
|
||||||
|
const ex = l.end.x * this.canvas.width, ey = l.end.y * this.canvas.height;
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(sx, sy);
|
||||||
|
this.ctx.lineTo(ex, ey);
|
||||||
|
this.ctx.strokeStyle = l.color;
|
||||||
|
this.ctx.lineWidth = l.thickness;
|
||||||
|
this.ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawRectangle(r) {
|
||||||
|
const sx = r.start.x * this.canvas.width, sy = r.start.y * this.canvas.height;
|
||||||
|
const ex = r.end.x * this.canvas.width, ey = r.end.y * this.canvas.height;
|
||||||
|
const w = ex - sx, h = ey - sy;
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.rect(sx, sy, w, h);
|
||||||
|
if (r.fill) { this.ctx.fillStyle = r.color; this.ctx.fill(); }
|
||||||
|
else { this.ctx.strokeStyle = r.color; this.ctx.lineWidth = r.thickness; this.ctx.stroke(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
drawCircle(c) {
|
||||||
|
const sx = c.start.x * this.canvas.width, sy = c.start.y * this.canvas.height;
|
||||||
|
const ex = c.end.x * this.canvas.width, ey = c.end.y * this.canvas.height;
|
||||||
|
const cx = (sx + ex) / 2, cy = (sy + ey) / 2;
|
||||||
|
const rx = Math.abs(ex - sx) / 2, ry = Math.abs(ey - sy) / 2;
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.ellipse(cx, cy, rx, ry, 0, 0, 2 * Math.PI);
|
||||||
|
if (c.fill) { this.ctx.fillStyle = c.color; this.ctx.fill(); }
|
||||||
|
else { this.ctx.strokeStyle = c.color; this.ctx.lineWidth = c.thickness; this.ctx.stroke(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
drawEraser(e, shapeObj) {
|
||||||
|
const sx = e.start.x * this.canvas.width, sy = e.start.y * this.canvas.height;
|
||||||
|
const ex = e.end.x * this.canvas.width, ey = e.end.y * this.canvas.height;
|
||||||
|
const w = Math.abs(ex - sx), h = Math.abs(ey - sy);
|
||||||
|
const x = Math.min(sx, ex), y = Math.min(sy, ey);
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.rect(x, y, w, h);
|
||||||
|
this.ctx.fillStyle = 'rgba(255,255,255)';
|
||||||
|
this.ctx.fill();
|
||||||
|
|
||||||
|
// 仅当前正在绘制的橡皮擦显示边框
|
||||||
|
if (this.currentShape && shapeObj === this.currentShape) {
|
||||||
|
this.ctx.save();
|
||||||
|
this.ctx.strokeStyle = '#000';
|
||||||
|
this.ctx.lineWidth = 1;
|
||||||
|
this.ctx.strokeRect(x, y, w, h);
|
||||||
|
this.ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.canvas.removeEventListener('mousedown', this.handleMouseDown);
|
||||||
|
this.canvas.removeEventListener('mousemove', this.handleMouseMove);
|
||||||
|
this.canvas.removeEventListener('mouseup', this.handleMouseUp);
|
||||||
|
this.canvas.removeEventListener('mouseleave', this.handleMouseLeave);
|
||||||
|
window.removeEventListener('resize', this.resize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Canvas;
|
||||||
49
src/layout/components/AppMain.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<section class="app-main">
|
||||||
|
<router-view v-slot="{ Component, route }">
|
||||||
|
<transition name="fade-transform" mode="out-in">
|
||||||
|
<keep-alive :include="tagsViewStore.cachedViews">
|
||||||
|
<component
|
||||||
|
v-if="!route.meta.link"
|
||||||
|
:is="Component"
|
||||||
|
:key="route.path"
|
||||||
|
/>
|
||||||
|
</keep-alive>
|
||||||
|
</transition>
|
||||||
|
</router-view>
|
||||||
|
<iframe-toggle />
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import iframeToggle from "./IframeToggle/index.vue";
|
||||||
|
import useTagsViewStore from "@/stores/modules/tagsView.js";
|
||||||
|
|
||||||
|
const tagsViewStore = useTagsViewStore();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.app-main {
|
||||||
|
/* 50= navbar 50 */
|
||||||
|
min-height: calc(100vh - 176px);
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixed-header + .app-main {
|
||||||
|
padding-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hasTagsView {
|
||||||
|
.app-main {
|
||||||
|
/* 84 = navbar + tags-view = 50 + 34 */
|
||||||
|
// min-height: calc(100vh - 84px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixed-header + .app-main {
|
||||||
|
padding-top: 84px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
19
src/layout/components/IframeToggle/index.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<transition-group name="fade-transform" mode="out-in">
|
||||||
|
<inner-link
|
||||||
|
v-for="(item, index) in tagsViewStore.iframeViews"
|
||||||
|
:key="item.path"
|
||||||
|
:iframeId="'iframe' + index"
|
||||||
|
v-show="route.path === item.path"
|
||||||
|
:src="item.meta.link"
|
||||||
|
></inner-link>
|
||||||
|
</transition-group>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import InnerLink from "../InnerLink/index.vue"
|
||||||
|
import useTagsViewStore from '@/stores/modules/tagsView.js'
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const tagsViewStore = useTagsViewStore()
|
||||||
|
</script>
|
||||||
24
src/layout/components/InnerLink/index.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<div :style="'height:' + height">
|
||||||
|
<iframe
|
||||||
|
:id="iframeId"
|
||||||
|
style="width: 100%; height: 100%"
|
||||||
|
:src="src"
|
||||||
|
frameborder="no"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
default: "/"
|
||||||
|
},
|
||||||
|
iframeId: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const height = ref(document.documentElement.clientHeight - 94.5 + "px");
|
||||||
|
</script>
|
||||||
149
src/layout/components/InviteJoin/index.vue
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="inviteDialog"
|
||||||
|
title="远程协作"
|
||||||
|
width="400px"
|
||||||
|
:close-on-press-escape="false"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:show-close="false"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div style="width: 100%; margin-bottom: 30px; font-size: 20px">
|
||||||
|
"
|
||||||
|
{{
|
||||||
|
socketInformation.room_name
|
||||||
|
? socketInformation.room_name
|
||||||
|
: ''
|
||||||
|
}}
|
||||||
|
" 邀请您参加远程协作
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center">
|
||||||
|
<el-button
|
||||||
|
size="large"
|
||||||
|
type="danger"
|
||||||
|
style="font-size: 16px"
|
||||||
|
@click="clickRefuseJoin"
|
||||||
|
>
|
||||||
|
拒 绝
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="large"
|
||||||
|
type="primary"
|
||||||
|
style="font-size: 16px"
|
||||||
|
@click="clickJoin"
|
||||||
|
>
|
||||||
|
加 入
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref ,onMounted} from 'vue'
|
||||||
|
import { getStatusApi } from '@/api/conferencingRoom.js'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { mqttClient } from "@/utils/mqtt.js";
|
||||||
|
import { useUserStore } from '@/stores/modules/user.js'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const inviteDialog = ref(false)
|
||||||
|
const socketInformation = ref(null)
|
||||||
|
|
||||||
|
|
||||||
|
/** 拒绝加入 */
|
||||||
|
const clickRefuseJoin = async () => {
|
||||||
|
//status 1: 同意加入, 5: 拒绝加入
|
||||||
|
try{
|
||||||
|
const res = await getStatusApi(socketInformation.value.room_uid,{status:5})
|
||||||
|
if(res.meta.code == 200){
|
||||||
|
ElMessage({
|
||||||
|
message: '已拒绝加入该协作',
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
inviteDialog.value = false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error,'error')
|
||||||
|
inviteDialog.value = false
|
||||||
|
} finally {
|
||||||
|
inviteDialog.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clickJoin = async () => {
|
||||||
|
const res = await getStatusApi(socketInformation.value.room_uid,{status:1})
|
||||||
|
if(res.meta.code == 200){
|
||||||
|
ElMessage({
|
||||||
|
message: '成功加入该协作',
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
inviteDialog.value = false
|
||||||
|
router.push({
|
||||||
|
path: '/conferencingRoom',
|
||||||
|
query:{
|
||||||
|
type:2,//创建房间,加入房间 2
|
||||||
|
room_uid:socketInformation.value.room_uid
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
inviteDialog.value = false
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 浏览器通知 */
|
||||||
|
const showNotification = (data) => {
|
||||||
|
if ('Notification' in window) {
|
||||||
|
Notification.requestPermission().then((permission) => {
|
||||||
|
if (permission === 'granted') {
|
||||||
|
const notification = new Notification('协作邀请', {
|
||||||
|
// body: String(data.room_name) + '邀请您参加远程协作'
|
||||||
|
body: '远程协作有新的邀请'
|
||||||
|
// icon: logo,
|
||||||
|
})
|
||||||
|
notification.onclick = () => { clickJoin() }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理加入房间和拒接房间 mqtt 消息 */
|
||||||
|
const processingSocket = (message) => {
|
||||||
|
const res = JSON.parse(message)
|
||||||
|
console.log(res,'收到用户信息 邀请')
|
||||||
|
if (!res?.status) {
|
||||||
|
socketInformation.value = res
|
||||||
|
inviteDialog.value = true
|
||||||
|
showNotification(socketInformation.value)
|
||||||
|
}else if(res.status == 5){
|
||||||
|
ElMessage({
|
||||||
|
message: `${res?.display_name}拒绝加入该协作`,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
processingSocket,
|
||||||
|
});
|
||||||
|
|
||||||
|
// onMounted(async () => {
|
||||||
|
// await mqttClient.connect(`room${Math.random().toString(16).substr(2, 8)}`);
|
||||||
|
// const res = await userStore.getInfo()
|
||||||
|
// const topic = `xSynergy/ROOM/+/rooms/${res.uid}`;
|
||||||
|
// mqttClient.subscribe(topic, async (shapeData) => {
|
||||||
|
// // console.log(shapeData.toString(),'shapeData发送邀请')
|
||||||
|
// processingSocket(shapeData.toString())
|
||||||
|
// });
|
||||||
|
// })
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
69
src/layout/components/Navbar.vue
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<div class="navbar">
|
||||||
|
<hamburger
|
||||||
|
id="hamburger-container"
|
||||||
|
:is-active="appStore.sidebar.opened"
|
||||||
|
class="hamburger-container"
|
||||||
|
@toggleClick="toggleSideBar"
|
||||||
|
/>
|
||||||
|
<breadcrumb
|
||||||
|
id="breadcrumb-container"
|
||||||
|
class="breadcrumb-container"
|
||||||
|
v-if="!settingsStore.topNav"
|
||||||
|
/>
|
||||||
|
<top-nav id="topmenu-container" class="topmenu-container" v-if="settingsStore.topNav" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Breadcrumb from '@/components/Breadcrumb/index.vue'
|
||||||
|
import TopNav from '@/components/TopNav/index.vue'
|
||||||
|
import Hamburger from '@/components/Hamburger/index.vue'
|
||||||
|
import { useAppStore } from '@/stores/modules/app.js'
|
||||||
|
import { useSettingsStore } from '@/stores/modules/settings.js'
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
|
||||||
|
function toggleSideBar() {
|
||||||
|
appStore.toggleSideBar(true)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.navbar {
|
||||||
|
height: 56px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.hamburger-container {
|
||||||
|
line-height: 56px;
|
||||||
|
height: 100%;
|
||||||
|
float: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.025);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-container {
|
||||||
|
display: flex;
|
||||||
|
align-content: center;
|
||||||
|
float: left;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topmenu-container {
|
||||||
|
position: absolute;
|
||||||
|
left: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errLog-container {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
162
src/layout/components/ResetPwd/index.vue
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
title="修改密码"
|
||||||
|
width="500"
|
||||||
|
:show-close='false'
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
|
||||||
|
>
|
||||||
|
<div v-loading="loading">
|
||||||
|
<el-form ref="pwdRef" :model="user" :rules="rules" label-width="100px">
|
||||||
|
<el-form-item label="旧密码" prop="oldPassword">
|
||||||
|
<el-input
|
||||||
|
v-model="user.oldPassword"
|
||||||
|
placeholder="请输入旧密码"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新密码" prop="newPassword">
|
||||||
|
<el-input
|
||||||
|
v-model="user.newPassword"
|
||||||
|
placeholder="请输入新密码"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确认密码" prop="confirmPassword">
|
||||||
|
<el-input
|
||||||
|
v-model="user.confirmPassword"
|
||||||
|
placeholder="请确认新密码"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="submit">保存</el-button>
|
||||||
|
<el-button type="danger" @click="close">关闭</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { changePwd ,checkPwdStrength } from '@/api/login.js'
|
||||||
|
import { useUserStore } from '@/stores/modules/user.js'
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
// 密码规则
|
||||||
|
function passwords(value) {
|
||||||
|
let validatorA = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$/
|
||||||
|
let validatorB = /^(?=.*\d)(?=.*[a-z])(?=.*[!@#$%^&*()\-_=+{};:,<.>]).{8,}$/
|
||||||
|
let validatorC = /^(?=.*\d)(?=.*[A-Z])(?=.*[!@#$%^&*()\-_=+{};:,<.>]).{8,}$/
|
||||||
|
let validatorD = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()\-_=+{};:,<.>]).{8,}$/
|
||||||
|
if (
|
||||||
|
validatorA.test(value) ||
|
||||||
|
validatorB.test(value) ||
|
||||||
|
validatorC.test(value) ||
|
||||||
|
validatorD.test(value)
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增用户检验密码规则
|
||||||
|
const validatorPasswords = async (rule, value, callback) => {
|
||||||
|
const resPwd = await checkPwdStrength(user.newPassword);
|
||||||
|
if(resPwd.meta.code !== 200) {
|
||||||
|
ElMessage.error(resPwd.meta?.message || '密码强度校验失败');
|
||||||
|
return
|
||||||
|
}else{
|
||||||
|
if(!resPwd.data.is_strong){
|
||||||
|
callback(new Error(resPwd.data.suggestions.join(', ')|| '密码强度校验失败'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = reactive({
|
||||||
|
oldPassword: undefined,
|
||||||
|
newPassword: undefined,
|
||||||
|
confirmPassword: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const equalToPassword = (rule, value, callback) => {
|
||||||
|
if (user.newPassword !== value) {
|
||||||
|
callback(new Error('两次输入的密码不一致'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rules = ref({
|
||||||
|
oldPassword: [{ required: true, message: '旧密码不能为空', trigger: 'blur' }],
|
||||||
|
newPassword: [
|
||||||
|
{ required: true, message: '新密码不能为空', trigger: 'blur' },
|
||||||
|
{ required: true, validator: validatorPasswords, trigger: 'blur' }
|
||||||
|
],
|
||||||
|
confirmPassword: [
|
||||||
|
{ required: true, message: '确认密码不能为空', trigger: 'blur' },
|
||||||
|
{ required: true, validator: equalToPassword, trigger: 'blur' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 提交按钮 */
|
||||||
|
async function submit() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const valid = await proxy.$refs.pwdRef.validate();
|
||||||
|
if (!valid) return;
|
||||||
|
|
||||||
|
const resPwd = await checkPwdStrength(user.newPassword);
|
||||||
|
if(resPwd.meta.code !== 200) {
|
||||||
|
ElMessage.error(resPwd.meta?.message || '密码强度校验失败');
|
||||||
|
return
|
||||||
|
}else{
|
||||||
|
if(!resPwd.data.is_strong){
|
||||||
|
ElMessage.error(resPwd.data.suggestions.join(', ') || '密码强度校验失败');
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const res = await changePwd(user.oldPassword, user.newPassword);
|
||||||
|
if(res.meta.code !== 200){
|
||||||
|
ElMessage.error(res.meta?.message || '密码修改失败');
|
||||||
|
return
|
||||||
|
}else{
|
||||||
|
ElMessage.success('密码修改成功,请重新登录');
|
||||||
|
}
|
||||||
|
await userStore.logOut();
|
||||||
|
location.href = '/';
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error,'error')
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 关闭按钮 */
|
||||||
|
function close() {
|
||||||
|
dialogVisible.value = false
|
||||||
|
proxy.$refs.pwdRef.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
function open(){
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
defineExpose({
|
||||||
|
open
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
238
src/layout/components/Settings/index.vue
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
<template>
|
||||||
|
<el-drawer v-model="showSettings" :withHeader="false" direction="rtl" size="300px">
|
||||||
|
<div class="setting-drawer-title">
|
||||||
|
<h3 class="drawer-title">主题风格设置</h3>
|
||||||
|
</div>
|
||||||
|
<div class="setting-drawer-block-checbox">
|
||||||
|
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-dark')">
|
||||||
|
<img src="@/assets/images/dark.svg" alt="dark" />
|
||||||
|
<div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
|
||||||
|
<i aria-label="图标: check" class="anticon anticon-check">
|
||||||
|
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
|
||||||
|
<path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" />
|
||||||
|
</svg>
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-light')">
|
||||||
|
<img src="@/assets/images/light.svg" alt="light" />
|
||||||
|
<div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
|
||||||
|
<i aria-label="图标: check" class="anticon anticon-check">
|
||||||
|
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
|
||||||
|
<path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" />
|
||||||
|
</svg>
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="drawer-item">
|
||||||
|
<span>主题颜色</span>
|
||||||
|
<span class="comp-style">
|
||||||
|
<el-color-picker v-model="theme" :predefine="predefineColors" @change="themeChange"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<h3 class="drawer-title">系统布局配置</h3>
|
||||||
|
|
||||||
|
<div class="drawer-item">
|
||||||
|
<span>开启 TopNav</span>
|
||||||
|
<span class="comp-style">
|
||||||
|
<el-switch v-model="topNav" class="drawer-switch" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-item">
|
||||||
|
<span>开启 Tags-Views</span>
|
||||||
|
<span class="comp-style">
|
||||||
|
<el-switch v-model="tagsView" class="drawer-switch" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-item">
|
||||||
|
<span>固定 Header</span>
|
||||||
|
<span class="comp-style">
|
||||||
|
<el-switch v-model="fixedHeader" class="drawer-switch" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-item">
|
||||||
|
<span>显示 Logo</span>
|
||||||
|
<span class="comp-style">
|
||||||
|
<el-switch v-model="sidebarLogo" class="drawer-switch" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-item">
|
||||||
|
<span>动态标题</span>
|
||||||
|
<span class="comp-style">
|
||||||
|
<el-switch v-model="dynamicTitle" class="drawer-switch" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button>
|
||||||
|
<el-button plain icon="Refresh" @click="resetSetting">重置配置</el-button>
|
||||||
|
</el-drawer>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import variables from '@/assets/styles/variables.module.scss'
|
||||||
|
// import originElementPlus from 'element-plus/theme-chalk/index.css'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { ElLoading, ElMessage } from 'element-plus'
|
||||||
|
import { useDynamicTitle } from '@/utils/dynamicTitle.js'
|
||||||
|
import { useAppStore } from '@/stores/modules/app.js'
|
||||||
|
import { useSettingsStore } from '@/stores/modules/settings.js'
|
||||||
|
import { handleThemeStyle } from '@/utils/theme'
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance();
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
const showSettings = ref(false);
|
||||||
|
const theme = ref(settingsStore.theme);
|
||||||
|
const sideTheme = ref(settingsStore.sideTheme);
|
||||||
|
const storeSettings = computed(() => settingsStore);
|
||||||
|
const predefineColors = ref(["#409EFF", "#ff4500", "#ff8c00", "#ffd700", "#90ee90", "#00ced1", "#1e90ff", "#c71585"]);
|
||||||
|
|
||||||
|
/** 是否需要topnav */
|
||||||
|
const topNav = computed({
|
||||||
|
get: () => storeSettings.value.topNav,
|
||||||
|
set: (val) => {
|
||||||
|
settingsStore.changeSetting({ key: 'topNav', value: val })
|
||||||
|
if (!val) {
|
||||||
|
appStore.toggleSideBarHide(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
/** 是否需要tagview */
|
||||||
|
const tagsView = computed({
|
||||||
|
get: () => storeSettings.value.tagsView,
|
||||||
|
set: (val) => {
|
||||||
|
settingsStore.changeSetting({ key: 'tagsView', value: val })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
/**是否需要固定头部 */
|
||||||
|
const fixedHeader = computed({
|
||||||
|
get: () => storeSettings.value.fixedHeader,
|
||||||
|
set: (val) => {
|
||||||
|
settingsStore.changeSetting({ key: 'fixedHeader', value: val })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
/**是否需要侧边栏的logo */
|
||||||
|
const sidebarLogo = computed({
|
||||||
|
get: () => storeSettings.value.sidebarLogo,
|
||||||
|
set: (val) => {
|
||||||
|
settingsStore.changeSetting({ key: 'sidebarLogo', value: val })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
/**是否需要侧边栏的动态网页的title */
|
||||||
|
const dynamicTitle = computed({
|
||||||
|
get: () => storeSettings.value.dynamicTitle,
|
||||||
|
set: (val) => {
|
||||||
|
settingsStore.changeSetting({ key: 'dynamicTitle', value: val })
|
||||||
|
// 动态设置网页标题
|
||||||
|
useDynamicTitle()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function themeChange(val) {
|
||||||
|
settingsStore.changeSetting({ key: 'theme', value: val })
|
||||||
|
theme.value = val;
|
||||||
|
handleThemeStyle(val);
|
||||||
|
}
|
||||||
|
function handleTheme(val) {
|
||||||
|
settingsStore.changeSetting({ key: 'sideTheme', value: val })
|
||||||
|
sideTheme.value = val;
|
||||||
|
}
|
||||||
|
function saveSetting() {
|
||||||
|
proxy.$modal.loading("正在保存到本地,请稍候...");
|
||||||
|
let layoutSetting = {
|
||||||
|
"topNav": storeSettings.value.topNav,
|
||||||
|
"tagsView": storeSettings.value.tagsView,
|
||||||
|
"fixedHeader": storeSettings.value.fixedHeader,
|
||||||
|
"sidebarLogo": storeSettings.value.sidebarLogo,
|
||||||
|
"dynamicTitle": storeSettings.value.dynamicTitle,
|
||||||
|
"sideTheme": storeSettings.value.sideTheme,
|
||||||
|
"theme": storeSettings.value.theme
|
||||||
|
};
|
||||||
|
localStorage.setItem("layout-setting", JSON.stringify(layoutSetting));
|
||||||
|
setTimeout(proxy.$modal.closeLoading(), 1000)
|
||||||
|
}
|
||||||
|
function resetSetting() {
|
||||||
|
proxy.$modal.loading("正在清除设置缓存并刷新,请稍候...");
|
||||||
|
localStorage.removeItem("layout-setting")
|
||||||
|
setTimeout("window.location.reload()", 1000)
|
||||||
|
}
|
||||||
|
function openSetting() {
|
||||||
|
showSettings.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
openSetting,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang='scss' scoped>
|
||||||
|
.setting-drawer-title {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
line-height: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
.drawer-title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setting-drawer-block-checbox {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.setting-drawer-block-checbox-item {
|
||||||
|
position: relative;
|
||||||
|
margin-right: 16px;
|
||||||
|
border-radius: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-img {
|
||||||
|
width: 48px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 1px 1px 2px #898484;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-drawer-block-checbox-selectIcon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding-top: 15px;
|
||||||
|
padding-left: 24px;
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-item {
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
padding: 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.comp-style {
|
||||||
|
float: right;
|
||||||
|
margin: -3px 8px 0px 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
40
src/layout/components/Sidebar/Link.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<component :is="type" v-bind="linkProps()">
|
||||||
|
<slot />
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { isExternal } from '@/utils/validate'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
to: {
|
||||||
|
type: [String, Object],
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const isExt = computed(() => {
|
||||||
|
return isExternal(props.to)
|
||||||
|
})
|
||||||
|
|
||||||
|
const type = computed(() => {
|
||||||
|
if (isExt.value) {
|
||||||
|
return 'a'
|
||||||
|
}
|
||||||
|
return 'router-link'
|
||||||
|
})
|
||||||
|
|
||||||
|
function linkProps() {
|
||||||
|
if (isExt.value) {
|
||||||
|
return {
|
||||||
|
href: props.to,
|
||||||
|
target: '_blank',
|
||||||
|
rel: 'noopener'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
to: props.to
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
81
src/layout/components/Sidebar/Logo.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<div class="sidebar-logo-container" :class="{ 'collapse': collapse }" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
|
||||||
|
<transition name="sidebarLogoFade">
|
||||||
|
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
|
||||||
|
<img v-if="logo" :src="logo" class="sidebar-logo" />
|
||||||
|
<h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }}</h1>
|
||||||
|
</router-link>
|
||||||
|
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
|
||||||
|
<img v-if="logo" :src="logo" class="sidebar-logo" />
|
||||||
|
<h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }}</h1>
|
||||||
|
</router-link>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import variables from '@/assets/styles/variables.module.scss'
|
||||||
|
import logo from '@/assets/logo/logo.png'
|
||||||
|
import useSettingsStore from '@/store/modules/settings'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
collapse: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const title = import.meta.env.VITE_APP_TITLE;
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
const sideTheme = computed(() => settingsStore.sideTheme);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sidebarLogoFade-enter-active {
|
||||||
|
transition: opacity 1.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarLogoFade-enter,
|
||||||
|
.sidebarLogoFade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-logo-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
background: #2b2f3a;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
& .sidebar-logo-link {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
& .sidebar-logo {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .sidebar-title {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 50px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.collapse {
|
||||||
|
.sidebar-logo {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
116
src/layout/components/Sidebar/SidebarItem.vue
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="!item.hidden">
|
||||||
|
<template
|
||||||
|
v-if="
|
||||||
|
hasOneShowingChild(item.children, item) &&
|
||||||
|
(!onlyOneChild.children || onlyOneChild.noShowingChildren) &&
|
||||||
|
!item.alwaysShow
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
|
||||||
|
<el-menu-item
|
||||||
|
:index="resolvePath(onlyOneChild.path)"
|
||||||
|
:class="{ 'submenu-title-noDropdown': !isNest }"
|
||||||
|
>
|
||||||
|
<!-- <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" /> -->
|
||||||
|
<!-- {{ onlyOneChild.meta.title }} -->
|
||||||
|
<!-- <template #title> -->
|
||||||
|
<span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">
|
||||||
|
{{ onlyOneChild.meta.title }}
|
||||||
|
</span>
|
||||||
|
<!-- </template> -->
|
||||||
|
</el-menu-item>
|
||||||
|
</app-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
|
||||||
|
<template v-if="item.meta" #title>
|
||||||
|
<!-- <svg-icon :icon-class="item.meta && item.meta.icon" /> -->
|
||||||
|
<span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<sidebar-item
|
||||||
|
v-for="child in item.children"
|
||||||
|
:key="child.path"
|
||||||
|
:is-nest="true"
|
||||||
|
:item="child"
|
||||||
|
:base-path="resolvePath(child.path)"
|
||||||
|
class="nest-menu"
|
||||||
|
/>
|
||||||
|
</el-sub-menu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { isExternal } from '@/utils/validate.js'
|
||||||
|
import AppLink from './Link.vue'
|
||||||
|
import { getNormalPath } from '@/utils/ruoyi'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// route object
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isNest: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
basePath: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const onlyOneChild = ref({})
|
||||||
|
|
||||||
|
function hasOneShowingChild(children = [], parent) {
|
||||||
|
if (!children) {
|
||||||
|
children = []
|
||||||
|
}
|
||||||
|
const showingChildren = children.filter((item) => {
|
||||||
|
if (item.hidden) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
// Temp set(will be used if only has one showing child)
|
||||||
|
onlyOneChild.value = item
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// When there is only one child router, the child router is displayed by default
|
||||||
|
if (showingChildren.length === 1) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show parent if there are no child router to display
|
||||||
|
if (showingChildren.length === 0) {
|
||||||
|
onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePath(routePath, routeQuery) {
|
||||||
|
if (isExternal(routePath)) {
|
||||||
|
return routePath
|
||||||
|
}
|
||||||
|
if (isExternal(props.basePath)) {
|
||||||
|
return props.basePath
|
||||||
|
}
|
||||||
|
if (routeQuery) {
|
||||||
|
let query = JSON.parse(routeQuery)
|
||||||
|
return { path: getNormalPath(props.basePath + '/' + routePath), query: query }
|
||||||
|
}
|
||||||
|
return getNormalPath(props.basePath + '/' + routePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasTitle(title) {
|
||||||
|
if (title.length > 5) {
|
||||||
|
return title
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
60
src/layout/components/Sidebar/index.vue
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="{ 'has-logo': showLogo }"
|
||||||
|
:style="{
|
||||||
|
backgroundColor:
|
||||||
|
sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- <logo v-if="showLogo" :collapse="isCollapse" /> -->
|
||||||
|
<el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper">
|
||||||
|
<el-menu
|
||||||
|
:default-active="activeMenu"
|
||||||
|
:collapse="false"
|
||||||
|
:background-color="
|
||||||
|
sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground
|
||||||
|
"
|
||||||
|
:text-color="sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor"
|
||||||
|
:unique-opened="true"
|
||||||
|
:active-text-color="theme"
|
||||||
|
:collapse-transition="false"
|
||||||
|
mode="vertical"
|
||||||
|
>
|
||||||
|
<sidebar-item
|
||||||
|
v-for="(route, index) in constantRoutes"
|
||||||
|
:key="route.path + index"
|
||||||
|
:item="route"
|
||||||
|
:base-path="route.path"
|
||||||
|
/>
|
||||||
|
</el-menu>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import SidebarItem from './SidebarItem.vue'
|
||||||
|
import variables from '@/assets/styles/variables.module.scss'
|
||||||
|
import { useAppStore } from '@/stores/modules/app.js'
|
||||||
|
import { useSettingsStore } from '@/stores/modules/settings.js'
|
||||||
|
import router, { constantRoutes, dynamicRoutes } from '@/router'
|
||||||
|
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
|
||||||
|
|
||||||
|
const showLogo = computed(() => settingsStore.sidebarLogo)
|
||||||
|
const sideTheme = computed(() => settingsStore.sideTheme)
|
||||||
|
const theme = computed(() => settingsStore.theme)
|
||||||
|
const isCollapse = computed(() => !appStore.sidebar.opened)
|
||||||
|
|
||||||
|
const activeMenu = computed(() => {
|
||||||
|
const { meta, path } = route
|
||||||
|
// if set path, the sidebar will highlight the path you set
|
||||||
|
if (meta.activeMenu) {
|
||||||
|
return meta.activeMenu
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
})
|
||||||
|
</script>
|
||||||
105
src/layout/components/TagsView/ScrollPane.vue
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<el-scrollbar
|
||||||
|
ref="scrollContainer"
|
||||||
|
:vertical="false"
|
||||||
|
class="scroll-container"
|
||||||
|
@wheel.prevent="handleScroll"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</el-scrollbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import useTagsViewStore from '@/stores/modules/tagsView.js'
|
||||||
|
|
||||||
|
const tagAndTagSpacing = ref(4);
|
||||||
|
const { proxy } = getCurrentInstance();
|
||||||
|
|
||||||
|
const scrollWrapper = computed(() => proxy.$refs.scrollContainer.$refs.wrapRef);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
scrollWrapper.value.addEventListener('scroll', emitScroll, true)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
scrollWrapper.value.removeEventListener('scroll', emitScroll)
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleScroll(e) {
|
||||||
|
const eventDelta = e.wheelDelta || -e.deltaY * 40
|
||||||
|
const $scrollWrapper = scrollWrapper.value;
|
||||||
|
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
|
||||||
|
}
|
||||||
|
const emits = defineEmits()
|
||||||
|
const emitScroll = () => {
|
||||||
|
emits('scroll')
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagsViewStore = useTagsViewStore()
|
||||||
|
const visitedViews = computed(() => tagsViewStore.visitedViews);
|
||||||
|
|
||||||
|
function moveToTarget(currentTag) {
|
||||||
|
const $container = proxy.$refs.scrollContainer.$el
|
||||||
|
const $containerWidth = $container.offsetWidth
|
||||||
|
const $scrollWrapper = scrollWrapper.value;
|
||||||
|
|
||||||
|
let firstTag = null
|
||||||
|
let lastTag = null
|
||||||
|
|
||||||
|
// find first tag and last tag
|
||||||
|
if (visitedViews.value.length > 0) {
|
||||||
|
firstTag = visitedViews.value[0]
|
||||||
|
lastTag = visitedViews.value[visitedViews.value.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstTag === currentTag) {
|
||||||
|
$scrollWrapper.scrollLeft = 0
|
||||||
|
} else if (lastTag === currentTag) {
|
||||||
|
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
|
||||||
|
} else {
|
||||||
|
const tagListDom = document.getElementsByClassName('tags-view-item');
|
||||||
|
const currentIndex = visitedViews.value.findIndex(item => item === currentTag)
|
||||||
|
let prevTag = null
|
||||||
|
let nextTag = null
|
||||||
|
for (const k in tagListDom) {
|
||||||
|
if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) {
|
||||||
|
if (tagListDom[k].dataset.path === visitedViews.value[currentIndex - 1].path) {
|
||||||
|
prevTag = tagListDom[k];
|
||||||
|
}
|
||||||
|
if (tagListDom[k].dataset.path === visitedViews.value[currentIndex + 1].path) {
|
||||||
|
nextTag = tagListDom[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the tag's offsetLeft after of nextTag
|
||||||
|
const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + tagAndTagSpacing.value
|
||||||
|
|
||||||
|
// the tag's offsetLeft before of prevTag
|
||||||
|
const beforePrevTagOffsetLeft = prevTag.offsetLeft - tagAndTagSpacing.value
|
||||||
|
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
|
||||||
|
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
|
||||||
|
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
|
||||||
|
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
moveToTarget,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang='scss' scoped>
|
||||||
|
.scroll-container {
|
||||||
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
:deep(.el-scrollbar__bar) {
|
||||||
|
bottom: 0px;
|
||||||
|
}
|
||||||
|
:deep(.el-scrollbar__wrap) {
|
||||||
|
height: 39px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
336
src/layout/components/TagsView/index.vue
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
<template>
|
||||||
|
<div id="tags-view-container" class="tags-view-container">
|
||||||
|
<scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll">
|
||||||
|
<router-link
|
||||||
|
v-for="tag in visitedViews"
|
||||||
|
:key="tag.path"
|
||||||
|
:data-path="tag.path"
|
||||||
|
:class="isActive(tag) ? 'active' : ''"
|
||||||
|
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
|
||||||
|
class="tags-view-item"
|
||||||
|
:style="activeStyle(tag)"
|
||||||
|
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
|
||||||
|
@contextmenu.prevent="openMenu(tag, $event)"
|
||||||
|
>
|
||||||
|
{{ tag.title }}
|
||||||
|
<span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
|
||||||
|
<close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;" />
|
||||||
|
</span>
|
||||||
|
</router-link>
|
||||||
|
</scroll-pane>
|
||||||
|
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
|
||||||
|
<li @click="refreshSelectedTag(selectedTag)">
|
||||||
|
<refresh-right style="width: 1em; height: 1em;" /> 刷新页面
|
||||||
|
</li>
|
||||||
|
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
|
||||||
|
<close style="width: 1em; height: 1em;" /> 关闭当前
|
||||||
|
</li>
|
||||||
|
<li @click="closeOthersTags">
|
||||||
|
<circle-close style="width: 1em; height: 1em;" /> 关闭其他
|
||||||
|
</li>
|
||||||
|
<li v-if="!isFirstView()" @click="closeLeftTags">
|
||||||
|
<back style="width: 1em; height: 1em;" /> 关闭左侧
|
||||||
|
</li>
|
||||||
|
<li v-if="!isLastView()" @click="closeRightTags">
|
||||||
|
<right style="width: 1em; height: 1em;" /> 关闭右侧
|
||||||
|
</li>
|
||||||
|
<li @click="closeAllTags(selectedTag)">
|
||||||
|
<circle-close style="width: 1em; height: 1em;" /> 全部关闭
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import ScrollPane from './ScrollPane.vue'
|
||||||
|
import { getNormalPath } from '@/utils/ruoyi'
|
||||||
|
import useTagsViewStore from '@/stores/modules/tagsView.js'
|
||||||
|
import { useSettingsStore } from '@/stores/modules/settings.js'
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const top = ref(0);
|
||||||
|
const left = ref(0);
|
||||||
|
const selectedTag = ref({});
|
||||||
|
const affixTags = ref([]);
|
||||||
|
const scrollPaneRef = ref(null);
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance();
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const visitedViews = computed(() => useTagsViewStore().visitedViews);
|
||||||
|
const theme = computed(() => useSettingsStore().theme);
|
||||||
|
|
||||||
|
watch(route, () => {
|
||||||
|
addTags()
|
||||||
|
moveToCurrentTag()
|
||||||
|
})
|
||||||
|
watch(visible, (value) => {
|
||||||
|
if (value) {
|
||||||
|
document.body.addEventListener('click', closeMenu)
|
||||||
|
} else {
|
||||||
|
document.body.removeEventListener('click', closeMenu)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onMounted(() => {
|
||||||
|
initTags()
|
||||||
|
addTags()
|
||||||
|
})
|
||||||
|
|
||||||
|
function isActive(r) {
|
||||||
|
return r.path === route.path
|
||||||
|
}
|
||||||
|
function activeStyle(tag) {
|
||||||
|
if (!isActive(tag)) return {};
|
||||||
|
return {
|
||||||
|
"background-color": theme.value,
|
||||||
|
"border-color": theme.value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function isAffix(tag) {
|
||||||
|
return tag.meta && tag.meta.affix
|
||||||
|
}
|
||||||
|
function isFirstView() {
|
||||||
|
try {
|
||||||
|
return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath
|
||||||
|
} catch (err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function isLastView() {
|
||||||
|
try {
|
||||||
|
return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath
|
||||||
|
} catch (err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function filterAffixTags(routes, basePath = '') {
|
||||||
|
let tags = []
|
||||||
|
routes.forEach(route => {
|
||||||
|
if (route.meta && route.meta.affix) {
|
||||||
|
const tagPath = getNormalPath(basePath + '/' + route.path)
|
||||||
|
tags.push({
|
||||||
|
fullPath: tagPath,
|
||||||
|
path: tagPath,
|
||||||
|
name: route.name,
|
||||||
|
meta: { ...route.meta }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (route.children) {
|
||||||
|
const tempTags = filterAffixTags(route.children, route.path)
|
||||||
|
if (tempTags.length >= 1) {
|
||||||
|
tags = [...tags, ...tempTags]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
function initTags() {
|
||||||
|
const res = filterAffixTags(routes.value);
|
||||||
|
affixTags.value = res;
|
||||||
|
for (const tag of res) {
|
||||||
|
// Must have tag name
|
||||||
|
if (tag.name) {
|
||||||
|
useTagsViewStore().addVisitedView(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function addTags() {
|
||||||
|
const { name } = route
|
||||||
|
if (name) {
|
||||||
|
useTagsViewStore().addView(route)
|
||||||
|
if (route.meta.link) {
|
||||||
|
useTagsViewStore().addIframeView(route);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
function moveToCurrentTag() {
|
||||||
|
nextTick(() => {
|
||||||
|
for (const r of visitedViews.value) {
|
||||||
|
if (r.path === route.path) {
|
||||||
|
scrollPaneRef.value.moveToTarget(r);
|
||||||
|
// when query is different then update
|
||||||
|
if (r.fullPath !== route.fullPath) {
|
||||||
|
useTagsViewStore().updateVisitedView(route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function refreshSelectedTag(view) {
|
||||||
|
proxy.$tab.refreshPage(view);
|
||||||
|
if (route.meta.link) {
|
||||||
|
useTagsViewStore().delIframeView(route);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function closeSelectedTag(view) {
|
||||||
|
proxy.$tab.closePage(view).then(({ visitedViews }) => {
|
||||||
|
if (isActive(view)) {
|
||||||
|
toLastView(visitedViews, view)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function closeRightTags() {
|
||||||
|
proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => {
|
||||||
|
if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
|
||||||
|
toLastView(visitedViews)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function closeLeftTags() {
|
||||||
|
proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => {
|
||||||
|
if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
|
||||||
|
toLastView(visitedViews)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function closeOthersTags() {
|
||||||
|
router.push(selectedTag.value).catch(() => { });
|
||||||
|
proxy.$tab.closeOtherPage(selectedTag.value).then(() => {
|
||||||
|
moveToCurrentTag()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function closeAllTags(view) {
|
||||||
|
proxy.$tab.closeAllPage().then(({ visitedViews }) => {
|
||||||
|
if (affixTags.value.some(tag => tag.path === route.path)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toLastView(visitedViews, view)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function toLastView(visitedViews, view) {
|
||||||
|
const latestView = visitedViews.slice(-1)[0]
|
||||||
|
if (latestView) {
|
||||||
|
router.push(latestView.fullPath)
|
||||||
|
} else {
|
||||||
|
// now the default is to redirect to the home page if there is no tags-view,
|
||||||
|
// you can adjust it according to your needs.
|
||||||
|
if (view.name === 'Dashboard') {
|
||||||
|
// to reload home page
|
||||||
|
router.replace({ path: '/redirect' + view.fullPath })
|
||||||
|
} else {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function openMenu(tag, e) {
|
||||||
|
const menuMinWidth = 105
|
||||||
|
const offsetLeft = proxy.$el.getBoundingClientRect().left // container margin left
|
||||||
|
const offsetWidth = proxy.$el.offsetWidth // container width
|
||||||
|
const maxLeft = offsetWidth - menuMinWidth // left boundary
|
||||||
|
const l = e.clientX - offsetLeft + 15 // 15: margin right
|
||||||
|
|
||||||
|
if (l > maxLeft) {
|
||||||
|
left.value = maxLeft
|
||||||
|
} else {
|
||||||
|
left.value = l
|
||||||
|
}
|
||||||
|
|
||||||
|
top.value = e.clientY
|
||||||
|
visible.value = true
|
||||||
|
selectedTag.value = tag
|
||||||
|
}
|
||||||
|
function closeMenu() {
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
function handleScroll() {
|
||||||
|
closeMenu()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang='scss' scoped>
|
||||||
|
.tags-view-container {
|
||||||
|
height: 34px;
|
||||||
|
width: 100%;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #d8dce5;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
|
||||||
|
.tags-view-wrapper {
|
||||||
|
.tags-view-item {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 26px;
|
||||||
|
line-height: 26px;
|
||||||
|
border: 1px solid #d8dce5;
|
||||||
|
color: #495060;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-top: 4px;
|
||||||
|
&:first-of-type {
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
&:last-of-type {
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
background-color: #42b983;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #42b983;
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
background: #fff;
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contextmenu {
|
||||||
|
margin: 0;
|
||||||
|
background: #fff;
|
||||||
|
z-index: 3000;
|
||||||
|
position: absolute;
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 5px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
|
||||||
|
li {
|
||||||
|
margin: 0;
|
||||||
|
padding: 7px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
//reset element css of el-icon-close
|
||||||
|
.tags-view-wrapper {
|
||||||
|
.tags-view-item {
|
||||||
|
.el-icon-close {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
vertical-align: 2px;
|
||||||
|
border-radius: 50%;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
transform-origin: 100% 50%;
|
||||||
|
&:before {
|
||||||
|
transform: scale(0.6);
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: -3px;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: #b4bccc;
|
||||||
|
color: #fff;
|
||||||
|
width: 12px !important;
|
||||||
|
height: 12px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
6
src/layout/components/index.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export { default as AppMain } from './AppMain.vue'
|
||||||
|
export { default as Navbar } from './Navbar.vue'
|
||||||
|
export { default as Settings } from './Settings/index.vue'
|
||||||
|
export { default as TagsView } from './TagsView/index.vue'
|
||||||
|
export { default as ResetPwd } from './ResetPwd/index.vue'
|
||||||
|
export { default as InviteJoin } from './InviteJoin/index.vue'
|
||||||
326
src/layout/index.vue
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
<template>
|
||||||
|
<div class="wrapper-content">
|
||||||
|
<div class="content-nav">
|
||||||
|
<div class="nav-left">
|
||||||
|
<div>xSynergy远程协作系统</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-right">
|
||||||
|
<el-dropdown trigger="click" @command="handleCommand">
|
||||||
|
<div class="avatar-wrapper">
|
||||||
|
<span class="username">{{ nickName }}</span>
|
||||||
|
<!-- <el-icon><caret-bottom /></el-icon> -->
|
||||||
|
</div>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="changePassword">
|
||||||
|
<span>修改密码</span>
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="logout" divided>
|
||||||
|
<span>退出登录</span>
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
|
||||||
|
<div
|
||||||
|
v-if="device === 'mobile' && sidebar.opened"
|
||||||
|
class="drawer-bg"
|
||||||
|
@click="handleClickOutside"
|
||||||
|
/>
|
||||||
|
<div class="main-container" :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }">
|
||||||
|
<sidebar v-if="!sidebar.hide" class="sidebar-container" />
|
||||||
|
<div class="sidebar-right">
|
||||||
|
<app-main />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ResetPwd ref="resetPwdRef"/>
|
||||||
|
<InviteJoin ref="inviteJoinRef"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ElMessageBox ,ElMessage} from 'element-plus'
|
||||||
|
import { useWindowSize } from '@vueuse/core'
|
||||||
|
import Sidebar from './components/Sidebar/index.vue'
|
||||||
|
import { AppMain, TagsView ,ResetPwd,InviteJoin} from './components/index.js'
|
||||||
|
import { useAppStore } from '@/stores/modules/app.js'
|
||||||
|
import { useSettingsStore } from '@/stores/modules/settings.js'
|
||||||
|
import { useUserStore } from '@/stores/modules/user.js'
|
||||||
|
import { removeToken } from '@/utils/auth.js'
|
||||||
|
import { onMounted ,ref} from 'vue'
|
||||||
|
import { mqttClient } from "@/utils/mqtt.js";
|
||||||
|
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const useAppStoreStore = useAppStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const inviteJoinRef = ref(null)
|
||||||
|
|
||||||
|
const theme = computed(() => settingsStore.theme)
|
||||||
|
const sidebar = computed(() => useAppStoreStore.sidebar)
|
||||||
|
const device = computed(() => useAppStoreStore.device)
|
||||||
|
const needTagsView = computed(() => settingsStore.tagsView)
|
||||||
|
const nickName = computed(() => {
|
||||||
|
// 优先从 userStore 获取
|
||||||
|
if (userStore.name) {
|
||||||
|
return userStore.name
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userData = sessionStorage.getItem('userData')
|
||||||
|
if (userData) {
|
||||||
|
const parsedData = JSON.parse(userData)
|
||||||
|
return parsedData.name || ''
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析 userData 失败:', error)
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const classObj = computed(() => ({
|
||||||
|
hideSidebar: !sidebar.value.opened,
|
||||||
|
openSidebar: sidebar.value.opened,
|
||||||
|
withoutAnimation: sidebar.value.withoutAnimation,
|
||||||
|
mobile: device.value === 'mobile',
|
||||||
|
}))
|
||||||
|
|
||||||
|
const resetPwdRef = ref(null)
|
||||||
|
|
||||||
|
const { width, height } = useWindowSize()
|
||||||
|
const WIDTH = 992
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (device.value === 'mobile' && sidebar.value.opened) {
|
||||||
|
useAppStoreStore.closeSideBar({ withoutAnimation: false })
|
||||||
|
}
|
||||||
|
if (width.value - 1 < WIDTH) {
|
||||||
|
useAppStoreStore.toggleDevice('mobile')
|
||||||
|
useAppStoreStore.closeSideBar({ withoutAnimation: true })
|
||||||
|
} else {
|
||||||
|
useAppStoreStore.toggleDevice('desktop')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
function handleCommand(command) {
|
||||||
|
switch (command) {
|
||||||
|
case 'changePassword':
|
||||||
|
resetPwdRef.value.open()
|
||||||
|
break
|
||||||
|
case 'logout':
|
||||||
|
logout()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function logout() {
|
||||||
|
ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
userStore.logOut().then(() => {
|
||||||
|
location.href = '/'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
ElMessage({
|
||||||
|
type: 'info',
|
||||||
|
message: '取消注销'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClickOutside() {
|
||||||
|
useAppStoreStore.closeSideBar({ withoutAnimation: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await mqttClient.connect(`room${Math.random().toString(16).substr(2, 8)}`);
|
||||||
|
const res = await userStore.getInfo()
|
||||||
|
const topic = `xSynergy/ROOM/+/rooms/${res.uid}`;
|
||||||
|
mqttClient.subscribe(topic, async (shapeData) => {
|
||||||
|
if(inviteJoinRef.value){
|
||||||
|
inviteJoinRef.value.processingSocket(shapeData.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '@/assets/styles/mixin.scss';
|
||||||
|
@import '@/assets/styles/variables.module.scss';
|
||||||
|
|
||||||
|
.tags-view {
|
||||||
|
opacity: 0;
|
||||||
|
top: 0;
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
.main-container {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
height: calc(100vh - 50px);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
// border-radius: 30px;
|
||||||
|
box-shadow: 0 0 10px 4px #ece9e3;
|
||||||
|
|
||||||
|
.sidebar-container {
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.15) 1.95px 1.95px 2.6px;
|
||||||
|
width: 200px;
|
||||||
|
// box-shadow: 0 0 10px 4px #f6f6f6;
|
||||||
|
}
|
||||||
|
.sidebar-right {
|
||||||
|
width: calc(100% - 200px);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
// padding: 0 20px;
|
||||||
|
.right-bar {
|
||||||
|
width: 100%;
|
||||||
|
background: #f6f6f6;
|
||||||
|
overflow-x: hidden;
|
||||||
|
// border-radius: 0 30px 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* 滚动条整体 */
|
||||||
|
.sidebar-right::-webkit-scrollbar {
|
||||||
|
height: 0px;
|
||||||
|
width: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-wrapper {
|
||||||
|
@include clearfix;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: #f6f6f6;
|
||||||
|
// padding: 20px;
|
||||||
|
&.mobile.openSidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// @media screen and (max-width: 995px) {
|
||||||
|
// .wrapper-content {
|
||||||
|
// .content-nav {
|
||||||
|
// z-index: 0 !important;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
.wrapper-content {
|
||||||
|
.content-nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1111;
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
-webkit-box-shadow: 2px 0 9px rgba(0, 21, 41, 0.35);
|
||||||
|
box-shadow: 2px 0 9px rgba(0, 21, 41, 0.35);
|
||||||
|
padding: 3px 20px;
|
||||||
|
// background-color: #8290f0;
|
||||||
|
background-color: #434343;
|
||||||
|
.nav-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
div {
|
||||||
|
font-size: 19px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.nav-right {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
.screenfull {
|
||||||
|
font-size: 22px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #ffe565;
|
||||||
|
}
|
||||||
|
.avatar-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 30px;
|
||||||
|
.user-avatar {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
padding: 0 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickName {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
|
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 6px;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #ffe565;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.drawer-bg {
|
||||||
|
background: #000;
|
||||||
|
opacity: 0.3;
|
||||||
|
width: 100%;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixed-header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 9;
|
||||||
|
width: calc(100% - #{$base-sidebar-width});
|
||||||
|
transition: width 0.28s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hideSidebar .fixed-header {
|
||||||
|
width: calc(100% - 54px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarHide .fixed-header {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile .fixed-header {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
29
src/main.js
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
|
import * as Sentry from '@sentry/vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
deepClone
|
deepClone
|
||||||
@@ -17,14 +17,39 @@ import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
app,
|
||||||
|
dsn: 'https://34a0d76174a64db09d31d13a8042560b@sentry.cnsdt.com/2', // 替换为你的 DSN
|
||||||
|
integrations: [
|
||||||
|
// 浏览器性能追踪集成
|
||||||
|
Sentry.browserTracingIntegration({
|
||||||
|
router,
|
||||||
|
}),
|
||||||
|
// 会话回放集成
|
||||||
|
Sentry.replayIntegration({
|
||||||
|
maskAllText: false,
|
||||||
|
blockAllMedia: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
// 性能监控采样率
|
||||||
|
tracesSampleRate: 1.0, // 生产环境建议设置为 0.1-0.2
|
||||||
|
// 会话回放采样率
|
||||||
|
replaysSessionSampleRate: 0.1,
|
||||||
|
replaysOnErrorSampleRate: 1.0,
|
||||||
|
// 环境配置
|
||||||
|
environment: import.meta.env.MODE,
|
||||||
|
// 开发环境下可禁用 Sentry
|
||||||
|
enabled: import.meta.env.PROD,
|
||||||
|
});
|
||||||
|
|
||||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
app.component(key, component)
|
app.component(key, component)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(createPinia())
|
app.use(pinia)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(ElementPlus, {
|
app.use(ElementPlus, {
|
||||||
locale: zhCn,
|
locale: zhCn,
|
||||||
|
|||||||
60
src/plugins/auth.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { useUserStore } from '@/stores/modules/user.js'
|
||||||
|
|
||||||
|
function authPermission(permission) {
|
||||||
|
const all_permission = "*:*:*";
|
||||||
|
const permissions = useUserStore().permissions
|
||||||
|
if (permission && permission.length > 0) {
|
||||||
|
return permissions.some(v => {
|
||||||
|
return all_permission === v || v === permission
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function authRole(role) {
|
||||||
|
const super_admin = "admin";
|
||||||
|
const roles = useUserStore().roles
|
||||||
|
if (role && role.length > 0) {
|
||||||
|
return roles.some(v => {
|
||||||
|
return super_admin === v || v === role
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// 验证用户是否具备某权限
|
||||||
|
hasPermi(permission) {
|
||||||
|
return authPermission(permission);
|
||||||
|
},
|
||||||
|
// 验证用户是否含有指定权限,只需包含其中一个
|
||||||
|
hasPermiOr(permissions) {
|
||||||
|
return permissions.some(item => {
|
||||||
|
return authPermission(item)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 验证用户是否含有指定权限,必须全部拥有
|
||||||
|
hasPermiAnd(permissions) {
|
||||||
|
return permissions.every(item => {
|
||||||
|
return authPermission(item)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 验证用户是否具备某角色
|
||||||
|
hasRole(role) {
|
||||||
|
return authRole(role);
|
||||||
|
},
|
||||||
|
// 验证用户是否含有指定角色,只需包含其中一个
|
||||||
|
hasRoleOr(roles) {
|
||||||
|
return roles.some(item => {
|
||||||
|
return authRole(item)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 验证用户是否含有指定角色,必须全部拥有
|
||||||
|
hasRoleAnd(roles) {
|
||||||
|
return roles.every(item => {
|
||||||
|
return authRole(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,57 +1,151 @@
|
|||||||
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
||||||
|
import Layout from "@/layout/index.vue";
|
||||||
|
|
||||||
|
export const constantRoutes = [
|
||||||
const router = createRouter({
|
{
|
||||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
path: '/redirect',
|
||||||
routes: [
|
component: Layout,
|
||||||
// {
|
hidden: true,
|
||||||
// path: '/',
|
children: [
|
||||||
// component: () => import("@/views/custom/tabulaRase/index.vue"),
|
{
|
||||||
// },
|
path: '/redirect/:path(.*)',
|
||||||
|
component: () => import('@/views/redirect/index.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
redirect: '/login', // 这里做重定向
|
redirect: '/login', // 这里做重定向
|
||||||
},
|
hidden: true,
|
||||||
{
|
|
||||||
path: '/whiteboard',
|
|
||||||
component: () => import('@/views/custom/tabulaRase/index.vue'),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/login",
|
path: "/login",
|
||||||
component: () => import("@/views/login.vue"),
|
component: () => import("@/views/login.vue"),
|
||||||
|
meta: { title: "登录" },
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/whiteboard',
|
||||||
|
component: () => import('@/views/custom/tabulaRase/index.vue'),
|
||||||
|
meta: { title: "白板" },
|
||||||
|
hidden: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/coordinate",
|
path: "/coordinate",
|
||||||
|
redirect: "/coordinate/CoordinatePage",
|
||||||
|
component: Layout,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: 'CoordinatePage',
|
||||||
name: "Coordinate",
|
name: "Coordinate",
|
||||||
component: () => import("@/views/coordinate/personnelList/index.vue")
|
component: () => import("@/views/coordinate/personnelList/index.vue"),
|
||||||
|
meta: { title: "远程协作", icon: "client", affix: true },
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/conferencingRoom",
|
path: "/conferencingRoom",
|
||||||
|
// component: Layout,
|
||||||
|
hidden: true,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
name: "ConferencingRoom",
|
name: "ConferencingRoom",
|
||||||
component: () => import("@/views/conferencingRoom/index.vue")
|
component: () => import("@/views/conferencingRoom/index.vue"),
|
||||||
|
meta: { title: "会议房间", icon: "client", affix: true },
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
// 错误页面路由
|
// 错误页面路由
|
||||||
{
|
{
|
||||||
path: "/:pathMatch(.*)*",
|
path: "/:pathMatch(.*)*",
|
||||||
component: () => import("@/views/error/404.vue"),
|
component: () => import("@/views/error/404.vue"),
|
||||||
|
hidden: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/401",
|
path: "/401",
|
||||||
component: () => import("@/views/error/401.vue"),
|
component: () => import("@/views/error/401.vue"),
|
||||||
|
hidden: true,
|
||||||
|
meta: { title: "401未授权" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/assistWx',
|
||||||
|
component: () => import('@/views/coordinate/personnelList/components/assistWx/index.vue'),
|
||||||
|
meta: { title: "白板" },
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const dynamicRoutes = [
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
// const router = createRouter({
|
||||||
|
// history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||||
|
// routes: [
|
||||||
|
// // {
|
||||||
|
// // path: '/',
|
||||||
|
// // component: () => import("@/views/custom/tabulaRase/index.vue"),
|
||||||
|
// // },
|
||||||
|
// {
|
||||||
|
// path: '/',
|
||||||
|
// redirect: '/login', // 这里做重定向
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// path: '/whiteboard',
|
||||||
|
// component: () => import('@/views/custom/tabulaRase/index.vue'),
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// path: "/login",
|
||||||
|
// component: () => import("@/views/login.vue"),
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// path: "/coordinate",
|
||||||
|
// component: Layout,
|
||||||
|
// meta: { title: "远程协作", icon: "client", affix: true },
|
||||||
|
// children: [
|
||||||
|
// {
|
||||||
|
// path: '',
|
||||||
|
// name: "Coordinate",
|
||||||
|
// component: () => import("@/views/coordinate/personnelList/index.vue")
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// path: "/conferencingRoom",
|
||||||
|
// children: [
|
||||||
|
// {
|
||||||
|
// path: '',
|
||||||
|
// name: "ConferencingRoom",
|
||||||
|
// component: () => import("@/views/conferencingRoom/index.vue")
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
|
||||||
|
|
||||||
|
// // 错误页面路由
|
||||||
|
// {
|
||||||
|
// path: "/:pathMatch(.*)*",
|
||||||
|
// component: () => import("@/views/error/404.vue"),
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// path: "/401",
|
||||||
|
// component: () => import("@/views/error/401.vue"),
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// })
|
||||||
|
const router = createRouter({
|
||||||
|
// history: createWebHistory(import.meta.env.VITE_BASE_PATH),
|
||||||
|
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: constantRoutes,
|
||||||
|
scrollBehavior(to, from, savedPosition) {
|
||||||
|
if (savedPosition) {
|
||||||
|
return savedPosition;
|
||||||
|
} else {
|
||||||
|
return { top: 0 };
|
||||||
}
|
}
|
||||||
],
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
47
src/settings.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* 网页标题
|
||||||
|
*/
|
||||||
|
title: import.meta.env.VITE_APP_TITLE,
|
||||||
|
/**
|
||||||
|
* 侧边栏主题 深色主题theme-dark,浅色主题theme-light
|
||||||
|
*/
|
||||||
|
sideTheme: 'theme-dark',
|
||||||
|
/**
|
||||||
|
* 是否系统布局配置
|
||||||
|
*/
|
||||||
|
showSettings: true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否显示顶部导航
|
||||||
|
*/
|
||||||
|
topNav: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否显示 tagsView
|
||||||
|
*/
|
||||||
|
tagsView: true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否固定头部
|
||||||
|
*/
|
||||||
|
fixedHeader: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否显示logo
|
||||||
|
*/
|
||||||
|
sidebarLogo: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否显示动态标题
|
||||||
|
*/
|
||||||
|
dynamicTitle: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string | array} 'production' | ['production', 'development']
|
||||||
|
* @description Need show err logs component.
|
||||||
|
* The default is only used in the production env
|
||||||
|
* If you want to also use it in dev, you can pass ['production', 'development']
|
||||||
|
*/
|
||||||
|
errorLog: 'production'
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { ref, computed } from 'vue'
|
|
||||||
import { defineStore } from 'pinia'
|
|
||||||
|
|
||||||
export const useCounterStore = defineStore('counter', () => {
|
|
||||||
const count = ref(0)
|
|
||||||
const doubleCount = computed(() => count.value * 2)
|
|
||||||
function increment() {
|
|
||||||
count.value++
|
|
||||||
}
|
|
||||||
|
|
||||||
return { count, doubleCount, increment }
|
|
||||||
})
|
|
||||||
45
src/stores/modules/app.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import Cookies from 'js-cookie'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useAppStore = defineStore(
|
||||||
|
'app',
|
||||||
|
{
|
||||||
|
state: () => ({
|
||||||
|
sidebar: {
|
||||||
|
opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
|
||||||
|
withoutAnimation: false,
|
||||||
|
hide: false
|
||||||
|
},
|
||||||
|
device: 'desktop',
|
||||||
|
size: Cookies.get('size') || 'default'
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
toggleSideBar(withoutAnimation) {
|
||||||
|
if (this.sidebar.hide) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.sidebar.opened = !this.sidebar.opened
|
||||||
|
this.sidebar.withoutAnimation = withoutAnimation
|
||||||
|
if (this.sidebar.opened) {
|
||||||
|
// Cookies.set('sidebarStatus', 1)
|
||||||
|
} else {
|
||||||
|
// Cookies.set('sidebarStatus', 0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeSideBar({ withoutAnimation }) {
|
||||||
|
// Cookies.set('sidebarStatus', 0)
|
||||||
|
this.sidebar.opened = false
|
||||||
|
this.sidebar.withoutAnimation = withoutAnimation
|
||||||
|
},
|
||||||
|
toggleDevice(device) {
|
||||||
|
this.device = device
|
||||||
|
},
|
||||||
|
setSize(size) {
|
||||||
|
this.size = size;
|
||||||
|
Cookies.set('size', size)
|
||||||
|
},
|
||||||
|
toggleSideBarHide(status) {
|
||||||
|
this.sidebar.hide = status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineStore } from 'pinia'
|
|
||||||
import { generateUUID } from '@/utils/tools.js'
|
import { generateUUID } from '@/utils/tools.js'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
export const useMeterStore = defineStore('meter', {
|
export const useMeterStore = defineStore('meter', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ export const useRoomStore = defineStore('room', {
|
|||||||
roomId: '',
|
roomId: '',
|
||||||
token: '',
|
token: '',
|
||||||
userUid: '',
|
userUid: '',
|
||||||
|
//邀请进入房间的用户uid
|
||||||
detailUid: '',
|
detailUid: '',
|
||||||
|
//邀请用户名称
|
||||||
|
detailName: '',
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setUserUid(data) {
|
setUserUid(data) {
|
||||||
@@ -13,6 +16,9 @@ export const useRoomStore = defineStore('room', {
|
|||||||
},
|
},
|
||||||
setDetailUid(data) {
|
setDetailUid(data) {
|
||||||
this.detailUid = data
|
this.detailUid = data
|
||||||
|
},
|
||||||
|
setDetailName(data) {
|
||||||
|
this.detailName = data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
37
src/stores/modules/settings.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import defaultSettings from '@/settings'
|
||||||
|
import { useDynamicTitle } from '@/utils/dynamicTitle'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
const { sideTheme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dynamicTitle } = defaultSettings
|
||||||
|
|
||||||
|
const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
|
||||||
|
|
||||||
|
export const useSettingsStore = defineStore(
|
||||||
|
'settings',
|
||||||
|
{
|
||||||
|
state: () => ({
|
||||||
|
title: '',
|
||||||
|
theme: storageSetting.theme === undefined ? '#434343' : storageSetting.theme,
|
||||||
|
sideTheme: storageSetting.sideTheme === undefined ? sideTheme : storageSetting.sideTheme,
|
||||||
|
showSettings: showSettings,
|
||||||
|
topNav: storageSetting.topNav === undefined ? topNav : storageSetting.topNav,
|
||||||
|
tagsView: storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView,
|
||||||
|
fixedHeader: storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader,
|
||||||
|
sidebarLogo: storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo,
|
||||||
|
dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
// 修改布局设置
|
||||||
|
changeSetting(data) {
|
||||||
|
const { key, value } = data
|
||||||
|
if (this.hasOwnProperty(key)) {
|
||||||
|
this[key] = value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 设置网页标题
|
||||||
|
setTitle(title) {
|
||||||
|
this.title = title
|
||||||
|
useDynamicTitle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
183
src/stores/modules/tagsView.js
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
const useTagsViewStore = defineStore(
|
||||||
|
'tags-view',
|
||||||
|
{
|
||||||
|
state: () => ({
|
||||||
|
visitedViews: [],
|
||||||
|
cachedViews: [],
|
||||||
|
iframeViews: []
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
addView(view) {
|
||||||
|
this.addVisitedView(view)
|
||||||
|
this.addCachedView(view)
|
||||||
|
},
|
||||||
|
addIframeView(view) {
|
||||||
|
if (this.iframeViews.some(v => v.path === view.path)) return
|
||||||
|
this.iframeViews.push(
|
||||||
|
Object.assign({}, view, {
|
||||||
|
title: view.meta.title || 'no-name'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
addVisitedView(view) {
|
||||||
|
if (this.visitedViews.some(v => v.path === view.path)) return
|
||||||
|
this.visitedViews.push(
|
||||||
|
Object.assign({}, view, {
|
||||||
|
title: view.meta.title || 'no-name'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
addCachedView(view) {
|
||||||
|
if (this.cachedViews.includes(view.name)) return
|
||||||
|
if (!view.meta.noCache) {
|
||||||
|
this.cachedViews.push(view.name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
delView(view) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.delVisitedView(view)
|
||||||
|
this.delCachedView(view)
|
||||||
|
resolve({
|
||||||
|
visitedViews: [...this.visitedViews],
|
||||||
|
cachedViews: [...this.cachedViews]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
delVisitedView(view) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
for (const [i, v] of this.visitedViews.entries()) {
|
||||||
|
if (v.path === view.path) {
|
||||||
|
this.visitedViews.splice(i, 1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.iframeViews = this.iframeViews.filter(item => item.path !== view.path)
|
||||||
|
resolve([...this.visitedViews])
|
||||||
|
})
|
||||||
|
},
|
||||||
|
delIframeView(view) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.iframeViews = this.iframeViews.filter(item => item.path !== view.path)
|
||||||
|
resolve([...this.iframeViews])
|
||||||
|
})
|
||||||
|
},
|
||||||
|
delCachedView(view) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const index = this.cachedViews.indexOf(view.name)
|
||||||
|
index > -1 && this.cachedViews.splice(index, 1)
|
||||||
|
resolve([...this.cachedViews])
|
||||||
|
})
|
||||||
|
},
|
||||||
|
delOthersViews(view) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.delOthersVisitedViews(view)
|
||||||
|
this.delOthersCachedViews(view)
|
||||||
|
resolve({
|
||||||
|
visitedViews: [...this.visitedViews],
|
||||||
|
cachedViews: [...this.cachedViews]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
delOthersVisitedViews(view) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.visitedViews = this.visitedViews.filter(v => {
|
||||||
|
return v.meta.affix || v.path === view.path
|
||||||
|
})
|
||||||
|
this.iframeViews = this.iframeViews.filter(item => item.path === view.path)
|
||||||
|
resolve([...this.visitedViews])
|
||||||
|
})
|
||||||
|
},
|
||||||
|
delOthersCachedViews(view) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const index = this.cachedViews.indexOf(view.name)
|
||||||
|
if (index > -1) {
|
||||||
|
this.cachedViews = this.cachedViews.slice(index, index + 1)
|
||||||
|
} else {
|
||||||
|
this.cachedViews = []
|
||||||
|
}
|
||||||
|
resolve([...this.cachedViews])
|
||||||
|
})
|
||||||
|
},
|
||||||
|
delAllViews(view) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.delAllVisitedViews(view)
|
||||||
|
this.delAllCachedViews(view)
|
||||||
|
resolve({
|
||||||
|
visitedViews: [...this.visitedViews],
|
||||||
|
cachedViews: [...this.cachedViews]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
delAllVisitedViews(view) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const affixTags = this.visitedViews.filter(tag => tag.meta.affix)
|
||||||
|
this.visitedViews = affixTags
|
||||||
|
this.iframeViews = []
|
||||||
|
resolve([...this.visitedViews])
|
||||||
|
})
|
||||||
|
},
|
||||||
|
delAllCachedViews(view) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.cachedViews = []
|
||||||
|
resolve([...this.cachedViews])
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateVisitedView(view) {
|
||||||
|
for (let v of this.visitedViews) {
|
||||||
|
if (v.path === view.path) {
|
||||||
|
v = Object.assign(v, view)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
delRightTags(view) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const index = this.visitedViews.findIndex(v => v.path === view.path)
|
||||||
|
if (index === -1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.visitedViews = this.visitedViews.filter((item, idx) => {
|
||||||
|
if (idx <= index || (item.meta && item.meta.affix)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const i = this.cachedViews.indexOf(item.name)
|
||||||
|
if (i > -1) {
|
||||||
|
this.cachedViews.splice(i, 1)
|
||||||
|
}
|
||||||
|
if(item.meta.link) {
|
||||||
|
const fi = this.iframeViews.findIndex(v => v.path === item.path)
|
||||||
|
this.iframeViews.splice(fi, 1)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
resolve([...this.visitedViews])
|
||||||
|
})
|
||||||
|
},
|
||||||
|
delLeftTags(view) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const index = this.visitedViews.findIndex(v => v.path === view.path)
|
||||||
|
if (index === -1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.visitedViews = this.visitedViews.filter((item, idx) => {
|
||||||
|
if (idx >= index || (item.meta && item.meta.affix)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const i = this.cachedViews.indexOf(item.name)
|
||||||
|
if (i > -1) {
|
||||||
|
this.cachedViews.splice(i, 1)
|
||||||
|
}
|
||||||
|
if(item.meta.link) {
|
||||||
|
const fi = this.iframeViews.findIndex(v => v.path === item.path)
|
||||||
|
this.iframeViews.splice(fi, 1)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
resolve([...this.visitedViews])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default useTagsViewStore
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import { login, logout, getInfo } from '@/api/login.js'
|
||||||
import { login, logout, getInfo } from '@/api/login'
|
import { getToken, setToken, removeToken } from '@/utils/auth.js'
|
||||||
import { getToken, setToken, removeToken } from '@/utils/auth'
|
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
const useUserStore = defineStore(
|
export const useUserStore = defineStore(
|
||||||
'user',
|
'user',
|
||||||
{
|
{
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@@ -18,10 +18,14 @@ const useUserStore = defineStore(
|
|||||||
try {
|
try {
|
||||||
const { username, password } = userInfo;
|
const { username, password } = userInfo;
|
||||||
const trimmedUsername = username.trim();
|
const trimmedUsername = username.trim();
|
||||||
|
|
||||||
const res = await login(trimmedUsername, password);
|
const res = await login(trimmedUsername, password);
|
||||||
|
if (res.meta.code !== 200) {
|
||||||
|
ElMessage({ message: res.meta?.message || '登录失败', type: 'error' });
|
||||||
|
return Promise.reject(res);
|
||||||
|
}
|
||||||
const { token, user } = res.data;
|
const { token, user } = res.data;
|
||||||
localStorage.setItem('userData', JSON.stringify(user));
|
this.name = user.name;
|
||||||
|
sessionStorage.setItem('userData', JSON.stringify(user));
|
||||||
setToken(token);
|
setToken(token);
|
||||||
this.token = token;
|
this.token = token;
|
||||||
|
|
||||||
@@ -34,7 +38,7 @@ const useUserStore = defineStore(
|
|||||||
getInfo() {
|
getInfo() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const userData = localStorage.getItem('userData');
|
const userData = sessionStorage.getItem('userData');
|
||||||
|
|
||||||
if (!userData) {
|
if (!userData) {
|
||||||
return reject(new Error('未找到用户数据'));
|
return reject(new Error('未找到用户数据'));
|
||||||
@@ -53,9 +57,14 @@ const useUserStore = defineStore(
|
|||||||
// 退出系统
|
// 退出系统
|
||||||
async logOut() {
|
async logOut() {
|
||||||
try {
|
try {
|
||||||
await logout();
|
const res = await logout();
|
||||||
|
if(res.meta.code !== 200){
|
||||||
|
ElMessage({ message: res.meta?.message || '退出登录失败', type: 'error' });
|
||||||
|
return
|
||||||
|
}
|
||||||
this.token = '';
|
this.token = '';
|
||||||
this.roles = '';
|
this.roles = '';
|
||||||
|
sessionStorage.removeItem('userData');
|
||||||
removeToken();
|
removeToken();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('退出登录失败:', error);
|
console.error('退出登录失败:', error);
|
||||||
@@ -68,5 +77,3 @@ const useUserStore = defineStore(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default useUserStore
|
|
||||||
|
|||||||
@@ -1,30 +1,71 @@
|
|||||||
|
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
import router from '@/router';
|
||||||
const TokenKey = "token";
|
const TokenKey = "token";
|
||||||
|
|
||||||
const ExpiresInKey = "Meta-Enterprise-Expires-In";
|
|
||||||
|
|
||||||
export function getToken() {
|
export function getToken() {
|
||||||
return Cookies.get(TokenKey);
|
// return Cookies.get(TokenKey);
|
||||||
|
return sessionStorage.getItem(TokenKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setToken(token) {
|
export function setToken(token) {
|
||||||
return Cookies.set(TokenKey, token);
|
// return Cookies.set(TokenKey, token);
|
||||||
|
return sessionStorage.setItem(TokenKey, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeToken() {
|
export function removeToken() {
|
||||||
return Cookies.remove(TokenKey);
|
// return Cookies.remove(TokenKey);
|
||||||
|
return sessionStorage.removeItem(TokenKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExpiresIn() {
|
//获取用户信息
|
||||||
return Cookies.get(ExpiresInKey) || -1;
|
export function getUserInfo() {
|
||||||
|
try {
|
||||||
|
const userData = sessionStorage.getItem("userData");
|
||||||
|
|
||||||
|
// 如果userData不存在,执行未授权处理
|
||||||
|
if (!userData) {
|
||||||
|
handleUnauthorized();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析JSON数据
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(userData);
|
||||||
|
return parsedData;
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error('用户数据格式错误,无法解析JSON:', parseError);
|
||||||
|
// 数据格式错误也视为未登录
|
||||||
|
sessionStorage.removeItem("userData");
|
||||||
|
handleUnauthorized();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户信息时发生错误:', error);
|
||||||
|
handleUnauthorized();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setExpiresIn(time) {
|
|
||||||
return Cookies.set(ExpiresInKey, time);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeExpiresIn() {
|
function handleUnauthorized() {
|
||||||
return Cookies.remove(ExpiresInKey);
|
removeToken();
|
||||||
|
|
||||||
|
// 使用 nextTick 确保路由状态已更新
|
||||||
|
import('vue').then(({ nextTick }) => {
|
||||||
|
nextTick(() => {
|
||||||
|
const currentPath = router.currentRoute.value.fullPath;
|
||||||
|
if (router.currentRoute.value.path !== '/login') {
|
||||||
|
router.push({
|
||||||
|
path: '/login',
|
||||||
|
query: {
|
||||||
|
redirect: currentPath !== '/login' ? currentPath : undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
2
src/utils/bus.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import mitt from 'mitt'
|
||||||
|
export const emitter = mitt()
|
||||||
15
src/utils/dynamicTitle.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// import store from '@/stores/index.js'
|
||||||
|
import defaultSettings from '@/settings'
|
||||||
|
import { useSettingsStore } from '@/stores/modules/settings.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态修改标题
|
||||||
|
*/
|
||||||
|
export function useDynamicTitle() {
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
if (settingsStore.dynamicTitle) {
|
||||||
|
document.title = settingsStore.title + ' - ' + defaultSettings.title;
|
||||||
|
} else {
|
||||||
|
document.title = defaultSettings.title;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,22 +8,50 @@ import { tansParams } from "@/utils/ruoyi";
|
|||||||
import cache from "@/plugins/cache";
|
import cache from "@/plugins/cache";
|
||||||
import { getToken, removeToken } from "@/utils/auth";
|
import { getToken, removeToken } from "@/utils/auth";
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
import { useMeterStore } from '@/stores/modules/meter'
|
import { useMeterStore } from '@/stores/modules/meter.js'
|
||||||
|
|
||||||
axios.defaults.headers["Content-Type"] = "application/json;charset=utf-8";
|
axios.defaults.headers["Content-Type"] = "application/json;charset=utf-8";
|
||||||
const meterStore = useMeterStore()
|
|
||||||
meterStore.initUdid()
|
// 动态获取 baseURL
|
||||||
|
const getBaseURL = () => {
|
||||||
|
// 开发环境使用配置的完整 URL
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
return import.meta.env.VITE_APP_BASE_API;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生产环境使用相对路径
|
||||||
|
// 返回空字符串,让浏览器自动使用当前域名
|
||||||
|
return '';
|
||||||
|
|
||||||
|
// 或者如果后端 API 有固定路径前缀,可以这样设置:
|
||||||
|
// return '/api'; // 这样请求会变成 https://当前域名/api/xxx
|
||||||
|
};
|
||||||
|
|
||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
const service = axios.create({
|
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,
|
||||||
|
baseURL: getBaseURL(),
|
||||||
// 超时
|
// 超时
|
||||||
timeout: 10000,
|
// timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// request拦截器
|
// request拦截器
|
||||||
service.interceptors.request.use(
|
service.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
|
// 在拦截器内部安全地使用 store
|
||||||
|
let sudid = ''
|
||||||
|
try {
|
||||||
|
const meterStore = useMeterStore()
|
||||||
|
if (!meterStore.udid) {
|
||||||
|
meterStore.initUdid();
|
||||||
|
sudid = meterStore.getSudid();
|
||||||
|
} else {
|
||||||
|
sudid = meterStore.getSudid();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('MeterStore 初始化失败:', error);
|
||||||
|
}
|
||||||
|
|
||||||
// 是否需要设置 token
|
// 是否需要设置 token
|
||||||
const isToken = (config.headers || {}).isToken === false;
|
const isToken = (config.headers || {}).isToken === false;
|
||||||
@@ -33,8 +61,8 @@ service.interceptors.request.use(
|
|||||||
|
|
||||||
// 是否需要防止数据重复提交
|
// 是否需要防止数据重复提交
|
||||||
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false;
|
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false;
|
||||||
if (meterStore.getSudid()) {
|
if (sudid) {
|
||||||
config.headers["X-User-Agent"] = `gxtech/web 1.0.0: c=GxTech, udid=${meterStore.getSudid()}, sv=15.4.1, app=stt`;
|
config.headers["X-User-Agent"] = `gxtech/web 1.0.0: c=GxTech, udid=${sudid}, sv=15.4.1, app=stt`;
|
||||||
}
|
}
|
||||||
// get请求映射params参数
|
// get请求映射params参数
|
||||||
if (config.method === "get" && config.params) {
|
if (config.method === "get" && config.params) {
|
||||||
@@ -122,14 +150,17 @@ service.interceptors.response.use(
|
|||||||
case 200:
|
case 200:
|
||||||
case 201:
|
case 201:
|
||||||
return Promise.resolve(responseData);
|
return Promise.resolve(responseData);
|
||||||
|
|
||||||
case 401:
|
case 401:
|
||||||
console.log('未授权', responseData)
|
// return Promise.resolve(responseData);
|
||||||
return Promise.resolve(responseData);
|
// const currentPath = router.currentRoute.value.name;
|
||||||
// return handleUnauthorized().then(() => {
|
// if(currentPath == 'ConferencingRoom'){
|
||||||
|
// return Promise.resolve(responseData);
|
||||||
|
// }else{
|
||||||
|
return handleUnauthorized()
|
||||||
|
// .then(() => {
|
||||||
// return Promise.reject({ code: 401, message: '未授权' });
|
// 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' });
|
||||||
@@ -164,30 +195,51 @@ 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',
|
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('用户取消操作');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -716,3 +716,26 @@ export function removeDuplicate(arr) {
|
|||||||
});
|
});
|
||||||
return newArr; // 返回一个新数组
|
return newArr; // 返回一个新数组
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function createThrottle(func, delay){
|
||||||
|
let timeoutId;
|
||||||
|
let lastExecTime = 0;
|
||||||
|
|
||||||
|
return (...args) => {
|
||||||
|
const currentTime = Date.now();
|
||||||
|
|
||||||
|
// 立即执行第一次
|
||||||
|
if (currentTime - lastExecTime > delay) {
|
||||||
|
func.apply(this, args);
|
||||||
|
lastExecTime = currentTime;
|
||||||
|
} else {
|
||||||
|
// 清除之前的定时器,设置新的定时器
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
func.apply(this, args);
|
||||||
|
lastExecTime = Date.now();
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
49
src/utils/theme.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// 处理主题样式
|
||||||
|
export function handleThemeStyle(theme) {
|
||||||
|
document.documentElement.style.setProperty('--el-color-primary', theme)
|
||||||
|
for (let i = 1; i <= 9; i++) {
|
||||||
|
document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(theme, i / 10)}`)
|
||||||
|
}
|
||||||
|
for (let i = 1; i <= 9; i++) {
|
||||||
|
document.documentElement.style.setProperty(`--el-color-primary-dark-${i}`, `${getDarkColor(theme, i / 10)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hex颜色转rgb颜色
|
||||||
|
export function hexToRgb(str) {
|
||||||
|
str = str.replace('#', '')
|
||||||
|
let hexs = str.match(/../g)
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
hexs[i] = parseInt(hexs[i], 16)
|
||||||
|
}
|
||||||
|
return hexs
|
||||||
|
}
|
||||||
|
|
||||||
|
// rgb颜色转Hex颜色
|
||||||
|
export function rgbToHex(r, g, b) {
|
||||||
|
let hexs = [r.toString(16), g.toString(16), b.toString(16)]
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
if (hexs[i].length == 1) {
|
||||||
|
hexs[i] = `0${hexs[i]}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `#${hexs.join('')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 变浅颜色值
|
||||||
|
export function getLightColor(color, level) {
|
||||||
|
let rgb = hexToRgb(color)
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i])
|
||||||
|
}
|
||||||
|
return rgbToHex(rgb[0], rgb[1], rgb[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 变深颜色值
|
||||||
|
export function getDarkColor(color, level) {
|
||||||
|
let rgb = hexToRgb(color)
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
rgb[i] = Math.floor(rgb[i] * (1 - level))
|
||||||
|
}
|
||||||
|
return rgbToHex(rgb[0], rgb[1], rgb[2])
|
||||||
|
}
|
||||||
93
src/utils/validate.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* 判断url是否是http或https
|
||||||
|
* @param {string} path
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
export function isHttp(url) {
|
||||||
|
return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断path是否为外链
|
||||||
|
* @param {string} path
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
export function isExternal(path) {
|
||||||
|
return /^(https?:|mailto:|tel:)/.test(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
export function validUsername(str) {
|
||||||
|
const valid_map = ['admin', 'editor']
|
||||||
|
return valid_map.indexOf(str.trim()) >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} url
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
export function validURL(url) {
|
||||||
|
const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
|
||||||
|
return reg.test(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
export function validLowerCase(str) {
|
||||||
|
const reg = /^[a-z]+$/
|
||||||
|
return reg.test(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
export function validUpperCase(str) {
|
||||||
|
const reg = /^[A-Z]+$/
|
||||||
|
return reg.test(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
export function validAlphabets(str) {
|
||||||
|
const reg = /^[A-Za-z]+$/
|
||||||
|
return reg.test(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} email
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
export function validEmail(email) {
|
||||||
|
const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||||
|
return reg.test(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
export function isString(str) {
|
||||||
|
if (typeof str === 'string' || str instanceof String) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array} arg
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
export function isArray(arg) {
|
||||||
|
if (typeof Array.isArray === 'undefined') {
|
||||||
|
return Object.prototype.toString.call(arg) === '[object Array]'
|
||||||
|
}
|
||||||
|
return Array.isArray(arg)
|
||||||
|
}
|
||||||
@@ -2,7 +2,8 @@ import { mqttClient } from "./mqtt";
|
|||||||
import { getWhiteboardShapes, getWhiteboardHistory } from "@/views/custom/api";
|
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 { emitter } from "@/utils/bus.js";
|
||||||
const meterStore = useMeterStore();
|
const meterStore = useMeterStore();
|
||||||
meterStore.initUdid();
|
meterStore.initUdid();
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ let canvasInstance = null;
|
|||||||
|
|
||||||
// 获取本地缓存 userData
|
// 获取本地缓存 userData
|
||||||
function getLocalUserData() {
|
function getLocalUserData() {
|
||||||
const dataStr = localStorage.getItem('userData');
|
const dataStr = sessionStorage.getItem('userData');
|
||||||
if (!dataStr) return null;
|
if (!dataStr) return null;
|
||||||
try {
|
try {
|
||||||
return JSON.parse(dataStr);
|
return JSON.parse(dataStr);
|
||||||
@@ -24,29 +25,26 @@ function getLocalUserData() {
|
|||||||
export const WhiteboardSync = {
|
export const WhiteboardSync = {
|
||||||
async init(canvas, roomUid) {
|
async init(canvas, roomUid) {
|
||||||
if (!canvas || !roomUid) return;
|
if (!canvas || !roomUid) return;
|
||||||
console.log('初始化多人同步:', roomUid);
|
|
||||||
canvasInstance = canvas;
|
canvasInstance = canvas;
|
||||||
|
|
||||||
const localUser = getLocalUserData();
|
const localUser = getLocalUserData();
|
||||||
const localUid = localUser?.user?.uid;
|
const localUid = localUser?.uid;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 先连接 MQTT
|
// 先连接 MQTT
|
||||||
await mqttClient.connect(meterStore.getSudid());
|
await mqttClient.connect(meterStore.getSudid());
|
||||||
console.log("✅ MQTT 已连接");
|
|
||||||
|
|
||||||
// 获取历史数据
|
// 获取历史数据
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 订阅当前房间
|
// 订阅当前房间
|
||||||
const topic = `xSynergy/ROOM/${roomUid}/whiteboard/#`;
|
const topic = `xSynergy/ROOM/${roomUid}/whiteboard/#`;
|
||||||
mqttClient.subscribe(topic, async (shapeData) => {
|
mqttClient.subscribe(topic, async (shapeData) => {
|
||||||
// console.log(shapeData, 'shapeData++格式装换')
|
const shapeDataNew = JSON.parse(shapeData.toString())
|
||||||
const shapeDataNew = decode(shapeData);
|
// const shapeDataNew = decode(message);
|
||||||
// console.log(shapeDataNew, '格式解码')
|
|
||||||
try {
|
try {
|
||||||
isRemote = true;
|
isRemote = true;
|
||||||
// 如果 shape 来自本地用户,则跳过
|
// 如果 shape 来自本地用户,则跳过
|
||||||
@@ -55,6 +53,7 @@ export const WhiteboardSync = {
|
|||||||
if (res.meta.code === 200) {
|
if (res.meta.code === 200) {
|
||||||
canvasInstance.addShape(res.data.shapes);
|
canvasInstance.addShape(res.data.shapes);
|
||||||
} else {
|
} else {
|
||||||
|
ElMessage.error("获取历史数据失败");
|
||||||
console.error("获取历史数据失败");
|
console.error("获取历史数据失败");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -63,8 +62,6 @@ export const WhiteboardSync = {
|
|||||||
isRemote = false;
|
isRemote = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("✅ 已订阅:", topic);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("初始化多人同步失败:", err)
|
console.log("初始化多人同步失败:", err)
|
||||||
// console.error("❌ 连接或订阅失败:", err);
|
// console.error("❌ 连接或订阅失败:", err);
|
||||||
@@ -82,6 +79,7 @@ export const WhiteboardSync = {
|
|||||||
try {
|
try {
|
||||||
await getWhiteboardShapes(shape, roomUid);
|
await getWhiteboardShapes(shape, roomUid);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
ElMessage.error("提交形状失败");
|
||||||
console.error("提交形状失败:", err);
|
console.error("提交形状失败:", err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
103
src/utils/whiteboardSync_old.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { mqttClient } from "./mqtt";
|
||||||
|
import { getWhiteboardShapes, getWhiteboardHistory } from "@/views/custom/api";
|
||||||
|
import { useMeterStore } from '@/stores/modules/meter';
|
||||||
|
import { encode, decode } from '@msgpack/msgpack'
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
const meterStore = useMeterStore();
|
||||||
|
meterStore.initUdid();
|
||||||
|
|
||||||
|
let isRemote = false;
|
||||||
|
let canvasInstance = null;
|
||||||
|
|
||||||
|
// 获取本地缓存 userData
|
||||||
|
function getLocalUserData() {
|
||||||
|
const dataStr = sessionStorage.getItem('userData');
|
||||||
|
if (!dataStr) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(dataStr);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("解析 userData 失败:", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WhiteboardSync = {
|
||||||
|
async init(canvas, roomUid) {
|
||||||
|
if (!canvas || !roomUid) return;
|
||||||
|
console.log('初始化多人同步:', roomUid);
|
||||||
|
canvasInstance = canvas;
|
||||||
|
|
||||||
|
const localUser = getLocalUserData();
|
||||||
|
const localUid = localUser?.uid;
|
||||||
|
try {
|
||||||
|
// 先连接 MQTT
|
||||||
|
await mqttClient.connect(meterStore.getSudid());
|
||||||
|
console.log("✅ MQTT 已连接");
|
||||||
|
|
||||||
|
// 获取历史数据
|
||||||
|
const res = await getWhiteboardHistory({ after_timestamp: 0 }, roomUid);
|
||||||
|
if (res.meta.code === 200 && res.data.shapes.length > 0) {
|
||||||
|
canvasInstance.addShape(res.data.shapes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订阅当前房间
|
||||||
|
const topic = `xSynergy/ROOM/${roomUid}/whiteboard/#`;
|
||||||
|
mqttClient.subscribe(topic, async (shapeData) => {
|
||||||
|
const shapeDataNew = JSON.parse(shapeData.toString())
|
||||||
|
// const shapeDataNew = decode(message);
|
||||||
|
// console.log(shapeDataNew, '格式解码')
|
||||||
|
try {
|
||||||
|
isRemote = true;
|
||||||
|
// 如果 shape 来自本地用户,则跳过
|
||||||
|
if (shapeDataNew.user_uid === localUid) return;
|
||||||
|
const res = await getWhiteboardHistory({ after_timestamp: shapeDataNew.created_at }, roomUid);
|
||||||
|
if (res.meta.code === 200) {
|
||||||
|
canvasInstance.addShape(res.data.shapes);
|
||||||
|
} else {
|
||||||
|
ElMessage.error("获取历史数据失败");
|
||||||
|
console.error("获取历史数据失败");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("处理MQTT数据失败:", e);
|
||||||
|
} finally {
|
||||||
|
isRemote = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("✅ 已订阅:", topic);
|
||||||
|
} catch (err) {
|
||||||
|
console.log("初始化多人同步失败:", err)
|
||||||
|
// console.error("❌ 连接或订阅失败:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听画布事件:新增图形
|
||||||
|
canvas.on('drawingEnd', async (shape) => {
|
||||||
|
// 如果来自远程,或不是需要同步的类型,跳过
|
||||||
|
if (isRemote || !['pencil', 'line', 'rectangle', 'circle', 'eraser'].includes(shape.type)) return;
|
||||||
|
|
||||||
|
// 如果是本地用户自己的 shape,则不调用接口
|
||||||
|
if (shape.user_uid && shape.user_uid === localUid) return;
|
||||||
|
|
||||||
|
shape.room_uid = roomUid;
|
||||||
|
try {
|
||||||
|
await getWhiteboardShapes(shape, roomUid);
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error("提交形状失败");
|
||||||
|
console.error("提交形状失败:", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听画布事件:清空
|
||||||
|
canvas.on('clear', async () => {
|
||||||
|
if (!isRemote) {
|
||||||
|
try {
|
||||||
|
// TODO: 调用接口,后端再发 MQTT
|
||||||
|
// await clearWhiteboard(roomUid);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("提交清空失败:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,3 +1,107 @@
|
|||||||
export function generateUUID() {
|
import { ElMessage } from 'element-plus';
|
||||||
|
export function errorHandling(error,type) {
|
||||||
|
switch (error.name) {
|
||||||
|
case 'NotAllowedError':
|
||||||
|
ElMessage.error(`用户拒绝了权限请求,请允许此网站使用${type}权限`);
|
||||||
|
break;
|
||||||
|
case 'NotFoundError':
|
||||||
|
ElMessage.error(`未检测到可用的${type}设备,请检查${type}是否已正确连接`);
|
||||||
|
break;
|
||||||
|
case 'NotSupportedError':
|
||||||
|
ElMessage.error(`当前浏览器不支持${type}功能,请使用现代浏览器如Chrome、Firefox或Edge`);
|
||||||
|
break;
|
||||||
|
case 'NotReadableError':
|
||||||
|
ElMessage.error(`${type}设备正被其他应用程序占用,请关闭其他使用${type}的应用后重试`);
|
||||||
|
break;
|
||||||
|
case 'OverconstrainedError':
|
||||||
|
ElMessage.error(`${type}配置不兼容,请尝试调整${type}设置`);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ElMessage.error('服务错误,请刷新重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理数据接收事件
|
||||||
|
export function handleDataReceived(payload, participant, kind) {
|
||||||
|
try {
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const strData = decoder.decode(payload);
|
||||||
|
ElMessage.info(`收到消息 from ${participant.identity}: ${strData}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理接收消息失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleReconnected() {
|
||||||
|
ElMessage.success('已重新连接到房间');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取设备名称
|
||||||
|
export function getDeviceName(devices, deviceId) {
|
||||||
|
const device = devices.find(d => d.deviceId === deviceId);
|
||||||
|
return device ? (device.label || '未知设备') : '未知设备';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleVideoLoaded(identity, type) {
|
||||||
|
// console.log(`视频加载完成: ${identity}的${type}视频`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleConnectionStateChanged(state) {
|
||||||
|
// console.log('连接状态改变:', state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 平滑曲线绘制函数
|
||||||
|
export function drawSmoothCurve(ctx, path) {
|
||||||
|
if (path.length < 3) {
|
||||||
|
// 如果点太少,直接绘制直线
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(path[0].x, path[0].y);
|
||||||
|
for (let i = 1; i < path.length; i++) {
|
||||||
|
ctx.lineTo(path[i].x, path[i].y);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(path[0].x, path[0].y);
|
||||||
|
|
||||||
|
const threshold = 5; // 距离阈值,用于控制平滑度
|
||||||
|
|
||||||
|
for (let i = 1; i < path.length - 2;) {
|
||||||
|
let a = 1;
|
||||||
|
// 寻找下一个足够远的点
|
||||||
|
while (i + a < path.length - 2 &&
|
||||||
|
Math.sqrt(Math.pow(path[i].x - path[i + a].x, 2) +
|
||||||
|
Math.pow(path[i].y - path[i + a].y, 2)) < threshold) {
|
||||||
|
a++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const xc = (path[i].x + path[i + a].x) / 2;
|
||||||
|
const yc = (path[i].y + path[i + a].y) / 2;
|
||||||
|
|
||||||
|
ctx.quadraticCurveTo(path[i].x, path[i].y, xc, yc);
|
||||||
|
i += a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接最后两个点
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成唯一ID
|
||||||
|
export function generateElementId() {
|
||||||
|
return `laser_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
}
|
}
|
||||||
366
src/views/conferencingRoom/components/InviterJoinRoom/index.vue
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
<template>
|
||||||
|
<el-drawer v-model="drawerVisible" direction="rtl" title="请选择要加入房间的人员" size="40%">
|
||||||
|
<template #header>
|
||||||
|
<h4>请选择要加入房间的人员</h4>
|
||||||
|
</template>
|
||||||
|
<div class="drawer-content">
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<div class="search-section">
|
||||||
|
<el-input
|
||||||
|
v-model="searchKeyword"
|
||||||
|
placeholder="搜索人员或部门"
|
||||||
|
clearable
|
||||||
|
prefix-icon="Search"
|
||||||
|
@input="handleSearch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已选人员展示 -->
|
||||||
|
<div class="selected-section" v-if="selectedUsers.length > 0">
|
||||||
|
<div class="selected-header">
|
||||||
|
<span>已选择 ({{ selectedUsers.length }})</span>
|
||||||
|
<el-button type="text" @click="clearAllSelected">清空</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="selected-tags">
|
||||||
|
<el-tag
|
||||||
|
v-for="user in selectedUsers"
|
||||||
|
:key="user.uid"
|
||||||
|
closable
|
||||||
|
@close="removeSelectedUser(user)"
|
||||||
|
class="selected-tag"
|
||||||
|
>
|
||||||
|
{{ user.name }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 树形结构 -->
|
||||||
|
<el-scrollbar v-loading="leftListLoading" class="left-list-scrollbar" height="calc(100vh - 240px)">
|
||||||
|
<el-tree
|
||||||
|
ref="treeRef"
|
||||||
|
lazy
|
||||||
|
:load="handleLoadNode"
|
||||||
|
:props="treeProps"
|
||||||
|
node-key="uid"
|
||||||
|
show-checkbox
|
||||||
|
:default-expand-all="false"
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
:check-strictly="false"
|
||||||
|
:filter-node-method="filterNode"
|
||||||
|
@check="handleCheckChange"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<template #default="{ node, data }">
|
||||||
|
<div class="tree-item">
|
||||||
|
<div class="tree-item-content">
|
||||||
|
<span v-if="data.uid" class="user-icon">👤</span>
|
||||||
|
<span v-else class="dept-icon">📁</span>
|
||||||
|
<span class="tree-item-text">{{ data.name }}</span>
|
||||||
|
<span v-if="data.users_count" class="user-count">({{ data.users_count }})</span>
|
||||||
|
<!-- <span v-if="isNodeDisabled(data)" class="already-joined-tag">已在房间中</span> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="footer-actions">
|
||||||
|
<el-button @click="cancelClick">取 消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmClick" :disabled="selectedUsers.length === 0">
|
||||||
|
确 定 ({{ selectedUsers.length }})
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
getDirectories,
|
||||||
|
getDirectoriesUsers
|
||||||
|
} from '@/api/coordinate.js'
|
||||||
|
import { getParticipantsApi } from '@/api/conferencingRoom.js'
|
||||||
|
import { nextTick, reactive, toRefs, watch, onMounted, ref } from "vue";
|
||||||
|
|
||||||
|
// 定义 emit
|
||||||
|
const emit = defineEmits(["confirmSelection"]);
|
||||||
|
|
||||||
|
// 接收 props
|
||||||
|
const props = defineProps({
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const drawerVisible = ref(false);
|
||||||
|
const treeRef = ref();
|
||||||
|
const searchKeyword = ref('');
|
||||||
|
const selectedUsers = ref([]);
|
||||||
|
const checkedNodes = ref([]);
|
||||||
|
const leftListLoading = ref(false)
|
||||||
|
const joinedUserIds = ref([]); // 存储已在房间中的用户ID
|
||||||
|
// 树形配置
|
||||||
|
const treeProps = reactive({
|
||||||
|
children: 'users',
|
||||||
|
label: 'name',
|
||||||
|
value: 'uid',
|
||||||
|
disabled: (data) => {
|
||||||
|
// 只有人员节点(有uid)且该用户已在房间中时才禁用
|
||||||
|
return data.uid && joinedUserIds.value.includes(data.uid);
|
||||||
|
},
|
||||||
|
isLeaf: (node) => {
|
||||||
|
return !!node.uid; // 有 uid 的为叶子节点(人员)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isNodeDisabled = (data) => {
|
||||||
|
// 只有人员节点(有uid)且该用户已在房间中时才禁用
|
||||||
|
return data.uid && joinedUserIds.value.includes(data.uid);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 显示抽屉
|
||||||
|
async function show(roomId) {
|
||||||
|
drawerVisible.value = true;
|
||||||
|
// 重置选择状态
|
||||||
|
selectedUsers.value = [];
|
||||||
|
checkedNodes.value = [];
|
||||||
|
searchKeyword.value = '';
|
||||||
|
|
||||||
|
// 获取已在房间中的用户
|
||||||
|
try {
|
||||||
|
const res = await getParticipantsApi(roomId);
|
||||||
|
// 提取状态为1(在房间中)的用户ID
|
||||||
|
joinedUserIds.value = res.data
|
||||||
|
.filter(item => item.status == 1)
|
||||||
|
.map(item => item.user_uid);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取房间参与者失败:', error);
|
||||||
|
joinedUserIds.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟加载树形数据,确保 DOM 已渲染
|
||||||
|
nextTick(() => {
|
||||||
|
if (treeRef.value) {
|
||||||
|
treeRef.value.setCheckedKeys([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function getJoinUsers(){
|
||||||
|
const res = await getParticipantsApi(roomId)
|
||||||
|
const joinUsers = res.data.filter(item => item.status == 1).map(item => item.user_uid)
|
||||||
|
return joinUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认选择
|
||||||
|
function confirmClick() {
|
||||||
|
if (selectedUsers.value.length > 0) {
|
||||||
|
emit('confirmSelection', selectedUsers.value);
|
||||||
|
drawerVisible.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消选择
|
||||||
|
function cancelClick() {
|
||||||
|
drawerVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载树节点
|
||||||
|
const handleLoadNode = async (node, resolve) => {
|
||||||
|
if(node?.level === 0){
|
||||||
|
loadNode(resolve,'',node?.level)
|
||||||
|
}else if(node?.level > 0){
|
||||||
|
if(node.data.directory_uid){
|
||||||
|
loadUserNode(resolve,node.data.directory_uid,node?.level)
|
||||||
|
}else{
|
||||||
|
resolve(resolve)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载节点数据
|
||||||
|
const loadNode = async (resolve, id) => {
|
||||||
|
try {
|
||||||
|
leftListLoading.value = true
|
||||||
|
let res = await getDirectories({level:1})
|
||||||
|
if(res.meta.code == 200){
|
||||||
|
resolve(res.data)
|
||||||
|
}
|
||||||
|
leftListLoading.value = false
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
leftListLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadUserNode = async(resolve,id,level)=>{
|
||||||
|
try {
|
||||||
|
leftListLoading.value = true
|
||||||
|
let userData = []
|
||||||
|
let orgData = []
|
||||||
|
const resOrg = await getDirectories({level: 1,parent_uuid:id})
|
||||||
|
if(resOrg?.data){
|
||||||
|
orgData = resOrg.data
|
||||||
|
}
|
||||||
|
if(id){
|
||||||
|
const res = await getDirectoriesUsers(id,{directory_uuid:id})
|
||||||
|
userData = res.data
|
||||||
|
}
|
||||||
|
resolve([...orgData, ...userData])
|
||||||
|
leftListLoading.value = false
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
leftListLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理复选框选择变化
|
||||||
|
const handleCheckChange = (checkedNode, checkedNodesInfo) => {
|
||||||
|
// 获取所有选中的节点
|
||||||
|
const checkedKeys = treeRef.value.getCheckedKeys();
|
||||||
|
const halfCheckedKeys = treeRef.value.getHalfCheckedKeys();
|
||||||
|
|
||||||
|
// 获取所有选中节点数据
|
||||||
|
const allCheckedNodes = treeRef.value.getCheckedNodes(false, true);
|
||||||
|
|
||||||
|
// 筛选出人员节点(有 uid 的节点)且不在已加入用户列表中的
|
||||||
|
const userNodes = allCheckedNodes.filter(node =>
|
||||||
|
node.uid && !joinedUserIds.value.includes(node.uid)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 更新已选用户列表
|
||||||
|
selectedUsers.value = userNodes;
|
||||||
|
checkedNodes.value = allCheckedNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除已选用户
|
||||||
|
const removeSelectedUser = (user) => {
|
||||||
|
// 从已选列表中移除
|
||||||
|
const index = selectedUsers.value.findIndex(u => u.uid === user.uid);
|
||||||
|
if (index !== -1) {
|
||||||
|
selectedUsers.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新树的选中状态
|
||||||
|
if (treeRef.value) {
|
||||||
|
treeRef.value.setChecked(user.uid, false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空所有选择
|
||||||
|
const clearAllSelected = () => {
|
||||||
|
selectedUsers.value = [];
|
||||||
|
checkedNodes.value = [];
|
||||||
|
if (treeRef.value) {
|
||||||
|
treeRef.value.setCheckedKeys([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索过滤
|
||||||
|
const handleSearch = () => {
|
||||||
|
if (treeRef.value) {
|
||||||
|
treeRef.value.filter(searchKeyword.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 节点过滤方法
|
||||||
|
const filterNode = (value, data) => {
|
||||||
|
if (!value) return true;
|
||||||
|
return data.name && data.name.toLowerCase().includes(value.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
show,
|
||||||
|
getSelectedUsers: () => selectedUsers.value
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.drawer-content {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-tag {
|
||||||
|
max-width: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 36px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.tree-item-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-icon, .dept-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item-text {
|
||||||
|
color: #333333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-count {
|
||||||
|
margin-left: 6px;
|
||||||
|
color: #999999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tree-node__content) {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tree-node.is-current > .el-tree-node__content) {
|
||||||
|
background-color: #f0f7ff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
871
src/views/conferencingRoom/components/fileUpload/browseFile.vue
Normal file
@@ -0,0 +1,871 @@
|
|||||||
|
<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>
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogFileVisible"
|
||||||
|
title="文件转换"
|
||||||
|
width="50%"
|
||||||
|
class="file-preview-dialog"
|
||||||
|
>
|
||||||
|
<div class="preview-container">
|
||||||
|
<div class="loading-container">
|
||||||
|
<el-icon class="is-loading" size="48">
|
||||||
|
<Loading />
|
||||||
|
</el-icon>
|
||||||
|
<p>正在下载文件,请稍候...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</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('')
|
||||||
|
const dialogFileVisible = ref(false)
|
||||||
|
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
emitter.on('fileSuccess',fileSuccess)
|
||||||
|
|
||||||
|
function fileSuccess(){
|
||||||
|
dialogFileVisible.value = true
|
||||||
|
}
|
||||||
|
//
|
||||||
|
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){
|
||||||
|
// console.log('文件上传成功mqtt消息')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化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')
|
||||||
|
console.log(data,'data文件转换完成,预览通知')
|
||||||
|
const userId = JSON.parse(sessionStorage.getItem('userData'))?.uid
|
||||||
|
if(dialogFileVisible.value){
|
||||||
|
dialogFileVisible.value = false
|
||||||
|
}
|
||||||
|
if(dialogFormVisible.value && userId != data.user_uid){
|
||||||
|
// 显示确认对话框
|
||||||
|
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>
|
||||||
268
src/views/conferencingRoom/components/fileUpload/fileList.vue
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
<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){
|
||||||
|
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>
|
||||||
@@ -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>
|
||||||
293
src/views/conferencingRoom/components/fileUpload/upLoadFile.vue
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
<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
|
||||||
|
|
||||||
|
emitter.emit('fileSuccess')
|
||||||
|
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>
|
||||||
3547
src/views/conferencingRoom/latest.vue
Normal file
3175
src/views/conferencingRoom/pathTransit.vue
Normal file
3519
src/views/conferencingRoom/text.vue
Normal file
3325
src/views/conferencingRoom/transit.vue
Normal file
3326
src/views/conferencingRoom/transits.vue
Normal file
236
src/views/coordinate/personnelList/components/assistWx/index.vue
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogFormVisible"
|
||||||
|
width="80%"
|
||||||
|
:show-close="false"
|
||||||
|
:destroy-on-close="true"
|
||||||
|
:modal="false"
|
||||||
|
:lock-scroll="true"
|
||||||
|
:before-close="handleClose"
|
||||||
|
class="call-dialog"
|
||||||
|
>
|
||||||
|
<div class="call-wrapper">
|
||||||
|
<!-- 上方头像与状态 -->
|
||||||
|
<div class="avatar-section">
|
||||||
|
<img class="avatar" :src="avatarUrl" alt="头像" />
|
||||||
|
<div class="user-name">{{ userName }}</div>
|
||||||
|
<div class="status-text">{{ statusText }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部操作按钮 -->
|
||||||
|
<div class="control-section">
|
||||||
|
<!-- 呼叫模式 -->
|
||||||
|
<template v-if="mode === 'call'">
|
||||||
|
<el-button
|
||||||
|
v-if="callStatus === 'calling'"
|
||||||
|
type="danger"
|
||||||
|
round
|
||||||
|
class="control-btn hangup"
|
||||||
|
@click="hangup"
|
||||||
|
>
|
||||||
|
挂断
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
type="primary"
|
||||||
|
round
|
||||||
|
class="control-btn call"
|
||||||
|
@click="startCall"
|
||||||
|
>
|
||||||
|
呼叫中...
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 接听模式 -->
|
||||||
|
<template v-else>
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
round
|
||||||
|
class="control-btn accept"
|
||||||
|
@click="acceptCall"
|
||||||
|
>
|
||||||
|
接听
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
round
|
||||||
|
class="control-btn hangup"
|
||||||
|
@click="hangup"
|
||||||
|
>
|
||||||
|
挂断
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
mode: { type: String, default: "call" }, // call: 呼叫模式, receive: 接听模式
|
||||||
|
avatarUrl: {
|
||||||
|
type: String,
|
||||||
|
default: "https://cdn-icons-png.flaticon.com/512/1946/1946429.png",
|
||||||
|
},
|
||||||
|
userName: { type: String, default: "对方用户" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialogFormVisible = ref(false);
|
||||||
|
const callStatus = ref("calling"); // calling | active | ended
|
||||||
|
|
||||||
|
const statusText = computed(() => {
|
||||||
|
if (props.mode === "call") {
|
||||||
|
if (callStatus.value === "calling") return "正在呼叫对方...";
|
||||||
|
if (callStatus.value === "active") return "通话中";
|
||||||
|
return "通话结束";
|
||||||
|
} else {
|
||||||
|
return callStatus.value === "active" ? "通话中" : "对方来电...";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function startCall() {
|
||||||
|
callStatus.value = "active";
|
||||||
|
ElMessage.success("开始通话");
|
||||||
|
}
|
||||||
|
|
||||||
|
function acceptCall() {
|
||||||
|
callStatus.value = "active";
|
||||||
|
ElMessage.success("已接听");
|
||||||
|
}
|
||||||
|
|
||||||
|
function hangup() {
|
||||||
|
callStatus.value = "ended";
|
||||||
|
ElMessage.error("通话已结束");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
dialogFormVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
dialogFormVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
show,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* ::v-deep .el-dialog__header{
|
||||||
|
background-color:red;
|
||||||
|
} */
|
||||||
|
/* 去除 el-dialog 默认样式,让它全屏覆盖且透明 */
|
||||||
|
.call-dialog :deep(.el-dialog) {
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
height: calc(100vh - 30vh);
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.call-dialog :deep(.el-dialog__body) {
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通话主背景 */
|
||||||
|
.call-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: calc(100vh - 30vh);
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(180deg, #0f2027, #203a43, #2c5364);
|
||||||
|
color: #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 80px 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头像部分 */
|
||||||
|
.avatar-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 6px solid rgba(255, 255, 255, 0.25);
|
||||||
|
box-shadow: 0 0 40px rgba(0, 0, 0, 0.4);
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
margin-top: 24px;
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 控制按钮部分 */
|
||||||
|
.control-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
width: 160px;
|
||||||
|
height: 60px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn.accept {
|
||||||
|
background: linear-gradient(135deg, #00c853, #4caf50);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn.hangup {
|
||||||
|
background: linear-gradient(135deg, #ff1744, #d50000);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn.call {
|
||||||
|
background: linear-gradient(135deg, #2196f3, #1976d2);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -1,25 +1,56 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- v-loading="leftListLoading || loading" -->
|
|
||||||
<div class="left-list" >
|
<div class="left-list" v-loading="leftListLoading || loading">
|
||||||
<div class="list-tab">
|
<div class="list-tab">
|
||||||
<div
|
<div
|
||||||
: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">
|
||||||
<div class="content-top-input">
|
<div class="content-top-input" v-if="leftTab == 1">
|
||||||
|
<!-- <el-input
|
||||||
|
v-model="queryFrom.nickName"
|
||||||
|
placeholder="搜索成员"
|
||||||
|
type="text"
|
||||||
|
prefix-icon="Search"
|
||||||
|
@change="searchList"
|
||||||
|
/> -->
|
||||||
|
<el-select
|
||||||
|
v-model="participant_user"
|
||||||
|
multiple
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
remote
|
||||||
|
reserve-keyword
|
||||||
|
placeholder="搜索成员"
|
||||||
|
:remote-method="remoteMethod"
|
||||||
|
:loading="loading"
|
||||||
|
collapse-tags
|
||||||
|
collapse-tags-tooltip
|
||||||
|
style="width: 100%"
|
||||||
|
@change="searchList"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in userList"
|
||||||
|
:key="item.uid"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.uid"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="content-top-input" v-if="leftTab == 2">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="queryFrom.nickName"
|
v-model="queryFrom.nickName"
|
||||||
placeholder="搜索成员"
|
placeholder="搜索成员"
|
||||||
@@ -47,49 +78,50 @@
|
|||||||
v-infinite-scroll="infinite"
|
v-infinite-scroll="infinite"
|
||||||
v-if="dataList?.length"
|
v-if="dataList?.length"
|
||||||
>
|
>
|
||||||
|
<!-- @click="updateDetail(item)" -->
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in dataList"
|
v-for="(item, index) in dataList"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="content-list-item"
|
class="content-list-item"
|
||||||
@click="updateDetail(item)"
|
@click="updateDetail(item)"
|
||||||
:style="
|
:style="
|
||||||
item.assistanceId == assistanceId
|
item.id == assistanceId
|
||||||
? 'border-color: #409EFF; '
|
? 'border-color: #409EFF; '
|
||||||
: ''
|
: ''
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="list-item-top">
|
<div class="list-item-top">
|
||||||
<span>
|
<span>
|
||||||
{{ parseTime(item.beginTime, '{m}月{d}日') }}
|
{{ parseTime(item.created_at, '{m}月{d}日') }}
|
||||||
{{ weekName[new Date(item.beginTime).getDay()] }}
|
{{ weekName[new Date(item.created_at).getDay()] }}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{{ parseTime(item.beginTime, '{y}年') }}
|
{{ parseTime(item.created_at, '{y}年') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-item-content">
|
<div class="list-item-content">
|
||||||
<div class="list-item-content-text">
|
<div class="list-item-content-text">
|
||||||
<div style="display: flex; flex-wrap: wrap">
|
<div style="display: flex; flex-wrap: wrap">
|
||||||
<span
|
<span
|
||||||
v-for="(items, indexs) in item.assistanceMemberList"
|
v-for="(items, indexs) in item.all_participants"
|
||||||
:key="indexs"
|
:key="indexs"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
items.nickName +
|
items.display_name +
|
||||||
(indexs + 1 == item.assistanceMemberList.length
|
(indexs + 1 == item.all_participants.length
|
||||||
? ''
|
? ''
|
||||||
: '、')
|
: '、')
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span>
|
<span>
|
||||||
发起人:{{ item.initiatorName ? item.initiatorName : '' }}
|
发起人:{{ item.all_participants.find(item => item.participant_role == 'moderator')?.display_name || ''}}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
时间:{{
|
时间:{{
|
||||||
parseTime(item.beginTime, '{h}:{i}') +
|
parseTime(item.created_at, '{h}:{i}') +
|
||||||
' ~ ' +
|
' ~ ' +
|
||||||
(item.endTime ? parseTime(item.endTime, '{h}:{i}') : '')
|
(item.updated_at ? parseTime(item.updated_at, '{h}:{i}') : '')
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,15 +165,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
// getAssistanceList,
|
// getAssistanceList,
|
||||||
|
getParticipantsHistoryApi,
|
||||||
getDirectories,
|
getDirectories,
|
||||||
getDirectoriesUsers
|
getDirectoriesUsers,
|
||||||
|
getInfo,
|
||||||
} from '@/api/coordinate.js'
|
} from '@/api/coordinate.js'
|
||||||
import { nextTick, reactive, toRefs, watch, onMounted } from 'vue'
|
import { nextTick, reactive, toRefs, watch, onMounted } from 'vue'
|
||||||
|
import { deepClone,parseTime ,createThrottle} from '@/utils/ruoyi.js'
|
||||||
|
import { getUserInfo } from '@/utils/auth.js'
|
||||||
|
|
||||||
// 接收 props
|
// 接收 props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -157,13 +193,15 @@ const emit = defineEmits(['updateDetail', 'updateTab'])
|
|||||||
// state
|
// state
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
isFirst: true,
|
isFirst: true,
|
||||||
leftTab: 2,
|
leftTab: 1,
|
||||||
queryFrom: {
|
queryFrom: {
|
||||||
pageNum: 1,
|
page_size: 10,
|
||||||
pageSize: 10,
|
page: 1,
|
||||||
nickName: '',
|
nickName: '',
|
||||||
leftDatePicker: null,
|
leftDatePicker: null,
|
||||||
|
participant_user_uids:''
|
||||||
},
|
},
|
||||||
|
participant_user:[],
|
||||||
leftListLoading: true,
|
leftListLoading: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
dataList: [],
|
dataList: [],
|
||||||
@@ -216,7 +254,22 @@ const state = reactive({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
assistanceId: '',
|
assistanceId: '',
|
||||||
|
userList:[],
|
||||||
})
|
})
|
||||||
|
// state.loading = true;
|
||||||
|
const remoteMethod = createThrottle(async (query) => {
|
||||||
|
if (!query || query.trim() === '') {
|
||||||
|
state.userList = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await getInfo(query, { search_type: 'name' });
|
||||||
|
state.userList = res.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('搜索用户失败:', error);
|
||||||
|
state.userList = [];
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 树状列表筛选
|
* 树状列表筛选
|
||||||
@@ -231,11 +284,11 @@ const filterNode = (value, data) => {
|
|||||||
*/
|
*/
|
||||||
const searchList = () => {
|
const searchList = () => {
|
||||||
if (state.leftTab == 1) {
|
if (state.leftTab == 1) {
|
||||||
state.queryFrom.pageNum = 1
|
state.queryFrom.page = 1
|
||||||
state.dataList = []
|
state.dataList = []
|
||||||
|
state.queryFrom.participant_user_uids = state.participant_user.join(',')
|
||||||
getList()
|
getList()
|
||||||
} else {
|
} else {
|
||||||
console.log('treeRef.filter',state.treeRef)
|
|
||||||
state.treeRef.filter(state.queryFrom.nickName)
|
state.treeRef.filter(state.queryFrom.nickName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,7 +298,7 @@ const searchList = () => {
|
|||||||
*/
|
*/
|
||||||
const updateDetail = (item) => {
|
const updateDetail = (item) => {
|
||||||
if (state.leftTab == 1) {
|
if (state.leftTab == 1) {
|
||||||
state.assistanceId = item.assistanceId
|
state.assistanceId = item.id
|
||||||
emit('updateDetail', item)
|
emit('updateDetail', item)
|
||||||
} else {
|
} else {
|
||||||
if (item.uid) {
|
if (item.uid) {
|
||||||
@@ -257,12 +310,12 @@ const updateDetail = (item) => {
|
|||||||
/**
|
/**
|
||||||
* 触底加载
|
* 触底加载
|
||||||
*/
|
*/
|
||||||
// const infinite = () => {
|
const infinite = () => {
|
||||||
// if (state.more) {
|
if (state.more) {
|
||||||
// state.queryFrom.pageNum++
|
state.queryFrom.page++
|
||||||
// getList()
|
getList()
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 协作记录
|
* 协作记录
|
||||||
@@ -270,30 +323,50 @@ const updateDetail = (item) => {
|
|||||||
const getList = async () => {
|
const getList = async () => {
|
||||||
try {
|
try {
|
||||||
state.leftListLoading = true
|
state.leftListLoading = true
|
||||||
|
let query = deepClone(state.queryFrom)
|
||||||
|
// if (query.leftDatePicker?.length) {
|
||||||
|
// query.beginSignTime = query.leftDatePicker[0]
|
||||||
|
// query.endSignTime = query.leftDatePicker[1]
|
||||||
|
// }
|
||||||
|
|
||||||
let query = structuredClone(state.queryFrom)
|
|
||||||
if (query.leftDatePicker?.length) {
|
if (query.leftDatePicker?.length) {
|
||||||
query.beginSignTime = query.leftDatePicker[0]
|
const startTime = new Date(query.leftDatePicker[0]);
|
||||||
query.endSignTime = query.leftDatePicker[1]
|
const endTime = new Date(query.leftDatePicker[1]);
|
||||||
|
|
||||||
|
// 添加日期有效性验证
|
||||||
|
if (!isNaN(startTime.getTime())) {
|
||||||
|
query.start_time = Math.floor(startTime.getTime());
|
||||||
|
} else {
|
||||||
|
console.error('开始时间格式无效:', query.leftDatePicker[0]);
|
||||||
|
query.start_time = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isNaN(endTime.getTime())) {
|
||||||
|
query.end_time = Math.floor(endTime.getTime());
|
||||||
|
} else {
|
||||||
|
console.error('结束时间格式无效:', query.leftDatePicker[1]);
|
||||||
|
query.end_time = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delete query.leftDatePicker
|
delete query.leftDatePicker
|
||||||
|
const userData = await getUserInfo()
|
||||||
let infoData = await getAssistanceList({ ...query })
|
if(!userData) return
|
||||||
|
let infoData = await getParticipantsHistoryApi( userData?.uid ,{ ...query })
|
||||||
state.dataList = infoData.rows.length
|
state.dataList = infoData.data.history?.length
|
||||||
? state.dataList.concat(infoData.rows)
|
? state.dataList.concat(infoData.data.history)
|
||||||
: []
|
: []
|
||||||
|
|
||||||
if (state.isFirst) {
|
if (state.isFirst) {
|
||||||
emit('updateDetail', state.dataList.length ? state.dataList[0] : null)
|
emit('updateDetail', state.dataList?.length ? state.dataList[0] : null)
|
||||||
state.assistanceId = state.dataList.length
|
state.assistanceId = state.dataList?.length
|
||||||
? state.dataList[0].assistanceId
|
? state.dataList[0].id
|
||||||
: ''
|
: ''
|
||||||
state.isFirst = false
|
state.isFirst = false
|
||||||
}
|
}
|
||||||
|
|
||||||
state.more = state.dataList.length < infoData.total
|
state.more = state.dataList?.length < infoData.data.total
|
||||||
state.isShow = Boolean(state.dataList.length)
|
state.isShow = Boolean(state.dataList?.length)
|
||||||
state.leftListLoading = false
|
state.leftListLoading = false
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
@@ -305,42 +378,52 @@ const getList = async () => {
|
|||||||
* 通讯录 人员信息树
|
* 通讯录 人员信息树
|
||||||
*/
|
*/
|
||||||
const HandleLoadNode = async (node, resolve) => {
|
const HandleLoadNode = async (node, resolve) => {
|
||||||
if(node.level === 0){
|
if(node?.level === 0){
|
||||||
loadNode(resolve)
|
loadNode(resolve,'',node?.level)
|
||||||
}else if(node.level === 1){
|
}else if(node?.level > 0){
|
||||||
loadNode(resolve,node.data.directory_uid)
|
if(node.data.directory_uid){
|
||||||
|
loadUserNode(resolve,node.data.directory_uid,node?.level)
|
||||||
|
}else{
|
||||||
|
resolve(resolve)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadNode = async(resolve,id)=>{
|
const loadNode = async(resolve,id,level)=>{
|
||||||
try {
|
try {
|
||||||
// state.leftListLoading = true
|
state.leftListLoading = true
|
||||||
if(!id){
|
|
||||||
let res = await getDirectories({level:1})
|
let res = await getDirectories({level:1})
|
||||||
resolve(res.data)
|
if(res.meta.code == 200){
|
||||||
}else{
|
|
||||||
let res = await getDirectoriesUsers(id,{directory_uuid:id})
|
|
||||||
resolve(res.data)
|
resolve(res.data)
|
||||||
}
|
}
|
||||||
// state.leftListLoading = false
|
state.leftListLoading = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
// state.leftListLoading = false
|
state.leftListLoading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// try {
|
const loadUserNode = async(resolve,id,level)=>{
|
||||||
// if (userList.data.sub_units?.length) {
|
try {
|
||||||
// state.dataList = userList.data.sub_units
|
state.leftListLoading = true
|
||||||
// let user = getUser(state.dataList)
|
let userData = []
|
||||||
// emit('updateDetail', user)
|
let orgData = []
|
||||||
// } else {
|
const resOrg = await getDirectories({level: 1,parent_uuid:id})
|
||||||
// emit('updateDetail', null)
|
if(resOrg?.data){
|
||||||
// }
|
orgData = resOrg.data
|
||||||
// state.leftListLoading = false
|
}
|
||||||
// }
|
if(id){
|
||||||
|
const res = await getDirectoriesUsers(id,{directory_uuid:id})
|
||||||
|
userData = res.data
|
||||||
|
}
|
||||||
|
resolve([...orgData, ...userData])
|
||||||
|
state.leftListLoading = false
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
state.leftListLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getUser = (list) => {
|
const getUser = (list) => {
|
||||||
for (let i = 0; i < list.length; i++) {
|
for (let i = 0; i < list.length; i++) {
|
||||||
@@ -375,9 +458,10 @@ watch(
|
|||||||
state.dataList = []
|
state.dataList = []
|
||||||
state.isFirst = true
|
state.isFirst = true
|
||||||
state.queryFrom = {
|
state.queryFrom = {
|
||||||
pageNum: 1,
|
page_size: 10,
|
||||||
pageSize: 10,
|
page: 1,
|
||||||
nickName: '',
|
nickName: '',
|
||||||
|
participant_user_uids:'',
|
||||||
leftDatePicker: null,
|
leftDatePicker: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +477,7 @@ watch(
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
state.dataList = []
|
state.dataList = []
|
||||||
state.isFirst = true
|
state.isFirst = true
|
||||||
// getList()
|
getList()
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -402,7 +486,7 @@ onMounted(() => {
|
|||||||
const {
|
const {
|
||||||
isFirst, leftTab, queryFrom, leftListLoading, loading,
|
isFirst, leftTab, queryFrom, leftListLoading, loading,
|
||||||
dataList, more, isShow, shortcuts, weekName,
|
dataList, more, isShow, shortcuts, weekName,
|
||||||
treeRef, treeProps, assistanceId
|
treeRef, treeProps, assistanceId,userList,participant_user
|
||||||
} = toRefs(state)
|
} = toRefs(state)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -430,8 +514,8 @@ const {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 50px;
|
width: 40px;
|
||||||
height: 50px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -447,7 +531,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,4 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div>
|
||||||
<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">
|
||||||
@@ -11,9 +12,8 @@
|
|||||||
<el-col :xs="24" :sm="24" :md="16" :lg="18">
|
<el-col :xs="24" :sm="24" :md="16" :lg="18">
|
||||||
<div
|
<div
|
||||||
class="right-content"
|
class="right-content"
|
||||||
|
v-loading='isShowLoading'
|
||||||
>
|
>
|
||||||
<!-- v-loading="!detail?.appId && !detail?.userId && isShow" -->
|
|
||||||
<div class="right-content-title">
|
<div class="right-content-title">
|
||||||
{{ tabValue == 1 ? '协作信息' : '员工信息' }}
|
{{ tabValue == 1 ? '协作信息' : '员工信息' }}
|
||||||
</div>
|
</div>
|
||||||
@@ -24,35 +24,37 @@
|
|||||||
<div class="agency-detail-cont-item">
|
<div class="agency-detail-cont-item">
|
||||||
<span class="agency-detail-item-title">发起人</span>
|
<span class="agency-detail-item-title">发起人</span>
|
||||||
<span class="agency-detail-item-content">
|
<span class="agency-detail-item-content">
|
||||||
{{ detail.initiatorName }}
|
{{ detail.initiator }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="agency-detail-cont-item">
|
<div class="agency-detail-cont-item">
|
||||||
<span class="agency-detail-item-title">协作时间</span>
|
<span class="agency-detail-item-title">协作时间</span>
|
||||||
<span class="agency-detail-item-content">
|
<span class="agency-detail-item-content">
|
||||||
<!-- {{
|
{{
|
||||||
parseTime(detail.beginTime, '{y}年{m}月{d}日') +
|
detail.created_at ?
|
||||||
|
parseTime(detail.created_at, '{y}年{m}月{d}日') +
|
||||||
' ' +
|
' ' +
|
||||||
weekName[new Date(detail.beginTime).getDay()] +
|
weekName[new Date(detail.created_at).getDay()] +
|
||||||
' ' +
|
' ' +
|
||||||
parseTime(detail.beginTime, '{h}:{i}')
|
parseTime(detail.created_at, '{h}:{i}')
|
||||||
}} -->
|
: '暂无'
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="agency-detail-cont-item">
|
<div class="agency-detail-cont-item">
|
||||||
<span class="agency-detail-item-title">成员</span>
|
<span class="agency-detail-item-title">成员</span>
|
||||||
<span
|
<span
|
||||||
class="agency-detail-item-content"
|
class="agency-detail-item-content"
|
||||||
v-if="detail?.assistanceMemberList?.length"
|
v-if="detail?.all_participants?.length"
|
||||||
style="display: flex; flex-wrap: wrap"
|
style="display: flex; flex-wrap: wrap"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-for="(items, indexs) in detail.assistanceMemberList"
|
v-for="(items, indexs) in detail.all_participants"
|
||||||
:key="indexs"
|
:key="indexs"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
items.nickName +
|
items.display_name +
|
||||||
(indexs + 1 == detail.assistanceMemberList.length
|
(indexs + 1 == detail.all_participants.length
|
||||||
? ''
|
? ''
|
||||||
: '、')
|
: '、')
|
||||||
}}
|
}}
|
||||||
@@ -62,7 +64,7 @@
|
|||||||
<div class="agency-detail-cont-item">
|
<div class="agency-detail-cont-item">
|
||||||
<span class="agency-detail-item-title">协作时长</span>
|
<span class="agency-detail-item-title">协作时长</span>
|
||||||
<span class="agency-detail-item-content">
|
<span class="agency-detail-item-content">
|
||||||
<!-- {{ getTime() }} -->
|
{{ getTime() }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,16 +77,11 @@
|
|||||||
<div class="file-video-bottom">
|
<div class="file-video-bottom">
|
||||||
<!-- autoplay="autoplay" -->
|
<!-- autoplay="autoplay" -->
|
||||||
<video
|
<video
|
||||||
v-if="
|
v-if="detail.remoteVideoFile?.storage_url"
|
||||||
detail.remoteVideoFile?.prefix &&
|
:src="detail.remoteVideoFile.storage_url"
|
||||||
detail.remoteVideoFile.path
|
|
||||||
"
|
|
||||||
:src="
|
|
||||||
detail.remoteVideoFile.prefix +
|
|
||||||
detail.remoteVideoFile.path
|
|
||||||
"
|
|
||||||
id="videoPlayer"
|
id="videoPlayer"
|
||||||
loop
|
loop
|
||||||
|
autoplay
|
||||||
controls
|
controls
|
||||||
></video>
|
></video>
|
||||||
<div v-else class="video-null">暂无视频</div>
|
<div v-else class="video-null">暂无视频</div>
|
||||||
@@ -115,10 +112,18 @@
|
|||||||
<div class="file-list-item-icon"></div>
|
<div class="file-list-item-icon"></div>
|
||||||
<div class="file-list-item-text">
|
<div class="file-list-item-text">
|
||||||
<div class="list-item-text text-out-of-hiding-1">
|
<div class="list-item-text text-out-of-hiding-1">
|
||||||
{{ item.name }}
|
{{ item.file_name }}
|
||||||
</div>
|
</div>
|
||||||
|
<el-icon
|
||||||
|
:size="18"
|
||||||
|
color="#0d74ff"
|
||||||
|
style="cursor: pointer"
|
||||||
|
@click="handlePreview(item)"
|
||||||
|
>
|
||||||
|
<View />
|
||||||
|
</el-icon>
|
||||||
<el-link
|
<el-link
|
||||||
:href="item.prefix + item.path"
|
:href="item.source_url"
|
||||||
type="primary"
|
type="primary"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:underline="false"
|
:underline="false"
|
||||||
@@ -131,14 +136,6 @@
|
|||||||
<Download />
|
<Download />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
</el-link>
|
</el-link>
|
||||||
<el-icon
|
|
||||||
:size="18"
|
|
||||||
color="#FF4646"
|
|
||||||
style="cursor: pointer"
|
|
||||||
@click="clickDeleteFile(item)"
|
|
||||||
>
|
|
||||||
<Delete />
|
|
||||||
</el-icon>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -152,6 +149,7 @@
|
|||||||
class="message-user"
|
class="message-user"
|
||||||
v-else-if="isShow && tabValue == 2"
|
v-else-if="isShow && tabValue == 2"
|
||||||
style="height: calc(100vh - 90px)"
|
style="height: calc(100vh - 90px)"
|
||||||
|
v-loading="userLoading"
|
||||||
>
|
>
|
||||||
<div class="message-user-card">
|
<div class="message-user-card">
|
||||||
<div class="user-card-nickName">
|
<div class="user-card-nickName">
|
||||||
@@ -205,7 +203,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>
|
||||||
@@ -218,7 +216,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-dialog
|
<!-- <el-dialog
|
||||||
v-model="inviteDialog"
|
v-model="inviteDialog"
|
||||||
title="远程协作"
|
title="远程协作"
|
||||||
width="400px"
|
width="400px"
|
||||||
@@ -226,7 +224,7 @@
|
|||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
:show-close="false"
|
:show-close="false"
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<div style="width: 100%; margin-bottom: 30px; font-size: 20px">
|
<div style="width: 100%; margin-bottom: 30px; font-size: 20px">
|
||||||
"
|
"
|
||||||
{{
|
{{
|
||||||
@@ -254,21 +252,27 @@
|
|||||||
加 入
|
加 入
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
</el-dialog> -->
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
<!-- 文件预览 -->
|
||||||
|
<BrowseFile ref="browseFileRef" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onActivated, onMounted, reactive, toRefs, watch, getCurrentInstance } from 'vue'
|
import { onActivated, onMounted, reactive, toRefs, watch, getCurrentInstance ,ref} from 'vue'
|
||||||
import leftTab from './components/leftTab/index.vue'
|
import leftTab from './components/leftTab/index.vue'
|
||||||
|
import AssistWx from './components/assistWx/index.vue'
|
||||||
|
import BrowseFile from '@/views/conferencingRoom/components/fileUpload/browseFile.vue'
|
||||||
import { getInfo } from '@/api/login.js'
|
import { getInfo } from '@/api/login.js'
|
||||||
import { getStatusApi } from '@/api/conferencingRoom.js'
|
import { getStatusApi ,getFileListApi ,getvideoUrlApi} from '@/api/conferencingRoom.js'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useRoomStore } from '@/stores/modules/room'
|
import { useRoomStore } from '@/stores/modules/room'
|
||||||
import useUserStore from '@/stores/modules/user'
|
import { useUserStore } from '@/stores/modules/user.js'
|
||||||
import { mqttClient } from "@/utils/mqtt.js";
|
import { mqttClient } from "@/utils/mqtt.js";
|
||||||
|
import { getToken ,getUserInfo} from '@/utils/auth.js';
|
||||||
|
import { deepClone,parseTime } from '@/utils/ruoyi.js'
|
||||||
const roomStore = useRoomStore()
|
const roomStore = useRoomStore()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -276,8 +280,9 @@ const { proxy } = getCurrentInstance()
|
|||||||
const state = reactive({
|
const state = reactive({
|
||||||
detail: {},
|
detail: {},
|
||||||
weekName: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
|
weekName: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
|
||||||
tabValue: 2,
|
tabValue: 1,
|
||||||
isShow: true,
|
isShow: true,
|
||||||
|
isShowLoading: false,
|
||||||
load: false,
|
load: false,
|
||||||
loadText: '数据加载中',
|
loadText: '数据加载中',
|
||||||
isLinkKnow: 'F',
|
isLinkKnow: 'F',
|
||||||
@@ -285,6 +290,9 @@ const state = reactive({
|
|||||||
inviteDialog: false,
|
inviteDialog: false,
|
||||||
cooperation: import.meta.env.VITE_APP_COOPERATION_TYPE,
|
cooperation: import.meta.env.VITE_APP_COOPERATION_TYPE,
|
||||||
})
|
})
|
||||||
|
const userLoading = ref(false); // 用户信息加载状态
|
||||||
|
//文件预览
|
||||||
|
const browseFileRef = ref(null);
|
||||||
|
|
||||||
const isEmptyObject = (obj) => {
|
const isEmptyObject = (obj) => {
|
||||||
return !obj || Object.keys(obj).length === 0
|
return !obj || Object.keys(obj).length === 0
|
||||||
@@ -294,10 +302,12 @@ const isEmptyObject = (obj) => {
|
|||||||
const clickInitiate = () => {
|
const clickInitiate = () => {
|
||||||
let userData = null
|
let userData = null
|
||||||
try {
|
try {
|
||||||
userData = JSON.parse(localStorage.getItem('userData')) || null
|
userData = JSON.parse(sessionStorage.getItem('userData')) || null
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('解析 userData 失败:', e)
|
console.error('解析 userData 失败:', e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (isEmptyObject(state.detail)) {
|
if (isEmptyObject(state.detail)) {
|
||||||
ElMessage({
|
ElMessage({
|
||||||
message: '请先选择人员',
|
message: '请先选择人员',
|
||||||
@@ -314,6 +324,7 @@ const clickInitiate = () => {
|
|||||||
}
|
}
|
||||||
roomStore.setUserUid(userData.uid)
|
roomStore.setUserUid(userData.uid)
|
||||||
roomStore.setDetailUid(state.detail.uid)
|
roomStore.setDetailUid(state.detail.uid)
|
||||||
|
roomStore.setDetailName(state.detail.name)
|
||||||
router.push({
|
router.push({
|
||||||
path: '/conferencingRoom',
|
path: '/conferencingRoom',
|
||||||
query:{
|
query:{
|
||||||
@@ -330,29 +341,48 @@ const updateTab = (newValue) => {
|
|||||||
|
|
||||||
/** 修改展示区内容 */
|
/** 修改展示区内容 */
|
||||||
const updateDetail = async (details) => {
|
const updateDetail = async (details) => {
|
||||||
|
userLoading.value = true
|
||||||
if (details) {
|
if (details) {
|
||||||
console.log(details,'details')
|
|
||||||
state.detail = {}
|
state.detail = {}
|
||||||
if (state.tabValue == 1) {
|
if (state.tabValue == 1) {
|
||||||
state.isShow = true
|
state.isShowLoading = true
|
||||||
|
getTheFileList(details)
|
||||||
} else {
|
} else {
|
||||||
const res = await getInfo(details.uid)
|
const res = await getInfo(details.uid)
|
||||||
state.detail = res.data
|
state.detail = res.data
|
||||||
// getInfo(details.uid)
|
userLoading.value = false
|
||||||
// .then((res) => {
|
|
||||||
// console.log(res,'人员详细信息')
|
|
||||||
// })
|
|
||||||
// .finally(() => { state.isShow = true })
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
state.isShow = false
|
userLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTheFileList = async (details) => {
|
||||||
|
try {
|
||||||
|
let detail = deepClone(details);
|
||||||
|
const [fileResponse, videoResponse] = await Promise.all([
|
||||||
|
getFileListApi(details.room_uid),
|
||||||
|
getvideoUrlApi(details.room_uid)
|
||||||
|
]);
|
||||||
|
const processedDetail = {
|
||||||
|
...details,
|
||||||
|
fileList: fileResponse.data?.files || [],
|
||||||
|
remoteVideoFile: videoResponse.data?.recordings?.[0] || {},
|
||||||
|
initiator: details.all_participants?.find(item =>
|
||||||
|
item.participant_role === 'moderator' // 使用严格相等
|
||||||
|
)?.display_name || '未知发起人'
|
||||||
|
};
|
||||||
|
state.detail = processedDetail;
|
||||||
|
state.isShowLoading = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取文件列表失败:', error);
|
||||||
|
// 可以根据需要添加错误处理逻辑
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取通话时长 */
|
/** 获取通话时长 */
|
||||||
const getTime = () => {
|
const getTime = () => {
|
||||||
let begin = new Date(state.detail.beginTime).getTime()
|
let begin = new Date(state.detail.created_at).getTime()
|
||||||
let end = new Date(state.detail.endTime).getTime()
|
let end = new Date(state.detail.updated_at).getTime()
|
||||||
if (begin && end) {
|
if (begin && end) {
|
||||||
let diff = end - begin
|
let diff = end - begin
|
||||||
const h = Math.floor(diff / (1000 * 60 * 60))
|
const h = Math.floor(diff / (1000 * 60 * 60))
|
||||||
@@ -366,77 +396,92 @@ const getTime = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 加入会议 */
|
//文件预览
|
||||||
const clickJoin = async () => {
|
function handlePreview(file) {
|
||||||
const res = await getStatusApi(state.socketInformation.room_uid,{status:1})
|
if (!file.preview_url) {
|
||||||
if(res.meta.code == 200){
|
ElMessage.error('文件链接无效');
|
||||||
ElMessage({
|
return;
|
||||||
message: '成功加入该协作',
|
|
||||||
type: 'success',
|
|
||||||
})
|
|
||||||
state.inviteDialog = false
|
|
||||||
router.push({
|
|
||||||
path: '/conferencingRoom',
|
|
||||||
query:{
|
|
||||||
type:2,//创建房间,加入房间 2
|
|
||||||
room_uid:state.socketInformation.room_uid
|
|
||||||
}
|
}
|
||||||
})
|
browseFileRef.value.showEdit(file)
|
||||||
}
|
|
||||||
state.inviteDialog = false
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 加入会议 */
|
||||||
|
// const clickJoin = async () => {
|
||||||
|
// const res = await getStatusApi(state.socketInformation.room_uid,{status:1})
|
||||||
|
// if(res.meta.code == 200){
|
||||||
|
// ElMessage({
|
||||||
|
// message: '成功加入该协作',
|
||||||
|
// type: 'success',
|
||||||
|
// })
|
||||||
|
// state.inviteDialog = false
|
||||||
|
// router.push({
|
||||||
|
// path: '/conferencingRoom',
|
||||||
|
// query:{
|
||||||
|
// type:2,//创建房间,加入房间 2
|
||||||
|
// room_uid:state.socketInformation.room_uid
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// state.inviteDialog = false
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
/** 拒绝加入 */
|
/** 拒绝加入 */
|
||||||
const clickRefuseJoin = async () => {
|
// const clickRefuseJoin = async () => {
|
||||||
//status 1: 同意加入, 5: 拒绝加入
|
// //status 1: 同意加入, 5: 拒绝加入
|
||||||
const res = await getStatusApi(state.socketInformation.room_uid,{status:5})
|
// const res = await getStatusApi(state.socketInformation.room_uid,{status:5})
|
||||||
if(res.meta.code == 200){
|
// if(res.meta.code == 200){
|
||||||
ElMessage({
|
// ElMessage({
|
||||||
message: '已拒绝加入该协作',
|
// message: '已拒绝加入该协作',
|
||||||
type: 'error',
|
// type: 'error',
|
||||||
})
|
// })
|
||||||
state.inviteDialog = false
|
// state.inviteDialog = false
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
/** 处理 mqtt 消息 */
|
/** 处理加入房间和拒接房间 mqtt 消息 */
|
||||||
const processingSocket = (message) => {
|
// const processingSocket = (message) => {
|
||||||
if (message) {
|
// const res = JSON.parse(message)
|
||||||
state.socketInformation = JSON.parse(message)
|
// if (!res?.status) {
|
||||||
console.log(state.socketInformation,'state.socketInformation')
|
// state.socketInformation = res
|
||||||
state.inviteDialog = true
|
// state.inviteDialog = true
|
||||||
showNotification(state.socketInformation)
|
// showNotification(state.socketInformation)
|
||||||
}
|
// }else if(res.status == 5){
|
||||||
}
|
// ElMessage({
|
||||||
|
// message: `${res?.display_name}拒绝加入该协作`,
|
||||||
|
// type: 'error',
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
/** 浏览器通知 */
|
/** 浏览器通知 */
|
||||||
const showNotification = (data) => {
|
// const showNotification = (data) => {
|
||||||
if ('Notification' in window) {
|
// if ('Notification' in window) {
|
||||||
Notification.requestPermission().then((permission) => {
|
// Notification.requestPermission().then((permission) => {
|
||||||
if (permission === 'granted') {
|
// if (permission === 'granted') {
|
||||||
const notification = new Notification('协作邀请', {
|
// const notification = new Notification('协作邀请', {
|
||||||
// body: String(data.room_name) + '邀请您参加远程协作'
|
// // body: String(data.room_name) + '邀请您参加远程协作'
|
||||||
body: '远程协作有新的邀请'
|
// body: '远程协作有新的邀请'
|
||||||
// icon: logo,
|
// // icon: logo,
|
||||||
})
|
// })
|
||||||
notification.onclick = () => { clickJoin() }
|
// notification.onclick = () => { clickJoin() }
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
// 暴露给模板
|
// 暴露给模板
|
||||||
const { detail, weekName, tabValue, isShow, load, loadText, isLinkKnow, socketInformation, inviteDialog, cooperation } = toRefs(state)
|
const { detail, weekName, tabValue, isShow, load, loadText, isLinkKnow, socketInformation, inviteDialog, cooperation,isShowLoading } = toRefs(state)
|
||||||
onMounted(async () => {
|
// onMounted(async () => {
|
||||||
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}`;
|
||||||
mqttClient.subscribe(topic, async (shapeData) => {
|
// mqttClient.subscribe(topic, async (shapeData) => {
|
||||||
processingSocket(shapeData.toString())
|
// // console.log(shapeData.toString(),'shapeData发送邀请')
|
||||||
});
|
// processingSocket(shapeData.toString())
|
||||||
})
|
// });
|
||||||
|
// })
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -451,6 +496,7 @@ onMounted(async () => {
|
|||||||
.app-container {
|
.app-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
// margin: 0 17px;
|
// margin: 0 17px;
|
||||||
|
// height: calc(100vh - 50px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-null {
|
.message-null {
|
||||||
@@ -468,7 +514,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;
|
||||||
}
|
}
|
||||||
@@ -576,8 +622,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,15 +1,10 @@
|
|||||||
<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" round plain @click="joinWhiteboard">
|
<el-button type="primary" size="large" link @click="joinWhiteboard">
|
||||||
加入互动画板
|
正在进入互动画板。。。
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -27,15 +22,23 @@ import { ref, nextTick, onUnmounted, onMounted } from "vue";
|
|||||||
import { ElLoading, ElMessage } from "element-plus";
|
import { ElLoading, ElMessage } from "element-plus";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
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";
|
||||||
|
|
||||||
const showLogin = ref(false); // 是否显示登录页面
|
const props = defineProps({
|
||||||
|
roomId: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const hasJoined = ref(false); // 是否加入白板
|
const hasJoined = ref(false); // 是否加入白板
|
||||||
const canvas = ref(null);
|
const canvas = ref(null);
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -51,7 +54,7 @@ async function joinWhiteboard() {
|
|||||||
try {
|
try {
|
||||||
const clientId = `whiteboard-${uuidv4()}`;
|
const clientId = `whiteboard-${uuidv4()}`;
|
||||||
await mqttClient.connect(clientId);
|
await mqttClient.connect(clientId);
|
||||||
console.log("✅ 已连接 MQTT:", clientId);
|
// console.log("✅ 已连接 MQTT:", clientId);
|
||||||
|
|
||||||
hasJoined.value = true;
|
hasJoined.value = true;
|
||||||
|
|
||||||
@@ -68,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");
|
||||||
@@ -80,55 +78,17 @@ function initWhiteboard() {
|
|||||||
console.error("⚠️ 找不到 canvas 元素");
|
console.error("⚠️ 找不到 canvas 元素");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置画布宽高(保持16:9)
|
|
||||||
const { width, height } = getCanvasSize(el.parentElement);
|
|
||||||
Object.assign(el, { width, height });
|
|
||||||
Object.assign(el.style, {
|
|
||||||
width: `${width}px`,
|
|
||||||
height: `${height}px`,
|
|
||||||
border: "2px solid #000",
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化画布
|
// 初始化画布
|
||||||
canvas.value = new Canvas("whiteboard");
|
canvas.value = new Canvas("whiteboard");
|
||||||
|
|
||||||
// 获取房间号
|
// 获取房间号
|
||||||
const roomUid = route.query.room_uid || "default-room";
|
const roomUid = route.query.room_uid || props.roomId || "default-room";
|
||||||
|
|
||||||
// 初始化多人同步
|
// 初始化多人同步
|
||||||
WhiteboardSync.init(canvas.value, roomUid);
|
WhiteboardSync.init(canvas.value, roomUid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 计算画布大小,保持16:9 */
|
|
||||||
function getCanvasSize(container) {
|
|
||||||
const containerWidth = container.offsetWidth;
|
|
||||||
const containerHeight = container.offsetHeight;
|
|
||||||
|
|
||||||
let width = containerWidth;
|
|
||||||
let height = Math.floor((width * 9) / 16);
|
|
||||||
|
|
||||||
if (height > containerHeight) {
|
|
||||||
height = containerHeight;
|
|
||||||
width = Math.floor((height * 16) / 9);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { width, height };
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
joinWhiteboard()
|
||||||
showLogin.value = false;
|
|
||||||
const res = await getInfo("self");
|
|
||||||
console.log(res, "用户信息校验")
|
|
||||||
if (res.meta.code === 401) {
|
|
||||||
showLogin.value = true;
|
|
||||||
} else {
|
|
||||||
showLogin.value = false;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("⚠️ 用户信息校验失败:", err);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -136,17 +96,16 @@ onUnmounted(() => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 外层容器全屏居中 */
|
/* 外层容器全屏居中 */
|
||||||
.wrapper-content {
|
.wrapper-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100vw;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
position: relative;
|
position: relative; /* 关键:为绝对定位子元素提供参照 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 登录按钮容器居中 */
|
/* 登录按钮容器居中 */
|
||||||
@@ -159,9 +118,8 @@ onUnmounted(() => {
|
|||||||
/* 白板容器 */
|
/* 白板容器 */
|
||||||
.whiteboard-wrapper {
|
.whiteboard-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 90vw;
|
width: 72vw;
|
||||||
max-width: 1280px;
|
height: 69vh;
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -176,11 +134,11 @@ onUnmounted(() => {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 工具栏左侧垂直居中 */
|
/* 工具栏固定在 wrapper-content 内 */
|
||||||
.toolbox {
|
.toolbox {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 10px;
|
left: 40px; /* 距离左侧的间距 */
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|||||||
185
src/views/custom/tabulaRase/index_old.vue
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<template>
|
||||||
|
<div class="wrapper-content">
|
||||||
|
<div v-if="showLogin">
|
||||||
|
<!-- 登录界面 -->
|
||||||
|
<Login @loginSuccess="handleLoginSuccess" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<!-- 未加入时显示按钮 -->
|
||||||
|
<div v-if="!hasJoined" class="login-button-container">
|
||||||
|
<el-button type="primary" size="large" round plain @click="joinWhiteboard">
|
||||||
|
加入互动画板
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已加入时显示白板 -->
|
||||||
|
<div v-else class="whiteboard-wrapper">
|
||||||
|
<ToolBox v-if="canvas" class="toolbox" :canvas="canvas" />
|
||||||
|
<canvas id="whiteboard" class="whiteboard-canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, nextTick, onUnmounted, onMounted } from "vue";
|
||||||
|
import { ElLoading, ElMessage } from "element-plus";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { mqttClient } from "@/utils/mqtt";
|
||||||
|
import { WhiteboardSync } from "@/utils/whiteboardSync";
|
||||||
|
import ToolBox from "@/components/ToolBox/index.vue";
|
||||||
|
import Login from "@/components/Login/index.vue";
|
||||||
|
import Canvas from "@/core/index.js";
|
||||||
|
import { getInfo } from "@/api/login";
|
||||||
|
|
||||||
|
const showLogin = ref(false); // 是否显示登录页面
|
||||||
|
const hasJoined = ref(false); // 是否加入白板
|
||||||
|
const canvas = ref(null);
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
/** 进入白板 */
|
||||||
|
async function joinWhiteboard() {
|
||||||
|
const loading = ElLoading.service({
|
||||||
|
lock: true,
|
||||||
|
text: "正在进入互动画板...",
|
||||||
|
background: "rgba(0, 0, 0, 0.4)",
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const clientId = `whiteboard-${uuidv4()}`;
|
||||||
|
await mqttClient.connect(clientId);
|
||||||
|
console.log("✅ 已连接 MQTT:", clientId);
|
||||||
|
|
||||||
|
hasJoined.value = true;
|
||||||
|
|
||||||
|
// 等待 DOM 更新后再初始化画布
|
||||||
|
await nextTick();
|
||||||
|
initWhiteboard();
|
||||||
|
|
||||||
|
ElMessage.success("已进入互动画板 🎉");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ 连接失败:", err);
|
||||||
|
ElMessage.error("连接白板失败,请重试");
|
||||||
|
} finally {
|
||||||
|
loading.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 登录成功回调 */
|
||||||
|
function handleLoginSuccess() {
|
||||||
|
showLogin.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化白板 */
|
||||||
|
function initWhiteboard() {
|
||||||
|
const el = document.getElementById("whiteboard");
|
||||||
|
if (!el) {
|
||||||
|
console.error("⚠️ 找不到 canvas 元素");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置画布宽高(保持16:9)
|
||||||
|
const { width, height } = getCanvasSize(el.parentElement);
|
||||||
|
Object.assign(el, { width, height });
|
||||||
|
Object.assign(el.style, {
|
||||||
|
width: `${width}px`,
|
||||||
|
height: `${height}px`,
|
||||||
|
border: "2px solid #000",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化画布
|
||||||
|
canvas.value = new Canvas("whiteboard");
|
||||||
|
|
||||||
|
// 获取房间号
|
||||||
|
const roomUid = route.query.room_uid || "default-room";
|
||||||
|
|
||||||
|
// 初始化多人同步
|
||||||
|
WhiteboardSync.init(canvas.value, roomUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 计算画布大小,保持16:9 */
|
||||||
|
function getCanvasSize(container) {
|
||||||
|
const containerWidth = container.offsetWidth;
|
||||||
|
const containerHeight = container.offsetHeight;
|
||||||
|
|
||||||
|
let width = containerWidth;
|
||||||
|
let height = Math.floor((width * 9) / 16);
|
||||||
|
|
||||||
|
if (height > containerHeight) {
|
||||||
|
height = containerHeight;
|
||||||
|
width = Math.floor((height * 16) / 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (canvas.value) canvas.value.destroy();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 外层容器全屏居中 */
|
||||||
|
.wrapper-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: #fff;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录按钮容器居中 */
|
||||||
|
.login-button-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 白板容器 */
|
||||||
|
.whiteboard-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 90vw;
|
||||||
|
max-width: 1280px;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 画布占满白板容器 */
|
||||||
|
.whiteboard-canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 工具栏左侧垂直居中 */
|
||||||
|
.toolbox {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 10px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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">
|
||||||
@@ -37,22 +31,26 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import useUserStore from '@/stores/modules/user'
|
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 } from 'element-plus'
|
import { ElNotification,ElMessage } from 'element-plus'
|
||||||
import { getInfo } from "@/api/login";
|
import CryptoJS from 'crypto-js';
|
||||||
|
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 redirect = ref(undefined);
|
const redirect = ref(undefined);
|
||||||
const loginView = ref(false)
|
const loginView = ref(true)
|
||||||
|
|
||||||
watch(() => router.currentRoute.value, (newRoute) => {
|
// 监听路由变化,获取重定向参数
|
||||||
|
watch(() => route, (newRoute) => {
|
||||||
redirect.value = newRoute.query && newRoute.query.redirect;
|
redirect.value = newRoute.query && newRoute.query.redirect;
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
@@ -69,26 +67,31 @@ 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
|
||||||
|
if(!localStorage?.getItem('UDID')){
|
||||||
|
ElMessage({
|
||||||
|
message: '服务错误,请刷新页面',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const secretKey = ((loginForm.value.username + localStorage?.getItem('UDID')).toLowerCase()).replaceAll('-', ''); // 用户名+UDID(32位16进制,全小写)
|
||||||
|
const randomChars = generateRandomChars(6);
|
||||||
|
const message = `Gx${randomChars}${loginForm.value.password}`;
|
||||||
|
const ciphertext = CryptoJS.Blowfish.encrypt(message, secretKey).toString();
|
||||||
// 调用action的登录方法
|
// 调用action的登录方法
|
||||||
userStore
|
userStore
|
||||||
.login({
|
.login({
|
||||||
password: loginForm.value.password,
|
// password: loginForm.value.password,
|
||||||
|
password: ciphertext,
|
||||||
username: loginForm.value.username,
|
username: loginForm.value.username,
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
const userInfo = JSON.parse(localStorage.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)
|
||||||
@@ -99,6 +102,51 @@ function handleLogin() {
|
|||||||
requestNotificationPermission()
|
requestNotificationPermission()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理登录成功后的跳转逻辑
|
||||||
|
*/
|
||||||
|
async function handleLoginSuccess() {
|
||||||
|
try {
|
||||||
|
// 如果有重定向路径且不是登录页,则跳转到重定向页面
|
||||||
|
if (redirect.value && redirect.value !== '/login') {
|
||||||
|
|
||||||
|
// 确保路由存在,如果不存在则跳转到默认页面
|
||||||
|
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) {
|
||||||
|
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 请求浏览器的通知权限
|
* @description 请求浏览器的通知权限
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
@@ -113,27 +161,7 @@ function requestNotificationPermission() {
|
|||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
meterStore.initUdid()
|
||||||
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
@@ -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>
|
||||||
@@ -1,18 +1,15 @@
|
|||||||
import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig ,loadEnv} from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import { codeInspectorPlugin } from 'code-inspector-plugin'
|
import createVitePlugins from './vite/plugins/index.js'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode, command }) => {
|
||||||
plugins: [
|
const env = loadEnv(mode, process.cwd());
|
||||||
vue(),
|
const { VITE_BASE_PATH } = env;
|
||||||
codeInspectorPlugin({
|
return {
|
||||||
bundler: 'vite', // 使用 vite
|
plugins: createVitePlugins(env, command === "build"),
|
||||||
showSwitch: true,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0', // 关键配置,允许局域网访问
|
host: '0.0.0.0', // 关键配置,允许局域网访问
|
||||||
port: 3000,
|
port: 3000,
|
||||||
@@ -20,14 +17,14 @@ export default defineConfig({
|
|||||||
hmr: { overlay: false },
|
hmr: { overlay: false },
|
||||||
proxy: {
|
proxy: {
|
||||||
'/dev-api': {
|
'/dev-api': {
|
||||||
target: 'https://xsynergy.gxtech.ltd', // 从环境变量读取
|
target: 'https://xsynergy.gxtech.ltd',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
ws: true,
|
ws: true,
|
||||||
rewrite: (path) =>
|
rewrite: (path) =>
|
||||||
path.replace(new RegExp(`^/dev-api`), '')
|
path.replace(new RegExp(`^/dev-api`), '')
|
||||||
},
|
},
|
||||||
'/livekit-api': {
|
'/livekit-api': {
|
||||||
target: 'https://meeting.cnsdt.com/api/v1', // 从环境变量读取
|
target: 'https://meeting.cnsdt.com/api/v1',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
ws: true,
|
ws: true,
|
||||||
rewrite: (path) =>
|
rewrite: (path) =>
|
||||||
@@ -43,4 +40,5 @@ export default defineConfig({
|
|||||||
css: {
|
css: {
|
||||||
postcss: './postcss.config.js' // 纯外部配置
|
postcss: './postcss.config.js' // 纯外部配置
|
||||||
},
|
},
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
13
vite/plugins/auto-import.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import autoImport from 'unplugin-auto-import/vite'
|
||||||
|
|
||||||
|
export default function createAutoImport() {
|
||||||
|
return autoImport({
|
||||||
|
imports: [
|
||||||
|
'vue',
|
||||||
|
'vue-router',
|
||||||
|
'pinia',
|
||||||
|
'@vueuse/core'
|
||||||
|
],
|
||||||
|
dts: false
|
||||||
|
})
|
||||||
|
}
|
||||||
28
vite/plugins/compression.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import compression from 'vite-plugin-compression'
|
||||||
|
|
||||||
|
export default function createCompression(env) {
|
||||||
|
const { VITE_BUILD_COMPRESS } = env
|
||||||
|
const plugin = []
|
||||||
|
if (VITE_BUILD_COMPRESS) {
|
||||||
|
const compressList = VITE_BUILD_COMPRESS.split(',')
|
||||||
|
if (compressList.includes('gzip')) {
|
||||||
|
// http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件
|
||||||
|
plugin.push(
|
||||||
|
compression({
|
||||||
|
ext: '.gz',
|
||||||
|
deleteOriginFile: false
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (compressList.includes('brotli')) {
|
||||||
|
plugin.push(
|
||||||
|
compression({
|
||||||
|
ext: '.br',
|
||||||
|
algorithm: 'brotliCompress',
|
||||||
|
deleteOriginFile: false
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
19
vite/plugins/index.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
|
||||||
|
import createAutoImport from "./auto-import";
|
||||||
|
import createSvgIcon from "./svg-icon";
|
||||||
|
import createCompression from "./compression";
|
||||||
|
import createSetupExtend from "./setup-extend";
|
||||||
|
import { codeInspectorPlugin } from 'code-inspector-plugin'
|
||||||
|
export default function createVitePlugins(viteEnv, isBuild = false) {
|
||||||
|
const vitePlugins = [vue()];
|
||||||
|
vitePlugins.push(codeInspectorPlugin({
|
||||||
|
bundler: 'vite',
|
||||||
|
showSwitch: true,
|
||||||
|
}))
|
||||||
|
vitePlugins.push(createAutoImport());
|
||||||
|
vitePlugins.push(createSetupExtend());
|
||||||
|
vitePlugins.push(createSvgIcon(isBuild));
|
||||||
|
isBuild && vitePlugins.push(...createCompression(viteEnv));
|
||||||
|
return vitePlugins;
|
||||||
|
}
|
||||||
5
vite/plugins/setup-extend.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import setupExtend from 'unplugin-vue-setup-extend-plus/vite'
|
||||||
|
|
||||||
|
export default function createSetupExtend() {
|
||||||
|
return setupExtend({})
|
||||||
|
}
|
||||||
10
vite/plugins/svg-icon.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export default function createSvgIcon(isBuild) {
|
||||||
|
return createSvgIconsPlugin({
|
||||||
|
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/svg')],
|
||||||
|
symbolId: 'icon-[dir]-[name]',
|
||||||
|
svgoOptions: isBuild
|
||||||
|
})
|
||||||
|
}
|
||||||