feat:更新协作信息

This commit is contained in:
leilei
2025-11-18 17:42:15 +08:00
parent ca93e91326
commit a894367dcc
22 changed files with 1085 additions and 458 deletions

BIN
dist.zip

Binary file not shown.

View File

@@ -129,3 +129,12 @@ export function getParticipantsApi(roomId) {
})
}
// 获取当前房间中视频
export function getvideoUrlApi(room_uid) {
return request({
url: `/api/v1/room/${ room_uid }/recordings`,
method: 'get',
})
}

View File

@@ -17,3 +17,22 @@ export function getDirectoriesUsers(directory_uuid,data) {
params:data
})
}
//获取参与者历史参会记录
export function getParticipantsHistoryApi(userId,data) {
return request({
url: `/api/v1/rooms/${ userId }/participants/history`,
method: 'get',
params:data
})
}
// 获取用户详细信息
export function getInfo(userUid,type) {
return request({
url: `/api/v1/auth/users/${userUid}`,
method: 'get',
params:type
})
}

View File

@@ -20,6 +20,7 @@ import iframeToggle from "./IframeToggle/index.vue";
import useTagsViewStore from "@/stores/modules/tagsView.js";
const tagsViewStore = useTagsViewStore();
</script>
<style lang="scss" scoped>

View File

@@ -0,0 +1,149 @@
<template>
<el-dialog
v-model="inviteDialog"
title="远程协作"
width="400px"
:close-on-press-escape="false"
:close-on-click-modal="false"
:show-close="false"
>
<div style="width: 100%; margin-bottom: 30px; font-size: 20px">
"
{{
socketInformation.room_name
? socketInformation.room_name
: ''
}}
" 邀请您参加远程协作
</div>
<div style="text-align: center">
<el-button
size="large"
type="danger"
style="font-size: 16px"
@click="clickRefuseJoin"
>
</el-button>
<el-button
size="large"
type="primary"
style="font-size: 16px"
@click="clickJoin"
>
</el-button>
</div>
</el-dialog>
</template>
<script setup>
import { ref ,onMounted} from 'vue'
import { getStatusApi } from '@/api/conferencingRoom.js'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { mqttClient } from "@/utils/mqtt.js";
import { useUserStore } from '@/stores/modules/user.js'
const router = useRouter()
const userStore = useUserStore()
const inviteDialog = ref(false)
const socketInformation = ref(null)
/** 拒绝加入 */
const clickRefuseJoin = async () => {
//status 1: 同意加入, 5: 拒绝加入
try{
const res = await getStatusApi(socketInformation.value.room_uid,{status:5})
if(res.meta.code == 200){
ElMessage({
message: '已拒绝加入该协作',
type: 'error',
})
inviteDialog.value = false
}
} catch (error) {
console.log(error,'error')
inviteDialog.value = false
} finally {
inviteDialog.value = false
}
}
const clickJoin = async () => {
const res = await getStatusApi(socketInformation.value.room_uid,{status:1})
if(res.meta.code == 200){
ElMessage({
message: '成功加入该协作',
type: 'success',
})
inviteDialog.value = false
router.push({
path: '/conferencingRoom',
query:{
type:2,//创建房间,加入房间 2
room_uid:socketInformation.value.room_uid
}
})
}
inviteDialog.value = false
}
/** 浏览器通知 */
const showNotification = (data) => {
if ('Notification' in window) {
Notification.requestPermission().then((permission) => {
if (permission === 'granted') {
const notification = new Notification('协作邀请', {
// body: String(data.room_name) + '邀请您参加远程协作'
body: '远程协作有新的邀请'
// icon: logo,
})
notification.onclick = () => { clickJoin() }
}
})
}
}
/** 处理加入房间和拒接房间 mqtt 消息 */
const processingSocket = (message) => {
const res = JSON.parse(message)
console.log(res,'收到用户信息 邀请')
if (!res?.status) {
socketInformation.value = res
inviteDialog.value = true
showNotification(socketInformation.value)
}else if(res.status == 5){
ElMessage({
message: `${res?.display_name}拒绝加入该协作`,
type: 'error',
})
}
}
defineExpose({
processingSocket,
});
// onMounted(async () => {
// await mqttClient.connect(`room${Math.random().toString(16).substr(2, 8)}`);
// const res = await userStore.getInfo()
// const topic = `xSynergy/ROOM/+/rooms/${res.uid}`;
// mqttClient.subscribe(topic, async (shapeData) => {
// // console.log(shapeData.toString(),'shapeData发送邀请')
// processingSocket(shapeData.toString())
// });
// })
</script>
<style lang="scss" scoped>
</style>

View File

@@ -3,3 +3,4 @@ export { default as Navbar } from './Navbar.vue'
export { default as Settings } from './Settings/index.vue'
export { default as TagsView } from './TagsView/index.vue'
export { default as ResetPwd } from './ResetPwd/index.vue'
export { default as InviteJoin } from './InviteJoin/index.vue'

View File

@@ -37,24 +37,29 @@
</div>
</div>
<ResetPwd ref="resetPwdRef"/>
<InviteJoin ref="inviteJoinRef"/>
</div>
</template>
<script setup>
import { ElMessageBox } from 'element-plus'
import { ElMessageBox ,ElMessage} from 'element-plus'
import { useWindowSize } from '@vueuse/core'
import Sidebar from './components/Sidebar/index.vue'
import { AppMain, TagsView ,ResetPwd} from './components/index.js'
import { AppMain, TagsView ,ResetPwd,InviteJoin} from './components/index.js'
import { useAppStore } from '@/stores/modules/app.js'
import { useSettingsStore } from '@/stores/modules/settings.js'
import { useUserStore } from '@/stores/modules/user.js'
import { removeToken } from '@/utils/auth.js'
import { onMounted ,ref} from 'vue'
import { mqttClient } from "@/utils/mqtt.js";
const settingsStore = useSettingsStore()
const userStore = useUserStore()
const useAppStoreStore = useAppStore()
const router = useRouter()
const inviteJoinRef = ref(null)
const theme = computed(() => settingsStore.theme)
const sidebar = computed(() => useAppStoreStore.sidebar)
const device = computed(() => useAppStoreStore.device)
@@ -136,6 +141,18 @@ function handleClickOutside() {
useAppStoreStore.closeSideBar({ withoutAnimation: false })
}
onMounted(async () => {
await mqttClient.connect(`room${Math.random().toString(16).substr(2, 8)}`);
const res = await userStore.getInfo()
const topic = `xSynergy/ROOM/+/rooms/${res.uid}`;
mqttClient.subscribe(topic, async (shapeData) => {
if(inviteJoinRef.value){
inviteJoinRef.value.processingSocket(shapeData.toString())
}
});
})
</script>
<style lang="scss" scoped>

