feat:跟新密码加密 共享白板功能
This commit is contained in:
6
package-lock.json
generated
6
package-lock.json
generated
@@ -14,6 +14,7 @@
|
|||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"code-inspector-plugin": "^0.20.12",
|
"code-inspector-plugin": "^0.20.12",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"element-plus": "^2.2.27",
|
"element-plus": "^2.2.27",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"livekit-client": "^2.7.5",
|
"livekit-client": "^2.7.5",
|
||||||
@@ -1597,6 +1598,11 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crypto-js": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||||
|
},
|
||||||
"node_modules/cssesc": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"code-inspector-plugin": "^0.20.12",
|
"code-inspector-plugin": "^0.20.12",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"element-plus": "^2.2.27",
|
"element-plus": "^2.2.27",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"livekit-client": "^2.7.5",
|
"livekit-client": "^2.7.5",
|
||||||
|
|||||||
@@ -27,14 +27,17 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import useUserStore from '@/stores/modules/user'
|
import useUserStore from '@/stores/modules/user'
|
||||||
import { watch, ref, getCurrentInstance } from 'vue'
|
import { watch, ref, getCurrentInstance,onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useMeterStore } from '@/stores/modules/meter'
|
||||||
|
import CryptoJS from 'crypto-js';
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
const redirect = ref(undefined);
|
const redirect = ref(undefined);
|
||||||
const emit = defineEmits(['loginSuccess'])
|
const emit = defineEmits(['loginSuccess'])
|
||||||
|
const meterStore = useMeterStore()
|
||||||
|
|
||||||
const loginForm = ref({
|
const loginForm = ref({
|
||||||
username: '',
|
username: '',
|
||||||
@@ -52,14 +55,31 @@ function handleLogin() {
|
|||||||
proxy.$refs.loginRef.validate((valid) => {
|
proxy.$refs.loginRef.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
if(!localStorage?.getItem('UDID')){
|
||||||
|
ElMessage({
|
||||||
|
message: '服务错误,请刷新页面',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const secretKey = ((loginForm.value.username + localStorage?.getItem('UDID')).toLowerCase()).replaceAll('-', ''); // 用户名+UDID(32位16进制,全小写)
|
||||||
|
|
||||||
|
const randomChars = generateRandomChars(6);
|
||||||
|
|
||||||
|
const message = `Gx${randomChars}${loginForm.value.password}`;
|
||||||
|
|
||||||
|
const ciphertext = CryptoJS.Blowfish.encrypt(message, secretKey).toString();
|
||||||
|
|
||||||
|
|
||||||
// 调用action的登录方法
|
// 调用action的登录方法
|
||||||
userStore
|
userStore
|
||||||
.login({
|
.login({
|
||||||
password: loginForm.value.password,
|
password: ciphertext,
|
||||||
|
// password: loginForm.value.password,
|
||||||
username: loginForm.value.username,
|
username: loginForm.value.username,
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
const userInfo = JSON.parse(localStorage.getItem('userData'))
|
const userInfo = JSON.parse(sessionStorage.getItem('userData'))
|
||||||
emit('loginSuccess', userInfo)
|
emit('loginSuccess', userInfo)
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@@ -71,6 +91,16 @@ function handleLogin() {
|
|||||||
requestNotificationPermission()
|
requestNotificationPermission()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 生成随机字符串
|
||||||
|
function generateRandomChars(length) {
|
||||||
|
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 请求浏览器的通知权限
|
* @description 请求浏览器的通知权限
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
@@ -83,6 +113,10 @@ function requestNotificationPermission() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
meterStore.initUdid()
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.wrapper-content {
|
.wrapper-content {
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ class Canvas extends EventEmitter {
|
|||||||
this.emit('drawingUpdate', this.currentShape);
|
this.emit('drawingUpdate', this.currentShape);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseUp(e) {
|
handleMouseUp(e) {
|
||||||
if (!this.isDrawing || !this.currentShape) return;
|
if (!this.isDrawing || !this.currentShape) return;
|
||||||
this.isDrawing = false;
|
this.isDrawing = false;
|
||||||
const coords = this.getMouseCoordinates(e);
|
const coords = this.getMouseCoordinates(e);
|
||||||
|
|||||||
285
src/core/index_old.js
Normal file
285
src/core/index_old.js
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
import EventEmitter from "@/utils/emitter";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canvas绘图类,支持多种图形绘制和多人同步
|
||||||
|
* 使用百分比坐标系统确保跨设备一致性
|
||||||
|
*/
|
||||||
|
class Canvas extends EventEmitter {
|
||||||
|
constructor(canvasId) {
|
||||||
|
super();
|
||||||
|
this.canvas = document.getElementById(canvasId);
|
||||||
|
if (!this.canvas) {
|
||||||
|
throw new Error(`Canvas element with id ${canvasId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ctx = this.canvas.getContext('2d');
|
||||||
|
this.shapes = []; // 所有已绘制形状
|
||||||
|
this.currentShape = null; // 当前正在绘制的形状
|
||||||
|
this.isDrawing = false;
|
||||||
|
this.drawingTool = 'pencil';
|
||||||
|
this.pathOptimizationEnabled = true;
|
||||||
|
this.optimizationThreshold = 0.005;
|
||||||
|
this.currentColor = '#ffcc00';
|
||||||
|
this.currentThickness = 2;
|
||||||
|
|
||||||
|
this.resize();
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
this.handleMouseDown = this.handleMouseDown.bind(this);
|
||||||
|
this.handleMouseMove = this.handleMouseMove.bind(this);
|
||||||
|
this.handleMouseUp = this.handleMouseUp.bind(this);
|
||||||
|
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
||||||
|
|
||||||
|
this.canvas.addEventListener('mousedown', this.handleMouseDown);
|
||||||
|
this.canvas.addEventListener('mousemove', this.handleMouseMove);
|
||||||
|
this.canvas.addEventListener('mouseup', this.handleMouseUp);
|
||||||
|
this.canvas.addEventListener('mouseleave', this.handleMouseLeave);
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => this.resize());
|
||||||
|
}
|
||||||
|
|
||||||
|
resize() {
|
||||||
|
const parent = this.canvas.parentElement;
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
const containerWidth = parent.offsetWidth;
|
||||||
|
const containerHeight = parent.offsetHeight;
|
||||||
|
|
||||||
|
let width = containerWidth;
|
||||||
|
let height = Math.floor((width * 9) / 16);
|
||||||
|
|
||||||
|
if (height > containerHeight) {
|
||||||
|
height = containerHeight;
|
||||||
|
width = Math.floor((height * 16) / 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.canvas.width === width && this.canvas.height === height) return;
|
||||||
|
|
||||||
|
this.canvas.width = width;
|
||||||
|
this.canvas.height = height;
|
||||||
|
this.canvas.style.width = width + "px";
|
||||||
|
this.canvas.style.height = height + "px";
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
setDrawingTool(tool) { this.drawingTool = tool; }
|
||||||
|
setColor(color) { this.currentColor = color; }
|
||||||
|
setThickness(size) { this.currentThickness = size; }
|
||||||
|
setPathOptimization(enabled) { this.pathOptimizationEnabled = enabled; }
|
||||||
|
setOptimizationThreshold(threshold) { this.optimizationThreshold = threshold; }
|
||||||
|
getShapes() { return this.shapes; }
|
||||||
|
setShapes(shapes) { this.shapes = shapes; this.render(); }
|
||||||
|
|
||||||
|
getMouseCoordinates(e) {
|
||||||
|
const rect = this.canvas.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: (e.clientX - rect.left) / this.canvas.width,
|
||||||
|
y: (e.clientY - rect.top) / this.canvas.height
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseDown(e) {
|
||||||
|
this.isDrawing = true;
|
||||||
|
const coords = this.getMouseCoordinates(e);
|
||||||
|
|
||||||
|
if (this.drawingTool === 'pencil') {
|
||||||
|
this.currentShape = { type: 'pencil', data: { color: this.currentColor, path: [coords], thickness: this.currentThickness } };
|
||||||
|
} else if (this.drawingTool === 'line') {
|
||||||
|
this.currentShape = { type: 'line', data: { color: this.currentColor, start: coords, end: coords, thickness: this.currentThickness } };
|
||||||
|
} else if (this.drawingTool === 'rectangle') {
|
||||||
|
this.currentShape = { type: 'rectangle', data: { color: this.currentColor, start: coords, end: coords, thickness: this.currentThickness, fill: false } };
|
||||||
|
} else if (this.drawingTool === 'circle') {
|
||||||
|
this.currentShape = { type: 'circle', data: { color: this.currentColor, start: coords, end: coords, thickness: this.currentThickness, fill: false } };
|
||||||
|
} else if (this.drawingTool === 'eraser') {
|
||||||
|
this.currentShape = { type: 'eraser', data: { color: '#ffffff', start: coords, end: coords, thickness: 3 } };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit('drawingStart', this.currentShape);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseMove(e) {
|
||||||
|
if (!this.isDrawing || !this.currentShape) return;
|
||||||
|
const coords = this.getMouseCoordinates(e);
|
||||||
|
|
||||||
|
if (this.drawingTool === 'pencil') {
|
||||||
|
this.currentShape.data.path.push(coords);
|
||||||
|
} else {
|
||||||
|
this.currentShape.data.end = coords;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
this.emit('drawingUpdate', this.currentShape);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseUp(e) {
|
||||||
|
if (!this.isDrawing || !this.currentShape) return;
|
||||||
|
this.isDrawing = false;
|
||||||
|
const coords = this.getMouseCoordinates(e);
|
||||||
|
if (this.drawingTool === 'pencil' && this.pathOptimizationEnabled && this.currentShape.data.path.length > 10) {
|
||||||
|
this.currentShape.data.path = this.optimizePath(this.currentShape.data.path);
|
||||||
|
} else {
|
||||||
|
this.currentShape.data.end = coords;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.shapes.push({ ...this.currentShape });
|
||||||
|
this.emit('drawingEnd', this.currentShape);
|
||||||
|
this.currentShape = null;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseLeave(e) {
|
||||||
|
if (this.isDrawing) this.handleMouseUp(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
optimizePath(path) {
|
||||||
|
if (path.length < 3) return path;
|
||||||
|
const optimizedPath = [path[0]];
|
||||||
|
for (let i = 1; i < path.length - 1;) {
|
||||||
|
let a = 1;
|
||||||
|
while (i + a < path.length && this.calculateDistance(path[i], path[i + a]) < this.optimizationThreshold) a++;
|
||||||
|
optimizedPath.push(path[i]);
|
||||||
|
i += a;
|
||||||
|
}
|
||||||
|
optimizedPath.push(path[path.length - 1]);
|
||||||
|
return optimizedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateDistance(p1, p2) {
|
||||||
|
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
addShape(shapeData) {
|
||||||
|
if (Array.isArray(shapeData)) this.shapes.push(...shapeData);
|
||||||
|
else this.shapes.push(shapeData);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCanvas() {
|
||||||
|
this.shapes = [];
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
this.emit('clear');
|
||||||
|
}
|
||||||
|
|
||||||
|
exportToDataURL(type = 'image/png', quality = 1) {
|
||||||
|
return this.canvas.toDataURL(type, quality);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
// 绘制历史形状
|
||||||
|
this.shapes.forEach(shape => this.drawShape(shape));
|
||||||
|
|
||||||
|
// 绘制当前正在绘制的形状
|
||||||
|
if (this.currentShape) this.drawShape(this.currentShape);
|
||||||
|
|
||||||
|
// 画画布边框
|
||||||
|
this.ctx.save();
|
||||||
|
this.ctx.strokeStyle = "#000";
|
||||||
|
this.ctx.lineWidth = 2;
|
||||||
|
this.ctx.strokeRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
this.ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawShape(shape) {
|
||||||
|
switch (shape.type) {
|
||||||
|
case 'pencil': this.drawPencil(shape.data); break;
|
||||||
|
case 'line': this.drawLine(shape.data); break;
|
||||||
|
case 'rectangle': this.drawRectangle(shape.data); break;
|
||||||
|
case 'circle': this.drawCircle(shape.data); break;
|
||||||
|
case 'eraser': this.drawEraser(shape.data, shape); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawPencil(p) {
|
||||||
|
if (p.path.length < 2) return;
|
||||||
|
const path = p.path.map(pt => ({ x: pt.x * this.canvas.width, y: pt.y * this.canvas.height }));
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.strokeStyle = p.color;
|
||||||
|
this.ctx.lineWidth = p.thickness;
|
||||||
|
this.ctx.lineCap = 'round';
|
||||||
|
this.ctx.lineJoin = 'round';
|
||||||
|
this.drawSmoothCurve(this.ctx, path);
|
||||||
|
this.ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSmoothCurve(ctx, path) {
|
||||||
|
if (path.length < 3) { ctx.moveTo(path[0].x, path[0].y); for (let i = 1; i < path.length; i++) ctx.lineTo(path[i].x, path[i].y); return; }
|
||||||
|
ctx.moveTo(path[0].x, path[0].y);
|
||||||
|
const threshold = 5;
|
||||||
|
for (let i = 1; i < path.length - 2;) {
|
||||||
|
let a = 1;
|
||||||
|
while (i + a < path.length - 2 &&
|
||||||
|
Math.sqrt(Math.pow(path[i].x - path[i + a].x, 2) + Math.pow(path[i].y - path[i + a].y, 2)) < threshold) a++;
|
||||||
|
const xc = (path[i].x + path[i + a].x) / 2;
|
||||||
|
const yc = (path[i].y + path[i + a].y) / 2;
|
||||||
|
ctx.quadraticCurveTo(path[i].x, path[i].y, xc, yc);
|
||||||
|
i += a;
|
||||||
|
}
|
||||||
|
ctx.lineTo(path[path.length - 1].x, path[path.length - 1].y);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawLine(l) {
|
||||||
|
const sx = l.start.x * this.canvas.width, sy = l.start.y * this.canvas.height;
|
||||||
|
const ex = l.end.x * this.canvas.width, ey = l.end.y * this.canvas.height;
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(sx, sy);
|
||||||
|
this.ctx.lineTo(ex, ey);
|
||||||
|
this.ctx.strokeStyle = l.color;
|
||||||
|
this.ctx.lineWidth = l.thickness;
|
||||||
|
this.ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawRectangle(r) {
|
||||||
|
const sx = r.start.x * this.canvas.width, sy = r.start.y * this.canvas.height;
|
||||||
|
const ex = r.end.x * this.canvas.width, ey = r.end.y * this.canvas.height;
|
||||||
|
const w = ex - sx, h = ey - sy;
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.rect(sx, sy, w, h);
|
||||||
|
if (r.fill) { this.ctx.fillStyle = r.color; this.ctx.fill(); }
|
||||||
|
else { this.ctx.strokeStyle = r.color; this.ctx.lineWidth = r.thickness; this.ctx.stroke(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
drawCircle(c) {
|
||||||
|
const sx = c.start.x * this.canvas.width, sy = c.start.y * this.canvas.height;
|
||||||
|
const ex = c.end.x * this.canvas.width, ey = c.end.y * this.canvas.height;
|
||||||
|
const cx = (sx + ex) / 2, cy = (sy + ey) / 2;
|
||||||
|
const rx = Math.abs(ex - sx) / 2, ry = Math.abs(ey - sy) / 2;
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.ellipse(cx, cy, rx, ry, 0, 0, 2 * Math.PI);
|
||||||
|
if (c.fill) { this.ctx.fillStyle = c.color; this.ctx.fill(); }
|
||||||
|
else { this.ctx.strokeStyle = c.color; this.ctx.lineWidth = c.thickness; this.ctx.stroke(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
drawEraser(e, shapeObj) {
|
||||||
|
const sx = e.start.x * this.canvas.width, sy = e.start.y * this.canvas.height;
|
||||||
|
const ex = e.end.x * this.canvas.width, ey = e.end.y * this.canvas.height;
|
||||||
|
const w = Math.abs(ex - sx), h = Math.abs(ey - sy);
|
||||||
|
const x = Math.min(sx, ex), y = Math.min(sy, ey);
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.rect(x, y, w, h);
|
||||||
|
this.ctx.fillStyle = 'rgba(255,255,255)';
|
||||||
|
this.ctx.fill();
|
||||||
|
|
||||||
|
// 仅当前正在绘制的橡皮擦显示边框
|
||||||
|
if (this.currentShape && shapeObj === this.currentShape) {
|
||||||
|
this.ctx.save();
|
||||||
|
this.ctx.strokeStyle = '#000';
|
||||||
|
this.ctx.lineWidth = 1;
|
||||||
|
this.ctx.strokeRect(x, y, w, h);
|
||||||
|
this.ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.canvas.removeEventListener('mousedown', this.handleMouseDown);
|
||||||
|
this.canvas.removeEventListener('mousemove', this.handleMouseMove);
|
||||||
|
this.canvas.removeEventListener('mouseup', this.handleMouseUp);
|
||||||
|
this.canvas.removeEventListener('mouseleave', this.handleMouseLeave);
|
||||||
|
window.removeEventListener('resize', this.resize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Canvas;
|
||||||
@@ -9,7 +9,7 @@ export const useMeterStore = defineStore('meter', {
|
|||||||
initUdid() {
|
initUdid() {
|
||||||
var udid = window.localStorage.getItem('UDID')
|
var udid = window.localStorage.getItem('UDID')
|
||||||
if (!udid) {
|
if (!udid) {
|
||||||
udid = generateUUID();
|
udid = generateUUID();
|
||||||
window.localStorage.setItem("UDID", udid);
|
window.localStorage.setItem("UDID", udid);
|
||||||
}
|
}
|
||||||
this.setUdid(udid)
|
this.setUdid(udid)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { login, logout, getInfo } from '@/api/login'
|
import { login, logout, getInfo } from '@/api/login'
|
||||||
import { getToken, setToken, removeToken } from '@/utils/auth'
|
import { getToken, setToken, removeToken } from '@/utils/auth'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
const useUserStore = defineStore(
|
const useUserStore = defineStore(
|
||||||
'user',
|
'user',
|
||||||
@@ -20,8 +21,12 @@ const useUserStore = defineStore(
|
|||||||
const trimmedUsername = username.trim();
|
const trimmedUsername = username.trim();
|
||||||
|
|
||||||
const res = await login(trimmedUsername, password);
|
const res = await login(trimmedUsername, password);
|
||||||
|
if (res.meta.code !== 200) {
|
||||||
|
ElMessage({ message: res.meta?.message || '登录失败', type: 'error' });
|
||||||
|
return Promise.reject(res);
|
||||||
|
}
|
||||||
const { token, user } = res.data;
|
const { token, user } = res.data;
|
||||||
localStorage.setItem('userData', JSON.stringify(user));
|
sessionStorage.setItem('userData', JSON.stringify(user));
|
||||||
setToken(token);
|
setToken(token);
|
||||||
this.token = token;
|
this.token = token;
|
||||||
|
|
||||||
@@ -34,7 +39,7 @@ const useUserStore = defineStore(
|
|||||||
getInfo() {
|
getInfo() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const userData = localStorage.getItem('userData');
|
const userData = sessionStorage.getItem('userData');
|
||||||
|
|
||||||
if (!userData) {
|
if (!userData) {
|
||||||
return reject(new Error('未找到用户数据'));
|
return reject(new Error('未找到用户数据'));
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
|
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
const TokenKey = "token";
|
const TokenKey = "token";
|
||||||
|
|
||||||
const ExpiresInKey = "Meta-Enterprise-Expires-In";
|
|
||||||
|
|
||||||
export function getToken() {
|
export function getToken() {
|
||||||
return Cookies.get(TokenKey);
|
return Cookies.get(TokenKey);
|
||||||
@@ -17,14 +15,3 @@ export function removeToken() {
|
|||||||
return Cookies.remove(TokenKey);
|
return Cookies.remove(TokenKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExpiresIn() {
|
|
||||||
return Cookies.get(ExpiresInKey) || -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setExpiresIn(time) {
|
|
||||||
return Cookies.set(ExpiresInKey, time);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeExpiresIn() {
|
|
||||||
return Cookies.remove(ExpiresInKey);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ service.interceptors.request.use(
|
|||||||
|
|
||||||
// 是否需要防止数据重复提交
|
// 是否需要防止数据重复提交
|
||||||
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false;
|
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false;
|
||||||
if (meterStore.getSudid()) {
|
if (meterStore.getSudid()) {
|
||||||
config.headers["X-User-Agent"] = `gxtech/web 1.0.0: c=GxTech, udid=${meterStore.getSudid()}, sv=15.4.1, app=stt`;
|
config.headers["X-User-Agent"] = `gxtech/web 1.0.0: c=GxTech, udid=${meterStore.getSudid()}, sv=15.4.1, app=stt`;
|
||||||
}
|
}
|
||||||
// get请求映射params参数
|
// get请求映射params参数
|
||||||
@@ -121,10 +121,8 @@ service.interceptors.response.use(
|
|||||||
switch (businessCode) {
|
switch (businessCode) {
|
||||||
case 200:
|
case 200:
|
||||||
case 201:
|
case 201:
|
||||||
return Promise.resolve(responseData);
|
return Promise.resolve(responseData);
|
||||||
|
case 401:
|
||||||
case 401:
|
|
||||||
console.log('未授权', responseData)
|
|
||||||
return Promise.resolve(responseData);
|
return Promise.resolve(responseData);
|
||||||
// return handleUnauthorized().then(() => {
|
// return handleUnauthorized().then(() => {
|
||||||
// return Promise.reject({ code: 401, message: '未授权' });
|
// return Promise.reject({ code: 401, message: '未授权' });
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { mqttClient } from "./mqtt";
|
|||||||
import { getWhiteboardShapes, getWhiteboardHistory } from "@/views/custom/api";
|
import { getWhiteboardShapes, getWhiteboardHistory } from "@/views/custom/api";
|
||||||
import { useMeterStore } from '@/stores/modules/meter';
|
import { useMeterStore } from '@/stores/modules/meter';
|
||||||
import { encode, decode } from '@msgpack/msgpack'
|
import { encode, decode } from '@msgpack/msgpack'
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
const meterStore = useMeterStore();
|
const meterStore = useMeterStore();
|
||||||
meterStore.initUdid();
|
meterStore.initUdid();
|
||||||
@@ -11,7 +12,7 @@ let canvasInstance = null;
|
|||||||
|
|
||||||
// 获取本地缓存 userData
|
// 获取本地缓存 userData
|
||||||
function getLocalUserData() {
|
function getLocalUserData() {
|
||||||
const dataStr = localStorage.getItem('userData');
|
const dataStr = sessionStorage.getItem('userData');
|
||||||
if (!dataStr) return null;
|
if (!dataStr) return null;
|
||||||
try {
|
try {
|
||||||
return JSON.parse(dataStr);
|
return JSON.parse(dataStr);
|
||||||
@@ -28,8 +29,7 @@ export const WhiteboardSync = {
|
|||||||
canvasInstance = canvas;
|
canvasInstance = canvas;
|
||||||
|
|
||||||
const localUser = getLocalUserData();
|
const localUser = getLocalUserData();
|
||||||
const localUid = localUser?.user?.uid;
|
const localUid = localUser?.uid;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 先连接 MQTT
|
// 先连接 MQTT
|
||||||
await mqttClient.connect(meterStore.getSudid());
|
await mqttClient.connect(meterStore.getSudid());
|
||||||
@@ -43,18 +43,19 @@ export const WhiteboardSync = {
|
|||||||
|
|
||||||
// 订阅当前房间
|
// 订阅当前房间
|
||||||
const topic = `xSynergy/ROOM/${roomUid}/whiteboard/#`;
|
const topic = `xSynergy/ROOM/${roomUid}/whiteboard/#`;
|
||||||
mqttClient.subscribe(topic, async (shapeData) => {
|
mqttClient.subscribe(topic, async (shapeData) => {
|
||||||
// console.log(shapeData, 'shapeData++格式装换')
|
const shapeDataNew = JSON.parse(shapeData.toString())
|
||||||
const shapeDataNew = decode(shapeData);
|
// const shapeDataNew = decode(message);
|
||||||
// console.log(shapeDataNew, '格式解码')
|
// console.log(shapeDataNew, '格式解码')
|
||||||
try {
|
try {
|
||||||
isRemote = true;
|
isRemote = true;
|
||||||
// 如果 shape 来自本地用户,则跳过
|
// 如果 shape 来自本地用户,则跳过
|
||||||
if (shapeDataNew.user_uid === localUid) return;
|
if (shapeDataNew.user_uid === localUid) return;
|
||||||
const res = await getWhiteboardHistory({ after_timestamp: shapeDataNew.created_at }, roomUid);
|
const res = await getWhiteboardHistory({ after_timestamp: shapeDataNew.created_at }, roomUid);
|
||||||
if (res.meta.code === 200) {
|
if (res.meta.code === 200) {
|
||||||
canvasInstance.addShape(res.data.shapes);
|
canvasInstance.addShape(res.data.shapes);
|
||||||
} else {
|
} else {
|
||||||
|
ElMessage.error("获取历史数据失败");
|
||||||
console.error("获取历史数据失败");
|
console.error("获取历史数据失败");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -71,7 +72,7 @@ export const WhiteboardSync = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 监听画布事件:新增图形
|
// 监听画布事件:新增图形
|
||||||
canvas.on('drawingEnd', async (shape) => {
|
canvas.on('drawingEnd', async (shape) => {
|
||||||
// 如果来自远程,或不是需要同步的类型,跳过
|
// 如果来自远程,或不是需要同步的类型,跳过
|
||||||
if (isRemote || !['pencil', 'line', 'rectangle', 'circle', 'eraser'].includes(shape.type)) return;
|
if (isRemote || !['pencil', 'line', 'rectangle', 'circle', 'eraser'].includes(shape.type)) return;
|
||||||
|
|
||||||
@@ -82,6 +83,7 @@ export const WhiteboardSync = {
|
|||||||
try {
|
try {
|
||||||
await getWhiteboardShapes(shape, roomUid);
|
await getWhiteboardShapes(shape, roomUid);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
ElMessage.error("提交形状失败");
|
||||||
console.error("提交形状失败:", err);
|
console.error("提交形状失败:", err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
103
src/utils/whiteboardSync_old.js
Normal file
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -5,17 +5,27 @@
|
|||||||
<div class="livekit-container">
|
<div class="livekit-container">
|
||||||
<div v-if="!status" class="meeting-container">
|
<div v-if="!status" class="meeting-container">
|
||||||
<!-- 视频容器 -->
|
<!-- 视频容器 -->
|
||||||
<div class="video-layout" :class="{ 'screen-sharing-active': hasActiveScreenShare }">
|
<div class="video-layout" :class="{ 'screen-sharing-active': hasActiveScreenShare || isWhiteboardActive}">
|
||||||
<!-- 左侧共享屏幕区域 -->
|
<!-- 左侧共享屏幕/白板区域 -->
|
||||||
<div class="screen-share-area" v-if="hasActiveScreenShare">
|
<div class="screen-share-area" v-if="hasActiveScreenShare || isWhiteboardActive">
|
||||||
<div class="screen-share-header">
|
<div class="screen-share-header">
|
||||||
<h3>共享屏幕</h3>
|
<h3>{{ isWhiteboardActive ? '共享白板' : '共享屏幕' }}</h3>
|
||||||
<div class="sharing-user">
|
<div class="sharing-user">
|
||||||
由 {{ screenSharingUser }} 共享
|
由 {{ screenSharingUser }} 共享
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="screen-share-video">
|
<div class="screen-share-video">
|
||||||
<div class="video-wrapper screen-share-wrapper">
|
<!-- 白板模式 -->
|
||||||
|
<div v-if="isWhiteboardActive" class="whiteboard-container">
|
||||||
|
<tabulaRase
|
||||||
|
ref="whiteboardRef"
|
||||||
|
:roomId="roomId"
|
||||||
|
:userId="hostUid"
|
||||||
|
class="whiteboard-component"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- 屏幕共享模式 -->
|
||||||
|
<div v-else class="video-wrapper screen-share-wrapper">
|
||||||
<div class="video-tracks">
|
<div class="video-tracks">
|
||||||
<!-- 屏幕共享视频 -->
|
<!-- 屏幕共享视频 -->
|
||||||
<video
|
<video
|
||||||
@@ -40,9 +50,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧视频区域 -->
|
<!-- 右侧视频区域 -->
|
||||||
<div class="participants-area" :class="{ 'with-screen-share': hasActiveScreenShare }">
|
<div class="participants-area" :class="{ 'with-screen-share': hasActiveScreenShare || isWhiteboardActive }">
|
||||||
<div class="participants-header">
|
<div class="participants-header">
|
||||||
<h3>会议名称:测试会议名称</h3>
|
<h3>会议名称:{{ roomName }}</h3>
|
||||||
<h3>参会者 ({{ participantCount }})</h3>
|
<h3>参会者 ({{ participantCount }})</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="video-grid" :class="{ 'grid-layout': !hasActiveScreenShare && participantCount > 1 }">
|
<div class="video-grid" :class="{ 'grid-layout': !hasActiveScreenShare && participantCount > 1 }">
|
||||||
@@ -129,9 +139,25 @@
|
|||||||
<i :class="microphoneEnabled ? 'el-icon-microphone' : 'el-icon-turn-off-microphone'"></i>
|
<i :class="microphoneEnabled ? 'el-icon-microphone' : 'el-icon-turn-off-microphone'"></i>
|
||||||
{{ microphoneEnabled ? '关闭麦克风' : '开启麦克风' }}
|
{{ microphoneEnabled ? '关闭麦克风' : '开启麦克风' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="toggleScreenShare" :type="isScreenSharing ? 'danger' : 'primary'" class="control-btn" size="large">
|
<!-- <el-button @click="toggleScreenShare" :type="isScreenSharing ? 'danger' : 'primary'" class="control-btn" size="large">
|
||||||
<i class="el-icon-monitor"></i>
|
<i class="el-icon-monitor"></i>
|
||||||
{{ isScreenSharing ? '停止共享' : '共享屏幕' }}
|
{{ isScreenSharing ? '停止共享' : '共享屏幕' }}
|
||||||
|
</el-button> -->
|
||||||
|
<el-button
|
||||||
|
@click="toggleScreenShare"
|
||||||
|
:type="isScreenSharing ? 'danger' : (isGlobalScreenSharing ? 'info' : 'primary')"
|
||||||
|
:disabled="isGlobalScreenSharing && !isScreenSharing"
|
||||||
|
class="control-btn"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
<i class="el-icon-monitor"></i>
|
||||||
|
<span v-if="isScreenSharing">停止共享</span>
|
||||||
|
<span v-else-if="isGlobalScreenSharing">他人共享中</span>
|
||||||
|
<span v-else>共享屏幕</span>
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="toggleWhiteboard" :type="isWhiteboardActive ? 'danger' : 'info'" class="control-btn leave-btn" size="large">
|
||||||
|
<i class="el-icon-switch-button"></i>
|
||||||
|
{{ isWhiteboardActive ? '退出白板' : '共享白板' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="leaveRoom" type="warning" class="control-btn leave-btn" size="large">
|
<el-button @click="leaveRoom" type="warning" class="control-btn leave-btn" size="large">
|
||||||
<i class="el-icon-switch-button"></i>
|
<i class="el-icon-switch-button"></i>
|
||||||
@@ -145,12 +171,14 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref, onMounted, onUnmounted, nextTick, computed, watch } from "vue";
|
import { reactive, ref, onMounted, onUnmounted, nextTick, computed, watch } from "vue";
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage ,ElMessageBox} from 'element-plus';
|
||||||
import { getRoomToken, getRoomList ,getInvite,getTokenApi,exitRoomApi} from "@/api/conferencingRoom.js"
|
import { getRoomToken, getRoomList ,getInvite,getTokenApi,exitRoomApi} from "@/api/conferencingRoom.js"
|
||||||
import { Room, RoomEvent, ParticipantEvent, Track } from "livekit-client";
|
import { Room, RoomEvent, ParticipantEvent, Track } from "livekit-client";
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useRoomStore } from '@/stores/modules/room.js'
|
import { useRoomStore } from '@/stores/modules/room.js'
|
||||||
import useUserStore from '@/stores/modules/user'
|
import useUserStore from '@/stores/modules/user'
|
||||||
|
import tabulaRase from '@/views/custom/tabulaRase/index.vue'
|
||||||
|
import { mqttClient } from "@/utils/mqtt.js";
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const roomStore = useRoomStore()
|
const roomStore = useRoomStore()
|
||||||
@@ -162,6 +190,8 @@ const wsURL = "wss://meeting.cnsdt.com:443";
|
|||||||
|
|
||||||
const hostUid = ref('')
|
const hostUid = ref('')
|
||||||
const status = ref(true);
|
const status = ref(true);
|
||||||
|
const roomName = ref('')
|
||||||
|
const roomId = ref('')
|
||||||
|
|
||||||
// 音视频相关引用和数据
|
// 音视频相关引用和数据
|
||||||
const localVideo = ref(null);
|
const localVideo = ref(null);
|
||||||
@@ -181,6 +211,20 @@ const screenSharingUser = ref('');
|
|||||||
const activeScreenShareTrack = ref(null);
|
const activeScreenShareTrack = ref(null);
|
||||||
const screenShareVideo = ref(null);
|
const screenShareVideo = ref(null);
|
||||||
|
|
||||||
|
//共享白板
|
||||||
|
const isWhiteboardActive = ref(false);
|
||||||
|
const whiteboardRef = ref(null);
|
||||||
|
|
||||||
|
const globalScreenSharingUser = ref(''); // 当前正在共享屏幕的用户
|
||||||
|
const isGlobalScreenSharing = ref(false); // 是否有用户正在共享屏幕
|
||||||
|
|
||||||
|
// 白板消息类型
|
||||||
|
const WHITEBOARD_MESSAGE_TYPES = {
|
||||||
|
OPEN: 'open_whiteboard',
|
||||||
|
CLOSE: 'close_whiteboard',
|
||||||
|
SYNC: 'sync_whiteboard'
|
||||||
|
};
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const participantCount = computed(() => {
|
const participantCount = computed(() => {
|
||||||
return remoteParticipants.value.size + 1; // 包括自己
|
return remoteParticipants.value.size + 1; // 包括自己
|
||||||
@@ -194,6 +238,18 @@ const hasActiveScreenShare = computed(() => {
|
|||||||
return screenSharingUser.value !== '' || isScreenSharing.value;
|
return screenSharingUser.value !== '' || isScreenSharing.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 添加一个计算属性来显示屏幕共享状态提示
|
||||||
|
const screenShareStatusText = computed(() => {
|
||||||
|
if (isGlobalScreenSharing.value) {
|
||||||
|
if (globalScreenSharingUser.value === hostUid.value) {
|
||||||
|
return '您正在共享屏幕';
|
||||||
|
} else {
|
||||||
|
return `${globalScreenSharingUser.value} 正在共享屏幕`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '暂无屏幕共享';
|
||||||
|
});
|
||||||
|
|
||||||
// 创建 Room 实例
|
// 创建 Room 实例
|
||||||
const room = new Room({
|
const room = new Room({
|
||||||
adaptiveStream: true,
|
adaptiveStream: true,
|
||||||
@@ -220,11 +276,209 @@ const room = new Room({
|
|||||||
audioEncoding: {
|
audioEncoding: {
|
||||||
maxBitrate: 64000, // 64kbps for audio
|
maxBitrate: 64000, // 64kbps for audio
|
||||||
},
|
},
|
||||||
dtx: true, // 不连续传输,节省带宽
|
// dtx: true, // 不连续传输,节省带宽
|
||||||
red: true, // 冗余编码,提高抗丢包能力
|
red: true, // 冗余编码,提高抗丢包能力
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 初始化MQTT连接
|
||||||
|
async function initMqtt() {
|
||||||
|
try {
|
||||||
|
// 使用随机客户端ID连接
|
||||||
|
const clientId = `whiteboard_${Date.now()}`;
|
||||||
|
await mqttClient.connect(clientId);
|
||||||
|
console.log('MQTT连接成功,客户端ID:', clientId);
|
||||||
|
// 订阅白板主题
|
||||||
|
subscribeToWhiteboardTopic();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('MQTT连接失败:', error);
|
||||||
|
ElMessage.error('白板同步连接失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订阅白板主题
|
||||||
|
function subscribeToWhiteboardTopic() {
|
||||||
|
try {
|
||||||
|
mqttClient.subscribe(`xSynergy/shareWhiteboard/${room.name}`, handleWhiteboardMessage);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('订阅白板主题失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理白板消息
|
||||||
|
function handleWhiteboardMessage(payload, topic) {
|
||||||
|
try {
|
||||||
|
const messageStr = payload.toString();
|
||||||
|
const data = JSON.parse(messageStr);
|
||||||
|
console.log('收到白板消息:', data);
|
||||||
|
// 只处理当前房间的消息
|
||||||
|
if (data.roomId !== room.name) return;
|
||||||
|
// 忽略自己发送的消息
|
||||||
|
if (data.sender === hostUid.value) return;
|
||||||
|
switch (data.type) {
|
||||||
|
case WHITEBOARD_MESSAGE_TYPES.OPEN:
|
||||||
|
handleRemoteWhiteboardOpen(data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WHITEBOARD_MESSAGE_TYPES.CLOSE:
|
||||||
|
handleRemoteWhiteboardClose(data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WHITEBOARD_MESSAGE_TYPES.SYNC:
|
||||||
|
handleWhiteboardSync(data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn('未知的白板消息类型:', data.type);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理白板消息失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理远程打开白板
|
||||||
|
function handleRemoteWhiteboardOpen(data) {
|
||||||
|
ElMessage.info(`${data.senderName || data.sender} 开启了白板`);
|
||||||
|
isWhiteboardActive.value = true;
|
||||||
|
|
||||||
|
// 如果正在屏幕共享,自动停止
|
||||||
|
if (isScreenSharing.value) {
|
||||||
|
room.localParticipant.setScreenShareEnabled(false);
|
||||||
|
isScreenSharing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理远程关闭白板
|
||||||
|
function handleRemoteWhiteboardClose(data) {
|
||||||
|
ElMessage.info(`${data.senderName || data.sender} 关闭了白板`);
|
||||||
|
console.log('data',data)
|
||||||
|
if(data.type == '1'){
|
||||||
|
isWhiteboardActive.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理白板同步消息
|
||||||
|
function handleWhiteboardSync(data) {
|
||||||
|
// 这里可以处理白板内容的同步
|
||||||
|
if (whiteboardRef.value && data.payload) {
|
||||||
|
// 调用白板组件的同步方法
|
||||||
|
if (whiteboardRef.value.syncData) {
|
||||||
|
whiteboardRef.value.syncData(data.payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function publishWhiteboardMessage(type, payload = {}) {
|
||||||
|
try {
|
||||||
|
const message = {
|
||||||
|
type,
|
||||||
|
roomType: route.query.type,//用于判断是参与者还是发起者
|
||||||
|
roomId: roomId.value,
|
||||||
|
sender: hostUid.value,
|
||||||
|
senderName: hostUid.value,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
payload
|
||||||
|
};
|
||||||
|
|
||||||
|
mqttClient.publish(`xSynergy/shareWhiteboard/${room.name}`, message);
|
||||||
|
console.log('白板消息发布成功:', type);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发布白板消息失败:', error);
|
||||||
|
ElMessage.warning('消息发送失败,但白板功能正常');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//共享白板
|
||||||
|
// async function whiteboard(){
|
||||||
|
// const baseUrl = 'http://localhost:3000/#/whiteboard'
|
||||||
|
// const url = `${baseUrl}?room_uid=${room.name}`
|
||||||
|
// window.open(url, '_blank')
|
||||||
|
|
||||||
|
// }
|
||||||
|
async function toggleWhiteboard() {
|
||||||
|
if(hasActiveScreenShare.value){
|
||||||
|
ElMessage.error('请先关闭屏幕共享');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
roomId.value = room.name
|
||||||
|
if (isWhiteboardActive.value) {
|
||||||
|
// 如果白板已经激活,点击则退出白板
|
||||||
|
await exitWhiteboard();
|
||||||
|
} else {
|
||||||
|
// 否则开启白板
|
||||||
|
await startWhiteboard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startWhiteboard() {
|
||||||
|
try {
|
||||||
|
// 如果正在屏幕共享,先停止
|
||||||
|
if (isScreenSharing.value) {
|
||||||
|
await room.localParticipant.setScreenShareEnabled(false);
|
||||||
|
isScreenSharing.value = false;
|
||||||
|
ElMessage.info('已停止屏幕共享,开启白板');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 激活白板状态
|
||||||
|
isWhiteboardActive.value = true;
|
||||||
|
|
||||||
|
const success = publishWhiteboardMessage(WHITEBOARD_MESSAGE_TYPES.OPEN, {
|
||||||
|
action: 'open',
|
||||||
|
whiteboardId: roomId.value,
|
||||||
|
roomName: roomName.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
ElMessage.success('白板已开启,已通知其他参会者');
|
||||||
|
} else {
|
||||||
|
ElMessage.success('白板已开启');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('开启白板失败:', error);
|
||||||
|
ElMessage.error('开启白板失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exitWhiteboard() {
|
||||||
|
try {
|
||||||
|
// 确认退出
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'确定要退出白板吗?',
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 发布关闭白板消息
|
||||||
|
publishWhiteboardMessage(WHITEBOARD_MESSAGE_TYPES.CLOSE, {
|
||||||
|
action: 'close',
|
||||||
|
whiteboardId: roomId.value,
|
||||||
|
roomName: roomName.value
|
||||||
|
});
|
||||||
|
|
||||||
|
// 执行退出白板逻辑
|
||||||
|
isWhiteboardActive.value = false;
|
||||||
|
|
||||||
|
// 可以在这里添加白板清理逻辑
|
||||||
|
if (whiteboardRef.value && whiteboardRef.value.cleanup) {
|
||||||
|
whiteboardRef.value.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success('已退出白板');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('退出白板失败:', error);
|
||||||
|
ElMessage.error('退出白板失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 设置视频元素引用
|
// 设置视频元素引用
|
||||||
function setParticipantVideoRef(el, identity, type) {
|
function setParticipantVideoRef(el, identity, type) {
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@@ -286,7 +540,7 @@ function setupRoomListeners() {
|
|||||||
.on(RoomEvent.ParticipantConnected, handleParticipantConnected)
|
.on(RoomEvent.ParticipantConnected, handleParticipantConnected)
|
||||||
.on(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected)
|
.on(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected)
|
||||||
.on(RoomEvent.LocalTrackPublished, handleLocalTrackPublished)
|
.on(RoomEvent.LocalTrackPublished, handleLocalTrackPublished)
|
||||||
.on(RoomEvent.LocalTrackUnpublished, handleLocalTrackUnpublished)
|
.on(RoomEvent.LocalTrackUnpublished, handleLocalTrackUnpublished)//
|
||||||
.on(RoomEvent.TrackMuted, handleTrackMuted)
|
.on(RoomEvent.TrackMuted, handleTrackMuted)
|
||||||
.on(RoomEvent.TrackUnmuted, handleTrackUnmuted)
|
.on(RoomEvent.TrackUnmuted, handleTrackUnmuted)
|
||||||
.on(RoomEvent.ActiveSpeakersChanged, handleActiveSpeakersChanged)
|
.on(RoomEvent.ActiveSpeakersChanged, handleActiveSpeakersChanged)
|
||||||
@@ -295,12 +549,10 @@ function setupRoomListeners() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 事件处理函数
|
// 事件处理函数
|
||||||
async function handleConnected() {
|
async function handleConnected() {
|
||||||
console.log("成功连接到房间:", room.name);
|
await initMqtt();
|
||||||
status.value = false;
|
status.value = false;
|
||||||
|
ElMessage.success('已成功连接到房间');
|
||||||
ElMessage.success('已成功连接到房间');
|
|
||||||
|
|
||||||
// 初始化现有远程参与者
|
// 初始化现有远程参与者
|
||||||
room.remoteParticipants.forEach(participant => {
|
room.remoteParticipants.forEach(participant => {
|
||||||
addRemoteParticipant(participant);
|
addRemoteParticipant(participant);
|
||||||
@@ -310,16 +562,16 @@ async function handleConnected() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 自动开启麦克风
|
// 自动开启麦克风
|
||||||
try {
|
// try {
|
||||||
await enableMicrophone();
|
// await enableMicrophone();
|
||||||
ElMessage.success('麦克风已自动开启');
|
// ElMessage.success('麦克风已自动开启');
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.warn('自动开启麦克风失败:', error);
|
// console.warn('自动开启麦克风失败:', error);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDisconnected(reason) {
|
function handleDisconnected(reason) {
|
||||||
console.log("断开连接:", reason);
|
|
||||||
status.value = true;
|
status.value = true;
|
||||||
cameraEnabled.value = false;
|
cameraEnabled.value = false;
|
||||||
microphoneEnabled.value = false;
|
microphoneEnabled.value = false;
|
||||||
@@ -334,13 +586,11 @@ function handleDisconnected(reason) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleReconnected() {
|
function handleReconnected() {
|
||||||
console.log("已重新连接");
|
|
||||||
ElMessage.success('已重新连接到房间');
|
ElMessage.success('已重新连接到房间');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理轨道订阅事件
|
// 处理轨道订阅事件
|
||||||
function handleTrackSubscribed(track, publication, participant) {
|
function handleTrackSubscribed(track, publication, participant) {
|
||||||
console.log("轨道已订阅:", track.kind, "轨道来源:", publication.source, "来自:", participant.identity);
|
|
||||||
|
|
||||||
if (track) {
|
if (track) {
|
||||||
if (track.kind === Track.Kind.Video) {
|
if (track.kind === Track.Kind.Video) {
|
||||||
@@ -350,6 +600,9 @@ function handleTrackSubscribed(track, publication, participant) {
|
|||||||
// 如果是屏幕共享,更新屏幕共享状态
|
// 如果是屏幕共享,更新屏幕共享状态
|
||||||
if (publication.source === Track.Source.ScreenShare) {
|
if (publication.source === Track.Source.ScreenShare) {
|
||||||
updateScreenShareState(participant, track);
|
updateScreenShareState(participant, track);
|
||||||
|
// 设置全局屏幕共享用户
|
||||||
|
globalScreenSharingUser.value = participant.identity;
|
||||||
|
isGlobalScreenSharing.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 立即附加到视频元素(如果元素已存在)
|
// 立即附加到视频元素(如果元素已存在)
|
||||||
@@ -365,6 +618,7 @@ function handleTrackSubscribed(track, publication, participant) {
|
|||||||
updateParticipantTracks(participant);
|
updateParticipantTracks(participant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function handleTrackUnsubscribed(track, publication, participant) {
|
function handleTrackUnsubscribed(track, publication, participant) {
|
||||||
// 移除对应的轨道信息
|
// 移除对应的轨道信息
|
||||||
if (track.kind === Track.Kind.Video) {
|
if (track.kind === Track.Kind.Video) {
|
||||||
@@ -375,6 +629,11 @@ function handleTrackUnsubscribed(track, publication, participant) {
|
|||||||
// 如果是屏幕共享,更新屏幕共享状态
|
// 如果是屏幕共享,更新屏幕共享状态
|
||||||
if (publication.source === Track.Source.ScreenShare) {
|
if (publication.source === Track.Source.ScreenShare) {
|
||||||
updateScreenShareState(participant, null);
|
updateScreenShareState(participant, null);
|
||||||
|
// 清除全局屏幕共享状态
|
||||||
|
if (globalScreenSharingUser.value === participant.identity) {
|
||||||
|
globalScreenSharingUser.value = '';
|
||||||
|
isGlobalScreenSharing.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (track.kind === Track.Kind.Audio) {
|
} else if (track.kind === Track.Kind.Audio) {
|
||||||
// 处理音频轨道取消订阅
|
// 处理音频轨道取消订阅
|
||||||
@@ -402,6 +661,12 @@ function handleParticipantDisconnected(participant) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果离开的用户是全局屏幕共享者,清除全局状态
|
||||||
|
if (participant.identity === globalScreenSharingUser.value) {
|
||||||
|
globalScreenSharingUser.value = '';
|
||||||
|
isGlobalScreenSharing.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
removeRemoteParticipant(participant);
|
removeRemoteParticipant(participant);
|
||||||
ElMessage.info(`用户离开: ${participant.identity}`);
|
ElMessage.info(`用户离开: ${participant.identity}`);
|
||||||
}
|
}
|
||||||
@@ -415,6 +680,11 @@ function handleLocalTrackPublished(publication) {
|
|||||||
// 本地用户开始屏幕共享
|
// 本地用户开始屏幕共享
|
||||||
screenSharingUser.value = room.localParticipant.identity;
|
screenSharingUser.value = room.localParticipant.identity;
|
||||||
activeScreenShareTrack.value = publication.track;
|
activeScreenShareTrack.value = publication.track;
|
||||||
|
|
||||||
|
// 设置全局屏幕共享状态
|
||||||
|
globalScreenSharingUser.value = room.localParticipant.identity;
|
||||||
|
isGlobalScreenSharing.value = true;
|
||||||
|
|
||||||
if (screenShareVideo.value) {
|
if (screenShareVideo.value) {
|
||||||
attachTrackToVideo(screenShareVideo.value, publication.track);
|
attachTrackToVideo(screenShareVideo.value, publication.track);
|
||||||
}
|
}
|
||||||
@@ -427,15 +697,21 @@ function handleLocalTrackPublished(publication) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLocalTrackUnpublished(publication) {
|
function handleLocalTrackUnpublished(publication) {
|
||||||
if (publication.kind === Track.Kind.Video) {
|
if (publication.kind === Track.Kind.Video) {
|
||||||
if (publication.source === Track.Source.Camera) {
|
if (publication.source === Track.Source.Camera) {
|
||||||
cameraEnabled.value = false;
|
cameraEnabled.value = false;
|
||||||
} else if (publication.source === Track.Source.ScreenShare) {
|
} else if (publication.source === Track.Source.ScreenShare) {
|
||||||
// 本地用户停止屏幕共享
|
// 本地用户停止屏幕共享
|
||||||
if (screenSharingUser.value === room.localParticipant.identity) {
|
if (screenSharingUser.value === room.localParticipant.identity) {
|
||||||
|
isScreenSharing.value = false;
|
||||||
screenSharingUser.value = '';
|
screenSharingUser.value = '';
|
||||||
activeScreenShareTrack.value = null;
|
activeScreenShareTrack.value = null;
|
||||||
|
// 清除全局屏幕共享状态
|
||||||
|
if (globalScreenSharingUser.value === room.localParticipant.identity) {
|
||||||
|
globalScreenSharingUser.value = '';
|
||||||
|
isGlobalScreenSharing.value = false;
|
||||||
|
}
|
||||||
if (screenShareVideo.value && screenShareVideo.value.srcObject) {
|
if (screenShareVideo.value && screenShareVideo.value.srcObject) {
|
||||||
screenShareVideo.value.srcObject = null;
|
screenShareVideo.value.srcObject = null;
|
||||||
}
|
}
|
||||||
@@ -490,7 +766,6 @@ function handleActiveSpeakersChanged(speakers) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理数据接收事件
|
// 处理数据接收事件
|
||||||
function handleDataReceived(payload, participant, kind) {
|
function handleDataReceived(payload, participant, kind) {
|
||||||
try {
|
try {
|
||||||
@@ -505,7 +780,6 @@ function handleDataReceived(payload, participant, kind) {
|
|||||||
function handleConnectionStateChanged(state) {
|
function handleConnectionStateChanged(state) {
|
||||||
console.log('连接状态改变:', state);
|
console.log('连接状态改变:', state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新屏幕共享状态
|
// 更新屏幕共享状态
|
||||||
function updateScreenShareState(participant, track) {
|
function updateScreenShareState(participant, track) {
|
||||||
if (track) {
|
if (track) {
|
||||||
@@ -528,7 +802,6 @@ function updateScreenShareState(participant, track) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 参与者管理函数
|
// 参与者管理函数
|
||||||
function addRemoteParticipant(participant) {
|
function addRemoteParticipant(participant) {
|
||||||
if (!participant || participant.identity === room.localParticipant?.identity) {
|
if (!participant || participant.identity === room.localParticipant?.identity) {
|
||||||
@@ -561,7 +834,6 @@ function removeRemoteParticipant(participant) {
|
|||||||
audioElementsMap.value.delete(identity);
|
audioElementsMap.value.delete(identity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新参与者轨道信息
|
// 更新参与者轨道信息
|
||||||
function updateParticipantTrack(participant, source, track) {
|
function updateParticipantTrack(participant, source, track) {
|
||||||
const data = remoteParticipants.value.get(participant.identity);
|
const data = remoteParticipants.value.get(participant.identity);
|
||||||
@@ -605,7 +877,6 @@ function removeParticipantAudioTrack(participant) {
|
|||||||
data.audioEnabled = false;
|
data.audioEnabled = false;
|
||||||
remoteParticipants.value.set(participant.identity, { ...data });
|
remoteParticipants.value.set(participant.identity, { ...data });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 附加轨道到视频元素
|
// 附加轨道到视频元素
|
||||||
function attachTrackToVideo(videoElement, track) {
|
function attachTrackToVideo(videoElement, track) {
|
||||||
if (!videoElement || !track) return;
|
if (!videoElement || !track) return;
|
||||||
@@ -618,7 +889,6 @@ function attachTrackToVideo(videoElement, track) {
|
|||||||
console.error('附加轨道到视频元素失败:', error);
|
console.error('附加轨道到视频元素失败:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 附加轨道到音频元素
|
// 附加轨道到音频元素
|
||||||
function attachTrackToAudio(audioElement, track) {
|
function attachTrackToAudio(audioElement, track) {
|
||||||
if (!audioElement || !track) return;
|
if (!audioElement || !track) return;
|
||||||
@@ -634,7 +904,6 @@ function attachTrackToAudio(audioElement, track) {
|
|||||||
console.error('附加轨道到音频元素失败:', error);
|
console.error('附加轨道到音频元素失败:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 附加轨道到参与者的视频元素
|
// 附加轨道到参与者的视频元素
|
||||||
function attachTrackToParticipantVideo(identity, source, track) {
|
function attachTrackToParticipantVideo(identity, source, track) {
|
||||||
const videoElements = videoElementsMap.value.get(identity);
|
const videoElements = videoElementsMap.value.get(identity);
|
||||||
@@ -645,7 +914,6 @@ function attachTrackToParticipantVideo(identity, source, track) {
|
|||||||
attachTrackToVideo(videoElement, track);
|
attachTrackToVideo(videoElement, track);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 附加轨道到参与者的音频元素
|
// 附加轨道到参与者的音频元素
|
||||||
function attachTrackToParticipantAudio(identity, track) {
|
function attachTrackToParticipantAudio(identity, track) {
|
||||||
const audioElement = audioElementsMap.value.get(identity);
|
const audioElement = audioElementsMap.value.get(identity);
|
||||||
@@ -653,7 +921,6 @@ function attachTrackToParticipantAudio(identity, track) {
|
|||||||
attachTrackToAudio(audioElement, track);
|
attachTrackToAudio(audioElement, track);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从参与者的视频元素分离轨道
|
// 从参与者的视频元素分离轨道
|
||||||
function detachTrackFromParticipantVideo(identity, source) {
|
function detachTrackFromParticipantVideo(identity, source) {
|
||||||
const videoElements = videoElementsMap.value.get(identity);
|
const videoElements = videoElementsMap.value.get(identity);
|
||||||
@@ -664,7 +931,6 @@ function detachTrackFromParticipantVideo(identity, source) {
|
|||||||
videoElement.srcObject = null;
|
videoElement.srcObject = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从参与者的音频元素分离轨道
|
// 从参与者的音频元素分离轨道
|
||||||
function detachTrackFromParticipantAudio(identity) {
|
function detachTrackFromParticipantAudio(identity) {
|
||||||
const audioElement = audioElementsMap.value.get(identity);
|
const audioElement = audioElementsMap.value.get(identity);
|
||||||
@@ -731,7 +997,6 @@ function handleVideoLoaded(identity, type) {
|
|||||||
function handleScreenShareLoaded() {
|
function handleScreenShareLoaded() {
|
||||||
console.log('屏幕共享视频加载完成');
|
console.log('屏幕共享视频加载完成');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 视频轨道处理函数
|
// 视频轨道处理函数
|
||||||
function attachLocalVideoTrack(track) {
|
function attachLocalVideoTrack(track) {
|
||||||
if (localVideo.value && track) {
|
if (localVideo.value && track) {
|
||||||
@@ -745,7 +1010,6 @@ function attachLocalVideoTrack(track) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 媒体控制函数
|
// 媒体控制函数
|
||||||
async function enableCamera() {
|
async function enableCamera() {
|
||||||
try {
|
try {
|
||||||
@@ -799,8 +1063,7 @@ async function toggleCamera() {
|
|||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('切换摄像头失败:', error);
|
errorHandling(error,'摄像头');
|
||||||
ElMessage.error('切换摄像头失败: ' + error.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -838,7 +1101,15 @@ async function toggleMicrophone() {
|
|||||||
try {
|
try {
|
||||||
if (microphoneEnabled.value) {
|
if (microphoneEnabled.value) {
|
||||||
await room.localParticipant.setMicrophoneEnabled(false);
|
await room.localParticipant.setMicrophoneEnabled(false);
|
||||||
microphoneEnabled.value = false;
|
microphoneEnabled.value = false;
|
||||||
|
// 停止所有音频轨道
|
||||||
|
const audioPublications = Array.from(room.localParticipant.audioTrackPublications.values());
|
||||||
|
for (const publication of audioPublications) {
|
||||||
|
if (publication.track) {
|
||||||
|
publication.track.stop(); // 停止轨道
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ElMessage.info('麦克风已关闭');
|
ElMessage.info('麦克风已关闭');
|
||||||
} else {
|
} else {
|
||||||
// 确保有音频输入设备权限
|
// 确保有音频输入设备权限
|
||||||
@@ -864,32 +1135,80 @@ async function toggleMicrophone() {
|
|||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('切换麦克风失败:', error);
|
errorHandling(error,'麦克风');
|
||||||
ElMessage.error('切换麦克风失败: ' + error.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function errorHandling(error,type) {
|
||||||
|
switch (error.name) {
|
||||||
|
case 'NotAllowedError':
|
||||||
|
ElMessage.error('用户拒绝了权限请求');
|
||||||
|
break;
|
||||||
|
case 'NotFoundError':
|
||||||
|
ElMessage.error(`未找到${type}设备`);
|
||||||
|
break;
|
||||||
|
case 'NotSupportedError':
|
||||||
|
ElMessage.error('当前浏览器不支持此功能,请更换其他浏览器');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ElMessage.error('服务错误,请刷新重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function toggleScreenShare() {
|
async function toggleScreenShare() {
|
||||||
try {
|
try {
|
||||||
|
if(isWhiteboardActive.value){
|
||||||
|
ElMessage.error('请先关闭白板');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已经有其他用户在共享屏幕
|
||||||
|
if (!isScreenSharing.value && isGlobalScreenSharing.value && globalScreenSharingUser.value !== hostUid.value) {
|
||||||
|
ElMessage.error(`当前 ${globalScreenSharingUser.value} 正在共享屏幕,请等待其结束后再共享`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isScreenSharing.value) {
|
if (isScreenSharing.value) {
|
||||||
await room.localParticipant.setScreenShareEnabled(false);
|
await room.localParticipant.setScreenShareEnabled(false);
|
||||||
isScreenSharing.value = false;
|
isScreenSharing.value = false;
|
||||||
|
// 清除全局屏幕共享状态
|
||||||
|
if (globalScreenSharingUser.value === hostUid.value) {
|
||||||
|
globalScreenSharingUser.value = '';
|
||||||
|
isGlobalScreenSharing.value = false;
|
||||||
|
}
|
||||||
ElMessage.info('屏幕共享已停止');
|
ElMessage.info('屏幕共享已停止');
|
||||||
} else {
|
} else {
|
||||||
await room.localParticipant.setScreenShareEnabled(true);
|
await room.localParticipant.setScreenShareEnabled(true);
|
||||||
isScreenSharing.value = true;
|
isScreenSharing.value = true;
|
||||||
|
// 设置全局屏幕共享状态
|
||||||
|
globalScreenSharingUser.value = hostUid.value;
|
||||||
|
isGlobalScreenSharing.value = true;
|
||||||
ElMessage.success('屏幕共享已开始');
|
ElMessage.success('屏幕共享已开始');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('切换屏幕共享失败:', error);
|
errorHandling(error,'屏幕共享');
|
||||||
ElMessage.error('切换屏幕共享失败: ' + error.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleScreenShareEnded() {
|
||||||
|
console.log('用户通过浏览器控件停止了屏幕共享');
|
||||||
|
isScreenSharing.value = false;
|
||||||
|
ElMessage.info('屏幕共享已停止');
|
||||||
|
|
||||||
|
// 移除事件监听器
|
||||||
|
room.localParticipant.off('screenShareEnded', handleScreenShareEnded);
|
||||||
|
}
|
||||||
|
|
||||||
async function joinRoomBtn() {
|
async function joinRoomBtn() {
|
||||||
try {
|
try {
|
||||||
const res = await getRoomToken({max_participants: 20});
|
const res = await getRoomToken({max_participants: 20});
|
||||||
|
if(res.meta.code != 200){
|
||||||
|
ElMessage.error(res.meta.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const token = res.data.access_token;
|
const token = res.data.access_token;
|
||||||
|
roomName.value = res.data.room.name
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error('获取 token 失败');
|
throw new Error('获取 token 失败');
|
||||||
}
|
}
|
||||||
@@ -906,8 +1225,23 @@ async function joinRoomBtn() {
|
|||||||
// 离开会议函数
|
// 离开会议函数
|
||||||
async function leaveRoom() {
|
async function leaveRoom() {
|
||||||
try {
|
try {
|
||||||
const res = await exitRoomApi(room.name)
|
// 如果白板正在运行,先退出白板
|
||||||
console.log(res,'离开房间成功')
|
if (isWhiteboardActive.value) {
|
||||||
|
publishWhiteboardMessage(WHITEBOARD_MESSAGE_TYPES.CLOSE, {
|
||||||
|
action: 'close',
|
||||||
|
reason: 'host_left',
|
||||||
|
roomName: roomName.value
|
||||||
|
});
|
||||||
|
isWhiteboardActive.value = false;
|
||||||
|
if (whiteboardRef.value && whiteboardRef.value.cleanup) {
|
||||||
|
whiteboardRef.value.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 断开MQTT连接
|
||||||
|
mqttClient.disconnect();
|
||||||
|
|
||||||
|
const res = await exitRoomApi(room.name)
|
||||||
// 停止屏幕共享(如果正在共享)
|
// 停止屏幕共享(如果正在共享)
|
||||||
if (isScreenSharing.value) {
|
if (isScreenSharing.value) {
|
||||||
await room.localParticipant.setScreenShareEnabled(false);
|
await room.localParticipant.setScreenShareEnabled(false);
|
||||||
@@ -930,19 +1264,25 @@ async function leaveRoom() {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('离开会议失败:', error);
|
console.error('离开会议失败:', error);
|
||||||
ElMessage.error('离开会议失败: ' + error.message);
|
ElMessage.error('离开会议失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置房间状态函数
|
// 重置房间状态函数
|
||||||
function resetRoomState() {
|
function resetRoomState() {
|
||||||
// 重置本地状态
|
// 重置本地状态
|
||||||
|
globalScreenSharingUser.value = '';
|
||||||
|
isGlobalScreenSharing.value = false;
|
||||||
|
isWhiteboardActive.value = false;
|
||||||
cameraEnabled.value = false;
|
cameraEnabled.value = false;
|
||||||
microphoneEnabled.value = false;
|
microphoneEnabled.value = false;
|
||||||
isScreenSharing.value = false;
|
isScreenSharing.value = false;
|
||||||
isLocalSpeaking.value = false;
|
isLocalSpeaking.value = false;
|
||||||
status.value = true;
|
status.value = true;
|
||||||
hostUid.value = '';
|
hostUid.value = '';
|
||||||
|
|
||||||
|
// 断开MQTT连接
|
||||||
|
mqttClient.disconnect();
|
||||||
|
|
||||||
// 重置远程参与者
|
// 重置远程参与者
|
||||||
remoteParticipants.value.clear();
|
remoteParticipants.value.clear();
|
||||||
@@ -992,6 +1332,11 @@ function resetRoomState() {
|
|||||||
|
|
||||||
// 在组件卸载时也清理资源
|
// 在组件卸载时也清理资源
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
if (isWhiteboardActive.value && whiteboardRef.value && whiteboardRef.value.cleanup) {
|
||||||
|
whiteboardRef.value.cleanup();
|
||||||
|
}
|
||||||
|
// 清理MQTT
|
||||||
|
mqttClient.disconnect();
|
||||||
if (room && room.state === 'connected') {
|
if (room && room.state === 'connected') {
|
||||||
leaveRoom();
|
leaveRoom();
|
||||||
}
|
}
|
||||||
@@ -999,14 +1344,14 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 确保在连接前请求音频权限
|
// 确保在连接前请求音频权限
|
||||||
try {
|
// try {
|
||||||
// 预请求音频权限
|
// // 预请求音频权限
|
||||||
await navigator.mediaDevices.getUserMedia({ audio: true });
|
// await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
console.log('音频权限已获取');
|
// console.log('音频权限已获取');
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.warn('音频权限获取失败:', error);
|
// console.warn('音频权限获取失败:', error);
|
||||||
ElMessage.warning('请允许麦克风权限以使用音频功能');
|
// ElMessage.warning('请允许麦克风权限以使用音频功能');
|
||||||
}
|
// }
|
||||||
|
|
||||||
if(route.query.type == '1'){
|
if(route.query.type == '1'){
|
||||||
await joinRoomBtn()
|
await joinRoomBtn()
|
||||||
@@ -1014,23 +1359,47 @@ onMounted(async () => {
|
|||||||
// 邀请用户参与房间
|
// 邀请用户参与房间
|
||||||
await getInvite(room.name,{user_uids:[roomStore.detailUid], participant_role: "participant"})
|
await getInvite(room.name,{user_uids:[roomStore.detailUid], participant_role: "participant"})
|
||||||
} else {
|
} else {
|
||||||
const userInfo = await userStore.getInfo()
|
// const userInfo = await userStore.getInfo()
|
||||||
hostUid.value = userInfo.uid
|
// hostUid.value = userInfo.uid
|
||||||
// 确保 hostUid 更新到模板
|
|
||||||
await nextTick();
|
|
||||||
const res = await getTokenApi(route.query.room_uid)
|
const res = await getTokenApi(route.query.room_uid)
|
||||||
if(res.meta.code == 200){
|
if(res.meta.code == 200){
|
||||||
const token = res.data.access_token;
|
const token = res.data.access_token;
|
||||||
|
hostUid.value = res.data.user_uid
|
||||||
|
await nextTick();
|
||||||
setupRoomListeners();
|
setupRoomListeners();
|
||||||
await room.connect(wsURL, token, {
|
await room.connect(wsURL, token, {
|
||||||
autoSubscribe: true,
|
autoSubscribe: true,
|
||||||
});
|
});
|
||||||
|
}else{
|
||||||
|
ElMessage.error(res.meta.message);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* 白板容器样式 */
|
||||||
|
.whiteboard-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whiteboard-component {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 退出白板按钮样式 */
|
||||||
|
.exit-whiteboard-btn {
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 添加音频状态指示器样式 */
|
/* 添加音频状态指示器样式 */
|
||||||
.audio-indicator {
|
.audio-indicator {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
@@ -1246,7 +1615,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.screen-share-wrapper {
|
.screen-share-wrapper {
|
||||||
@@ -1490,6 +1859,9 @@ body {
|
|||||||
|
|
||||||
/* 响应式调整 */
|
/* 响应式调整 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.whiteboard-container {
|
||||||
|
height: 300px; /* 在移动端限制白板高度 */
|
||||||
|
}
|
||||||
.form-container {
|
.form-container {
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
|
|||||||
@@ -305,9 +305,9 @@ const getList = async () => {
|
|||||||
* 通讯录 人员信息树
|
* 通讯录 人员信息树
|
||||||
*/
|
*/
|
||||||
const HandleLoadNode = async (node, resolve) => {
|
const HandleLoadNode = async (node, resolve) => {
|
||||||
if(node.level === 0){
|
if(node?.level === 0){
|
||||||
loadNode(resolve)
|
loadNode(resolve)
|
||||||
}else if(node.level === 1){
|
}else if(node?.level === 1){
|
||||||
loadNode(resolve,node.data.directory_uid)
|
loadNode(resolve,node.data.directory_uid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -317,7 +317,13 @@ const loadNode = async(resolve,id)=>{
|
|||||||
// state.leftListLoading = true
|
// state.leftListLoading = true
|
||||||
if(!id){
|
if(!id){
|
||||||
let res = await getDirectories({level:1})
|
let res = await getDirectories({level:1})
|
||||||
resolve(res.data)
|
if(res.meta.code == 401){
|
||||||
|
emit('showLogin', true)
|
||||||
|
return
|
||||||
|
}else{
|
||||||
|
resolve(res.data)
|
||||||
|
}
|
||||||
|
|
||||||
}else{
|
}else{
|
||||||
let res = await getDirectoriesUsers(id,{directory_uuid:id})
|
let res = await getDirectoriesUsers(id,{directory_uuid:id})
|
||||||
resolve(res.data)
|
resolve(res.data)
|
||||||
@@ -383,7 +389,7 @@ watch(
|
|||||||
|
|
||||||
if (newValue == 1) {
|
if (newValue == 1) {
|
||||||
state.isShow = false
|
state.isShow = false
|
||||||
getList()
|
// getList()
|
||||||
} else {
|
} else {
|
||||||
HandleLoadNode()
|
HandleLoadNode()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,266 +1,273 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container" v-loading="load" :element-loading-text="loadText">
|
<div v-if="showLogin" style="height:100%">
|
||||||
<el-row :gutter="6" style="padding: 0 10px; background: #fefefe">
|
<!-- 登录界面 -->
|
||||||
<el-col :xs="24" :sm="24" :md="8" :lg="6">
|
<Login @loginSuccess="handleLoginSuccess" />
|
||||||
<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>
|
|
||||||
<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.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"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
items.nickName +
|
|
||||||
(indexs + 1 == detail.assistanceMemberList.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?.prefix &&
|
|
||||||
detail.remoteVideoFile.path
|
|
||||||
"
|
|
||||||
:src="
|
|
||||||
detail.remoteVideoFile.prefix +
|
|
||||||
detail.remoteVideoFile.path
|
|
||||||
"
|
|
||||||
id="videoPlayer"
|
|
||||||
loop
|
|
||||||
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.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>
|
|
||||||
</div>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="message-user"
|
|
||||||
v-else-if="isShow && tabValue == 2"
|
|
||||||
style="height: calc(100vh - 90px)"
|
|
||||||
>
|
|
||||||
<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="primary" @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>
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<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"
|
||||||
|
@showLogin="showLoginHandle"
|
||||||
|
: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>
|
||||||
|
<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.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"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
items.nickName +
|
||||||
|
(indexs + 1 == detail.assistanceMemberList.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?.prefix &&
|
||||||
|
detail.remoteVideoFile.path
|
||||||
|
"
|
||||||
|
:src="
|
||||||
|
detail.remoteVideoFile.prefix +
|
||||||
|
detail.remoteVideoFile.path
|
||||||
|
"
|
||||||
|
id="videoPlayer"
|
||||||
|
loop
|
||||||
|
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.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>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="message-user"
|
||||||
|
v-else-if="isShow && tabValue == 2"
|
||||||
|
style="height: calc(100vh - 90px)"
|
||||||
|
>
|
||||||
|
<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="primary" @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>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onActivated, onMounted, reactive, toRefs, watch, getCurrentInstance } from 'vue'
|
import { onActivated, onMounted, reactive, toRefs, watch, getCurrentInstance ,ref} from 'vue'
|
||||||
import leftTab from './components/leftTab/index.vue'
|
import leftTab from './components/leftTab/index.vue'
|
||||||
import { getInfo } from '@/api/login.js'
|
import { getInfo } from '@/api/login.js'
|
||||||
import { getStatusApi } from '@/api/conferencingRoom.js'
|
import { getStatusApi } from '@/api/conferencingRoom.js'
|
||||||
@@ -269,6 +276,8 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import { useRoomStore } from '@/stores/modules/room'
|
import { useRoomStore } from '@/stores/modules/room'
|
||||||
import useUserStore from '@/stores/modules/user'
|
import useUserStore from '@/stores/modules/user'
|
||||||
import { mqttClient } from "@/utils/mqtt.js";
|
import { mqttClient } from "@/utils/mqtt.js";
|
||||||
|
import { getToken } from '@/utils/auth.js'
|
||||||
|
import Login from "@/components/Login/index.vue";
|
||||||
const roomStore = useRoomStore()
|
const roomStore = useRoomStore()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -285,6 +294,17 @@ const state = reactive({
|
|||||||
inviteDialog: false,
|
inviteDialog: false,
|
||||||
cooperation: import.meta.env.VITE_APP_COOPERATION_TYPE,
|
cooperation: import.meta.env.VITE_APP_COOPERATION_TYPE,
|
||||||
})
|
})
|
||||||
|
const showLogin = ref(false); // 是否显示登录页面
|
||||||
|
|
||||||
|
function showLoginHandle(e){
|
||||||
|
showLogin.value = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 登录成功回调 */
|
||||||
|
function handleLoginSuccess() {
|
||||||
|
showLogin.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const isEmptyObject = (obj) => {
|
const isEmptyObject = (obj) => {
|
||||||
return !obj || Object.keys(obj).length === 0
|
return !obj || Object.keys(obj).length === 0
|
||||||
@@ -294,7 +314,7 @@ const isEmptyObject = (obj) => {
|
|||||||
const clickInitiate = () => {
|
const clickInitiate = () => {
|
||||||
let userData = null
|
let userData = null
|
||||||
try {
|
try {
|
||||||
userData = JSON.parse(localStorage.getItem('userData')) || null
|
userData = JSON.parse(sessionStorage.getItem('userData')) || null
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('解析 userData 失败:', e)
|
console.error('解析 userData 失败:', e)
|
||||||
}
|
}
|
||||||
@@ -404,7 +424,6 @@ const clickRefuseJoin = async () => {
|
|||||||
const processingSocket = (message) => {
|
const processingSocket = (message) => {
|
||||||
if (message) {
|
if (message) {
|
||||||
state.socketInformation = JSON.parse(message)
|
state.socketInformation = JSON.parse(message)
|
||||||
console.log(state.socketInformation,'state.socketInformation')
|
|
||||||
state.inviteDialog = true
|
state.inviteDialog = true
|
||||||
showNotification(state.socketInformation)
|
showNotification(state.socketInformation)
|
||||||
}
|
}
|
||||||
@@ -429,7 +448,12 @@ const showNotification = (data) => {
|
|||||||
|
|
||||||
// 暴露给模板
|
// 暴露给模板
|
||||||
const { detail, weekName, tabValue, isShow, load, loadText, isLinkKnow, socketInformation, inviteDialog, cooperation } = toRefs(state)
|
const { detail, weekName, tabValue, isShow, load, loadText, isLinkKnow, socketInformation, inviteDialog, cooperation } = toRefs(state)
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
if(!getToken()){
|
||||||
|
showLogin.value = true;
|
||||||
|
}else{
|
||||||
|
showLogin.value = false;
|
||||||
|
}
|
||||||
await mqttClient.connect(`room${Math.random().toString(16).substr(2, 8)}`);
|
await mqttClient.connect(`room${Math.random().toString(16).substr(2, 8)}`);
|
||||||
const res = await userStore.getInfo()
|
const res = await userStore.getInfo()
|
||||||
const topic = `xSynergy/ROOM/+/rooms/${res.uid}`;
|
const topic = `xSynergy/ROOM/+/rooms/${res.uid}`;
|
||||||
|
|||||||
@@ -26,8 +26,7 @@
|
|||||||
import { ref, nextTick, onUnmounted, onMounted } from "vue";
|
import { ref, nextTick, onUnmounted, onMounted } from "vue";
|
||||||
import { ElLoading, ElMessage } from "element-plus";
|
import { ElLoading, ElMessage } from "element-plus";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
import { mqttClient } from "@/utils/mqtt";
|
import { mqttClient } from "@/utils/mqtt";
|
||||||
import { WhiteboardSync } from "@/utils/whiteboardSync";
|
import { WhiteboardSync } from "@/utils/whiteboardSync";
|
||||||
import ToolBox from "@/components/ToolBox/index.vue";
|
import ToolBox from "@/components/ToolBox/index.vue";
|
||||||
@@ -35,6 +34,17 @@ import Login from "@/components/Login/index.vue";
|
|||||||
import Canvas from "@/core/index.js";
|
import Canvas from "@/core/index.js";
|
||||||
import { getInfo } from "@/api/login";
|
import { getInfo } from "@/api/login";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
roomId: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const showLogin = ref(false); // 是否显示登录页面
|
const showLogin = ref(false); // 是否显示登录页面
|
||||||
const hasJoined = ref(false); // 是否加入白板
|
const hasJoined = ref(false); // 是否加入白板
|
||||||
const canvas = ref(null);
|
const canvas = ref(null);
|
||||||
@@ -80,47 +90,21 @@ function initWhiteboard() {
|
|||||||
console.error("⚠️ 找不到 canvas 元素");
|
console.error("⚠️ 找不到 canvas 元素");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置画布宽高(保持16:9)
|
|
||||||
const { width, height } = getCanvasSize(el.parentElement);
|
|
||||||
Object.assign(el, { width, height });
|
|
||||||
Object.assign(el.style, {
|
|
||||||
width: `${width}px`,
|
|
||||||
height: `${height}px`,
|
|
||||||
border: "2px solid #000",
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化画布
|
// 初始化画布
|
||||||
canvas.value = new Canvas("whiteboard");
|
canvas.value = new Canvas("whiteboard");
|
||||||
|
|
||||||
// 获取房间号
|
// 获取房间号
|
||||||
const roomUid = route.query.room_uid || "default-room";
|
const roomUid = route.query.room_uid || props.roomId || "default-room";
|
||||||
|
|
||||||
// 初始化多人同步
|
// 初始化多人同步
|
||||||
WhiteboardSync.init(canvas.value, roomUid);
|
WhiteboardSync.init(canvas.value, roomUid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 计算画布大小,保持16:9 */
|
|
||||||
function getCanvasSize(container) {
|
|
||||||
const containerWidth = container.offsetWidth;
|
|
||||||
const containerHeight = container.offsetHeight;
|
|
||||||
|
|
||||||
let width = containerWidth;
|
|
||||||
let height = Math.floor((width * 9) / 16);
|
|
||||||
|
|
||||||
if (height > containerHeight) {
|
onMounted(async () => {
|
||||||
height = containerHeight;
|
|
||||||
width = Math.floor((height * 16) / 9);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { width, height };
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
try {
|
||||||
showLogin.value = false;
|
showLogin.value = false;
|
||||||
const res = await getInfo("self");
|
const res = await getInfo("self");
|
||||||
console.log(res, "用户信息校验")
|
|
||||||
if (res.meta.code === 401) {
|
if (res.meta.code === 401) {
|
||||||
showLogin.value = true;
|
showLogin.value = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -143,8 +127,8 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100vw;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -159,14 +143,12 @@ onUnmounted(() => {
|
|||||||
/* 白板容器 */
|
/* 白板容器 */
|
||||||
.whiteboard-wrapper {
|
.whiteboard-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 90vw;
|
width: 72vw;
|
||||||
max-width: 1280px;
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 画布占满白板容器 */
|
/* 画布占满白板容器 */
|
||||||
|
|||||||
185
src/views/custom/tabulaRase/index_old.vue
Normal file
185
src/views/custom/tabulaRase/index_old.vue
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<template>
|
||||||
|
<div class="wrapper-content">
|
||||||
|
<div v-if="showLogin">
|
||||||
|
<!-- 登录界面 -->
|
||||||
|
<Login @loginSuccess="handleLoginSuccess" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<!-- 未加入时显示按钮 -->
|
||||||
|
<div v-if="!hasJoined" class="login-button-container">
|
||||||
|
<el-button type="primary" size="large" round plain @click="joinWhiteboard">
|
||||||
|
加入互动画板
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已加入时显示白板 -->
|
||||||
|
<div v-else class="whiteboard-wrapper">
|
||||||
|
<ToolBox v-if="canvas" class="toolbox" :canvas="canvas" />
|
||||||
|
<canvas id="whiteboard" class="whiteboard-canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, nextTick, onUnmounted, onMounted } from "vue";
|
||||||
|
import { ElLoading, ElMessage } from "element-plus";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { mqttClient } from "@/utils/mqtt";
|
||||||
|
import { WhiteboardSync } from "@/utils/whiteboardSync";
|
||||||
|
import ToolBox from "@/components/ToolBox/index.vue";
|
||||||
|
import Login from "@/components/Login/index.vue";
|
||||||
|
import Canvas from "@/core/index.js";
|
||||||
|
import { getInfo } from "@/api/login";
|
||||||
|
|
||||||
|
const showLogin = ref(false); // 是否显示登录页面
|
||||||
|
const hasJoined = ref(false); // 是否加入白板
|
||||||
|
const canvas = ref(null);
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
/** 进入白板 */
|
||||||
|
async function joinWhiteboard() {
|
||||||
|
const loading = ElLoading.service({
|
||||||
|
lock: true,
|
||||||
|
text: "正在进入互动画板...",
|
||||||
|
background: "rgba(0, 0, 0, 0.4)",
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const clientId = `whiteboard-${uuidv4()}`;
|
||||||
|
await mqttClient.connect(clientId);
|
||||||
|
console.log("✅ 已连接 MQTT:", clientId);
|
||||||
|
|
||||||
|
hasJoined.value = true;
|
||||||
|
|
||||||
|
// 等待 DOM 更新后再初始化画布
|
||||||
|
await nextTick();
|
||||||
|
initWhiteboard();
|
||||||
|
|
||||||
|
ElMessage.success("已进入互动画板 🎉");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ 连接失败:", err);
|
||||||
|
ElMessage.error("连接白板失败,请重试");
|
||||||
|
} finally {
|
||||||
|
loading.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 登录成功回调 */
|
||||||
|
function handleLoginSuccess() {
|
||||||
|
showLogin.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化白板 */
|
||||||
|
function initWhiteboard() {
|
||||||
|
const el = document.getElementById("whiteboard");
|
||||||
|
if (!el) {
|
||||||
|
console.error("⚠️ 找不到 canvas 元素");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置画布宽高(保持16:9)
|
||||||
|
const { width, height } = getCanvasSize(el.parentElement);
|
||||||
|
Object.assign(el, { width, height });
|
||||||
|
Object.assign(el.style, {
|
||||||
|
width: `${width}px`,
|
||||||
|
height: `${height}px`,
|
||||||
|
border: "2px solid #000",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化画布
|
||||||
|
canvas.value = new Canvas("whiteboard");
|
||||||
|
|
||||||
|
// 获取房间号
|
||||||
|
const roomUid = route.query.room_uid || "default-room";
|
||||||
|
|
||||||
|
// 初始化多人同步
|
||||||
|
WhiteboardSync.init(canvas.value, roomUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 计算画布大小,保持16:9 */
|
||||||
|
function getCanvasSize(container) {
|
||||||
|
const containerWidth = container.offsetWidth;
|
||||||
|
const containerHeight = container.offsetHeight;
|
||||||
|
|
||||||
|
let width = containerWidth;
|
||||||
|
let height = Math.floor((width * 9) / 16);
|
||||||
|
|
||||||
|
if (height > containerHeight) {
|
||||||
|
height = containerHeight;
|
||||||
|
width = Math.floor((height * 16) / 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
showLogin.value = false;
|
||||||
|
const res = await getInfo("self");
|
||||||
|
if (res.meta.code === 401) {
|
||||||
|
showLogin.value = true;
|
||||||
|
} else {
|
||||||
|
showLogin.value = false;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("⚠️ 用户信息校验失败:", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (canvas.value) canvas.value.destroy();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 外层容器全屏居中 */
|
||||||
|
.wrapper-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: #fff;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录按钮容器居中 */
|
||||||
|
.login-button-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 白板容器 */
|
||||||
|
.whiteboard-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 90vw;
|
||||||
|
max-width: 1280px;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 画布占满白板容器 */
|
||||||
|
.whiteboard-canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 工具栏左侧垂直居中 */
|
||||||
|
.toolbox {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 10px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -40,15 +40,20 @@
|
|||||||
import useUserStore from '@/stores/modules/user'
|
import useUserStore from '@/stores/modules/user'
|
||||||
import { watch, ref, getCurrentInstance, onMounted } from 'vue'
|
import { watch, ref, getCurrentInstance, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElNotification } from 'element-plus'
|
import { ElNotification,ElMessage } from 'element-plus'
|
||||||
import { getInfo } from "@/api/login";
|
import { getInfo } from "@/api/login";
|
||||||
|
import CryptoJS from 'crypto-js';
|
||||||
|
import { useMeterStore } from '@/stores/modules/meter'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
const showLogin = ref(false)
|
const showLogin = ref(false)
|
||||||
|
|
||||||
|
const meterStore = useMeterStore()
|
||||||
|
|
||||||
|
|
||||||
const redirect = ref(undefined);
|
const redirect = ref(undefined);
|
||||||
const loginView = ref(false)
|
const loginView = ref(false)
|
||||||
|
|
||||||
@@ -73,14 +78,30 @@ function handleLogin() {
|
|||||||
proxy.$refs.loginRef.validate((valid) => {
|
proxy.$refs.loginRef.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
if(!localStorage?.getItem('UDID')){
|
||||||
|
ElMessage({
|
||||||
|
message: '服务错误,请刷新页面',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const secretKey = ((loginForm.value.username + localStorage?.getItem('UDID')).toLowerCase()).replaceAll('-', ''); // 用户名+UDID(32位16进制,全小写)
|
||||||
|
|
||||||
|
const randomChars = generateRandomChars(6);
|
||||||
|
|
||||||
|
const message = `Gx${randomChars}${loginForm.value.password}`;
|
||||||
|
|
||||||
|
const ciphertext = CryptoJS.Blowfish.encrypt(message, secretKey).toString();
|
||||||
|
console.log(secretKey,message,ciphertext)
|
||||||
// 调用action的登录方法
|
// 调用action的登录方法
|
||||||
userStore
|
userStore
|
||||||
.login({
|
.login({
|
||||||
password: loginForm.value.password,
|
// password: loginForm.value.password,
|
||||||
|
password: ciphertext,
|
||||||
username: loginForm.value.username,
|
username: loginForm.value.username,
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
const userInfo = JSON.parse(localStorage.getItem('userData'))
|
const userInfo = JSON.parse(sessionStorage.getItem('userData'))
|
||||||
router.push({
|
router.push({
|
||||||
path: '/coordinate',
|
path: '/coordinate',
|
||||||
})
|
})
|
||||||
@@ -99,6 +120,16 @@ function handleLogin() {
|
|||||||
requestNotificationPermission()
|
requestNotificationPermission()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 生成随机字符串
|
||||||
|
function generateRandomChars(length) {
|
||||||
|
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 请求浏览器的通知权限
|
* @description 请求浏览器的通知权限
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
@@ -113,6 +144,7 @@ function requestNotificationPermission() {
|
|||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
meterStore.initUdid()
|
||||||
try {
|
try {
|
||||||
loginView.value = true
|
loginView.value = true
|
||||||
const res = await getInfo("self");
|
const res = await getInfo("self");
|
||||||
@@ -125,8 +157,8 @@ onMounted(async () => {
|
|||||||
// query: { room_uid: 'nxst-ok4j' }
|
// query: { room_uid: 'nxst-ok4j' }
|
||||||
// })
|
// })
|
||||||
router.push({
|
router.push({
|
||||||
path: '/coordinate',
|
path: '/coordinate',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
loginView.value = false
|
loginView.value = false
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user