Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fadded872 | ||
|
|
f684fd49fb | ||
|
|
1a6b15ea79 | ||
|
|
c522e5f7f1 | ||
|
|
042e72925d | ||
|
|
a1faf63eb8 | ||
|
|
6600c7c282 | ||
| b9c3488915 | |||
| ee706b2540 | |||
| 6cf53f897b | |||
| fdd8dfbf98 | |||
| 6a4334797b | |||
| b4ba201631 | |||
| 9faf75265d | |||
| 39b11d0c10 | |||
| 0101bc8ad1 | |||
| 982a61ddc8 | |||
| 82d4a0865b | |||
| 5d853d565c | |||
| f10d2dc642 | |||
| cdddde25d1 | |||
| e213dcf656 | |||
| c8361daa8e | |||
| fec0b747ea | |||
| edc8945735 | |||
| ffb141383d | |||
| 3365c09b73 | |||
| bb83625fbf | |||
| 2e525d5ea1 | |||
| 6e471815cc | |||
| 0eb56d5732 | |||
| f8c1171656 | |||
| 0b77dca9da | |||
|
|
c9e5ae8da4 | ||
|
|
32e0ec0a2c | ||
|
|
1457a44f0b | ||
|
|
77b499ffb0 | ||
|
|
5bf4998cbd |
48
.gitea/workflows/deploy.yaml
Normal file
48
.gitea/workflows/deploy.yaml
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Deploy Lighttpd Admin
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'dev'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
volumes:
|
||||
- /app/lighttpd-admin:/app/xsy-admin
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Copy specific directory to runner's host machine
|
||||
run: |
|
||||
TARGET_DIR="./dist"
|
||||
|
||||
# 检查 dist 目录是否存在
|
||||
if [ ! -d "$TARGET_DIR" ]; then
|
||||
echo "Error: The source directory '$TARGET_DIR' does not exist in the repository."
|
||||
# 如果目录不存在,报错并退出当前步骤
|
||||
exit 1
|
||||
fi
|
||||
ls -lha /app/xsy-admin/
|
||||
rm -rf /app/xsy-admin/*
|
||||
cp -r ./dist/* /app/xsy-admin/
|
||||
|
||||
- name: Find and restart the app container
|
||||
run: |
|
||||
# 1. 使用 docker ps 过滤包含 'xsy-admin' 服务的容器
|
||||
# 2. 提取容器 ID 或名称
|
||||
CONTAINER_ID=$(docker ps -a --filter "name=xsy-admin" --format "{{.ID}}")
|
||||
|
||||
if [ -z "$CONTAINER_ID" ]; then
|
||||
echo "Error: Could not find any container matching name 'app1'."
|
||||
exit 1
|
||||
else
|
||||
echo "Found container ID: $CONTAINER_ID. Restarting..."
|
||||
# 使用标准的 docker restart 命令
|
||||
docker restart "$CONTAINER_ID"
|
||||
echo "Container restarted successfully."
|
||||
fi
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,8 +8,7 @@ pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
.DS_Store
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
1
dist/assets/401-BSBdQqy4.css
vendored
Normal file
1
dist/assets/401-BSBdQqy4.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.errPage-container[data-v-2c8b7582]{width:800px;max-width:100%;margin:100px auto}.errPage-container .pan-back-btn[data-v-2c8b7582]{background:#008489;color:#fff;border:none!important}.errPage-container .pan-gif[data-v-2c8b7582]{margin:0 auto;display:block}.errPage-container .pan-img[data-v-2c8b7582]{display:block;margin:0 auto;width:100%}.errPage-container .text-jumbo[data-v-2c8b7582]{font-size:60px;font-weight:700;color:#484848}.errPage-container .list-unstyled[data-v-2c8b7582]{font-size:14px}.errPage-container .list-unstyled li[data-v-2c8b7582]{padding-bottom:5px}.errPage-container .list-unstyled a[data-v-2c8b7582]{color:#008489;text-decoration:none}.errPage-container .list-unstyled a[data-v-2c8b7582]:hover{text-decoration:underline}
|
||||
1
dist/assets/401-DO7f-liB.js
vendored
Normal file
1
dist/assets/401-DO7f-liB.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{_ as d,g as f,r as m,c as k,f as o,w as n,h as s,o as g,p as a,k as e}from"./index-DRNenl-T.js";const h=""+new URL("401-HGF6Q5qM.gif",import.meta.url).href,w={class:"errPage-container"},x={class:"list-unstyled"},b={class:"link-type"},v=["src"],y={__name:"401",setup(B){let{proxy:r}=f();const u=m(h+"?"+ +new Date);function c(){r.$route.query.noGoBack?r.$router.push({path:"/"}):r.$router.go(-1)}return(C,t)=>{const _=s("el-button"),i=s("router-link"),l=s("el-col"),p=s("el-row");return g(),k("div",w,[o(_,{icon:"arrow-left",class:"pan-back-btn",onClick:c},{default:n(()=>[...t[0]||(t[0]=[a(" 返回 ",-1)])]),_:1}),o(p,null,{default:n(()=>[o(l,{span:12},{default:n(()=>[t[2]||(t[2]=e("h1",{class:"text-jumbo text-ginormous"}," 401错误! ",-1)),t[3]||(t[3]=e("h2",null,"您没有访问权限!",-1)),t[4]||(t[4]=e("h6",null,"对不起,您没有访问权限,请不要进行非法操作!您可以返回主页面",-1)),e("ul",x,[e("li",b,[o(i,{to:"/"},{default:n(()=>[...t[1]||(t[1]=[a(" 回首页 ",-1)])]),_:1})])])]),_:1}),o(l,{span:12},{default:n(()=>[e("img",{src:u.value,width:"313",height:"428",alt:"Girl has dropped her ice cream."},null,8,v)]),_:1})]),_:1})])}}},I=d(y,[["__scopeId","data-v-2c8b7582"]]);export{I as default};
|
||||
BIN
dist/assets/401-DO7f-liB.js.gz
vendored
Normal file
BIN
dist/assets/401-DO7f-liB.js.gz
vendored
Normal file
Binary file not shown.
BIN
dist/assets/401-HGF6Q5qM.gif
vendored
Normal file
BIN
dist/assets/401-HGF6Q5qM.gif
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
1
dist/assets/404-C8Inh8VK.js
vendored
Normal file
1
dist/assets/404-C8Inh8VK.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{_ as o,B as l,c as r,k as t,C as _,f as c,q as n,i as d,w as p,h as m,o as u,p as v}from"./index-DRNenl-T.js";const h=""+new URL("404-N4aRkdWY.png",import.meta.url).href,a=""+new URL("404_cloud-CPexjtDj.png",import.meta.url).href,f={class:"wscn-http404-container"},g={class:"wscn-http404"},x={class:"bullshit"},k={class:"bullshit__headline"},w={__name:"404",setup(b){let e=l(()=>"找不到网页!");return(N,s)=>{const i=m("router-link");return u(),r("div",f,[t("div",g,[s[3]||(s[3]=_('<div class="pic-404" data-v-328ae272><img class="pic-404__parent" src="'+h+'" alt="404" data-v-328ae272><img class="pic-404__child left" src="'+a+'" alt="404" data-v-328ae272><img class="pic-404__child mid" src="'+a+'" alt="404" data-v-328ae272><img class="pic-404__child right" src="'+a+'" alt="404" data-v-328ae272></div>',1)),t("div",x,[s[1]||(s[1]=t("div",{class:"bullshit__oops"}," 404错误! ",-1)),t("div",k,n(d(e)),1),s[2]||(s[2]=t("div",{class:"bullshit__info"}," 对不起,您正在寻找的页面不存在。尝试检查URL的错误,然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容。 ",-1)),c(i,{to:"/index",class:"bullshit__return-home"},{default:p(()=>[...s[0]||(s[0]=[v(" 返回首页 ",-1)])]),_:1})])])])}}},C=o(w,[["__scopeId","data-v-328ae272"]]);export{C as default};
|
||||
BIN
dist/assets/404-C8Inh8VK.js.gz
vendored
Normal file
BIN
dist/assets/404-C8Inh8VK.js.gz
vendored
Normal file
Binary file not shown.
1
dist/assets/404-Cb2yUGol.css
vendored
Normal file
1
dist/assets/404-Cb2yUGol.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.wscn-http404-container[data-v-328ae272]{transform:translate(-50%,-50%);position:absolute;top:40%;left:50%}.wscn-http404[data-v-328ae272]{position:relative;width:1200px;padding:0 50px;overflow:hidden}.wscn-http404 .pic-404[data-v-328ae272]{position:relative;float:left;width:600px;overflow:hidden}.wscn-http404 .pic-404__parent[data-v-328ae272]{width:100%}.wscn-http404 .pic-404__child[data-v-328ae272]{position:absolute}.wscn-http404 .pic-404__child.left[data-v-328ae272]{width:80px;top:17px;left:220px;opacity:0;animation-name:cloudLeft-328ae272;animation-duration:2s;animation-timing-function:linear;animation-fill-mode:forwards;animation-delay:1s}.wscn-http404 .pic-404__child.mid[data-v-328ae272]{width:46px;top:10px;left:420px;opacity:0;animation-name:cloudMid-328ae272;animation-duration:2s;animation-timing-function:linear;animation-fill-mode:forwards;animation-delay:1.2s}.wscn-http404 .pic-404__child.right[data-v-328ae272]{width:62px;top:100px;left:500px;opacity:0;animation-name:cloudRight-328ae272;animation-duration:2s;animation-timing-function:linear;animation-fill-mode:forwards;animation-delay:1s}@keyframes cloudLeft-328ae272{0%{top:17px;left:220px;opacity:0}20%{top:33px;left:188px;opacity:1}80%{top:81px;left:92px;opacity:1}to{top:97px;left:60px;opacity:0}}@keyframes cloudMid-328ae272{0%{top:10px;left:420px;opacity:0}20%{top:40px;left:360px;opacity:1}70%{top:130px;left:180px;opacity:1}to{top:160px;left:120px;opacity:0}}@keyframes cloudRight-328ae272{0%{top:100px;left:500px;opacity:0}20%{top:120px;left:460px;opacity:1}80%{top:180px;left:340px;opacity:1}to{top:200px;left:300px;opacity:0}}.wscn-http404 .bullshit[data-v-328ae272]{position:relative;float:left;width:300px;padding:30px 0;overflow:hidden}.wscn-http404 .bullshit__oops[data-v-328ae272]{font-size:32px;font-weight:700;line-height:40px;color:#1482f0;opacity:0;margin-bottom:20px;animation-name:slideUp-328ae272;animation-duration:.5s;animation-fill-mode:forwards}.wscn-http404 .bullshit__headline[data-v-328ae272]{font-size:20px;line-height:24px;color:#222;font-weight:700;opacity:0;margin-bottom:10px;animation-name:slideUp-328ae272;animation-duration:.5s;animation-delay:.1s;animation-fill-mode:forwards}.wscn-http404 .bullshit__info[data-v-328ae272]{font-size:13px;line-height:21px;color:gray;opacity:0;margin-bottom:30px;animation-name:slideUp-328ae272;animation-duration:.5s;animation-delay:.2s;animation-fill-mode:forwards}.wscn-http404 .bullshit__return-home[data-v-328ae272]{display:block;float:left;width:110px;height:36px;background:#1482f0;border-radius:100px;text-align:center;color:#fff;opacity:0;font-size:14px;line-height:36px;cursor:pointer;animation-name:slideUp-328ae272;animation-duration:.5s;animation-delay:.3s;animation-fill-mode:forwards}@keyframes slideUp-328ae272{0%{transform:translateY(60px);opacity:0}to{transform:translateY(0);opacity:1}}
|
||||
BIN
dist/assets/404-Cb2yUGol.css.gz
vendored
Normal file
BIN
dist/assets/404-Cb2yUGol.css.gz
vendored
Normal file
Binary file not shown.
BIN
dist/assets/404-N4aRkdWY.png
vendored
Normal file
BIN
dist/assets/404-N4aRkdWY.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
BIN
dist/assets/404_cloud-CPexjtDj.png
vendored
Normal file
BIN
dist/assets/404_cloud-CPexjtDj.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
1
dist/assets/authRole-BGeC_BV4.js
vendored
Normal file
1
dist/assets/authRole-BGeC_BV4.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
dist/assets/authRole-BGeC_BV4.js.gz
vendored
Normal file
BIN
dist/assets/authRole-BGeC_BV4.js.gz
vendored
Normal file
Binary file not shown.
1
dist/assets/authRoom-ScM_P5Lw.js
vendored
Normal file
1
dist/assets/authRoom-ScM_P5Lw.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
dist/assets/authRoom-ScM_P5Lw.js.gz
vendored
Normal file
BIN
dist/assets/authRoom-ScM_P5Lw.js.gz
vendored
Normal file
Binary file not shown.
1
dist/assets/index-27WP78gO.js
vendored
Normal file
1
dist/assets/index-27WP78gO.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
dist/assets/index-27WP78gO.js.gz
vendored
Normal file
BIN
dist/assets/index-27WP78gO.js.gz
vendored
Normal file
Binary file not shown.
1
dist/assets/index-B6TwTdR5.js
vendored
Normal file
1
dist/assets/index-B6TwTdR5.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
dist/assets/index-B6TwTdR5.js.gz
vendored
Normal file
BIN
dist/assets/index-B6TwTdR5.js.gz
vendored
Normal file
Binary file not shown.
1
dist/assets/index-B9qSM1WT.js
vendored
Normal file
1
dist/assets/index-B9qSM1WT.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
dist/assets/index-B9qSM1WT.js.gz
vendored
Normal file
BIN
dist/assets/index-B9qSM1WT.js.gz
vendored
Normal file
Binary file not shown.
1
dist/assets/index-BFgkfykF.js
vendored
Normal file
1
dist/assets/index-BFgkfykF.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{u as s,a as c,c as n,o as u}from"./index-DRNenl-T.js";const i={__name:"index",setup(p){const e=s(),t=c(),{params:o,query:a}=e,{path:r}=o;return t.replace({path:"/"+r,query:a}),(_,m)=>(u(),n("div"))}};export{i as default};
|
||||
1
dist/assets/index-CkP6b1E2.js
vendored
Normal file
1
dist/assets/index-CkP6b1E2.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
dist/assets/index-CkP6b1E2.js.gz
vendored
Normal file
BIN
dist/assets/index-CkP6b1E2.js.gz
vendored
Normal file
Binary file not shown.
1
dist/assets/index-DPF0UxJm.js
vendored
Normal file
1
dist/assets/index-DPF0UxJm.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
dist/assets/index-DPF0UxJm.js.gz
vendored
Normal file
BIN
dist/assets/index-DPF0UxJm.js.gz
vendored
Normal file
Binary file not shown.
80
dist/assets/index-DRNenl-T.js
vendored
Normal file
80
dist/assets/index-DRNenl-T.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
dist/assets/index-DRNenl-T.js.gz
vendored
Normal file
BIN
dist/assets/index-DRNenl-T.js.gz
vendored
Normal file
Binary file not shown.
1
dist/assets/index-Dz1wU-dg.css
vendored
Normal file
1
dist/assets/index-Dz1wU-dg.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
dist/assets/index-Dz1wU-dg.css.gz
vendored
Normal file
BIN
dist/assets/index-Dz1wU-dg.css.gz
vendored
Normal file
Binary file not shown.
1
dist/assets/login-C2MWIt8z.css
vendored
Normal file
1
dist/assets/login-C2MWIt8z.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
dist/assets/login-C2MWIt8z.css.gz
vendored
Normal file
BIN
dist/assets/login-C2MWIt8z.css.gz
vendored
Normal file
Binary file not shown.
14
dist/assets/login-dH8WbfOy.js
vendored
Normal file
14
dist/assets/login-dH8WbfOy.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
dist/assets/login-dH8WbfOy.js.gz
vendored
Normal file
BIN
dist/assets/login-dH8WbfOy.js.gz
vendored
Normal file
Binary file not shown.
BIN
dist/assets/loginBg-BZpWbmau.png
vendored
Normal file
BIN
dist/assets/loginBg-BZpWbmau.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
1
dist/assets/menu-Ca_cGcaO.js
vendored
Normal file
1
dist/assets/menu-Ca_cGcaO.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{A as e}from"./index-DRNenl-T.js";const i=s=>e({url:"/api/v1/permission/permissions/tree",method:"get",params:s}),o=s=>e({url:"/api/v1/permission/permissions",method:"post",data:s}),t=s=>e({url:"/api/v1/permission/permissions/"+s.uid,method:"put",data:s}),n=s=>e({url:"/api/v1/permission/permissions/"+s,method:"delete"}),p=s=>e({url:"/api/v1/permission/roles/"+s.roleId+"/permissions/add",method:"post",data:s}),a=s=>e({url:"/api/v1/permission/roles/"+s+"/all-permissions",method:"get"});export{o as a,p as b,n as d,a as g,i as l,t as u};
|
||||
BIN
dist/assets/profile-BkLdKdgI.jpg
vendored
Normal file
BIN
dist/assets/profile-BkLdKdgI.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
1
dist/assets/role-zDKXfY8i.js
vendored
Normal file
1
dist/assets/role-zDKXfY8i.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{A as s}from"./index-DRNenl-T.js";const t=e=>s({url:"/api/v1/permission/roles",method:"get",params:e}),a=e=>s({url:"/api/v1/permission/roles/"+e,method:"get"}),l=e=>s({url:"/api/v1/permission/roles",method:"post",data:e}),i=e=>s({url:"/api/v1/permission/roles/"+e.uid,method:"put",data:e}),u=e=>s({url:"/api/v1/permission/roles/"+e,method:"delete"}),n=e=>s({url:"/api/v1/permission/users/"+e.userId+"/roles",method:"get",params:e}),p=(e,r)=>s({url:"/api/v1/permission/users/"+r+"/roles",method:"get",params:e}),d=e=>s({url:"/api/v1/permission/users/"+e.userId+"/roles/remove",method:"post",data:e}),m=e=>s({url:"/api/v1/permission/users/"+e.userId+"/roles/add",method:"post",data:e});export{l as a,p as b,m as c,u as d,n as e,d as f,a as g,t as l,i as u};
|
||||
1
dist/assets/room-DnQmkEzy.js
vendored
Normal file
1
dist/assets/room-DnQmkEzy.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{A as r}from"./index-DRNenl-T.js";const o=t=>r({url:"/api/v1/rooms/list",method:"get",params:t}),s=(t,a)=>r({url:"/api/v1/rooms/"+t,method:"delete",params:a}),i=t=>r({url:"/api/v1/rooms/participants/list",method:"get",params:t}),p=(t,a)=>r({url:"/api/v1/rooms/"+t+"/participants",method:"delete",params:a}),m=(t,a)=>r({url:"/api/v1/meeting/"+t+"/participant/remove",method:"post",data:a}),n=(t,a)=>r({url:"/api/v1/meeting/"+t+"/participant/mute",method:"post",data:a});export{p as a,s as d,o as l,n as m,i as p,m as r};
|
||||
BIN
dist/favicon.ico
vendored
Normal file
BIN
dist/favicon.ico
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
14
dist/index.html
vendored
Normal file
14
dist/index.html
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="./favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>xSynergy远程协作系统</title>
|
||||
<script type="module" crossorigin src="./assets/index-DRNenl-T.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-Dz1wU-dg.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
148
src/layout/components/InviteJoin/index.vue
Normal file
148
src/layout/components/InviteJoin/index.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<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:{
|
||||
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>
|
||||
|
||||
99
src/utils/whiteboardSync.js
Normal file
99
src/utils/whiteboardSync.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import { mqttClient } from "./mqtt";
|
||||
import { getWhiteboardShapes, getWhiteboardHistory } from "@/views/custom/api";
|
||||
import { useMeterStore } from '@/stores/modules/meter';
|
||||
import { encode, decode } from '@msgpack/msgpack'
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { emitter } from "@/utils/bus.js";
|
||||
const meterStore = useMeterStore();
|
||||
meterStore.initUdid();
|
||||
|
||||
let isRemote = false;
|
||||
let canvasInstance = null;
|
||||
|
||||
// 获取本地缓存 userData
|
||||
function getLocalUserData() {
|
||||
const dataStr = sessionStorage.getItem('userData');
|
||||
if (!dataStr) return null;
|
||||
try {
|
||||
return JSON.parse(dataStr);
|
||||
} catch (e) {
|
||||
console.error("解析 userData 失败:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const WhiteboardSync = {
|
||||
async init(canvas, roomUid) {
|
||||
if (!canvas || !roomUid) return;
|
||||
canvasInstance = canvas;
|
||||
|
||||
const localUser = getLocalUserData();
|
||||
const localUid = localUser?.uid;
|
||||
try {
|
||||
// 先连接 MQTT
|
||||
await mqttClient.connect(meterStore.getSudid());
|
||||
// 获取历史数据
|
||||
const res = await getWhiteboardHistory({ after_timestamp: 0 }, roomUid);
|
||||
if (res.meta.code === 200 && res.data.shapes.length > 0) {
|
||||
canvasInstance.addShape(res.data.shapes);
|
||||
}else if(res.meta.code === 401){
|
||||
emitter.emit('whiteboardFailed',true);
|
||||
}
|
||||
|
||||
// 订阅当前房间
|
||||
const topic = `xsynergy/room/${roomUid}/whiteboard/#`;
|
||||
mqttClient.subscribe(topic, async (shapeData) => {
|
||||
const shapeDataNew = JSON.parse(shapeData.toString())
|
||||
// const shapeDataNew = decode(message);
|
||||
try {
|
||||
isRemote = true;
|
||||
// 如果 shape 来自本地用户,则跳过
|
||||
if (shapeDataNew.user_uid === localUid) return;
|
||||
const res = await getWhiteboardHistory({ after_timestamp: shapeDataNew.created_at }, roomUid);
|
||||
if (res.meta.code === 200) {
|
||||
canvasInstance.addShape(res.data.shapes);
|
||||
} else {
|
||||
ElMessage.error("获取历史数据失败");
|
||||
console.error("获取历史数据失败");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("处理MQTT数据失败:", e);
|
||||
} finally {
|
||||
isRemote = false;
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.log("初始化多人同步失败:", err)
|
||||
// console.error("❌ 连接或订阅失败:", err);
|
||||
}
|
||||
|
||||
// 监听画布事件:新增图形
|
||||
canvas.on('drawingEnd', async (shape) => {
|
||||
// 如果来自远程,或不是需要同步的类型,跳过
|
||||
if (isRemote || !['pencil', 'line', 'rectangle', 'circle', 'eraser'].includes(shape.type)) return;
|
||||
|
||||
// 如果是本地用户自己的 shape,则不调用接口
|
||||
if (shape.user_uid && shape.user_uid === localUid) return;
|
||||
|
||||
shape.room_uid = roomUid;
|
||||
try {
|
||||
await getWhiteboardShapes(shape, roomUid);
|
||||
} catch (err) {
|
||||
ElMessage.error("提交形状失败");
|
||||
console.error("提交形状失败:", err);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听画布事件:清空
|
||||
canvas.on('clear', async () => {
|
||||
if (!isRemote) {
|
||||
try {
|
||||
// TODO: 调用接口,后端再发 MQTT
|
||||
// await clearWhiteboard(roomUid);
|
||||
} catch (err) {
|
||||
console.error("提交清空失败:", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
103
src/utils/whiteboardSync_old.js
Normal file
103
src/utils/whiteboardSync_old.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import { mqttClient } from "./mqtt";
|
||||
import { getWhiteboardShapes, getWhiteboardHistory } from "@/views/custom/api";
|
||||
import { useMeterStore } from '@/stores/modules/meter';
|
||||
import { encode, decode } from '@msgpack/msgpack'
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const meterStore = useMeterStore();
|
||||
meterStore.initUdid();
|
||||
|
||||
let isRemote = false;
|
||||
let canvasInstance = null;
|
||||
|
||||
// 获取本地缓存 userData
|
||||
function getLocalUserData() {
|
||||
const dataStr = sessionStorage.getItem('userData');
|
||||
if (!dataStr) return null;
|
||||
try {
|
||||
return JSON.parse(dataStr);
|
||||
} catch (e) {
|
||||
console.error("解析 userData 失败:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const WhiteboardSync = {
|
||||
async init(canvas, roomUid) {
|
||||
if (!canvas || !roomUid) return;
|
||||
console.log('初始化多人同步:', roomUid);
|
||||
canvasInstance = canvas;
|
||||
|
||||
const localUser = getLocalUserData();
|
||||
const localUid = localUser?.uid;
|
||||
try {
|
||||
// 先连接 MQTT
|
||||
await mqttClient.connect(meterStore.getSudid());
|
||||
console.log("✅ MQTT 已连接");
|
||||
|
||||
// 获取历史数据
|
||||
const res = await getWhiteboardHistory({ after_timestamp: 0 }, roomUid);
|
||||
if (res.meta.code === 200 && res.data.shapes.length > 0) {
|
||||
canvasInstance.addShape(res.data.shapes);
|
||||
}
|
||||
|
||||
// 订阅当前房间
|
||||
const topic = `xsynergy/room/${roomUid}/whiteboard/#`;
|
||||
mqttClient.subscribe(topic, async (shapeData) => {
|
||||
const shapeDataNew = JSON.parse(shapeData.toString())
|
||||
// const shapeDataNew = decode(message);
|
||||
// console.log(shapeDataNew, '格式解码')
|
||||
try {
|
||||
isRemote = true;
|
||||
// 如果 shape 来自本地用户,则跳过
|
||||
if (shapeDataNew.user_uid === localUid) return;
|
||||
const res = await getWhiteboardHistory({ after_timestamp: shapeDataNew.created_at }, roomUid);
|
||||
if (res.meta.code === 200) {
|
||||
canvasInstance.addShape(res.data.shapes);
|
||||
} else {
|
||||
ElMessage.error("获取历史数据失败");
|
||||
console.error("获取历史数据失败");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("处理MQTT数据失败:", e);
|
||||
} finally {
|
||||
isRemote = false;
|
||||
}
|
||||
});
|
||||
|
||||
console.log("✅ 已订阅:", topic);
|
||||
} catch (err) {
|
||||
console.log("初始化多人同步失败:", err)
|
||||
// console.error("❌ 连接或订阅失败:", err);
|
||||
}
|
||||
|
||||
// 监听画布事件:新增图形
|
||||
canvas.on('drawingEnd', async (shape) => {
|
||||
// 如果来自远程,或不是需要同步的类型,跳过
|
||||
if (isRemote || !['pencil', 'line', 'rectangle', 'circle', 'eraser'].includes(shape.type)) return;
|
||||
|
||||
// 如果是本地用户自己的 shape,则不调用接口
|
||||
if (shape.user_uid && shape.user_uid === localUid) return;
|
||||
|
||||
shape.room_uid = roomUid;
|
||||
try {
|
||||
await getWhiteboardShapes(shape, roomUid);
|
||||
} catch (err) {
|
||||
ElMessage.error("提交形状失败");
|
||||
console.error("提交形状失败:", err);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听画布事件:清空
|
||||
canvas.on('clear', async () => {
|
||||
if (!isRemote) {
|
||||
try {
|
||||
// TODO: 调用接口,后端再发 MQTT
|
||||
// await clearWhiteboard(roomUid);
|
||||
} catch (err) {
|
||||
console.error("提交清空失败:", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
870
src/views/conferencingRoom/components/fileUpload/browseFile.vue
Normal file
870
src/views/conferencingRoom/components/fileUpload/browseFile.vue
Normal file
@@ -0,0 +1,870 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog
|
||||
v-model="dialogFormVisible"
|
||||
:title="title"
|
||||
width="80%"
|
||||
:before-close="handleClose"
|
||||
class="file-preview-dialog"
|
||||
>
|
||||
<div class="preview-container">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<el-icon class="is-loading" size="48">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<p>{{ loadingText }}</p>
|
||||
<!-- <p v-if="convertTaskId" class="task-id">任务ID: {{ convertTaskId }}</p> -->
|
||||
</div>
|
||||
|
||||
<!-- 转换状态 -->
|
||||
<div v-else-if="converting" class="loading-container">
|
||||
<el-icon class="is-loading" size="48">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<p>{{ conversionMessage }}</p>
|
||||
<!-- <p class="task-id">任务ID: {{ convertTaskId }}</p> -->
|
||||
</div>
|
||||
|
||||
<!-- 下载状态 -->
|
||||
<div v-else-if="downloading" class="loading-container">
|
||||
<el-icon class="is-loading" size="48">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<p>正在下载文件,请稍候...</p>
|
||||
<p class="download-progress" v-if="downloadProgress > 0">
|
||||
下载进度: {{ downloadProgress }}%
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="error" class="error-container">
|
||||
<el-icon size="48" color="#F56C6C">
|
||||
<CircleClose />
|
||||
</el-icon>
|
||||
<p>文件预览失败</p>
|
||||
<p class="error-message">{{ errorMessage }}</p>
|
||||
<el-button type="primary" @click="retryPreview">重试</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 文件预览内容 -->
|
||||
<div v-else class="file-content">
|
||||
<!-- 图片预览 -->
|
||||
<div v-if="isImage" class="image-preview">
|
||||
<img :src="previewUrl" :alt="fileName" @load="handleImageLoad" />
|
||||
</div>
|
||||
|
||||
<!-- PDF预览 - 使用 vue-pdf-embed -->
|
||||
<div v-else-if="isPdf" class="pdf-preview">
|
||||
<div class="pdf-controls" v-if="pageCount > 0">
|
||||
<el-button-group>
|
||||
<el-button :disabled="currentPage <= 1" @click="previousPage">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
上一页
|
||||
</el-button>
|
||||
<el-button>
|
||||
{{ currentPage }} / {{ pageCount }}
|
||||
</el-button>
|
||||
<el-button :disabled="currentPage >= pageCount" @click="nextPage">
|
||||
下一页
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
|
||||
<div class="pdf-viewer-container">
|
||||
<VuePdfEmbed
|
||||
:source="pdfSource"
|
||||
:page="currentPage"
|
||||
:scale="scale"
|
||||
@loaded="handlePdfLoaded"
|
||||
@rendered="handlePdfRendered"
|
||||
@error="handlePdfError"
|
||||
class="pdf-viewer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频预览 -->
|
||||
<div v-else-if="isVideo" class="video-preview">
|
||||
<video controls :src="previewUrl" class="video-player">
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<!-- 音频预览 -->
|
||||
<div v-else-if="isAudio" class="audio-preview">
|
||||
<audio controls :src="previewUrl" class="audio-player">
|
||||
您的浏览器不支持音频播放
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
<!-- 文本预览 -->
|
||||
<div v-else-if="isText" class="text-preview">
|
||||
<pre>{{ textContent }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- Office文档预览(转换后) -->
|
||||
<div v-else-if="isConvertedOffice" class="office-preview">
|
||||
<!-- 转换后的Office文件也是PDF,使用相同的PDF预览器 -->
|
||||
<div class="pdf-controls" v-if="pageCount > 0">
|
||||
<el-button-group>
|
||||
<el-button :disabled="currentPage <= 1" @click="previousPage">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
上一页
|
||||
</el-button>
|
||||
<el-button>
|
||||
{{ currentPage }} / {{ pageCount }}
|
||||
</el-button>
|
||||
<el-button :disabled="currentPage >= pageCount" @click="nextPage">
|
||||
下一页
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
|
||||
<div class="pdf-viewer-container">
|
||||
<VuePdfEmbed
|
||||
:source="pdfSource"
|
||||
:page="currentPage"
|
||||
:scale="scale"
|
||||
@loaded="handlePdfLoaded"
|
||||
@rendered="handlePdfRendered"
|
||||
@error="handlePdfError"
|
||||
class="pdf-viewer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 不支持预览的文件类型 -->
|
||||
<div v-else class="unsupported-preview">
|
||||
<el-icon size="64" color="#909399">
|
||||
<Document />
|
||||
</el-icon>
|
||||
<p>不支持在线预览此文件类型</p>
|
||||
<p class="file-name">{{ fileName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="close">
|
||||
关闭
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
v-model="dialogFileVisible"
|
||||
title="文件转换"
|
||||
width="50%"
|
||||
class="file-preview-dialog"
|
||||
>
|
||||
<div class="preview-container">
|
||||
<div class="loading-container">
|
||||
<el-icon class="is-loading" size="48">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<p>正在下载文件,请稍候...</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, getCurrentInstance, onUnmounted, nextTick, onMounted } from 'vue'
|
||||
import { convertFileApi, getConvertStatusApi } from '@/api/conferencingRoom'
|
||||
import { ElMessage ,ElMessageBox} from 'element-plus'
|
||||
import {
|
||||
Loading,
|
||||
CircleClose,
|
||||
Document,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
} from '@element-plus/icons-vue'
|
||||
import VuePdfEmbed from 'vue-pdf-embed'
|
||||
import { mqttClient } from "@/utils/mqtt.js";
|
||||
import { emitter } from "@/utils/bus.js";
|
||||
|
||||
// 定义props
|
||||
const props = defineProps({
|
||||
fileType: {
|
||||
type: Array,
|
||||
default: () => ["pdf", "png", "jpg", "jpeg", "gif", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "mp4", "mp3"],
|
||||
},
|
||||
roomId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
// 定义emits
|
||||
const emit = defineEmits(['fetch-data'])
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const enumType = ["doc", "docx", "xls", "xlsx", "ppt", "pptx"]
|
||||
|
||||
// 响应式数据
|
||||
const dialogFormVisible = ref(false)
|
||||
const title = ref('')
|
||||
const loading = ref(false)
|
||||
const converting = ref(false)
|
||||
const downloading = ref(false)
|
||||
const downloadProgress = ref(0)
|
||||
const loadingText = ref('正在加载...')
|
||||
const conversionMessage = ref('正在转换文件...')
|
||||
const error = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const previewUrl = ref('')
|
||||
const fileName = ref('')
|
||||
const textContent = ref('')
|
||||
const currentFileData = ref(null)
|
||||
const convertTaskId = ref('')
|
||||
const dialogFileVisible = ref(false)
|
||||
|
||||
|
||||
// PDF相关状态
|
||||
const pdfSource = ref('')
|
||||
const currentPage = ref(1)
|
||||
const pageCount = ref(0)
|
||||
const scale = ref(1.0)
|
||||
const pdfDocument = ref(null)
|
||||
|
||||
// MQTT相关
|
||||
const isMqttConnected = ref(false)
|
||||
|
||||
|
||||
// 计算属性
|
||||
const isImage = computed(() => {
|
||||
const ext = fileName.value.split('.').pop().toLowerCase()
|
||||
return ['png', 'jpg', 'jpeg', 'gif'].includes(ext)
|
||||
})
|
||||
|
||||
const isPdf = computed(() => {
|
||||
const ext = fileName.value.split('.').pop().toLowerCase()
|
||||
return ext === 'pdf'
|
||||
})
|
||||
|
||||
const isVideo = computed(() => {
|
||||
const ext = fileName.value.split('.').pop().toLowerCase()
|
||||
return ['mp4'].includes(ext)
|
||||
})
|
||||
|
||||
const isAudio = computed(() => {
|
||||
const ext = fileName.value.split('.').pop().toLowerCase()
|
||||
return ['mp3'].includes(ext)
|
||||
})
|
||||
|
||||
const isText = computed(() => {
|
||||
const ext = fileName.value.split('.').pop().toLowerCase()
|
||||
return ext === 'txt'
|
||||
})
|
||||
|
||||
const isOffice = computed(() => {
|
||||
const ext = fileName.value.split('.').pop().toLowerCase()
|
||||
return enumType.includes(ext)
|
||||
})
|
||||
|
||||
const isConvertedOffice = computed(() => {
|
||||
return previewUrl.value && previewUrl.value.endsWith('.pdf') && isOffice.value
|
||||
})
|
||||
|
||||
// 组件挂载时初始化MQTT连接
|
||||
onMounted(async () => {
|
||||
})
|
||||
emitter.on('subscribeToFileConversionStatusTopic',subscribeToFileConversionStatusTopic)
|
||||
emitter.on('fileUploadStatus',fileUploadStatus)
|
||||
emitter.on('subscribeToFilePreviewTopic',subscribeToFilePreviewTopic)
|
||||
emitter.on('fileSuccess',fileSuccess)
|
||||
|
||||
function fileSuccess(){
|
||||
dialogFileVisible.value = true
|
||||
}
|
||||
//
|
||||
function subscribeToFileConversionStatusTopic(data){
|
||||
try {
|
||||
const userId = JSON.parse(sessionStorage.getItem('userData'))?.uid
|
||||
if (!userId) {
|
||||
console.error('用户ID不存在')
|
||||
return
|
||||
}
|
||||
const topic = `xsynergy/room/${data.roomId}/file/${userId}/conversion_status`
|
||||
mqttClient.subscribe(topic, handlePdfMessage)
|
||||
|
||||
} catch (error) {
|
||||
console.error('订阅pdf转换事件失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function subscribeToFilePreviewTopic(data){
|
||||
try {
|
||||
const topic = `xsynergy/room/${data.roomId}/file/preview`
|
||||
mqttClient.subscribe(topic, handleFileUploadMessage)
|
||||
} catch (error) {
|
||||
console.error('订阅文件上传事件失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
//提交转换任务,但文件暂未转换完成
|
||||
function fileUploadStatus(data){
|
||||
// console.log('文件上传成功mqtt消息')
|
||||
}
|
||||
|
||||
// 初始化MQTT连接 pdf转换成功
|
||||
async function initMqttConnection() {
|
||||
try {
|
||||
if (isMqttConnected.value) return
|
||||
const clientId = `PdfConversion_${Date.now()}`
|
||||
await mqttClient.connect(clientId)
|
||||
isMqttConnected.value = true
|
||||
// 订阅主题
|
||||
subscribeToPdfConversionTopic()
|
||||
} catch (error) {
|
||||
console.error('MQTT连接失败:', error)
|
||||
ElMessage.error('文件转换服务连接失败')
|
||||
}
|
||||
}
|
||||
|
||||
function subscribeToPdfConversionTopic() {
|
||||
try {
|
||||
const userId = JSON.parse(sessionStorage.getItem('userData'))?.uid
|
||||
if (!userId) {
|
||||
console.error('用户ID不存在')
|
||||
return
|
||||
}
|
||||
const topic = `xsynergy/room/${props.roomId}/file/${userId}/conversion_status`
|
||||
mqttClient.subscribe(topic, handlePdfMessage)
|
||||
|
||||
} catch (error) {
|
||||
console.error('订阅pdf转换事件失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileUploadMessage(payload, topic){
|
||||
try {
|
||||
const messageStr = payload.toString()
|
||||
const data = JSON.parse(messageStr)
|
||||
// emitter.emit('fileUploadStatus')
|
||||
const userId = JSON.parse(sessionStorage.getItem('userData'))?.uid
|
||||
if(dialogFileVisible.value){
|
||||
dialogFileVisible.value = false
|
||||
}
|
||||
if(dialogFormVisible.value && userId != data.user_uid){
|
||||
// 显示确认对话框
|
||||
ElMessageBox.confirm(
|
||||
`用户${data.user_uid}上传了${data.file_name}文件,是否立即预览?`,
|
||||
'文件更新提示',
|
||||
{
|
||||
confirmButtonText: '预览',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
closeOnClickModal: false,
|
||||
closeOnPressEscape: false,
|
||||
showClose: false
|
||||
}
|
||||
).then(() => {
|
||||
// 用户点击"预览"
|
||||
resetPreviewState()
|
||||
getPreviewFileUrl(data)
|
||||
ElMessage({
|
||||
message: '已切换到新文件预览',
|
||||
type: 'success'
|
||||
})
|
||||
}).catch(() => {
|
||||
// 用户点击"取消"
|
||||
ElMessage({
|
||||
message: '已取消预览新文件,继续查看当前文件',
|
||||
type: 'info'
|
||||
})
|
||||
})
|
||||
// resetPreviewState()
|
||||
// getPreviewFileUrl(data)
|
||||
} else {
|
||||
ElMessage({
|
||||
message: `用户${data.user_uid}上传了${data.file_name}文件`,
|
||||
type: 'info',
|
||||
})
|
||||
showEdit(data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理转换状态消息失败:', error)
|
||||
handleError('处理转换状态失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 新增重置预览状态的方法
|
||||
function resetPreviewState() {
|
||||
loading.value = true
|
||||
converting.value = false
|
||||
downloading.value = false
|
||||
downloadProgress.value = 0
|
||||
error.value = false
|
||||
previewUrl.value = ''
|
||||
textContent.value = ''
|
||||
resetPdfState()
|
||||
|
||||
// 强制重新渲染
|
||||
nextTick(() => {
|
||||
// 确保DOM更新
|
||||
})
|
||||
}
|
||||
|
||||
function handlePdfMessage(payload, topic){
|
||||
try {
|
||||
const messageStr = payload.toString()
|
||||
const data = JSON.parse(messageStr)
|
||||
switch (data.status) {
|
||||
case 'converting':
|
||||
break
|
||||
case 'completed':
|
||||
getConvertedFile(data.task_id,data.room_uid)
|
||||
break
|
||||
case 'failed':
|
||||
break
|
||||
default:
|
||||
console.warn('未知的转换状态:', data.status)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理转换状态消息失败:', error)
|
||||
handleError('处理转换状态失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取转换后的文件
|
||||
const getConvertedFile = async (taskId,roomId) => {
|
||||
try {
|
||||
if (!taskId) {
|
||||
throw new Error('任务ID不存在')
|
||||
}
|
||||
const fileRes = await getConvertStatusApi(taskId,roomId)
|
||||
} catch (err) {
|
||||
console.error('获取转换文件失败:', err)
|
||||
handleError('获取转换文件失败', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 修改 getPreviewFileUrl 方法,确保状态正确更新
|
||||
async function getPreviewFileUrl(file){
|
||||
fileName.value = file.file_name || file.source_url.split('/').pop()
|
||||
const fileExt = fileName.value.split('.').pop().toLowerCase()
|
||||
try {
|
||||
// 根据文件类型处理转换后的文件
|
||||
if (fileExt === 'pdf' || enumType.includes(fileExt)) {
|
||||
// PDF和Office文档使用PDF预览器
|
||||
await loadPdfFile(file.preview_url || file.file_url)
|
||||
} else if (fileExt === 'txt') {
|
||||
// 文本文件
|
||||
await loadTextFile(file.preview_url || file.file_url)
|
||||
} else if (['png', 'jpg', 'jpeg', 'gif'].includes(fileExt)) {
|
||||
// 图片文件直接预览
|
||||
previewUrl.value = file.preview_url || file.file_url
|
||||
// 确保图片重新加载
|
||||
nextTick(() => {
|
||||
loading.value = false
|
||||
})
|
||||
} else if (fileExt === 'mp4') {
|
||||
// 视频文件
|
||||
previewUrl.value = file.preview_url || file.file_url
|
||||
nextTick(() => {
|
||||
loading.value = false
|
||||
})
|
||||
} else if (fileExt === 'mp3') {
|
||||
// 音频文件
|
||||
previewUrl.value = file.preview_url || file.file_url
|
||||
nextTick(() => {
|
||||
loading.value = false
|
||||
})
|
||||
} else {
|
||||
// 其他文件类型
|
||||
previewUrl.value = file.preview_url || file.file_url
|
||||
nextTick(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载预览文件失败:', error)
|
||||
handleError('加载预览文件失败', error)
|
||||
}
|
||||
}
|
||||
// 显示弹框
|
||||
const showEdit = async (data) => {
|
||||
// 重置状态
|
||||
resetPreviewState()
|
||||
title.value = '文件预览'
|
||||
dialogFormVisible.value = true
|
||||
currentFileData.value = data
|
||||
// fileName.value = data.file_name || data.source_url.split('/').pop()
|
||||
|
||||
await getPreviewFileUrl(data)
|
||||
}
|
||||
|
||||
// 加载PDF文件
|
||||
const loadPdfFile = async (fileUrl) => {
|
||||
try {
|
||||
loading.value = false
|
||||
downloading.value = true
|
||||
downloadProgress.value = 0
|
||||
|
||||
const response = await fetch(fileUrl)
|
||||
if (!response.ok) {
|
||||
throw new Error('文件下载失败')
|
||||
}
|
||||
|
||||
const contentLength = response.headers.get('content-length')
|
||||
const total = parseInt(contentLength, 10)
|
||||
let loaded = 0
|
||||
const reader = response.body.getReader()
|
||||
const chunks = []
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
chunks.push(value)
|
||||
loaded += value.length
|
||||
if (total) {
|
||||
downloadProgress.value = Math.round((loaded / total) * 100)
|
||||
}
|
||||
}
|
||||
|
||||
const blob = new Blob(chunks)
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
|
||||
// 先清理之前的URL
|
||||
if (pdfSource.value) {
|
||||
URL.revokeObjectURL(pdfSource.value)
|
||||
}
|
||||
|
||||
previewUrl.value = fileUrl
|
||||
pdfSource.value = blobUrl
|
||||
|
||||
// 重置PDF状态
|
||||
currentPage.value = 1
|
||||
pageCount.value = 0
|
||||
|
||||
downloading.value = false
|
||||
downloadProgress.value = 0
|
||||
|
||||
} catch (err) {
|
||||
downloading.value = false
|
||||
downloadProgress.value = 0
|
||||
handleError('PDF文件下载失败', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载文本文件
|
||||
const loadTextFile = async (fileUrl) => {
|
||||
try {
|
||||
const response = await fetch(fileUrl)
|
||||
if (!response.ok) throw new Error('无法加载文本文件')
|
||||
const text = await response.text()
|
||||
textContent.value = text
|
||||
loading.value = false
|
||||
} catch (err) {
|
||||
handleError('文本文件加载失败', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 上传文件获取office获取pdf文件
|
||||
async function getFilePdf(fileUrl) {
|
||||
try {
|
||||
loadingText.value = '正在转换文件...'
|
||||
const res = await convertFileApi({ file_url: fileUrl },props.roomId)
|
||||
if (res.meta.code !== 200) {
|
||||
throw new Error(res.meta.msg || '文件转换失败')
|
||||
}
|
||||
convertTaskId.value = res.data.task_id
|
||||
// 等待MQTT消息,不进行轮询
|
||||
loading.value = false
|
||||
converting.value = true
|
||||
conversionMessage.value = '已提交转换任务,等待处理...'
|
||||
|
||||
} catch (err) {
|
||||
handleError('文件转换失败', err)
|
||||
}
|
||||
}
|
||||
|
||||
// PDF相关方法
|
||||
const handlePdfLoaded = (data) => {
|
||||
pageCount.value = data.numPages
|
||||
pdfDocument.value = data
|
||||
loading.value = false
|
||||
converting.value = false
|
||||
}
|
||||
|
||||
const handlePdfRendered = () => {
|
||||
// console.log('PDF页面渲染完成')
|
||||
}
|
||||
|
||||
const handlePdfError = (error) => {
|
||||
console.error('PDF加载错误:', error)
|
||||
handleError('PDF文件加载失败', error)
|
||||
}
|
||||
|
||||
const previousPage = () => {
|
||||
if (currentPage.value > 1) {
|
||||
currentPage.value--
|
||||
}
|
||||
}
|
||||
|
||||
const nextPage = () => {
|
||||
if (currentPage.value < pageCount.value) {
|
||||
currentPage.value++
|
||||
}
|
||||
}
|
||||
|
||||
const resetPdfState = () => {
|
||||
pdfSource.value = ''
|
||||
currentPage.value = 1
|
||||
pageCount.value = 0
|
||||
scale.value = 1.0
|
||||
pdfDocument.value = null
|
||||
}
|
||||
|
||||
// 处理图片加载完成
|
||||
const handleImageLoad = () => {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 处理错误
|
||||
const handleError = (message, error) => {
|
||||
error.value = true
|
||||
errorMessage.value = message
|
||||
loading.value = false
|
||||
converting.value = false
|
||||
downloading.value = false
|
||||
downloadProgress.value = 0
|
||||
ElMessage.error(message)
|
||||
}
|
||||
|
||||
// 重试预览
|
||||
const retryPreview = () => {
|
||||
if (currentFileData.value) {
|
||||
showEdit(currentFileData.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭按钮点击事件
|
||||
const close = () => {
|
||||
dialogFormVisible.value = false
|
||||
resetState()
|
||||
}
|
||||
|
||||
// 处理对话框关闭
|
||||
const handleClose = (done) => {
|
||||
close()
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
const resetState = () => {
|
||||
loading.value = false
|
||||
converting.value = false
|
||||
downloading.value = false
|
||||
downloadProgress.value = 0
|
||||
error.value = false
|
||||
previewUrl.value = ''
|
||||
textContent.value = ''
|
||||
currentFileData.value = null
|
||||
convertTaskId.value = ''
|
||||
resetPdfState()
|
||||
}
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
// 可以在这里取消特定主题的订阅,但不断开连接
|
||||
// const userId = JSON.parse(sessionStorage.getItem('userData'))?.uid
|
||||
// if (userId) {
|
||||
// const topic = `xsynergy/room/${props.roomId}/file/${userId}/conversion_status`
|
||||
// mqttClient.unsubscribe(topic, handlePdfMessage)
|
||||
// }
|
||||
})
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
showEdit,
|
||||
close,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.file-preview-dialog {
|
||||
.preview-container {
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.loading-container, .error-container {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
|
||||
p {
|
||||
margin-top: 16px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 14px;
|
||||
color: #F56C6C;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.download-progress {
|
||||
font-size: 14px;
|
||||
color: #409EFF;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.file-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.image-preview {
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 70vh;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.pdf-preview, .office-preview {
|
||||
.pdf-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 8px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pdf-viewer-container {
|
||||
height: 70vh;
|
||||
overflow: auto;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
background: #f9f9f9;
|
||||
|
||||
.pdf-viewer {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
:deep(.vue-pdf-embed) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(canvas) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-preview {
|
||||
text-align: center;
|
||||
|
||||
.video-player {
|
||||
max-width: 100%;
|
||||
max-height: 70vh;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-preview {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
|
||||
.audio-player {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.text-preview {
|
||||
height: 70vh;
|
||||
overflow: auto;
|
||||
background: #f5f5f5;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
margin: 0;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.unsupported-preview {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
|
||||
p {
|
||||
margin-top: 16px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.file-preview-dialog {
|
||||
width: 95% !important;
|
||||
|
||||
.preview-container {
|
||||
min-height: 300px;
|
||||
|
||||
.file-content {
|
||||
.pdf-preview, .office-preview {
|
||||
.pdf-viewer-container {
|
||||
height: 50vh;
|
||||
}
|
||||
}
|
||||
|
||||
.text-preview {
|
||||
height: 50vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pdf-controls {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.el-button-group {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
.el-button {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
padding: 8px 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
3786
src/views/conferencingRoom/index.vue
Normal file
3786
src/views/conferencingRoom/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
3175
src/views/conferencingRoom/pathTransit.vue
Normal file
3175
src/views/conferencingRoom/pathTransit.vue
Normal file
File diff suppressed because it is too large
Load Diff
3831
src/views/conferencingRoom/text.vue
Normal file
3831
src/views/conferencingRoom/text.vue
Normal file
File diff suppressed because it is too large
Load Diff
3324
src/views/conferencingRoom/transit.vue
Normal file
3324
src/views/conferencingRoom/transit.vue
Normal file
File diff suppressed because it is too large
Load Diff
3324
src/views/conferencingRoom/transits.vue
Normal file
3324
src/views/conferencingRoom/transits.vue
Normal file
File diff suppressed because it is too large
Load Diff
683
src/views/coordinate/personnelList/components/leftTab/index.vue
Normal file
683
src/views/coordinate/personnelList/components/leftTab/index.vue
Normal file
@@ -0,0 +1,683 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="left-list" v-loading="leftListLoading || loading">
|
||||
<div class="list-tab">
|
||||
<div
|
||||
:class="'list-tab-item ' + (leftTab == 1 ? 'pitch-on' : '')"
|
||||
@click="() => (leftTab = 1)"
|
||||
>
|
||||
<img src="@/assets/images/Gc_114_line-Level-action.png" v-if="leftTab == 1" />
|
||||
<img src="@/assets/images/Gc_114_line-Level.png" v-else />
|
||||
</div>
|
||||
<div
|
||||
:class="'list-tab-item ' + (leftTab == 2 ? 'pitch-on' : '')"
|
||||
@click="() => (leftTab = 2)"
|
||||
>
|
||||
<img src="@/assets/images/book-read-fill-action.png" v-if="leftTab == 2" />
|
||||
<img src="@/assets/images/book-read-fill.png" v-else />
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-content">
|
||||
<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="搜索成员"
|
||||
type="text"
|
||||
prefix-icon="Search"
|
||||
@change="searchList"
|
||||
/>
|
||||
</div>
|
||||
<div class="content-datapicker" v-if="leftTab == 1">
|
||||
<el-date-picker
|
||||
v-model="queryFrom.leftDatePicker"
|
||||
type="datetimerange"
|
||||
:shortcuts="shortcuts"
|
||||
range-separator="到"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
value-format="YYYY-MM-DD HH:mm"
|
||||
@change="searchList"
|
||||
/>
|
||||
</div>
|
||||
<el-scrollbar class="left-list-scrollbar" v-if="isShow && leftTab == 1">
|
||||
<div
|
||||
class="content-list"
|
||||
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.id == assistanceId
|
||||
? 'border-color: #409EFF; '
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<div class="list-item-top">
|
||||
<span>
|
||||
{{ parseTime(item.created_at, '{m}月{d}日') }}
|
||||
{{ weekName[new Date(item.created_at).getDay()] }}
|
||||
</span>
|
||||
<span>
|
||||
{{ 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.all_participants"
|
||||
:key="indexs"
|
||||
>
|
||||
{{
|
||||
(items.display_name || items.user_uid) +
|
||||
(indexs + 1 == item.all_participants.length
|
||||
? ''
|
||||
: '、')
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<span>
|
||||
发起人:{{ item.all_participants.find(item => item.participant_role == 'moderator')?.display_name || ''}}
|
||||
</span>
|
||||
<span>
|
||||
时间:{{
|
||||
parseTime(item.created_at, '{h}:{i}') +
|
||||
' ~ ' +
|
||||
(item.updated_at ? parseTime(item.updated_at, '{h}:{i}') : '')
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: center" v-if="dataList?.length">
|
||||
<p v-if="more">Loading...</p>
|
||||
<p v-else>No more</p>
|
||||
</div>
|
||||
<div v-else class="list-empty">
|
||||
<el-empty description="暂无记录" />
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<div v-if="!isShow && leftTab == 1" class="list-empty">
|
||||
<el-empty description="暂无记录" />
|
||||
</div>
|
||||
|
||||
<el-scrollbar
|
||||
class="left-list-scrollbar1"
|
||||
height="calc(100vh - 120px)"
|
||||
v-if="leftTab == 2"
|
||||
>
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
lazy
|
||||
:load="HandleLoadNode"
|
||||
:filter-node-method="filterNode"
|
||||
highlight-current
|
||||
:props="treeProps"
|
||||
style="width: 100%"
|
||||
@node-click="updateDetail"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="tree-item">
|
||||
{{ data.name }}
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
// getAssistanceList,
|
||||
getParticipantsHistoryApi,
|
||||
getDirectories,
|
||||
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({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 定义 emit
|
||||
const emit = defineEmits(['updateDetail', 'updateTab'])
|
||||
|
||||
// state
|
||||
const state = reactive({
|
||||
isFirst: true,
|
||||
leftTab: 1,
|
||||
queryFrom: {
|
||||
page_size: 10,
|
||||
page: 1,
|
||||
nickName: '',
|
||||
leftDatePicker: null,
|
||||
participant_user_uids:''
|
||||
},
|
||||
participant_user:[],
|
||||
leftListLoading: true,
|
||||
loading: false,
|
||||
dataList: [],
|
||||
more: false,
|
||||
isShow: false,
|
||||
shortcuts: [
|
||||
{
|
||||
text: '本周',
|
||||
value: () => {
|
||||
const now = new Date()
|
||||
const nowDaty = now.getDay()
|
||||
const start = new Date(now)
|
||||
start.setDate(now.getDate() - nowDaty)
|
||||
const end = new Date(start)
|
||||
end.setDate(start.getDate() + 6)
|
||||
return [start, end]
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '最近三周',
|
||||
value: () => {
|
||||
const now = new Date()
|
||||
const nowDaty = now.getDay()
|
||||
let end = new Date(now)
|
||||
end.setDate(now.getDate() + (6 - nowDaty))
|
||||
const start = new Date(end)
|
||||
start.setDate(start.getDate() - 20)
|
||||
return [start, end]
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '本月',
|
||||
value: () => {
|
||||
const now = new Date()
|
||||
const start = new Date(now.getFullYear(), now.getMonth(), 1)
|
||||
const end = new Date(now.getFullYear(), now.getMonth() + 1, 0)
|
||||
return [start, end]
|
||||
},
|
||||
},
|
||||
],
|
||||
weekName: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
|
||||
treeRef: null,
|
||||
|
||||
treeProps: {
|
||||
children: 'users',
|
||||
label: 'name',
|
||||
value: 'uid',
|
||||
isLeaf: (node) => {
|
||||
if(node.uid) return true
|
||||
},
|
||||
},
|
||||
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);
|
||||
|
||||
/**
|
||||
* 树状列表筛选
|
||||
*/
|
||||
const filterNode = (value, data) => {
|
||||
if (!value) return true
|
||||
return data.name.includes(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索框变化
|
||||
*/
|
||||
const searchList = () => {
|
||||
if (state.leftTab == 1) {
|
||||
state.queryFrom.page = 1
|
||||
state.dataList = []
|
||||
state.queryFrom.participant_user_uids = state.participant_user.join(',')
|
||||
getList()
|
||||
} else {
|
||||
state.treeRef.filter(state.queryFrom.nickName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 变更详情
|
||||
*/
|
||||
const updateDetail = (item) => {
|
||||
if (state.leftTab == 1) {
|
||||
state.assistanceId = item.id
|
||||
emit('updateDetail', item)
|
||||
} else {
|
||||
if (item.uid) {
|
||||
emit('updateDetail', item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触底加载
|
||||
*/
|
||||
const infinite = () => {
|
||||
console.log('外面的加载')
|
||||
if (state.more) {
|
||||
console.log(state.more,'state.more执行了')
|
||||
state.queryFrom.page++
|
||||
getList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 协作记录
|
||||
*/
|
||||
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]
|
||||
// }
|
||||
|
||||
if (query.leftDatePicker?.length) {
|
||||
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
|
||||
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].id
|
||||
: ''
|
||||
state.isFirst = false
|
||||
}
|
||||
|
||||
state.more = state.dataList?.length < infoData.data.total
|
||||
state.isShow = Boolean(state.dataList?.length)
|
||||
state.leftListLoading = false
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
state.leftListLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通讯录 人员信息树
|
||||
*/
|
||||
const HandleLoadNode = async (node, resolve) => {
|
||||
if(node?.level === 0){
|
||||
loadNode(resolve,'',node?.level)
|
||||
}else if(node?.level > 0){
|
||||
if(node.data.directory_uid){
|
||||
loadUserNode(resolve,node.data.directory_uid,node?.level)
|
||||
}else{
|
||||
resolve(resolve)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const loadNode = async(resolve,id,level)=>{
|
||||
try {
|
||||
state.leftListLoading = true
|
||||
let res = await getDirectories({level:1})
|
||||
if(res.meta.code == 200){
|
||||
resolve(res.data)
|
||||
}
|
||||
state.leftListLoading = false
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
state.leftListLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadUserNode = async(resolve,id,level)=>{
|
||||
try {
|
||||
state.leftListLoading = true
|
||||
let userData = []
|
||||
let orgData = []
|
||||
const resOrg = await getDirectories({level: 1,parent_uuid:id})
|
||||
if(resOrg?.data){
|
||||
orgData = resOrg.data
|
||||
}
|
||||
if(id){
|
||||
const res = await getDirectoriesUsers(id,{directory_uuid:id})
|
||||
userData = res.data
|
||||
}
|
||||
resolve([...orgData, ...userData])
|
||||
state.leftListLoading = false
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
state.leftListLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
const getUser = (list) => {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i].type == 2) {
|
||||
return list[i]
|
||||
} else if (list[i].children?.length) {
|
||||
let user = getUser(list[i].children)
|
||||
if (user != null) {
|
||||
return user
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听 props.loading
|
||||
*/
|
||||
watch(
|
||||
() => props.loading,
|
||||
(newValue) => {
|
||||
state.loading = newValue
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 监听 tab 切换
|
||||
*/
|
||||
watch(
|
||||
() => state.leftTab,
|
||||
(newValue) => {
|
||||
emit('updateTab', newValue)
|
||||
state.dataList = []
|
||||
state.isFirst = true
|
||||
state.queryFrom = {
|
||||
page_size: 10,
|
||||
page: 1,
|
||||
nickName: '',
|
||||
participant_user_uids:'',
|
||||
leftDatePicker: null,
|
||||
}
|
||||
|
||||
if (newValue == 1) {
|
||||
state.isShow = false
|
||||
getList()
|
||||
} else {
|
||||
HandleLoadNode()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
state.dataList = []
|
||||
state.isFirst = true
|
||||
getList()
|
||||
})
|
||||
|
||||
/**
|
||||
* 暴露给模板
|
||||
*/
|
||||
const {
|
||||
isFirst, leftTab, queryFrom, leftListLoading, loading,
|
||||
dataList, more, isShow, shortcuts, weekName,
|
||||
treeRef, treeProps, assistanceId,userList,participant_user
|
||||
} = toRefs(state)
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.flex {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.left-list {
|
||||
display: flex;
|
||||
height: calc(100vh - 40px);
|
||||
|
||||
.list-tab {
|
||||
@extend .flex;
|
||||
justify-content: flex-start;
|
||||
flex-direction: column;
|
||||
margin-right: 6px;
|
||||
|
||||
.list-tab-item {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.list-content {
|
||||
@extend .flex;
|
||||
justify-content: flex-start;
|
||||
flex-direction: column;
|
||||
width: calc(100% - 56px);
|
||||
box-shadow: 0px 5px 15px 0px rgba(153, 153, 153, 0.3);
|
||||
|
||||
.content-top-input {
|
||||
@extend .flex;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
padding: 6px 20px;
|
||||
background: #666666;
|
||||
}
|
||||
|
||||
.content-datapicker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.left-list-scrollbar {
|
||||
width: 100%;
|
||||
height: calc(100vh - 170px);
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.left-list-scrollbar1 {
|
||||
width: 100%;
|
||||
height: calc(100vh - 120px);
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.content-list {
|
||||
width: 100%;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.content-list-item {
|
||||
width: 100%;
|
||||
margin-bottom: 15px;
|
||||
background: #f5f7fa;
|
||||
border: 1.5px solid #c9d4e6;
|
||||
cursor: pointer;
|
||||
|
||||
.list-item-top {
|
||||
@extend .flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
padding: 0 13px;
|
||||
background: #c9d4e6;
|
||||
|
||||
span {
|
||||
color: #333333;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.list-item-content {
|
||||
@extend .flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
padding: 13px;
|
||||
background: #f5f7fa;
|
||||
|
||||
img {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
border-radius: 50%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.list-item-content-text {
|
||||
@extend .flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
div {
|
||||
@extend .flex;
|
||||
justify-content: flex-start;
|
||||
|
||||
span {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
margin-top: 6px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.list-empty {
|
||||
@extend .flex;
|
||||
width: 100%;
|
||||
height: calc(100vh - 170px);
|
||||
}
|
||||
|
||||
.tree-item {
|
||||
@extend .flex;
|
||||
justify-content: flex-start;
|
||||
height: 45px;
|
||||
font-size: 18px;
|
||||
|
||||
|
||||
.tree-item-img1 {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.tree-item-text1 {
|
||||
margin-left: 10px;
|
||||
color: #333333;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tree-item-text2 {
|
||||
color: #333333;
|
||||
font-size: 18px;
|
||||
}
|
||||
.tree-item-text3 {
|
||||
margin-left: 15px;
|
||||
color: #999999;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .el-tree-node__content {
|
||||
@extend .flex;
|
||||
justify-content: flex-start;
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
::v-deep .list-content .el-input__wrapper {
|
||||
height: 38px;
|
||||
border-radius: 19px;
|
||||
}
|
||||
|
||||
::v-deep .content-datapicker .el-date-editor {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background: #e6f1ff;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
::v-deep .left-list-scrollbar .el-scrollbar__wrap {
|
||||
height: calc(100vh - 170px);
|
||||
}
|
||||
|
||||
::v-deep .left-list-scrollbar1 .el-scrollbar__wrap {
|
||||
height: calc(100vh - 120px);
|
||||
}
|
||||
</style>
|
||||
732
src/views/coordinate/personnelList/index.vue
Normal file
732
src/views/coordinate/personnelList/index.vue
Normal file
@@ -0,0 +1,732 @@
|
||||
<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='isShowLoading'
|
||||
>
|
||||
<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">
|
||||
{{
|
||||
detail.created_at ?
|
||||
parseTime(detail.created_at, '{y}年{m}月{d}日') +
|
||||
' ' +
|
||||
weekName[new Date(detail.created_at).getDay()] +
|
||||
' ' +
|
||||
parseTime(detail.created_at, '{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?.all_participants?.length"
|
||||
style="display: flex; flex-wrap: wrap"
|
||||
>
|
||||
<span
|
||||
v-for="(items, indexs) in detail.all_participants"
|
||||
:key="indexs"
|
||||
>
|
||||
{{
|
||||
(items.display_name || items.user_uid) +
|
||||
(indexs + 1 == detail.all_participants.length
|
||||
? ''
|
||||
: '、')
|
||||
}}
|
||||
</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?.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 - 390px)"
|
||||
>
|
||||
<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"
|
||||
:loading="initiateLoading"
|
||||
:disabled="initiateLoading"
|
||||
>
|
||||
{{ initiateLoading ? '正在发起邀请' : '发起协作' }}
|
||||
</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>
|
||||
<!-- 文件预览 -->
|
||||
<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 ,getFileListApi ,getvideoUrlApi,getRoomToken,getInvite} 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 ,getUserInfo} from '@/utils/auth.js';
|
||||
import { deepClone,parseTime } from '@/utils/ruoyi.js'
|
||||
const roomStore = useRoomStore()
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const { proxy } = getCurrentInstance()
|
||||
const state = reactive({
|
||||
detail: {},
|
||||
weekName: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
|
||||
tabValue: 1,
|
||||
isShow: true,
|
||||
isShowLoading: false,
|
||||
load: false,
|
||||
loadText: '数据加载中',
|
||||
isLinkKnow: 'F',
|
||||
socketInformation: null,
|
||||
inviteDialog: false,
|
||||
cooperation: import.meta.env.VITE_APP_COOPERATION_TYPE,
|
||||
initiateLoading: false,
|
||||
})
|
||||
const userLoading = ref(false); // 用户信息加载状态
|
||||
//文件预览
|
||||
const browseFileRef = ref(null);
|
||||
|
||||
const isEmptyObject = (obj) => {
|
||||
return !obj || Object.keys(obj).length === 0
|
||||
}
|
||||
|
||||
/** 发起协作邀请 */
|
||||
const clickInitiate = async () => {
|
||||
state.initiateLoading = true
|
||||
let userData = null
|
||||
try {
|
||||
try {
|
||||
userData = JSON.parse(sessionStorage.getItem('userData')) || null
|
||||
} catch (e) {
|
||||
console.error('解析 userData 失败:', e)
|
||||
}
|
||||
|
||||
|
||||
if (isEmptyObject(state.detail)) {
|
||||
ElMessage({
|
||||
message: '请先选择人员',
|
||||
type: 'warning',
|
||||
})
|
||||
return
|
||||
}
|
||||
if(state.detail.uid == userData.uid){
|
||||
ElMessage({
|
||||
message: '不能邀请自己',
|
||||
type: 'warning',
|
||||
})
|
||||
return
|
||||
}
|
||||
roomStore.setUserUid(userData.uid)
|
||||
roomStore.setDetailUid(state.detail.uid)
|
||||
roomStore.setDetailName(state.detail.name)
|
||||
|
||||
//创建房间
|
||||
const res = await getRoomToken({max_participants: 20});
|
||||
if(res.meta.code != 200){
|
||||
ElMessage.error(res.meta.message);
|
||||
return;
|
||||
}
|
||||
if(res.data.room.uid){
|
||||
await getInvite(res.data.room.uid,{participants:[{user_uid:state.detail.uid,display_name:state.detail.name}], participant_role: "participant"})
|
||||
}
|
||||
router.push({
|
||||
path: '/conferencingRoom',
|
||||
query:{
|
||||
room_uid:res.data.room.uid
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('发起协作失败:', error)
|
||||
ElMessage.error('发起协作失败,请重试')
|
||||
} finally {
|
||||
// 无论成功失败,都重置按钮状态
|
||||
state.initiateLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 修改展示列表 */
|
||||
const updateTab = (newValue) => {
|
||||
state.detail = {}
|
||||
state.tabValue = newValue
|
||||
}
|
||||
|
||||
/** 修改展示区内容 */
|
||||
const updateDetail = async (details) => {
|
||||
userLoading.value = true
|
||||
if (details) {
|
||||
state.detail = {}
|
||||
if (state.tabValue == 1) {
|
||||
state.isShowLoading = true
|
||||
getTheFileList(details)
|
||||
} else {
|
||||
const res = await getInfo(details.uid)
|
||||
state.detail = res.data
|
||||
userLoading.value = 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.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))
|
||||
const m = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
|
||||
const s = Math.floor((diff % (1000 * 60)) / 1000)
|
||||
return h > 0 ? `${h}小时 ${m}分钟 ${s}秒`
|
||||
: m > 0 ? `${m}分钟 ${s}秒`
|
||||
: `${s}秒`
|
||||
} else {
|
||||
return '暂无'
|
||||
}
|
||||
}
|
||||
|
||||
//文件预览
|
||||
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
|
||||
// }
|
||||
// }
|
||||
|
||||
/** 处理加入房间和拒接房间 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 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,isShowLoading ,initiateLoading } = 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>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.flex {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
// padding: 20px;
|
||||
// margin: 0 17px;
|
||||
height: calc(100vh - 50px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.message-null {
|
||||
@extend .flex;
|
||||
height: calc(100vh - 90px);
|
||||
}
|
||||
|
||||
.right-content {
|
||||
height: calc(100vh - 90px);
|
||||
background: #f4f9ff;
|
||||
.right-content-title {
|
||||
@extend .flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
padding: 0 20px;
|
||||
background: #666666;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.right-content-message {
|
||||
width: calc(100% - 30px);
|
||||
height: 150px;
|
||||
margin: 15px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.right-content-file {
|
||||
width: calc(100% - 30px);
|
||||
height:calc(100% - 230px);
|
||||
margin: 0 15px 15px;
|
||||
}
|
||||
|
||||
.file-top {
|
||||
@extend .flex;
|
||||
justify-content: flex-start;
|
||||
height: 50px;
|
||||
padding-left: 20px;
|
||||
background: #e6f1ff;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.content-file-video {
|
||||
.file-video-bottom {
|
||||
width: 100%;
|
||||
height: calc(100vh - 360px);
|
||||
padding: 15px;
|
||||
background: #fff;
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content-file-list {
|
||||
background: #fff;
|
||||
padding: 15px 5px 15px 10px;
|
||||
|
||||
.file-list {
|
||||
width: 100%;
|
||||
padding-right: 10px;
|
||||
|
||||
.file-list-content {
|
||||
@extend .flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.file-list-item {
|
||||
@extend .flex;
|
||||
width: 100%;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.file-list-item-icon {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
margin-right: 10px;
|
||||
border-radius: 50%;
|
||||
background: #89b2ff;
|
||||
}
|
||||
|
||||
.file-list-item-text {
|
||||
@extend .flex;
|
||||
justify-content: space-between;
|
||||
width: calc(100% - 23px);
|
||||
padding: 13px 6px 13px 6px;
|
||||
border-radius: 4px;
|
||||
background: #f4f9ff;
|
||||
|
||||
.list-item-text {
|
||||
display: inline-block;
|
||||
width: calc(100% - 50px);
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-user {
|
||||
@extend .flex;
|
||||
overflow: auto;
|
||||
|
||||
.message-user-card {
|
||||
@extend .flex;
|
||||
flex-direction: column;
|
||||
width: 500px;
|
||||
height: 95%;
|
||||
max-height: 550px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
.user-card-nickName {
|
||||
@extend .flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 40%;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(153, 153, 153, 0.22) 0%,
|
||||
rgba(153, 153, 153, 0) 100%
|
||||
);
|
||||
|
||||
img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-top: 10%;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 50%;
|
||||
background: #167bff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #051435;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-card-information {
|
||||
@extend .flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
height: 45%;
|
||||
|
||||
.user-information-item {
|
||||
@extend .flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.user-information-title {
|
||||
@extend .flex;
|
||||
justify-content: flex-start;
|
||||
width: 150px;
|
||||
|
||||
img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-information-text {
|
||||
width: 130px;
|
||||
color: #333333;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
.user-card-btn {
|
||||
@extend .flex;
|
||||
width: 100%;
|
||||
height: 15%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-null {
|
||||
color: #999;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
::v-deep .user-card-btn .el-button {
|
||||
width: 70%;
|
||||
height: 55%;
|
||||
border-radius: 25px;
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user