feat:更新侧边导航,修改密码等
This commit is contained in:
3634
package-lock.json
generated
3634
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@
|
|||||||
"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",
|
||||||
|
"@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",
|
||||||
@@ -24,14 +25,18 @@
|
|||||||
"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-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
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,11 @@ export function exitRoomApi(room_uid) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//获取当前房间信息
|
||||||
|
export function getRoomInfoApi(room_uid) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/meeting/${room_uid}/info`,
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
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'
|
||||||
|
})
|
||||||
|
}
|
||||||
39
src/assets/images/dark.svg
Normal file
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 |
39
src/assets/images/light.svg
Normal file
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 |
@@ -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
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
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,7 +26,7 @@
|
|||||||
</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 { useMeterStore } from '@/stores/modules/meter'
|
import { useMeterStore } from '@/stores/modules/meter'
|
||||||
@@ -123,7 +123,7 @@ onMounted(() => {
|
|||||||
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%);
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/components/ParentView/index.vue
Normal file
3
src/components/ParentView/index.vue
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<template >
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
210
src/components/TopNav/index.vue
Normal file
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>
|
||||||
@@ -38,13 +38,13 @@ class Canvas extends EventEmitter {
|
|||||||
window.addEventListener('resize', () => this.resize());
|
window.addEventListener('resize', () => this.resize());
|
||||||
}
|
}
|
||||||
|
|
||||||
resize() {
|
resize() {
|
||||||
const parent = this.canvas.parentElement;
|
const parent = this.canvas.parentElement;
|
||||||
if (!parent) return;
|
if (!parent) return;
|
||||||
|
|
||||||
const containerWidth = parent.offsetWidth;
|
const containerWidth = parent.offsetWidth;
|
||||||
const containerHeight = parent.offsetHeight;
|
const containerHeight = parent.offsetHeight;
|
||||||
|
|
||||||
let width = containerWidth;
|
let width = containerWidth;
|
||||||
let height = Math.floor((width * 9) / 16);
|
let height = Math.floor((width * 9) / 16);
|
||||||
|
|
||||||
@@ -62,6 +62,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; }
|
||||||
|
|||||||
48
src/layout/components/AppMain.vue
Normal file
48
src/layout/components/AppMain.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<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
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
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>
|
||||||
69
src/layout/components/Navbar.vue
Normal file
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>
|
||||||
161
src/layout/components/ResetPwd/index.vue
Normal file
161
src/layout/components/ResetPwd/index.vue
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
title="修改密码"
|
||||||
|
width="500"
|
||||||
|
:show-close='false'
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
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>
|
||||||
|
</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() {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
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
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
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
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
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
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
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
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>
|
||||||
5
src/layout/components/index.js
Normal file
5
src/layout/components/index.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
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'
|
||||||
309
src/layout/index.vue
Normal file
309
src/layout/index.vue
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
<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"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
import { useWindowSize } from '@vueuse/core'
|
||||||
|
import Sidebar from './components/Sidebar/index.vue'
|
||||||
|
import { AppMain, TagsView ,ResetPwd} 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'
|
||||||
|
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const useAppStoreStore = useAppStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
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 })
|
||||||
|
}
|
||||||
|
|
||||||
|
</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>
|
||||||
@@ -17,14 +17,14 @@ 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 app = createApp(App)
|
const pinia = createPinia()
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
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
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,134 @@
|
|||||||
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
||||||
|
import Layout from "@/layout/index.vue";
|
||||||
|
|
||||||
|
export const constantRoutes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: '/login', // 这里做重定向
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/login",
|
||||||
|
component: () => import("@/views/login.vue"),
|
||||||
|
meta: { title: "登录" },
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/whiteboard',
|
||||||
|
component: () => import('@/views/custom/tabulaRase/index.vue'),
|
||||||
|
meta: { title: "白板" },
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/coordinate",
|
||||||
|
redirect: "/coordinate/CoordinatePage",
|
||||||
|
component: Layout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'CoordinatePage',
|
||||||
|
name: "Coordinate",
|
||||||
|
component: () => import("@/views/coordinate/personnelList/index.vue"),
|
||||||
|
meta: { title: "远程协作", icon: "client", affix: true },
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/conferencingRoom",
|
||||||
|
// component: Layout,
|
||||||
|
hidden: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: "ConferencingRoom",
|
||||||
|
component: () => import("@/views/conferencingRoom/index.vue"),
|
||||||
|
meta: { title: "会议房间", icon: "client", affix: true },
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// 错误页面路由
|
||||||
|
{
|
||||||
|
path: "/:pathMatch(.*)*",
|
||||||
|
component: () => import("@/views/error/404.vue"),
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/401",
|
||||||
|
component: () => import("@/views/error/401.vue"),
|
||||||
|
hidden: true,
|
||||||
|
meta: { title: "401未授权" }
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
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({
|
const router = createRouter({
|
||||||
|
// history: createWebHistory(import.meta.env.VITE_BASE_PATH),
|
||||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: constantRoutes,
|
||||||
// {
|
scrollBehavior(to, from, savedPosition) {
|
||||||
// path: '/',
|
if (savedPosition) {
|
||||||
// component: () => import("@/views/custom/tabulaRase/index.vue"),
|
return savedPosition;
|
||||||
// },
|
} else {
|
||||||
{
|
return { top: 0 };
|
||||||
path: '/',
|
|
||||||
redirect: '/login', // 这里做重定向
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/whiteboard',
|
|
||||||
component: () => import('@/views/custom/tabulaRase/index.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/login",
|
|
||||||
component: () => import("@/views/login.vue"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/coordinate",
|
|
||||||
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"),
|
|
||||||
}
|
}
|
||||||
],
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
47
src/settings.js
Normal file
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
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: () => ({
|
||||||
@@ -30,4 +30,4 @@ export const useMeterStore = defineStore('meter', {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ export const useRoomStore = defineStore('room', {
|
|||||||
roomId: '',
|
roomId: '',
|
||||||
token: '',
|
token: '',
|
||||||
userUid: '',
|
userUid: '',
|
||||||
detailUid: '',
|
//邀请进入房间的用户uid
|
||||||
|
detailUid: '',
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setUserUid(data) {
|
setUserUid(data) {
|
||||||
|
|||||||
37
src/stores/modules/settings.js
Normal file
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 ? '#141414' : 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
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,10 +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'
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
const useUserStore = defineStore(
|
export const useUserStore = defineStore(
|
||||||
'user',
|
'user',
|
||||||
{
|
{
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@@ -26,6 +25,7 @@ const useUserStore = defineStore(
|
|||||||
return Promise.reject(res);
|
return Promise.reject(res);
|
||||||
}
|
}
|
||||||
const { token, user } = res.data;
|
const { token, user } = res.data;
|
||||||
|
this.name = user.name;
|
||||||
sessionStorage.setItem('userData', JSON.stringify(user));
|
sessionStorage.setItem('userData', JSON.stringify(user));
|
||||||
setToken(token);
|
setToken(token);
|
||||||
this.token = token;
|
this.token = token;
|
||||||
@@ -58,9 +58,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);
|
||||||
@@ -72,6 +77,4 @@ const useUserStore = defineStore(
|
|||||||
return !!getToken();
|
return !!getToken();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default useUserStore
|
|
||||||
@@ -4,14 +4,17 @@ import Cookies from "js-cookie";
|
|||||||
const TokenKey = "token";
|
const TokenKey = "token";
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
src/utils/dynamicTitle.js
Normal file
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,11 +8,9 @@ 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()
|
|
||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
const service = axios.create({
|
const service = axios.create({
|
||||||
// axios中请求配置有baseURL选项,表示请求URL公共部分
|
// axios中请求配置有baseURL选项,表示请求URL公共部分
|
||||||
@@ -24,7 +22,20 @@ const service = axios.create({
|
|||||||
// 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;
|
||||||
if (getToken() && !isToken) {
|
if (getToken() && !isToken) {
|
||||||
@@ -33,8 +44,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) {
|
||||||
|
|||||||
49
src/utils/theme.js
Normal file
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
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)
|
||||||
|
}
|
||||||
316
src/views/conferencingRoom/components/InviterJoinRoom/index.vue
Normal file
316
src/views/conferencingRoom/components/InviterJoinRoom/index.vue
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
<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 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>
|
||||||
|
</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 { 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 treeProps = reactive({
|
||||||
|
children: 'users',
|
||||||
|
label: 'name',
|
||||||
|
value: 'uid',
|
||||||
|
isLeaf: (node) => {
|
||||||
|
return !!node.uid; // 有 uid 的为叶子节点(人员)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 显示抽屉
|
||||||
|
function show() {
|
||||||
|
drawerVisible.value = true;
|
||||||
|
// 重置选择状态
|
||||||
|
selectedUsers.value = [];
|
||||||
|
checkedNodes.value = [];
|
||||||
|
searchKeyword.value = '';
|
||||||
|
|
||||||
|
// 延迟加载树形数据,确保 DOM 已渲染
|
||||||
|
nextTick(() => {
|
||||||
|
if (treeRef.value) {
|
||||||
|
treeRef.value.setCheckedKeys([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认选择
|
||||||
|
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);
|
||||||
|
} else if (node?.level === 1) {
|
||||||
|
loadNode(resolve, node.data.directory_uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载节点数据
|
||||||
|
const loadNode = async (resolve, id) => {
|
||||||
|
try {
|
||||||
|
if (!id) {
|
||||||
|
let res = await getDirectories({ level: 1 });
|
||||||
|
if (res.meta.code == 401) {
|
||||||
|
emit('showLogin', true);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
resolve(res.data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let res = await getDirectoriesUsers(id, { directory_uuid: id });
|
||||||
|
resolve(res.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理复选框选择变化
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 更新已选用户列表
|
||||||
|
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>
|
||||||
@@ -121,8 +121,9 @@
|
|||||||
<audio
|
<audio
|
||||||
:ref="el => setParticipantAudioRef(el, participant.identity)"
|
:ref="el => setParticipantAudioRef(el, participant.identity)"
|
||||||
autoplay
|
autoplay
|
||||||
class="participant-audio">
|
class="participant-audio"
|
||||||
</audio>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,54 +132,107 @@
|
|||||||
<!-- 固定在底部的控制按钮 -->
|
<!-- 固定在底部的控制按钮 -->
|
||||||
<div class="fixed-controls">
|
<div class="fixed-controls">
|
||||||
<div class="controls-container">
|
<div class="controls-container">
|
||||||
<el-button @click="toggleCamera" :type="cameraEnabled ? 'danger' : 'success'" class="control-btn" size="large">
|
|
||||||
<i :class="cameraEnabled ? 'el-icon-video-camera' : 'el-icon-video-camera-solid'"></i>
|
<div class="microphone-control-group">
|
||||||
{{ cameraEnabled ? '关闭摄像头' : '开启摄像头' }}
|
<el-button @click="toggleCamera" :type="cameraEnabled ? 'danger' : 'info'" class="control-btn microphone-btn" size="large">
|
||||||
</el-button>
|
{{ cameraEnabled ? '关闭摄像头' : '开启摄像头' }}
|
||||||
<el-button @click="toggleMicrophone" :type="microphoneEnabled ? 'danger' : 'success'" class="control-btn" size="large">
|
</el-button>
|
||||||
<i :class="microphoneEnabled ? 'el-icon-microphone' : 'el-icon-turn-off-microphone'"></i>
|
<!-- 摄像头选择下拉菜单 -->
|
||||||
{{ microphoneEnabled ? '关闭麦克风' : '开启麦克风' }}
|
<el-dropdown trigger="click" @command="handleCameraCommand" @visible-change="handleCameraVisibleChange" class="control-dropdown microphone-dropdown">
|
||||||
</el-button>
|
<el-button :type="cameraEnabled ? 'danger' : 'info'" class="control-btn dropdown-btn" size="large">
|
||||||
<!-- <el-button @click="toggleScreenShare" :type="isScreenSharing ? 'danger' : 'primary'" class="control-btn" size="large">
|
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||||
<i class="el-icon-monitor"></i>
|
</el-button>
|
||||||
{{ isScreenSharing ? '停止共享' : '共享屏幕' }}
|
<template #dropdown>
|
||||||
</el-button> -->
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item
|
||||||
|
v-for="device in cameraDevices"
|
||||||
|
:key="device.deviceId"
|
||||||
|
:command="device.deviceId"
|
||||||
|
:class="{ 'selected-device': selectedCameraId === device.deviceId }"
|
||||||
|
>
|
||||||
|
<i class="el-icon-video-camera"></i>
|
||||||
|
{{ device.label || `摄像头 ${cameraDevices.indexOf(device) + 1}` }}
|
||||||
|
<el-icon v-if="selectedCameraId === device.deviceId" class="check-icon"><check /></el-icon>
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item divided command="refresh">
|
||||||
|
<el-icon><refresh /></el-icon>
|
||||||
|
刷新设备列表
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
<div class="microphone-control-group">
|
||||||
|
<el-button @click="toggleMicrophone" :type="microphoneEnabled ? 'danger' : 'info'" class="control-btn microphone-btn" size="large">
|
||||||
|
{{ microphoneEnabled ? '关闭麦克风' : '开启麦克风' }}
|
||||||
|
</el-button>
|
||||||
|
<!-- 麦克风选择下拉菜单 -->
|
||||||
|
<el-dropdown trigger="click" @command="handleMicrophoneCommand" @visible-change="handleMicrophoneVisibleChange" class="control-dropdown microphone-dropdown">
|
||||||
|
<el-button :type="microphoneEnabled ? 'danger' : 'info'" class="control-btn dropdown-btn" size="large">
|
||||||
|
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item
|
||||||
|
v-for="device in microphoneDevices"
|
||||||
|
:key="device.deviceId"
|
||||||
|
:command="device.deviceId"
|
||||||
|
:class="{ 'selected-device': selectedMicrophoneId === device.deviceId }"
|
||||||
|
>
|
||||||
|
<i class="el-icon-microphone"></i>
|
||||||
|
{{ device.label || `麦克风 ${microphoneDevices.indexOf(device) + 1}` }}
|
||||||
|
<el-icon v-if="selectedMicrophoneId === device.deviceId" class="check-icon"><check /></el-icon>
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item divided command="refresh">
|
||||||
|
<el-icon><refresh /></el-icon>
|
||||||
|
刷新设备列表
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<el-button
|
<el-button
|
||||||
@click="toggleScreenShare"
|
@click="toggleScreenShare"
|
||||||
:type="isScreenSharing ? 'danger' : (isGlobalScreenSharing ? 'info' : 'primary')"
|
:type="isScreenSharing ? 'danger' : (isGlobalScreenSharing ? 'primary' : 'info')"
|
||||||
:disabled="isGlobalScreenSharing && !isScreenSharing"
|
:disabled="isGlobalScreenSharing && !isScreenSharing"
|
||||||
class="control-btn"
|
class="control-btn"
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
<i class="el-icon-monitor"></i>
|
|
||||||
<span v-if="isScreenSharing">停止共享</span>
|
<span v-if="isScreenSharing">停止共享</span>
|
||||||
<span v-else-if="isGlobalScreenSharing">他人共享中</span>
|
<span v-else-if="isGlobalScreenSharing">他人共享中</span>
|
||||||
<span v-else>共享屏幕</span>
|
<span v-else>共享屏幕</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="toggleWhiteboard" :type="isWhiteboardActive ? 'danger' : 'info'" class="control-btn leave-btn" size="large">
|
<el-button @click="toggleWhiteboard" :type="isWhiteboardActive ? 'danger' : 'info'" class="control-btn" size="large">
|
||||||
<i class="el-icon-switch-button"></i>
|
|
||||||
{{ isWhiteboardActive ? '退出白板' : '共享白板' }}
|
{{ isWhiteboardActive ? '退出白板' : '共享白板' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="leaveRoom" type="warning" class="control-btn leave-btn" size="large">
|
<el-button @click="inviterJoinRoom" type="info" class="control-btn" size="large">
|
||||||
<i class="el-icon-switch-button"></i>
|
邀请人员
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="leaveRoom" type="info" class="control-btn" size="large">
|
||||||
离开会议
|
离开会议
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 邀请人员组件 -->
|
||||||
|
<InviterJoinRoom ref="inviterJoinRoomRef" @confirmSelection="handleConfirmSelection" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref, onMounted, onUnmounted, nextTick, computed, watch } from "vue";
|
import { reactive, ref, onMounted, onUnmounted, nextTick, computed, watch } from "vue";
|
||||||
import { ElMessage ,ElMessageBox} from 'element-plus';
|
import { ElMessage ,ElMessageBox} from 'element-plus';
|
||||||
import { getRoomToken, getRoomList ,getInvite,getTokenApi,exitRoomApi} from "@/api/conferencingRoom.js"
|
import { ArrowDown, Refresh, Check } from '@element-plus/icons-vue';
|
||||||
|
import { getRoomToken, getRoomList ,getInvite,getTokenApi,exitRoomApi,getRoomInfoApi} from "@/api/conferencingRoom.js"
|
||||||
import { Room, RoomEvent, ParticipantEvent, Track } from "livekit-client";
|
import { Room, RoomEvent, ParticipantEvent, Track } from "livekit-client";
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useRoomStore } from '@/stores/modules/room.js'
|
import { useRoomStore } from '@/stores/modules/room.js'
|
||||||
import useUserStore from '@/stores/modules/user'
|
import { useUserStore } from '@/stores/modules/user.js'
|
||||||
import tabulaRase from '@/views/custom/tabulaRase/index.vue'
|
import tabulaRase from '@/views/custom/tabulaRase/index.vue'
|
||||||
import { mqttClient } from "@/utils/mqtt.js";
|
import { mqttClient } from "@/utils/mqtt.js";
|
||||||
|
import InviterJoinRoom from "@/views/conferencingRoom/components/InviterJoinRoom/index.vue"
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const roomStore = useRoomStore()
|
const roomStore = useRoomStore()
|
||||||
@@ -201,6 +255,12 @@ const microphoneEnabled = ref(false);
|
|||||||
const isScreenSharing = ref(false);
|
const isScreenSharing = ref(false);
|
||||||
const isLocalSpeaking = ref(false);
|
const isLocalSpeaking = ref(false);
|
||||||
|
|
||||||
|
// 设备列表
|
||||||
|
const cameraDevices = ref([]);
|
||||||
|
const microphoneDevices = ref([]);
|
||||||
|
const selectedCameraId = ref('');
|
||||||
|
const selectedMicrophoneId = ref('');
|
||||||
|
|
||||||
// 远程参与者管理
|
// 远程参与者管理
|
||||||
const remoteParticipants = ref(new Map());
|
const remoteParticipants = ref(new Map());
|
||||||
const videoElementsMap = ref(new Map());
|
const videoElementsMap = ref(new Map());
|
||||||
@@ -210,13 +270,19 @@ const audioElementsMap = ref(new Map());
|
|||||||
const screenSharingUser = ref('');
|
const screenSharingUser = ref('');
|
||||||
const activeScreenShareTrack = ref(null);
|
const activeScreenShareTrack = ref(null);
|
||||||
const screenShareVideo = ref(null);
|
const screenShareVideo = ref(null);
|
||||||
|
const globalScreenSharingUser = ref(''); // 当前正在共享屏幕的用户
|
||||||
|
const isGlobalScreenSharing = ref(false); // 是否有用户正在共享屏幕
|
||||||
|
|
||||||
//共享白板
|
//共享白板
|
||||||
const isWhiteboardActive = ref(false);
|
const isWhiteboardActive = ref(false);
|
||||||
const whiteboardRef = ref(null);
|
const whiteboardRef = ref(null);
|
||||||
|
const isGlobalWhiteboardSharing = ref(false); // 是否有用户正在共享白板
|
||||||
|
|
||||||
const globalScreenSharingUser = ref(''); // 当前正在共享屏幕的用户
|
//当前房间信息
|
||||||
const isGlobalScreenSharing = ref(false); // 是否有用户正在共享屏幕
|
const roomInfo = ref('')
|
||||||
|
|
||||||
|
//邀请参会人员
|
||||||
|
const inviterJoinRoomRef = ref()
|
||||||
|
|
||||||
// 白板消息类型
|
// 白板消息类型
|
||||||
const WHITEBOARD_MESSAGE_TYPES = {
|
const WHITEBOARD_MESSAGE_TYPES = {
|
||||||
@@ -261,7 +327,7 @@ const room = new Room({
|
|||||||
autoGainControl: true,
|
autoGainControl: true,
|
||||||
},
|
},
|
||||||
videoCaptureDefaults: {
|
videoCaptureDefaults: {
|
||||||
resolution: { width: 1280, height: 720 }
|
resolution: { width: 1280, height: 720 },
|
||||||
},
|
},
|
||||||
publishDefaults: {
|
publishDefaults: {
|
||||||
screenShareEncoding: {
|
screenShareEncoding: {
|
||||||
@@ -281,6 +347,208 @@ const room = new Room({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//摄像头打开下拉框触发
|
||||||
|
async function handleCameraVisibleChange(e){
|
||||||
|
try {
|
||||||
|
if(e){
|
||||||
|
// 请求摄像头权限
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
||||||
|
|
||||||
|
// 获取设备列表
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
|
||||||
|
// 过滤摄像头设备
|
||||||
|
cameraDevices.value = devices.filter(device => device.kind === 'videoinput');
|
||||||
|
// 重要:立即停止临时媒体流,避免占用摄像头
|
||||||
|
stream.getTracks().forEach(track => {
|
||||||
|
track.stop();
|
||||||
|
console.log('临时摄像头轨道已停止');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('摄像头访问失败:', error);
|
||||||
|
// 使用更友好的方式提示用户
|
||||||
|
errorHandling(error,'摄像头');
|
||||||
|
// 清空设备列表
|
||||||
|
cameraDevices.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理摄像头设备选择
|
||||||
|
async function handleCameraCommand(deviceId) {
|
||||||
|
if (deviceId === 'refresh') {
|
||||||
|
await handleCameraVisibleChange(true);
|
||||||
|
ElMessage.success('设备列表已刷新');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedCameraId.value = deviceId;
|
||||||
|
|
||||||
|
// 如果摄像头已经开启,重新开启以应用新设备
|
||||||
|
if (cameraEnabled.value) {
|
||||||
|
await switchCameraDevice(deviceId);
|
||||||
|
} else {
|
||||||
|
// 如果摄像头未开启,直接开启选中的设备
|
||||||
|
await enableCameraWithDevice(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success(`已切换到摄像头: ${getDeviceName(cameraDevices.value, deviceId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
//麦克风打开下拉框触发
|
||||||
|
async function handleMicrophoneVisibleChange(e){
|
||||||
|
try {
|
||||||
|
if(e){
|
||||||
|
// 请求麦克风权限
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
|
||||||
|
// 获取设备列表
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
|
||||||
|
// 过滤麦克风设备
|
||||||
|
microphoneDevices.value = devices.filter(device => device.kind === 'audioinput');
|
||||||
|
|
||||||
|
// 停止所有轨道来关闭临时的麦克风访问
|
||||||
|
stream.getTracks().forEach(track => track.stop());
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('麦克风访问失败:', error);
|
||||||
|
// 使用更友好的方式提示用户
|
||||||
|
errorHandling(error,'麦克风');
|
||||||
|
// 清空设备列表
|
||||||
|
microphoneDevices.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理麦克风设备选择
|
||||||
|
async function handleMicrophoneCommand(deviceId) {
|
||||||
|
if (deviceId === 'refresh') {
|
||||||
|
await handleMicrophoneVisibleChange();
|
||||||
|
ElMessage.success('设备列表已刷新');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedMicrophoneId.value = deviceId;
|
||||||
|
|
||||||
|
// 如果麦克风已经开启,重新开启以应用新设备
|
||||||
|
if (microphoneEnabled.value) {
|
||||||
|
await switchMicrophoneDevice(deviceId);
|
||||||
|
} else {
|
||||||
|
// 如果麦克风未开启,直接开启选中的设备
|
||||||
|
await enableMicrophoneWithDevice(deviceId);
|
||||||
|
}
|
||||||
|
ElMessage.success(`已切换到麦克风: ${getDeviceName(microphoneDevices.value, deviceId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用指定设备开启摄像头
|
||||||
|
async function enableCameraWithDevice(deviceId) {
|
||||||
|
try {
|
||||||
|
// 更新设备配置
|
||||||
|
room.options.videoCaptureDefaults.deviceId = deviceId;
|
||||||
|
|
||||||
|
// 开启摄像头
|
||||||
|
await room.localParticipant.setCameraEnabled(true);
|
||||||
|
cameraEnabled.value = true;
|
||||||
|
|
||||||
|
// 手动获取并附加视频轨道
|
||||||
|
setTimeout(() => {
|
||||||
|
attachLocalCameraTrack();
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(`使用指定设备开启摄像头失败`);
|
||||||
|
try {
|
||||||
|
if (cameraEnabled.value) {
|
||||||
|
await room.localParticipant.setCameraEnabled(true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
cameraEnabled.value = false;
|
||||||
|
selectedCameraId.value = '';
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用指定设备开启麦克风
|
||||||
|
async function enableMicrophoneWithDevice(deviceId) {
|
||||||
|
try {
|
||||||
|
// 更新设备配置
|
||||||
|
room.options.audioCaptureDefaults.deviceId = deviceId;
|
||||||
|
|
||||||
|
// 开启麦克风
|
||||||
|
await room.localParticipant.setMicrophoneEnabled(true);
|
||||||
|
microphoneEnabled.value = true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('使用指定设备开启麦克风失败:', error);
|
||||||
|
microphoneEnabled.value = false;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换摄像头设备
|
||||||
|
async function switchCameraDevice(deviceId) {
|
||||||
|
try {
|
||||||
|
// 先关闭当前摄像头
|
||||||
|
await room.localParticipant.setCameraEnabled(false);
|
||||||
|
|
||||||
|
// 更新设备配置
|
||||||
|
room.options.videoCaptureDefaults.deviceId = deviceId;
|
||||||
|
|
||||||
|
// 重新开启摄像头
|
||||||
|
await room.localParticipant.setCameraEnabled(true);
|
||||||
|
|
||||||
|
// 手动获取并附加视频轨道
|
||||||
|
setTimeout(() => {
|
||||||
|
attachLocalCameraTrack();
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('切换摄像头设备失败:', error);
|
||||||
|
// 如果切换失败,尝试重新开启之前的设备
|
||||||
|
try {
|
||||||
|
await room.localParticipant.setCameraEnabled(true);
|
||||||
|
} catch (e) {
|
||||||
|
cameraEnabled.value = false;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换麦克风设备
|
||||||
|
async function switchMicrophoneDevice(deviceId) {
|
||||||
|
try {
|
||||||
|
// 先关闭当前麦克风
|
||||||
|
await room.localParticipant.setMicrophoneEnabled(false);
|
||||||
|
|
||||||
|
// 更新设备配置
|
||||||
|
room.options.audioCaptureDefaults.deviceId = deviceId;
|
||||||
|
|
||||||
|
// 重新开启麦克风
|
||||||
|
await room.localParticipant.setMicrophoneEnabled(true);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('切换麦克风设备失败:', error);
|
||||||
|
// 如果切换失败,尝试重新开启之前的设备
|
||||||
|
try {
|
||||||
|
await room.localParticipant.setMicrophoneEnabled(true);
|
||||||
|
} catch (e) {
|
||||||
|
microphoneEnabled.value = false;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取设备名称
|
||||||
|
function getDeviceName(devices, deviceId) {
|
||||||
|
const device = devices.find(d => d.deviceId === deviceId);
|
||||||
|
return device ? (device.label || '未知设备') : '未知设备';
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化MQTT连接
|
// 初始化MQTT连接
|
||||||
async function initMqtt() {
|
async function initMqtt() {
|
||||||
try {
|
try {
|
||||||
@@ -309,8 +577,7 @@ function subscribeToWhiteboardTopic() {
|
|||||||
function handleWhiteboardMessage(payload, topic) {
|
function handleWhiteboardMessage(payload, topic) {
|
||||||
try {
|
try {
|
||||||
const messageStr = payload.toString();
|
const messageStr = payload.toString();
|
||||||
const data = JSON.parse(messageStr);
|
const data = JSON.parse(messageStr);
|
||||||
console.log('收到白板消息:', data);
|
|
||||||
// 只处理当前房间的消息
|
// 只处理当前房间的消息
|
||||||
if (data.roomId !== room.name) return;
|
if (data.roomId !== room.name) return;
|
||||||
// 忽略自己发送的消息
|
// 忽略自己发送的消息
|
||||||
@@ -350,9 +617,10 @@ function handleRemoteWhiteboardOpen(data) {
|
|||||||
|
|
||||||
// 处理远程关闭白板
|
// 处理远程关闭白板
|
||||||
function handleRemoteWhiteboardClose(data) {
|
function handleRemoteWhiteboardClose(data) {
|
||||||
|
console.log('关闭白板')
|
||||||
ElMessage.info(`${data.senderName || data.sender} 关闭了白板`);
|
ElMessage.info(`${data.senderName || data.sender} 关闭了白板`);
|
||||||
console.log('data',data)
|
console.log('data',data)
|
||||||
if(data.type == '1'){
|
if(data.roomType == '1'){
|
||||||
isWhiteboardActive.value = false;
|
isWhiteboardActive.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,6 +636,22 @@ function handleWhiteboardSync(data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//邀请进入房间
|
||||||
|
async function inviterJoinRoom(){
|
||||||
|
inviterJoinRoomRef.value.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认选择房间
|
||||||
|
async function handleConfirmSelection(userInfo){
|
||||||
|
console.log(userInfo,'加入房间人员信息')
|
||||||
|
if(userInfo.length < 0){
|
||||||
|
ElMessage.error('请选择加入房间的人员')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const joinUserIds = userInfo.map(item => item.uid)
|
||||||
|
await getInvite(room.name,{user_uids:joinUserIds, participant_role: "participant"})
|
||||||
|
}
|
||||||
|
|
||||||
function publishWhiteboardMessage(type, payload = {}) {
|
function publishWhiteboardMessage(type, payload = {}) {
|
||||||
try {
|
try {
|
||||||
const message = {
|
const message = {
|
||||||
@@ -390,13 +674,6 @@ function publishWhiteboardMessage(type, payload = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//共享白板
|
|
||||||
// async function whiteboard(){
|
|
||||||
// const baseUrl = 'http://localhost:3000/#/whiteboard'
|
|
||||||
// const url = `${baseUrl}?room_uid=${room.name}`
|
|
||||||
// window.open(url, '_blank')
|
|
||||||
|
|
||||||
// }
|
|
||||||
async function toggleWhiteboard() {
|
async function toggleWhiteboard() {
|
||||||
if(hasActiveScreenShare.value){
|
if(hasActiveScreenShare.value){
|
||||||
ElMessage.error('请先关闭屏幕共享');
|
ElMessage.error('请先关闭屏幕共享');
|
||||||
@@ -530,7 +807,6 @@ function setScreenShareVideoRef(el) {
|
|||||||
// 设置事件监听器
|
// 设置事件监听器
|
||||||
function setupRoomListeners() {
|
function setupRoomListeners() {
|
||||||
room.removeAllListeners();
|
room.removeAllListeners();
|
||||||
|
|
||||||
room
|
room
|
||||||
.on(RoomEvent.Connected, handleConnected)
|
.on(RoomEvent.Connected, handleConnected)
|
||||||
.on(RoomEvent.Disconnected, handleDisconnected)
|
.on(RoomEvent.Disconnected, handleDisconnected)
|
||||||
@@ -551,7 +827,9 @@ function setupRoomListeners() {
|
|||||||
// 事件处理函数
|
// 事件处理函数
|
||||||
async function handleConnected() {
|
async function handleConnected() {
|
||||||
await initMqtt();
|
await initMqtt();
|
||||||
|
roomId.value = room.name
|
||||||
status.value = false;
|
status.value = false;
|
||||||
|
|
||||||
ElMessage.success('已成功连接到房间');
|
ElMessage.success('已成功连接到房间');
|
||||||
// 初始化现有远程参与者
|
// 初始化现有远程参与者
|
||||||
room.remoteParticipants.forEach(participant => {
|
room.remoteParticipants.forEach(participant => {
|
||||||
@@ -560,14 +838,6 @@ async function handleConnected() {
|
|||||||
// 立即检查并更新参与者的轨道状态
|
// 立即检查并更新参与者的轨道状态
|
||||||
updateParticipantTracks(participant);
|
updateParticipantTracks(participant);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 自动开启麦克风
|
|
||||||
// try {
|
|
||||||
// await enableMicrophone();
|
|
||||||
// ElMessage.success('麦克风已自动开启');
|
|
||||||
// } catch (error) {
|
|
||||||
// console.warn('自动开启麦克风失败:', error);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDisconnected(reason) {
|
function handleDisconnected(reason) {
|
||||||
@@ -618,7 +888,6 @@ function handleTrackSubscribed(track, publication, participant) {
|
|||||||
updateParticipantTracks(participant);
|
updateParticipantTracks(participant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function handleTrackUnsubscribed(track, publication, participant) {
|
function handleTrackUnsubscribed(track, publication, participant) {
|
||||||
// 移除对应的轨道信息
|
// 移除对应的轨道信息
|
||||||
if (track.kind === Track.Kind.Video) {
|
if (track.kind === Track.Kind.Video) {
|
||||||
@@ -766,6 +1035,7 @@ function handleActiveSpeakersChanged(speakers) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理数据接收事件
|
// 处理数据接收事件
|
||||||
function handleDataReceived(payload, participant, kind) {
|
function handleDataReceived(payload, participant, kind) {
|
||||||
try {
|
try {
|
||||||
@@ -780,6 +1050,7 @@ function handleDataReceived(payload, participant, kind) {
|
|||||||
function handleConnectionStateChanged(state) {
|
function handleConnectionStateChanged(state) {
|
||||||
console.log('连接状态改变:', state);
|
console.log('连接状态改变:', state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新屏幕共享状态
|
// 更新屏幕共享状态
|
||||||
function updateScreenShareState(participant, track) {
|
function updateScreenShareState(participant, track) {
|
||||||
if (track) {
|
if (track) {
|
||||||
@@ -802,6 +1073,7 @@ function updateScreenShareState(participant, track) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 参与者管理函数
|
// 参与者管理函数
|
||||||
function addRemoteParticipant(participant) {
|
function addRemoteParticipant(participant) {
|
||||||
if (!participant || participant.identity === room.localParticipant?.identity) {
|
if (!participant || participant.identity === room.localParticipant?.identity) {
|
||||||
@@ -834,6 +1106,7 @@ function removeRemoteParticipant(participant) {
|
|||||||
audioElementsMap.value.delete(identity);
|
audioElementsMap.value.delete(identity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新参与者轨道信息
|
// 更新参与者轨道信息
|
||||||
function updateParticipantTrack(participant, source, track) {
|
function updateParticipantTrack(participant, source, track) {
|
||||||
const data = remoteParticipants.value.get(participant.identity);
|
const data = remoteParticipants.value.get(participant.identity);
|
||||||
@@ -877,6 +1150,7 @@ function removeParticipantAudioTrack(participant) {
|
|||||||
data.audioEnabled = false;
|
data.audioEnabled = false;
|
||||||
remoteParticipants.value.set(participant.identity, { ...data });
|
remoteParticipants.value.set(participant.identity, { ...data });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 附加轨道到视频元素
|
// 附加轨道到视频元素
|
||||||
function attachTrackToVideo(videoElement, track) {
|
function attachTrackToVideo(videoElement, track) {
|
||||||
if (!videoElement || !track) return;
|
if (!videoElement || !track) return;
|
||||||
@@ -889,6 +1163,7 @@ function attachTrackToVideo(videoElement, track) {
|
|||||||
console.error('附加轨道到视频元素失败:', error);
|
console.error('附加轨道到视频元素失败:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 附加轨道到音频元素
|
// 附加轨道到音频元素
|
||||||
function attachTrackToAudio(audioElement, track) {
|
function attachTrackToAudio(audioElement, track) {
|
||||||
if (!audioElement || !track) return;
|
if (!audioElement || !track) return;
|
||||||
@@ -904,6 +1179,7 @@ function attachTrackToAudio(audioElement, track) {
|
|||||||
console.error('附加轨道到音频元素失败:', error);
|
console.error('附加轨道到音频元素失败:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 附加轨道到参与者的视频元素
|
// 附加轨道到参与者的视频元素
|
||||||
function attachTrackToParticipantVideo(identity, source, track) {
|
function attachTrackToParticipantVideo(identity, source, track) {
|
||||||
const videoElements = videoElementsMap.value.get(identity);
|
const videoElements = videoElementsMap.value.get(identity);
|
||||||
@@ -914,6 +1190,7 @@ function attachTrackToParticipantVideo(identity, source, track) {
|
|||||||
attachTrackToVideo(videoElement, track);
|
attachTrackToVideo(videoElement, track);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 附加轨道到参与者的音频元素
|
// 附加轨道到参与者的音频元素
|
||||||
function attachTrackToParticipantAudio(identity, track) {
|
function attachTrackToParticipantAudio(identity, track) {
|
||||||
const audioElement = audioElementsMap.value.get(identity);
|
const audioElement = audioElementsMap.value.get(identity);
|
||||||
@@ -921,6 +1198,7 @@ function attachTrackToParticipantAudio(identity, track) {
|
|||||||
attachTrackToAudio(audioElement, track);
|
attachTrackToAudio(audioElement, track);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从参与者的视频元素分离轨道
|
// 从参与者的视频元素分离轨道
|
||||||
function detachTrackFromParticipantVideo(identity, source) {
|
function detachTrackFromParticipantVideo(identity, source) {
|
||||||
const videoElements = videoElementsMap.value.get(identity);
|
const videoElements = videoElementsMap.value.get(identity);
|
||||||
@@ -931,6 +1209,7 @@ function detachTrackFromParticipantVideo(identity, source) {
|
|||||||
videoElement.srcObject = null;
|
videoElement.srcObject = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从参与者的音频元素分离轨道
|
// 从参与者的音频元素分离轨道
|
||||||
function detachTrackFromParticipantAudio(identity) {
|
function detachTrackFromParticipantAudio(identity) {
|
||||||
const audioElement = audioElementsMap.value.get(identity);
|
const audioElement = audioElementsMap.value.get(identity);
|
||||||
@@ -997,6 +1276,7 @@ function handleVideoLoaded(identity, type) {
|
|||||||
function handleScreenShareLoaded() {
|
function handleScreenShareLoaded() {
|
||||||
console.log('屏幕共享视频加载完成');
|
console.log('屏幕共享视频加载完成');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 视频轨道处理函数
|
// 视频轨道处理函数
|
||||||
function attachLocalVideoTrack(track) {
|
function attachLocalVideoTrack(track) {
|
||||||
if (localVideo.value && track) {
|
if (localVideo.value && track) {
|
||||||
@@ -1010,6 +1290,7 @@ function attachLocalVideoTrack(track) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 媒体控制函数
|
// 媒体控制函数
|
||||||
async function enableCamera() {
|
async function enableCamera() {
|
||||||
try {
|
try {
|
||||||
@@ -1046,21 +1327,50 @@ async function toggleCamera() {
|
|||||||
localVideo.value.srcObject.getTracks().forEach(track => track.stop());
|
localVideo.value.srcObject.getTracks().forEach(track => track.stop());
|
||||||
localVideo.value.srcObject = null;
|
localVideo.value.srcObject = null;
|
||||||
}
|
}
|
||||||
|
// 清空选中的视屏设备ID
|
||||||
|
selectedCameraId.value = '';
|
||||||
ElMessage.info('摄像头已关闭');
|
ElMessage.info('摄像头已关闭');
|
||||||
} else {
|
} else {
|
||||||
// 确保视频元素存在
|
// 确保视频元素存在
|
||||||
if (!localVideo.value) {
|
if (!localVideo.value) {
|
||||||
console.warn('本地视频元素未找到,等待DOM更新');
|
console.warn('本地视频元素未找到,等待DOM更新');
|
||||||
await nextTick();
|
await nextTick();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开启摄像头
|
// 开启摄像头
|
||||||
await room.localParticipant.setCameraEnabled(true);
|
// 确保有视屏输入设备权限和设备列表
|
||||||
cameraEnabled.value = true;
|
if (cameraDevices.value.length === 0) {
|
||||||
|
// 如果没有设备列表,先获取
|
||||||
|
await handleCameraVisibleChange(true);
|
||||||
|
}
|
||||||
|
if (cameraDevices.value.length === 0) {
|
||||||
|
ElMessage.error('未找到可用的摄像头设备');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动选择第一个可用设备(如果当前没有选中设备)
|
||||||
|
let deviceToUse = selectedCameraId.value;
|
||||||
|
if (!deviceToUse && cameraDevices.value.length > 0) {
|
||||||
|
deviceToUse = cameraDevices.value[0].deviceId;
|
||||||
|
selectedCameraId.value = deviceToUse;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceToUse) {
|
||||||
|
await enableCameraWithDevice(deviceToUse);
|
||||||
|
ElMessage.success(`摄像头已开启 - ${getDeviceName(cameraDevices.value, deviceToUse)}`);
|
||||||
|
} else {
|
||||||
|
// 使用默认方式开启
|
||||||
|
await room.localParticipant.setMicrophoneEnabled(true);
|
||||||
|
microphoneEnabled.value = true;
|
||||||
|
// 手动获取并附加视频轨道
|
||||||
|
setTimeout(() => {
|
||||||
|
attachLocalCameraTrack();
|
||||||
|
}, 200);
|
||||||
|
ElMessage.success('麦克风已开启');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
ElMessage.success('摄像头已开启');
|
ElMessage.success('摄像头已开启');
|
||||||
// 手动获取并附加视频轨道,增加延迟
|
|
||||||
setTimeout(() => {
|
|
||||||
attachLocalCameraTrack();
|
|
||||||
}, 200);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorHandling(error,'摄像头');
|
errorHandling(error,'摄像头');
|
||||||
@@ -1101,61 +1411,81 @@ async function toggleMicrophone() {
|
|||||||
try {
|
try {
|
||||||
if (microphoneEnabled.value) {
|
if (microphoneEnabled.value) {
|
||||||
await room.localParticipant.setMicrophoneEnabled(false);
|
await room.localParticipant.setMicrophoneEnabled(false);
|
||||||
microphoneEnabled.value = false;
|
microphoneEnabled.value = false;
|
||||||
// 停止所有音频轨道
|
// 停止所有音频轨道
|
||||||
const audioPublications = Array.from(room.localParticipant.audioTrackPublications.values());
|
const audioPublications = Array.from(room.localParticipant.audioTrackPublications.values());
|
||||||
for (const publication of audioPublications) {
|
for (const publication of audioPublications) {
|
||||||
if (publication.track) {
|
if (publication.track) {
|
||||||
publication.track.stop(); // 停止轨道
|
publication.track.stop(); // 停止轨道
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 清空选中的麦克风设备ID
|
||||||
|
selectedMicrophoneId.value = '';
|
||||||
ElMessage.info('麦克风已关闭');
|
ElMessage.info('麦克风已关闭');
|
||||||
} else {
|
} else {
|
||||||
// 确保有音频输入设备权限
|
// 开启麦克风
|
||||||
try {
|
// 确保有音频输入设备权限和设备列表
|
||||||
// 先检查麦克风权限
|
if (microphoneDevices.value.length === 0) {
|
||||||
await navigator.mediaDevices.getUserMedia({ audio: true });
|
// 如果没有设备列表,先获取
|
||||||
} catch (error) {
|
await handleMicrophoneVisibleChange(true);
|
||||||
ElMessage.error('无法访问麦克风,请检查权限设置');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await room.localParticipant.setMicrophoneEnabled(true);
|
if (microphoneDevices.value.length === 0) {
|
||||||
microphoneEnabled.value = true;
|
ElMessage.error('未找到可用的麦克风设备');
|
||||||
ElMessage.success('麦克风已开启');
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 等待音频轨道发布
|
// 自动选择第一个可用设备(如果当前没有选中设备)
|
||||||
setTimeout(() => {
|
let deviceToUse = selectedMicrophoneId.value;
|
||||||
const audioPublications = Array.from(room.localParticipant.audioTrackPublications.values());
|
if (!deviceToUse && microphoneDevices.value.length > 0) {
|
||||||
const audioPublication = audioPublications.find(pub => pub.track);
|
deviceToUse = microphoneDevices.value[0].deviceId;
|
||||||
if (audioPublication && audioPublication.track) {
|
selectedMicrophoneId.value = deviceToUse;
|
||||||
console.log('本地音频轨道已发布:', audioPublication.track);
|
}
|
||||||
}
|
|
||||||
}, 500);
|
if (deviceToUse) {
|
||||||
|
await enableMicrophoneWithDevice(deviceToUse);
|
||||||
|
ElMessage.success(`麦克风已开启 - ${getDeviceName(microphoneDevices.value, deviceToUse)}`);
|
||||||
|
} else {
|
||||||
|
// 使用默认方式开启
|
||||||
|
await room.localParticipant.setMicrophoneEnabled(true);
|
||||||
|
microphoneEnabled.value = true;
|
||||||
|
ElMessage.success('麦克风已开启');
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success('麦克风已开启');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorHandling(error,'麦克风');
|
errorHandling(error, '麦克风');
|
||||||
|
// 如果开启失败,确保状态正确
|
||||||
|
if (!microphoneEnabled.value) {
|
||||||
|
selectedMicrophoneId.value = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function errorHandling(error,type) {
|
function errorHandling(error,type) {
|
||||||
switch (error.name) {
|
switch (error.name) {
|
||||||
case 'NotAllowedError':
|
case 'NotAllowedError':
|
||||||
ElMessage.error('用户拒绝了权限请求');
|
ElMessage.error('用户拒绝了权限请求,请允许此网站使用摄像头');
|
||||||
break;
|
break;
|
||||||
case 'NotFoundError':
|
case 'NotFoundError':
|
||||||
ElMessage.error(`未找到${type}设备`);
|
ElMessage.error(`未检测到可用的${type}设备,请检查${type}是否已正确连接`);
|
||||||
break;
|
break;
|
||||||
case 'NotSupportedError':
|
case 'NotSupportedError':
|
||||||
ElMessage.error('当前浏览器不支持此功能,请更换其他浏览器');
|
ElMessage.error(`当前浏览器不支持${type}功能,请使用现代浏览器如Chrome、Firefox或Edge`);
|
||||||
|
break;
|
||||||
|
case 'NotReadableError':
|
||||||
|
ElMessage.error(`${type}设备正被其他应用程序占用,请关闭其他使用${type}的应用后重试`);
|
||||||
|
break;
|
||||||
|
case 'OverconstrainedError':
|
||||||
|
ElMessage.error(`${type}配置不兼容,请尝试调整${type}设置`);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
ElMessage.error('服务错误,请刷新重试');
|
ElMessage.error('服务错误,请刷新重试');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function toggleScreenShare() {
|
async function toggleScreenShare() {
|
||||||
try {
|
try {
|
||||||
if(isWhiteboardActive.value){
|
if(isWhiteboardActive.value){
|
||||||
@@ -1241,7 +1571,7 @@ async function leaveRoom() {
|
|||||||
// 断开MQTT连接
|
// 断开MQTT连接
|
||||||
mqttClient.disconnect();
|
mqttClient.disconnect();
|
||||||
|
|
||||||
const res = await exitRoomApi(room.name)
|
// const res = await exitRoomApi(room.name)
|
||||||
// 停止屏幕共享(如果正在共享)
|
// 停止屏幕共享(如果正在共享)
|
||||||
if (isScreenSharing.value) {
|
if (isScreenSharing.value) {
|
||||||
await room.localParticipant.setScreenShareEnabled(false);
|
await room.localParticipant.setScreenShareEnabled(false);
|
||||||
@@ -1250,7 +1580,10 @@ async function leaveRoom() {
|
|||||||
// 关闭摄像头和麦克风
|
// 关闭摄像头和麦克风
|
||||||
await room.localParticipant.setCameraEnabled(false);
|
await room.localParticipant.setCameraEnabled(false);
|
||||||
await room.localParticipant.setMicrophoneEnabled(false);
|
await room.localParticipant.setMicrophoneEnabled(false);
|
||||||
|
microphoneEnabled.value = false;
|
||||||
|
cameraEnabled.value = false;
|
||||||
|
selectedMicrophoneId.value = '';
|
||||||
|
selectedCameraId.value = ''
|
||||||
// 断开与房间的连接
|
// 断开与房间的连接
|
||||||
await room.disconnect();
|
await room.disconnect();
|
||||||
|
|
||||||
@@ -1279,7 +1612,11 @@ function resetRoomState() {
|
|||||||
isScreenSharing.value = false;
|
isScreenSharing.value = false;
|
||||||
isLocalSpeaking.value = false;
|
isLocalSpeaking.value = false;
|
||||||
status.value = true;
|
status.value = true;
|
||||||
hostUid.value = '';
|
hostUid.value = '';
|
||||||
|
selectedMicrophoneId.value = '';
|
||||||
|
selectedCameraId.value = ''
|
||||||
|
microphoneDevices.value = [];
|
||||||
|
cameraDevices.value = [];
|
||||||
|
|
||||||
// 断开MQTT连接
|
// 断开MQTT连接
|
||||||
mqttClient.disconnect();
|
mqttClient.disconnect();
|
||||||
@@ -1343,24 +1680,12 @@ onUnmounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 确保在连接前请求音频权限
|
|
||||||
// try {
|
|
||||||
// // 预请求音频权限
|
|
||||||
// await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
||||||
// console.log('音频权限已获取');
|
|
||||||
// } catch (error) {
|
|
||||||
// console.warn('音频权限获取失败:', error);
|
|
||||||
// ElMessage.warning('请允许麦克风权限以使用音频功能');
|
|
||||||
// }
|
|
||||||
|
|
||||||
if(route.query.type == '1'){
|
if(route.query.type == '1'){
|
||||||
await joinRoomBtn()
|
await joinRoomBtn()
|
||||||
hostUid.value = roomStore.userUid
|
hostUid.value = roomStore.userUid
|
||||||
// 邀请用户参与房间
|
// 邀请用户参与房间
|
||||||
await getInvite(room.name,{user_uids:[roomStore.detailUid], participant_role: "participant"})
|
await getInvite(room.name,{user_uids:[roomStore.detailUid], participant_role: "participant"})
|
||||||
} else {
|
} else {
|
||||||
// const userInfo = await userStore.getInfo()
|
|
||||||
// hostUid.value = userInfo.uid
|
|
||||||
const res = await getTokenApi(route.query.room_uid)
|
const res = await getTokenApi(route.query.room_uid)
|
||||||
if(res.meta.code == 200){
|
if(res.meta.code == 200){
|
||||||
const token = res.data.access_token;
|
const token = res.data.access_token;
|
||||||
@@ -1376,9 +1701,10 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss" scoped>
|
||||||
/* 白板容器样式 */
|
/* 白板容器样式 */
|
||||||
.whiteboard-container {
|
.whiteboard-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -1429,6 +1755,37 @@ onMounted(async () => {
|
|||||||
.audio-element, .participant-audio {
|
.audio-element, .participant-audio {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 控制下拉菜单样式 */
|
||||||
|
.control-dropdown {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-dropdown .el-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中的设备样式 */
|
||||||
|
.selected-device {
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-icon {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 设备列表项样式 */
|
||||||
|
.el-dropdown-menu__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 全局样式 */
|
/* 全局样式 */
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -1857,8 +2214,86 @@ body {
|
|||||||
background: linear-gradient(135deg, #e64a4a 0%, #d63030 100%);
|
background: linear-gradient(135deg, #e64a4a 0%, #d63030 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 开启摄像头和麦克风下拉框 */
|
||||||
|
.control-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.dropdown-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.selected-device {
|
||||||
|
background-color: #f0f7ff;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
.check-icon {
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .microphone-btn {
|
||||||
|
border-radius: 12px 0 0 12px;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .microphone-dropdown {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .dropdown-btn {
|
||||||
|
border-radius: 0 12px 12px 0;
|
||||||
|
border: none;
|
||||||
|
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: none;
|
||||||
|
width: 44px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 悬停效果 */
|
||||||
|
.microphone-control-group:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .control-btn:hover {
|
||||||
|
transform: none; /* 取消单独的悬停位移 */
|
||||||
|
}
|
||||||
|
|
||||||
/* 响应式调整 */
|
/* 响应式调整 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.microphone-control-group {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .microphone-btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .dropdown-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
.whiteboard-container {
|
.whiteboard-container {
|
||||||
height: 300px; /* 在移动端限制白板高度 */
|
height: 300px; /* 在移动端限制白板高度 */
|
||||||
}
|
}
|
||||||
@@ -1919,6 +2354,19 @@ body {
|
|||||||
|
|
||||||
/* 超小屏幕调整 */
|
/* 超小屏幕调整 */
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
|
.microphone-control-group {
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .microphone-btn {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone-control-group .dropdown-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
.controls-container {
|
.controls-container {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
1721
src/views/conferencingRoom/text.vue
Normal file
1721
src/views/conferencingRoom/text.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
<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' : '')"
|
||||||
@@ -314,7 +314,7 @@ const HandleLoadNode = async (node, resolve) => {
|
|||||||
|
|
||||||
const loadNode = async(resolve,id)=>{
|
const loadNode = async(resolve,id)=>{
|
||||||
try {
|
try {
|
||||||
// state.leftListLoading = true
|
state.leftListLoading = true
|
||||||
if(!id){
|
if(!id){
|
||||||
let res = await getDirectories({level:1})
|
let res = await getDirectories({level:1})
|
||||||
if(res.meta.code == 401){
|
if(res.meta.code == 401){
|
||||||
@@ -328,10 +328,10 @@ const loadNode = async(resolve,id)=>{
|
|||||||
let res = await getDirectoriesUsers(id,{directory_uuid:id})
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,7 +157,8 @@
|
|||||||
<div
|
<div
|
||||||
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">
|
||||||
@@ -274,7 +275,7 @@ import { getStatusApi } 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 } from '@/utils/auth.js'
|
import { getToken } from '@/utils/auth.js'
|
||||||
import Login from "@/components/Login/index.vue";
|
import Login from "@/components/Login/index.vue";
|
||||||
@@ -282,7 +283,7 @@ const roomStore = useRoomStore()
|
|||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
detail: {},
|
detail: {},
|
||||||
weekName: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
|
weekName: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
|
||||||
tabValue: 2,
|
tabValue: 2,
|
||||||
@@ -295,6 +296,8 @@ const state = reactive({
|
|||||||
cooperation: import.meta.env.VITE_APP_COOPERATION_TYPE,
|
cooperation: import.meta.env.VITE_APP_COOPERATION_TYPE,
|
||||||
})
|
})
|
||||||
const showLogin = ref(false); // 是否显示登录页面
|
const showLogin = ref(false); // 是否显示登录页面
|
||||||
|
const userLoading = ref(false); // 用户信息加载状态
|
||||||
|
|
||||||
|
|
||||||
function showLoginHandle(e){
|
function showLoginHandle(e){
|
||||||
showLogin.value = e;
|
showLogin.value = e;
|
||||||
@@ -350,23 +353,26 @@ const updateTab = (newValue) => {
|
|||||||
|
|
||||||
/** 修改展示区内容 */
|
/** 修改展示区内容 */
|
||||||
const updateDetail = async (details) => {
|
const updateDetail = async (details) => {
|
||||||
if (details) {
|
userLoading.value = true
|
||||||
console.log(details,'details')
|
if (details) {
|
||||||
state.detail = {}
|
state.detail = {}
|
||||||
if (state.tabValue == 1) {
|
if (state.tabValue == 1) {
|
||||||
state.isShow = true
|
state.isShow = true
|
||||||
} else {
|
} else {
|
||||||
const res = await getInfo(details.uid)
|
const res = await getInfo(details.uid)
|
||||||
state.detail = res.data
|
state.detail = res.data
|
||||||
|
userLoading.value = false
|
||||||
// getInfo(details.uid)
|
// getInfo(details.uid)
|
||||||
// .then((res) => {
|
// .then((res) => {
|
||||||
// console.log(res,'人员详细信息')
|
// console.log(res,'人员详细信息')
|
||||||
// })
|
// })
|
||||||
// .finally(() => { state.isShow = true })
|
// .finally(() => { state.isShow = true })
|
||||||
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state.isShow = false
|
state.isShow = false
|
||||||
}
|
}
|
||||||
|
userLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取通话时长 */
|
/** 获取通话时长 */
|
||||||
@@ -420,12 +426,18 @@ const clickRefuseJoin = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 处理 mqtt 消息 */
|
/** 处理加入房间和拒接房间 mqtt 消息 */
|
||||||
const processingSocket = (message) => {
|
const processingSocket = (message) => {
|
||||||
if (message) {
|
const res = JSON.parse(message)
|
||||||
state.socketInformation = JSON.parse(message)
|
if (!res?.status) {
|
||||||
|
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',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,6 +470,7 @@ onMounted(async () => {
|
|||||||
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) => {
|
||||||
|
console.log(shapeData.toString(),'shapeData发送邀请')
|
||||||
processingSocket(shapeData.toString())
|
processingSocket(shapeData.toString())
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
<div v-else>
|
<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>
|
||||||
|
|
||||||
@@ -113,6 +113,7 @@ onMounted(async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("⚠️ 用户信息校验失败:", err);
|
console.warn("⚠️ 用户信息校验失败:", err);
|
||||||
}
|
}
|
||||||
|
joinWhiteboard()
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -144,6 +145,7 @@ onUnmounted(() => {
|
|||||||
.whiteboard-wrapper {
|
.whiteboard-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 72vw;
|
width: 72vw;
|
||||||
|
height: 69vh; /* 或者适当的高度 */
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -162,7 +164,7 @@ onUnmounted(() => {
|
|||||||
.toolbox {
|
.toolbox {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 10px;
|
left: 2vw;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,14 +37,14 @@
|
|||||||
</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,ElMessage } from 'element-plus'
|
import { ElNotification,ElMessage } from 'element-plus'
|
||||||
import { getInfo } from "@/api/login";
|
import { getInfo } from "@/api/login";
|
||||||
import CryptoJS from 'crypto-js';
|
import CryptoJS from 'crypto-js';
|
||||||
import { useMeterStore } from '@/stores/modules/meter'
|
import { useMeterStore } from '@/stores/modules/meter'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -92,7 +92,6 @@ function handleLogin() {
|
|||||||
const message = `Gx${randomChars}${loginForm.value.password}`;
|
const message = `Gx${randomChars}${loginForm.value.password}`;
|
||||||
|
|
||||||
const ciphertext = CryptoJS.Blowfish.encrypt(message, secretKey).toString();
|
const ciphertext = CryptoJS.Blowfish.encrypt(message, secretKey).toString();
|
||||||
console.log(secretKey,message,ciphertext)
|
|
||||||
// 调用action的登录方法
|
// 调用action的登录方法
|
||||||
userStore
|
userStore
|
||||||
.login({
|
.login({
|
||||||
|
|||||||
@@ -1,46 +1,44 @@
|
|||||||
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: {
|
||||||
}),
|
host: '0.0.0.0', // 关键配置,允许局域网访问
|
||||||
],
|
port: 3000,
|
||||||
server: {
|
open: true,
|
||||||
host: '0.0.0.0', // 关键配置,允许局域网访问
|
hmr: { overlay: false },
|
||||||
port: 3000,
|
proxy: {
|
||||||
open: true,
|
'/dev-api': {
|
||||||
hmr: { overlay: false },
|
target: 'https://xsynergy.gxtech.ltd', // 从环境变量读取
|
||||||
proxy: {
|
changeOrigin: true,
|
||||||
'/dev-api': {
|
ws: true,
|
||||||
target: 'https://xsynergy.gxtech.ltd', // 从环境变量读取
|
rewrite: (path) =>
|
||||||
changeOrigin: true,
|
path.replace(new RegExp(`^/dev-api`), '')
|
||||||
ws: true,
|
},
|
||||||
rewrite: (path) =>
|
'/livekit-api': {
|
||||||
path.replace(new RegExp(`^/dev-api`), '')
|
target: 'https://meeting.cnsdt.com/api/v1', // 从环境变量读取
|
||||||
},
|
changeOrigin: true,
|
||||||
'/livekit-api': {
|
ws: true,
|
||||||
target: 'https://meeting.cnsdt.com/api/v1', // 从环境变量读取
|
rewrite: (path) =>
|
||||||
changeOrigin: true,
|
path.replace(new RegExp(`^/livekit-api`), '')
|
||||||
ws: true,
|
}
|
||||||
rewrite: (path) =>
|
|
||||||
path.replace(new RegExp(`^/livekit-api`), '')
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
"@": path.resolve(__dirname, "./src"),
|
|
||||||
},
|
},
|
||||||
},
|
resolve: {
|
||||||
css: {
|
alias: {
|
||||||
postcss: './postcss.config.js' // 纯外部配置
|
"@": path.resolve(__dirname, "./src"),
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
postcss: './postcss.config.js' // 纯外部配置
|
||||||
|
},
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
13
vite/plugins/auto-import.js
Normal file
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
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
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
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
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
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user