View File

@@ -68,6 +68,12 @@ export const constantRoutes = [
hidden: true,
meta: { title: "401未授权" }
},
{
path: '/assistWx',
component: () => import('@/views/coordinate/personnelList/components/assistWx/index.vue'),
meta: { title: "白板" },
hidden: true,
},
]
export const dynamicRoutes = [

View File

@@ -7,6 +7,8 @@ export const useRoomStore = defineStore('room', {
userUid: '',
//邀请进入房间的用户uid
detailUid: '',
//邀请用户名称
detailName: '',
}),
actions: {
setUserUid(data) {
@@ -14,6 +16,9 @@ export const useRoomStore = defineStore('room', {
},
setDetailUid(data) {
this.detailUid = data
},
setDetailName(data) {
this.detailName = data
}
}

View File

@@ -18,7 +18,6 @@ export const useUserStore = defineStore(
try {
const { username, password } = userInfo;
const trimmedUsername = username.trim();
const res = await login(trimmedUsername, password);
if (res.meta.code !== 200) {
ElMessage({ message: res.meta?.message || '登录失败', type: 'error' });

View File

@@ -1,6 +1,6 @@
import Cookies from "js-cookie";
import router from '@/router';
const TokenKey = "token";
export function getToken() {
@@ -18,3 +18,54 @@ export function removeToken() {
return sessionStorage.removeItem(TokenKey);
}
//获取用户信息
export function getUserInfo() {
try {
const userData = sessionStorage.getItem("userData");
// 如果userData不存在执行未授权处理
if (!userData) {
handleUnauthorized();
return null;
}
// 尝试解析JSON数据
try {
const parsedData = JSON.parse(userData);
return parsedData;
} catch (parseError) {
console.error('用户数据格式错误无法解析JSON:', parseError);
// 数据格式错误也视为未登录
sessionStorage.removeItem("userData");
handleUnauthorized();
return null;
}
} catch (error) {
console.error('获取用户信息时发生错误:', error);
handleUnauthorized();
return null;
}
}
function handleUnauthorized() {
removeToken();
// 使用 nextTick 确保路由状态已更新
import('vue').then(({ nextTick }) => {
nextTick(() => {
const currentPath = router.currentRoute.value.fullPath;
if (router.currentRoute.value.path !== '/login') {
router.push({
path: '/login',
query: {
redirect: currentPath !== '/login' ? currentPath : undefined
}
});
} else {
window.location.reload();
}
});
});
}

View File

@@ -156,9 +156,10 @@ service.interceptors.response.use(
// if(currentPath == 'ConferencingRoom'){
// return Promise.resolve(responseData);
// }else{
return handleUnauthorized().then(() => {
return Promise.reject({ code: 401, message: '未授权' });
});
return handleUnauthorized()
// .then(() => {
// return Promise.reject({ code: 401, message: '未授权' });
// });
// }
case 500:
const serverErrorMsg = responseData.meta?.message || '服务器内部错误';

View File

@@ -716,3 +716,26 @@ export function removeDuplicate(arr) {
});
return newArr; // 返回一个新数组
}
export function createThrottle(func, delay){
let timeoutId;
let lastExecTime = 0;
return (...args) => {
const currentTime = Date.now();
// 立即执行第一次
if (currentTime - lastExecTime > delay) {
func.apply(this, args);
lastExecTime = currentTime;
} else {
// 清除之前的定时器,设置新的定时器
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
lastExecTime = Date.now();
}, delay);
}
};
};

View File

@@ -2,7 +2,7 @@ import { ElMessage } from 'element-plus';
export function errorHandling(error,type) {
switch (error.name) {
case 'NotAllowedError':
ElMessage.error('用户拒绝了权限请求,请允许此网站使用摄像头');
ElMessage.error(`用户拒绝了权限请求,请允许此网站使用${type}权限`);
break;
case 'NotFoundError':
ElMessage.error(`未检测到可用的${type}设备,请检查${type}是否已正确连接`);

View File

@@ -4,6 +4,14 @@
<Login @loginSuccess="handleLoginSuccess" />
</div>
<div v-else>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<div class="loading-content">
<el-icon class="loading-icon"><Loading /></el-icon>
<p>正在创建房间请稍候...</p>
</div>
</div>
<!-- 音频元素 -->
<audio ref="localAudio" autoplay muted class="audio-element"></audio>
<div id="audio"></div>
@@ -101,7 +109,6 @@
autoplay
playsinline
class="screen-share-element"
style='border:1px solid red'
@loadedmetadata="handleScreenShareLoaded">
</video>
<!-- 如果没有屏幕共享显示提示 -->
@@ -445,7 +452,7 @@ const enlargedLaserPointerCanvas = ref(null); // 放大视频激光笔 Canvas
const enlargedLaserPointerContext = ref(null); // 放大视频激光笔上下文
const lastActivatedLayer = ref(''); // 记录最后激活的图层:'screenVideo' 或 'whiteboard'
const loading = ref(false);
// 路径更新节流处理
let lastPublishTime = 0;
const publishThrottleTime = 100; // 100ms 节流
@@ -1935,8 +1942,11 @@ async function handleConfirmSelection(userInfo){
ElMessage.error('请选择加入房间的人员')
return
}
const joinUserIds = userInfo.map(item => item.uid)
await getInvite(room.name,{user_uids:joinUserIds, participant_role: "participant"})
const joinUserInfo = userInfo.map(item => ({
user_uid: item.uid,
display_name: item.name
}));
await getInvite(room.name,{participants:joinUserInfo, participant_role: "participant"})
}
function publishWhiteboardMessage(type, payload = {}) {
@@ -2832,9 +2842,13 @@ function handleScreenShareEnded() {
async function joinRoomBtn() {
try {
loading.value = true; // 开始加载
status.value = true; // 确保显示加载状态
const res = await getRoomToken({max_participants: 20});
if(res.meta.code != 200){
ElMessage.error(res.meta.message);
loading.value = false;
return;
}
const token = res.data.access_token;
@@ -2849,6 +2863,8 @@ async function joinRoomBtn() {
} catch (error) {
ElMessage.error(`连接失败: ${error.message}`);
status.value = true;
} finally {
loading.value = false; // 无论成功失败都结束加载
}
}
@@ -3094,7 +3110,7 @@ onMounted(async () => {
hostUid.value = roomStore.userUid
// 邀请用户参与房间
if(room.name){
await getInvite(room.name,{user_uids:[roomStore.detailUid], participant_role: "participant"})
await getInvite(room.name,{participants:[{user_uid:roomStore.detailUid,display_name:roomStore.detailName}], participant_role: "participant"})
}
} else {
const res = await getTokenApi(route.query.room_uid)
@@ -3115,6 +3131,42 @@ onMounted(async () => {
</script>
<style lang="scss" scoped>
.loading-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(15, 15, 26, 0.95);
backdrop-filter: blur(10px);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-content {
text-align: center;
color: #ffffff;
}
.loading-icon {
font-size: 48px;
color: #409eff;
margin-bottom: 16px;
animation: spin 1s linear infinite;
}
.loading-content p {
margin: 0;
font-size: 16px;
color: #a0a0b0;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 内容层级容器 */
.content-layers-container {
position: relative;
@@ -3145,7 +3197,7 @@ onMounted(async () => {
.enlarged-video-wrapper,
.screen-share-wrapper {
width: 100%;
height: 100%;
height: 95%;
}
.enlarged-video-element,
@@ -3438,7 +3490,7 @@ body {
}
.screen-share-element {
width: 100%;
height: 100%;
height: 96%;
object-fit: contain;
background: #000;
border-radius: 8px;
@@ -3733,7 +3785,7 @@ body {
}
.microphone-control-group .dropdown-btn {
width: 36px;
height: 36px;
height: 40px;
}
.controls-container {
gap: 6px;

View File

@@ -1,34 +1,236 @@
<template>
<div class="wrapper-content">
<div>
<el-dialog
v-model="dialogFormVisible"
width="80%"
:show-close="false"
:destroy-on-close="true"
:modal="false"
:lock-scroll="true"
:before-close="handleClose"
class="call-dialog"
>
<div class="call-wrapper">
<!-- 上方头像与状态 -->
<div class="avatar-section">
<img class="avatar" :src="avatarUrl" alt="头像" />
<div class="user-name">{{ userName }}</div>
<div class="status-text">{{ statusText }}</div>
</div>
<!-- 底部操作按钮 -->
<div class="control-section">
<!-- 呼叫模式 -->
<template v-if="mode === 'call'">
<el-button
v-if="callStatus === 'calling'"
type="danger"
round
class="control-btn hangup"
@click="hangup"
>
挂断
</el-button>
<el-button
v-else
type="primary"
round
class="control-btn call"
@click="startCall"
>
呼叫中...
</el-button>
</template>
<!-- 接听模式 -->
<template v-else>
<el-button
type="success"
round
class="control-btn accept"
@click="acceptCall"
>
接听
</el-button>
<el-button
type="danger"
round
class="control-btn hangup"
@click="hangup"
>
挂断
</el-button>
</template>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, nextTick, onUnmounted, onMounted } from "vue";
import { ElLoading, ElMessage } from "element-plus";
import { useRoute } from "vue-router";
import { mqttClient } from "@/utils/mqtt";
<script setup>
import { ref, computed } from "vue";
import { ElMessage } from "element-plus";
const props = defineProps({
roomId: {
type: String,
default: '',
const props = defineProps({
mode: { type: String, default: "call" }, // call: 呼叫模式, receive: 接听模式
avatarUrl: {
type: String,
default: "https://cdn-icons-png.flaticon.com/512/1946/1946429.png",
},
userName: { type: String, default: "对方用户" },
});
})
const route = useRoute();
const dialogFormVisible = ref(false);
const callStatus = ref("calling"); // calling | active | ended
const statusText = computed(() => {
if (props.mode === "call") {
if (callStatus.value === "calling") return "正在呼叫对方...";
if (callStatus.value === "active") return "通话中";
return "通话结束";
} else {
return callStatus.value === "active" ? "通话中" : "对方来电...";
}
});
onMounted(async () => {
function startCall() {
callStatus.value = "active";
ElMessage.success("开始通话");
}
});
function acceptCall() {
callStatus.value = "active";
ElMessage.success("已接听");
}
onUnmounted(() => {
function hangup() {
callStatus.value = "ended";
ElMessage.error("通话已结束");
}
});
</script>
function handleClose() {
dialogFormVisible.value = false;
}
<style scoped>
function show() {
dialogFormVisible.value = true;
}
defineExpose({
show,
});
</script>
<style scoped>
/* ::v-deep .el-dialog__header{
background-color:red;
} */
/* 去除 el-dialog 默认样式,让它全屏覆盖且透明 */
.call-dialog :deep(.el-dialog) {
background: transparent;
box-shadow: none;
padding: 0;
margin: 0;
border-radius: 0;
height: calc(100vh - 30vh);
width: 100%;
max-width: none;
display: flex;
align-items: center;
justify-content: center;
}
.call-dialog :deep(.el-dialog__body) {
padding: 0;
height: 100%;
}
/* 通话主背景 */
.call-wrapper {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
height: calc(100vh - 30vh);
width: 100%;
background: linear-gradient(180deg, #0f2027, #203a43, #2c5364);
color: #fff;
box-sizing: border-box;
padding: 80px 0;
position: relative;
}
/* 头像部分 */
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
}
.avatar {
width: 180px;
height: 180px;
border-radius: 50%;
border: 6px solid rgba(255, 255, 255, 0.25);
box-shadow: 0 0 40px rgba(0, 0, 0, 0.4);
object-fit: cover;
transition: transform 0.3s ease;
}
.avatar:hover {
transform: scale(1.05);
}
.user-name {
margin-top: 24px;
font-size: 26px;
font-weight: 600;
color: #fff;
}
.status-text {
margin-top: 12px;
font-size: 18px;
opacity: 0.8;
}
/* 控制按钮部分 */
.control-section {
display: flex;
justify-content: center;
align-items: center;
gap: 80px;
}
.control-btn {
width: 160px;
height: 60px;
font-size: 18px;
font-weight: 500;
letter-spacing: 1px;
transition: all 0.3s ease;
}
.control-btn:hover {
transform: scale(1.05);
}
.control-btn.accept {
background: linear-gradient(135deg, #00c853, #4caf50);
color: #fff;
border: none;
}
.control-btn.hangup {
background: linear-gradient(135deg, #ff1744, #d50000);
color: #fff;
border: none;
}
.control-btn.call {
background: linear-gradient(135deg, #2196f3, #1976d2);
color: #fff;
border: none;
}
</style>
</style>

View File

@@ -19,7 +19,38 @@
</div>
</div>
<div class="list-content">
<div class="content-top-input">
<div class="content-top-input" v-if="leftTab == 1">
<!-- <el-input
v-model="queryFrom.nickName"
placeholder="搜索成员"
type="text"
prefix-icon="Search"
@change="searchList"
/> -->
<el-select
v-model="participant_user"
multiple
filterable
clearable
remote
reserve-keyword
placeholder="搜索成员"
:remote-method="remoteMethod"
:loading="loading"
collapse-tags
collapse-tags-tooltip
style="width: 100%"
@change="searchList"
>
<el-option
v-for="item in userList"
:key="item.uid"
:label="item.name"
:value="item.uid"
/>
</el-select>
</div>
<div class="content-top-input" v-if="leftTab == 2">
<el-input
v-model="queryFrom.nickName"
placeholder="搜索成员"
@@ -47,49 +78,50 @@
v-infinite-scroll="infinite"
v-if="dataList?.length"
>
<!-- @click="updateDetail(item)" -->
<div
v-for="(item, index) in dataList"
:key="index"
class="content-list-item"
@click="updateDetail(item)"
:style="
item.assistanceId == assistanceId
item.id == assistanceId
? 'border-color: #409EFF; '
: ''
"
>
<div class="list-item-top">
<span>
{{ parseTime(item.beginTime, '{m}月{d}日') }}
{{ weekName[new Date(item.beginTime).getDay()] }}
{{ parseTime(item.created_at, '{m}月{d}日') }}
{{ weekName[new Date(item.created_at).getDay()] }}
</span>
<span>
{{ parseTime(item.beginTime, '{y}年') }}
{{ parseTime(item.created_at, '{y}年') }}
</span>
</div>
<div class="list-item-content">
<div class="list-item-content-text">
<div style="display: flex; flex-wrap: wrap">
<span
v-for="(items, indexs) in item.assistanceMemberList"
v-for="(items, indexs) in item.all_participants"
:key="indexs"
>
{{
items.nickName +
(indexs + 1 == item.assistanceMemberList.length
items.display_name +
(indexs + 1 == item.all_participants.length
? ''
: '、')
}}
</span>
</div>
<span>
发起人{{ item.initiatorName ? item.initiatorName : '' }}
发起人{{ item.all_participants.find(item => item.participant_role == 'moderator')?.display_name || ''}}
</span>
<span>
时间{{
parseTime(item.beginTime, '{h}:{i}') +
parseTime(item.created_at, '{h}:{i}') +
' ~ ' +
(item.endTime ? parseTime(item.endTime, '{h}:{i}') : '')
(item.updated_at ? parseTime(item.updated_at, '{h}:{i}') : '')
}}
</span>
</div>
@@ -138,10 +170,14 @@
<script setup>
import {
// getAssistanceList,
getParticipantsHistoryApi,
getDirectories,
getDirectoriesUsers
getDirectoriesUsers,
getInfo,
} from '@/api/coordinate.js'
import { nextTick, reactive, toRefs, watch, onMounted } from 'vue'
import { deepClone,parseTime ,createThrottle} from '@/utils/ruoyi.js'
import { getUserInfo } from '@/utils/auth.js'
// 接收 props
const props = defineProps({
@@ -157,13 +193,15 @@ const emit = defineEmits(['updateDetail', 'updateTab'])
// state
const state = reactive({
isFirst: true,
leftTab: 2,
leftTab: 1,
queryFrom: {
pageNum: 1,
pageSize: 10,
page_size: 10,
page: 1,
nickName: '',
leftDatePicker: null,
participant_user_uids:''
},
participant_user:[],
leftListLoading: true,
loading: false,
dataList: [],
@@ -216,7 +254,22 @@ const state = reactive({
},
},
assistanceId: '',
userList:[],
})
// state.loading = true;
const remoteMethod = createThrottle(async (query) => {
if (!query || query.trim() === '') {
state.userList = [];
return;
}
try {
const res = await getInfo(query, { search_type: 'name' });
state.userList = res.data || [];
} catch (error) {
console.error('搜索用户失败:', error);
state.userList = [];
}
}, 500);
/**
* 树状列表筛选
@@ -231,11 +284,11 @@ const filterNode = (value, data) => {
*/
const searchList = () => {
if (state.leftTab == 1) {
state.queryFrom.pageNum = 1
state.queryFrom.page = 1
state.dataList = []
state.queryFrom.participant_user_uids = state.participant_user.join(',')
getList()
} else {
// console.log('treeRef.filter',state.treeRef)
state.treeRef.filter(state.queryFrom.nickName)
}
}
@@ -245,7 +298,7 @@ const searchList = () => {
*/
const updateDetail = (item) => {
if (state.leftTab == 1) {
state.assistanceId = item.assistanceId
state.assistanceId = item.id
emit('updateDetail', item)
} else {
if (item.uid) {
@@ -257,12 +310,12 @@ const updateDetail = (item) => {
/**
* 触底加载
*/
// const infinite = () => {
// if (state.more) {
// state.queryFrom.pageNum++
// getList()
// }
// }
const infinite = () => {
if (state.more) {
state.queryFrom.page++
getList()
}
}
/**
* 协作记录
@@ -270,30 +323,50 @@ const updateDetail = (item) => {
const getList = async () => {
try {
state.leftListLoading = true
let query = deepClone(state.queryFrom)
// if (query.leftDatePicker?.length) {
// query.beginSignTime = query.leftDatePicker[0]
// query.endSignTime = query.leftDatePicker[1]
// }
let query = structuredClone(state.queryFrom)
if (query.leftDatePicker?.length) {
query.beginSignTime = query.leftDatePicker[0]
query.endSignTime = query.leftDatePicker[1]
const startTime = new Date(query.leftDatePicker[0]);
const endTime = new Date(query.leftDatePicker[1]);
// 添加日期有效性验证
if (!isNaN(startTime.getTime())) {
query.start_time = Math.floor(startTime.getTime());
} else {
console.error('开始时间格式无效:', query.leftDatePicker[0]);
query.start_time = '';
}
if (!isNaN(endTime.getTime())) {
query.end_time = Math.floor(endTime.getTime());
} else {
console.error('结束时间格式无效:', query.leftDatePicker[1]);
query.end_time = '';
}
}
delete query.leftDatePicker
let infoData = await getAssistanceList({ ...query })
state.dataList = infoData.rows.length
? state.dataList.concat(infoData.rows)
const userData = await getUserInfo()
if(!userData) return
let infoData = await getParticipantsHistoryApi( userData?.uid ,{ ...query })
state.dataList = infoData.data.history?.length
? state.dataList.concat(infoData.data.history)
: []
if (state.isFirst) {
emit('updateDetail', state.dataList.length ? state.dataList[0] : null)
state.assistanceId = state.dataList.length
? state.dataList[0].assistanceId
: ''
emit('updateDetail', state.dataList?.length ? state.dataList[0] : null)
state.assistanceId = state.dataList?.length
? state.dataList[0].id
: ''
state.isFirst = false
}
state.more = state.dataList.length < infoData.total
state.isShow = Boolean(state.dataList.length)
state.more = state.dataList?.length < infoData.data.total
state.isShow = Boolean(state.dataList?.length)
state.leftListLoading = false
} catch (err) {
console.log(err)
@@ -352,19 +425,6 @@ const loadUserNode = async(resolve,id,level)=>{
}
}
// try {
// if (userList.data.sub_units?.length) {
// state.dataList = userList.data.sub_units
// let user = getUser(state.dataList)
// emit('updateDetail', user)
// } else {
// emit('updateDetail', null)
// }
// state.leftListLoading = false
// }
const getUser = (list) => {
for (let i = 0; i < list.length; i++) {
if (list[i].type == 2) {
@@ -398,15 +458,16 @@ watch(
state.dataList = []
state.isFirst = true
state.queryFrom = {
pageNum: 1,
pageSize: 10,
page_size: 10,
page: 1,
nickName: '',
participant_user_uids:'',
leftDatePicker: null,
}
if (newValue == 1) {
state.isShow = false
// getList()
getList()
} else {
HandleLoadNode()
}
@@ -416,7 +477,7 @@ watch(
onMounted(() => {
state.dataList = []
state.isFirst = true
// getList()
getList()
})
/**
@@ -425,7 +486,7 @@ onMounted(() => {
const {
isFirst, leftTab, queryFrom, leftListLoading, loading,
dataList, more, isShow, shortcuts, weekName,
treeRef, treeProps, assistanceId
treeRef, treeProps, assistanceId,userList,participant_user
} = toRefs(state)
</script>

View File

@@ -1,278 +1,278 @@
<template>
<div>
<div class="app-container" v-loading="load" :element-loading-text="loadText">
<el-row :gutter="6" style="padding: 0 10px; background: #fefefe">
<el-col :xs="24" :sm="24" :md="8" :lg="6">
<leftTab
@updateDetail="updateDetail"
@updateTab="updateTab"
:loading="!detail?.appId && !detail?.userId && isShow"
/>
</el-col>
<el-col :xs="24" :sm="24" :md="16" :lg="18">
<div
class="right-content"
>
<!-- v-loading="!detail?.appId && !detail?.userId && isShow" -->
<div class="right-content-title">
{{ tabValue == 1 ? '协作信息' : '员工信息' }}
</div>
<el-row :gutter="6" style="padding: 0 10px; background: #fefefe">
<el-col :xs="24" :sm="24" :md="8" :lg="6">
<leftTab
@updateDetail="updateDetail"
@updateTab="updateTab"
:loading="!detail?.appId && !detail?.userId && isShow"
/>
</el-col>
<el-col :xs="24" :sm="24" :md="16" :lg="18">
<div
class="agency-detail-massage-cont right-content-message"
v-if="isShow && tabValue == 1"
class="right-content"
v-loading='isShowLoading'
>
<div class="agency-detail-cont-item">
<span class="agency-detail-item-title">发起人</span>
<span class="agency-detail-item-content">
{{ detail.initiatorName }}
</span>
</div>
<div class="agency-detail-cont-item">
<span class="agency-detail-item-title">协作时间</span>
<span class="agency-detail-item-content">
<!-- {{
parseTime(detail.beginTime, '{y}年{m}月{d}日') +
' ' +
weekName[new Date(detail.beginTime).getDay()] +
' ' +
parseTime(detail.beginTime, '{h}:{i}')
}} -->
</span>
</div>
<div class="agency-detail-cont-item">
<span class="agency-detail-item-title">成员</span>
<span
class="agency-detail-item-content"
v-if="detail?.assistanceMemberList?.length"
style="display: flex; flex-wrap: wrap"
>
<span
v-for="(items, indexs) in detail.assistanceMemberList"
:key="indexs"
<div class="right-content-title">
{{ tabValue == 1 ? '协作信息' : '员工信息' }}
</div>
<div
class="agency-detail-massage-cont right-content-message"
v-if="isShow && tabValue == 1"
>
<div class="agency-detail-cont-item">
<span class="agency-detail-item-title">发起人</span>
<span class="agency-detail-item-content">
{{ detail.initiator }}
</span>
</div>
<div class="agency-detail-cont-item">
<span class="agency-detail-item-title">协作时间</span>
<span class="agency-detail-item-content">
{{
items.nickName +
(indexs + 1 == detail.assistanceMemberList.length
? ''
: '、')
detail.created_at ?
parseTime(detail.created_at, '{y}年{m}月{d}日') +
' ' +
weekName[new Date(detail.created_at).getDay()] +
' ' +
parseTime(detail.created_at, '{h}:{i}')
: '暂无'
}}
</span>
</span>
</div>
<div class="agency-detail-cont-item">
<span class="agency-detail-item-title">协作时长</span>
<span class="agency-detail-item-content">
<!-- {{ getTime() }} -->
</span>
</div>
</div>
<div class="right-content-file" v-if="isShow && tabValue == 1">
<el-row :gutter="15">
<el-col :xs="24" :sm="24" :md="16" :lg="18">
<div class="content-file-video">
<div class="file-top">协作视频</div>
<div class="file-video-bottom">
<!-- autoplay="autoplay" -->
<video
v-if="
detail.remoteVideoFile?.prefix &&
detail.remoteVideoFile.path
"
:src="
detail.remoteVideoFile.prefix +
detail.remoteVideoFile.path
"
id="videoPlayer"
loop
controls
></video>
<div v-else class="video-null">暂无视频</div>
</div>
</span>
</div>
</el-col>
<el-col :xs="24" :sm="24" :md="8" :lg="6">
<div>
<div class="file-top">
附件({{
detail?.fileList?.length ? detail.fileList.length : 0
}}
</div>
<div class="content-file-list">
<el-scrollbar
class="file-list"
height="calc(100vh - 380px)"
<div class="agency-detail-cont-item">
<span class="agency-detail-item-title">成员</span>
<span
class="agency-detail-item-content"
v-if="detail?.all_participants?.length"
style="display: flex; flex-wrap: wrap"
>
<div
class="file-list-content"
v-if="detail?.fileList?.length"
>
<div
class="file-list-item"
v-for="(item, index) in detail.fileList"
:key="index"
>
<div class="file-list-item-icon"></div>
<div class="file-list-item-text">
<div class="list-item-text text-out-of-hiding-1">
{{ item.name }}
</div>
<el-link
:href="item.prefix + item.path"
type="primary"
target="_blank"
:underline="false"
>
<el-icon
:size="18"
color="#0d74ff"
style="cursor: pointer"
>
<Download />
</el-icon>
</el-link>
<el-icon
:size="18"
color="#FF4646"
style="cursor: pointer"
@click="clickDeleteFile(item)"
>
<Delete />
</el-icon>
</div>
</div>
</div>
</el-scrollbar>
</div>
<span
v-for="(items, indexs) in detail.all_participants"
:key="indexs"
>
{{
items.display_name +
(indexs + 1 == detail.all_participants.length
? ''
: '、')
}}
</span>
</span>
</div>
</el-col>
</el-row>
</div>
<div
class="message-user"
v-else-if="isShow && tabValue == 2"
style="height: calc(100vh - 90px)"
v-loading="userLoading"
>
<div class="message-user-card">
<div class="user-card-nickName">
<img v-if="detail.avatar" :src="detail.avatar" />
<img v-else src="@/assets/images/profile.jpg" />
<span>{{ detail.nickName || detail.name || '暂无信息' }}</span>
</div>
<div class="user-card-information">
<div class="user-information-item">
<div class="user-information-title">
<img src="@/assets/images/user-information1.png" alt="" />
<span>性别</span>
</div>
<div class="user-information-text">
<!-- <dict-tag
v-if="detail.sex"
:options="sys_user_sex"
:value="detail.sex"
/>
<div v-else>
{{ '暂无' }}
</div> -->
</div>
</div>
<div class="user-information-item">
<div class="user-information-title">
<img src="@/assets/images/user-information2.png" alt="" />
<span>手机号</span>
</div>
<div class="user-information-text">
{{ detail.phonenumber || '暂无' }}
</div>
</div>
<div class="user-information-item">
<div class="user-information-title">
<img src="@/assets/images/user-information3.png" alt="" />
<span>邮箱</span>
</div>
<div class="user-information-text">
{{ detail.email || '暂无' }}
</div>
</div>
<div class="user-information-item">
<div class="user-information-title">
<img src="@/assets/images/user-information4.png" alt="" />
<span>所属部门</span>
</div>
<div class="user-information-text">
{{ detail.organization || '暂无' }}
</div>
<div class="agency-detail-cont-item">
<span class="agency-detail-item-title">协作时长</span>
<span class="agency-detail-item-content">
{{ getTime() }}
</span>
</div>
</div>
<div class="user-card-btn">
<el-button type="info" @click="clickInitiate">
发起协作
</el-button>
</div>
</div>
</div>
<div v-else class="message-null">
<el-empty description="暂无内容" />
</div>
</div>
</el-col>
</el-row>
<el-dialog
v-model="inviteDialog"
title="远程协作"
width="400px"
:close-on-press-escape="false"
:close-on-click-modal="false"
:show-close="false"
>
<div>
<div style="width: 100%; margin-bottom: 30px; font-size: 20px">
"
{{
socketInformation.room_name
? socketInformation.room_name
: ''
}}
" 邀请您参加远程协作
</div>
<div style="text-align: center">
<el-button
size="large"
type="danger"
style="font-size: 16px"
@click="clickRefuseJoin"
>
拒 绝
</el-button>
<el-button
size="large"
type="primary"
style="font-size: 16px"
@click="clickJoin"
>
加 入
</el-button>
</div>
</div>
</el-dialog>
<div class="right-content-file" v-if="isShow && tabValue == 1">
<el-row :gutter="15">
<el-col :xs="24" :sm="24" :md="16" :lg="18">
<div class="content-file-video">
<div class="file-top">协作视频</div>
<div class="file-video-bottom">
<!-- autoplay="autoplay" -->
<video
v-if="detail.remoteVideoFile?.storage_url"
:src="detail.remoteVideoFile.storage_url"
id="videoPlayer"
loop
autoplay
controls
></video>
<div v-else class="video-null">暂无视频</div>
</div>
</div>
</el-col>
<el-col :xs="24" :sm="24" :md="8" :lg="6">
<div>
<div class="file-top">
附件{{
detail?.fileList?.length ? detail.fileList.length : 0
}}
</div>
<div class="content-file-list">
<el-scrollbar
class="file-list"
height="calc(100vh - 380px)"
>
<div
class="file-list-content"
v-if="detail?.fileList?.length"
>
<div
class="file-list-item"
v-for="(item, index) in detail.fileList"
:key="index"
>
<div class="file-list-item-icon"></div>
<div class="file-list-item-text">
<div class="list-item-text text-out-of-hiding-1">
{{ item.file_name }}
</div>
<el-icon
:size="18"
color="#0d74ff"
style="cursor: pointer"
@click="handlePreview(item)"
>
<View />
</el-icon>
<el-link
:href="item.source_url"
type="primary"
target="_blank"
:underline="false"
>
<el-icon
:size="18"
color="#0d74ff"
style="cursor: pointer"
>
<Download />
</el-icon>
</el-link>
</div>
</div>
</div>
</el-scrollbar>
</div>
</div>
</el-col>
</el-row>
</div>
<div
class="message-user"
v-else-if="isShow && tabValue == 2"
style="height: calc(100vh - 90px)"
v-loading="userLoading"
>
<div class="message-user-card">
<div class="user-card-nickName">
<img v-if="detail.avatar" :src="detail.avatar" />
<img v-else src="@/assets/images/profile.jpg" />
<span>{{ detail.nickName || detail.name || '暂无信息' }}</span>
</div>
<div class="user-card-information">
<div class="user-information-item">
<div class="user-information-title">
<img src="@/assets/images/user-information1.png" alt="" />
<span>性别</span>
</div>
<div class="user-information-text">
<!-- <dict-tag
v-if="detail.sex"
:options="sys_user_sex"
:value="detail.sex"
/>
<div v-else>
{{ '暂无' }}
</div> -->
</div>
</div>
<div class="user-information-item">
<div class="user-information-title">
<img src="@/assets/images/user-information2.png" alt="" />
<span>手机号</span>
</div>
<div class="user-information-text">
{{ detail.phonenumber || '暂无' }}
</div>
</div>
<div class="user-information-item">
<div class="user-information-title">
<img src="@/assets/images/user-information3.png" alt="" />
<span>邮箱</span>
</div>
<div class="user-information-text">
{{ detail.email || '暂无' }}
</div>
</div>
<div class="user-information-item">
<div class="user-information-title">
<img src="@/assets/images/user-information4.png" alt="" />
<span>所属部门</span>
</div>
<div class="user-information-text">
{{ detail.organization || '暂无' }}
</div>
</div>
</div>
<div class="user-card-btn">
<el-button type="info" @click="clickInitiate">
发起协作
</el-button>
</div>
</div>
</div>
<div v-else class="message-null">
<el-empty description="暂无内容" />
</div>
</div>
</el-col>
</el-row>
<!-- <el-dialog
v-model="inviteDialog"
title="远程协作"
width="400px"
:close-on-press-escape="false"
:close-on-click-modal="false"
:show-close="false"
>
<div style="width: 100%; margin-bottom: 30px; font-size: 20px">
"
{{
socketInformation.room_name
? socketInformation.room_name
: ''
}}
" 邀请您参加远程协作
</div>
<div style="text-align: center">
<el-button
size="large"
type="danger"
style="font-size: 16px"
@click="clickRefuseJoin"
>
</el-button>
<el-button
size="large"
type="primary"
style="font-size: 16px"
@click="clickJoin"
>
</el-button>
</div>
</el-dialog> -->
</div>
</div>
<!-- 文件预览 -->
<BrowseFile ref="browseFileRef" />
</div>
</template>
<script setup>
import { onActivated, onMounted, reactive, toRefs, watch, getCurrentInstance ,ref} from 'vue'
import leftTab from './components/leftTab/index.vue'
import AssistWx from './components/assistWx/index.vue'
import BrowseFile from '@/views/conferencingRoom/components/fileUpload/browseFile.vue'
import { getInfo } from '@/api/login.js'
import { getStatusApi } from '@/api/conferencingRoom.js'
import { getStatusApi ,getFileListApi ,getvideoUrlApi} from '@/api/conferencingRoom.js'
import { ElMessage } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
import { useRoomStore } from '@/stores/modules/room'
import { useUserStore } from '@/stores/modules/user.js'
import { mqttClient } from "@/utils/mqtt.js";
import { getToken } from '@/utils/auth.js'
import { getToken ,getUserInfo} from '@/utils/auth.js';
import { deepClone,parseTime } from '@/utils/ruoyi.js'
const roomStore = useRoomStore()
const userStore = useUserStore()
const router = useRouter()
@@ -280,8 +280,9 @@ const { proxy } = getCurrentInstance()
const state = reactive({
detail: {},
weekName: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
tabValue: 2,
tabValue: 1,
isShow: true,
isShowLoading: false,
load: false,
loadText: '数据加载中',
isLinkKnow: 'F',
@@ -290,6 +291,8 @@ const state = reactive({
cooperation: import.meta.env.VITE_APP_COOPERATION_TYPE,
})
const userLoading = ref(false); // 用户信息加载状态
//文件预览
const browseFileRef = ref(null);
const isEmptyObject = (obj) => {
return !obj || Object.keys(obj).length === 0
@@ -303,6 +306,8 @@ const clickInitiate = () => {
} catch (e) {
console.error('解析 userData 失败:', e)
}
if (isEmptyObject(state.detail)) {
ElMessage({
message: '请先选择人员',
@@ -319,6 +324,7 @@ const clickInitiate = () => {
}
roomStore.setUserUid(userData.uid)
roomStore.setDetailUid(state.detail.uid)
roomStore.setDetailName(state.detail.name)
router.push({
path: '/conferencingRoom',
query:{
@@ -339,28 +345,44 @@ const updateDetail = async (details) => {
if (details) {
state.detail = {}
if (state.tabValue == 1) {
state.isShow = true
state.isShowLoading = true
getTheFileList(details)
} else {
const res = await getInfo(details.uid)
state.detail = res.data
userLoading.value = false
// getInfo(details.uid)
// .then((res) => {
// console.log(res,'人员详细信息')
// })
// .finally(() => { state.isShow = true })
}
} else {
state.isShow = false
}
userLoading.value = false
}
const getTheFileList = async (details) => {
try {
let detail = deepClone(details);
const [fileResponse, videoResponse] = await Promise.all([
getFileListApi(details.room_uid),
getvideoUrlApi(details.room_uid)
]);
const processedDetail = {
...details,
fileList: fileResponse.data?.files || [],
remoteVideoFile: videoResponse.data?.recordings?.[0] || {},
initiator: details.all_participants?.find(item =>
item.participant_role === 'moderator' // 使用严格相等
)?.display_name || '未知发起人'
};
state.detail = processedDetail;
state.isShowLoading = false;
} catch (error) {
console.error('获取文件列表失败:', error);
// 可以根据需要添加错误处理逻辑
}
}
/** 获取通话时长 */
const getTime = () => {
let begin = new Date(state.detail.beginTime).getTime()
let end = new Date(state.detail.endTime).getTime()
let begin = new Date(state.detail.created_at).getTime()
let end = new Date(state.detail.updated_at).getTime()
if (begin && end) {
let diff = end - begin
const h = Math.floor(diff / (1000 * 60 * 60))
@@ -374,83 +396,92 @@ const getTime = () => {
}
}
/** 加入会议 */
const clickJoin = async () => {
const res = await getStatusApi(state.socketInformation.room_uid,{status:1})
if(res.meta.code == 200){
ElMessage({
message: '成功加入该协作',
type: 'success',
})
state.inviteDialog = false
router.push({
path: '/conferencingRoom',
query:{
type:2,//创建房间,加入房间 2
room_uid:state.socketInformation.room_uid
}
})
}
state.inviteDialog = false
//文件预览
function handlePreview(file) {
if (!file.preview_url) {
ElMessage.error('文件链接无效');
return;
}
browseFileRef.value.showEdit(file)
}
/** 加入会议 */
// const clickJoin = async () => {
// const res = await getStatusApi(state.socketInformation.room_uid,{status:1})
// if(res.meta.code == 200){
// ElMessage({
// message: '成功加入该协作',
// type: 'success',
// })
// state.inviteDialog = false
// router.push({
// path: '/conferencingRoom',
// query:{
// type:2,//创建房间,加入房间 2
// room_uid:state.socketInformation.room_uid
// }
// })
// }
// state.inviteDialog = false
// }
/** 拒绝加入 */
const clickRefuseJoin = async () => {
//status 1: 同意加入, 5: 拒绝加入
const res = await getStatusApi(state.socketInformation.room_uid,{status:5})
if(res.meta.code == 200){
ElMessage({
message: '已拒绝加入该协作',
type: 'error',
})
state.inviteDialog = false
}
}
// const clickRefuseJoin = async () => {
// //status 1: 同意加入, 5: 拒绝加入
// const res = await getStatusApi(state.socketInformation.room_uid,{status:5})
// if(res.meta.code == 200){
// ElMessage({
// message: '已拒绝加入该协作',
// type: 'error',
// })
// state.inviteDialog = false
// }
// }
/** 处理加入房间和拒接房间 mqtt 消息 */
const processingSocket = (message) => {
const res = JSON.parse(message)
if (!res?.status) {
state.socketInformation = res
state.inviteDialog = true
showNotification(state.socketInformation)
}else if(res.status == 5){
ElMessage({
message: `${res?.display_name}拒绝加入该协作`,
type: 'error',
})
}
}
// const processingSocket = (message) => {
// const res = JSON.parse(message)
// if (!res?.status) {
// state.socketInformation = res
// state.inviteDialog = true
// showNotification(state.socketInformation)
// }else if(res.status == 5){
// ElMessage({
// message: `${res?.display_name}拒绝加入该协作`,
// type: 'error',
// })
// }
// }
/** 浏览器通知 */
const showNotification = (data) => {
if ('Notification' in window) {
Notification.requestPermission().then((permission) => {
if (permission === 'granted') {
const notification = new Notification('协作邀请', {
// body: String(data.room_name) + '邀请您参加远程协作'
body: '远程协作有新的邀请'
// icon: logo,
})
notification.onclick = () => { clickJoin() }
}
})
}
}
// const showNotification = (data) => {
// if ('Notification' in window) {
// Notification.requestPermission().then((permission) => {
// if (permission === 'granted') {
// const notification = new Notification('协作邀请', {
// // body: String(data.room_name) + '邀请您参加远程协作'
// body: '远程协作有新的邀请'
// // icon: logo,
// })
// notification.onclick = () => { clickJoin() }
// }
// })
// }
// }
// 暴露给模板
const { detail, weekName, tabValue, isShow, load, loadText, isLinkKnow, socketInformation, inviteDialog, cooperation } = toRefs(state)
onMounted(async () => {
await mqttClient.connect(`room${Math.random().toString(16).substr(2, 8)}`);
const res = await userStore.getInfo()
const topic = `xSynergy/ROOM/+/rooms/${res.uid}`;
mqttClient.subscribe(topic, async (shapeData) => {
// console.log(shapeData.toString(),'shapeData发送邀请')
processingSocket(shapeData.toString())
});
})
const { detail, weekName, tabValue, isShow, load, loadText, isLinkKnow, socketInformation, inviteDialog, cooperation,isShowLoading } = toRefs(state)
// onMounted(async () => {
// await mqttClient.connect(`room${Math.random().toString(16).substr(2, 8)}`);
// const res = await userStore.getInfo()
// const topic = `xSynergy/ROOM/+/rooms/${res.uid}`;
// mqttClient.subscribe(topic, async (shapeData) => {
// // console.log(shapeData.toString(),'shapeData发送邀请')
// processingSocket(shapeData.toString())
// });
// })
</script>