feat:跟新密码加密 共享白板功能
This commit is contained in:
6
package-lock.json
generated
6
package-lock.json
generated
@@ -14,6 +14,7 @@
|
||||
"autoprefixer": "^10.4.21",
|
||||
"axios": "^0.27.2",
|
||||
"code-inspector-plugin": "^0.20.12",
|
||||
"crypto-js": "^4.2.0",
|
||||
"element-plus": "^2.2.27",
|
||||
"js-cookie": "^3.0.1",
|
||||
"livekit-client": "^2.7.5",
|
||||
@@ -1597,6 +1598,11 @@
|
||||
"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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"autoprefixer": "^10.4.21",
|
||||
"axios": "^0.27.2",
|
||||
"code-inspector-plugin": "^0.20.12",
|
||||
"crypto-js": "^4.2.0",
|
||||
"element-plus": "^2.2.27",
|
||||
"js-cookie": "^3.0.1",
|
||||
"livekit-client": "^2.7.5",
|
||||
|
||||
@@ -27,14 +27,17 @@
|
||||
|
||||
<script setup>
|
||||
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 { useMeterStore } from '@/stores/modules/meter'
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const { proxy } = getCurrentInstance()
|
||||
const redirect = ref(undefined);
|
||||
const emit = defineEmits(['loginSuccess'])
|
||||
const meterStore = useMeterStore()
|
||||
|
||||
const loginForm = ref({
|
||||
username: '',
|
||||
@@ -52,14 +55,31 @@ function handleLogin() {
|
||||
proxy.$refs.loginRef.validate((valid) => {
|
||||
if (valid) {
|
||||
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的登录方法
|
||||
userStore
|
||||
.login({
|
||||
password: loginForm.value.password,
|
||||
password: ciphertext,
|
||||
// password: loginForm.value.password,
|
||||
username: loginForm.value.username,
|
||||
})
|
||||
.then(async (res) => {
|
||||
const userInfo = JSON.parse(localStorage.getItem('userData'))
|
||||
const userInfo = JSON.parse(sessionStorage.getItem('userData'))
|
||||
emit('loginSuccess', userInfo)
|
||||
})
|
||||
.catch((e) => {
|
||||
@@ -71,6 +91,16 @@ function handleLogin() {
|
||||
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 请求浏览器的通知权限
|
||||
* @returns {*}
|
||||
@@ -83,6 +113,10 @@ function requestNotificationPermission() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
meterStore.initUdid()
|
||||
})
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.wrapper-content {
|
||||
|
||||
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;
|
||||
@@ -2,6 +2,7 @@
|
||||
import { login, logout, getInfo } from '@/api/login'
|
||||
import { getToken, setToken, removeToken } from '@/utils/auth'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const useUserStore = defineStore(
|
||||
'user',
|
||||
@@ -20,8 +21,12 @@ const useUserStore = defineStore(
|
||||
const trimmedUsername = username.trim();
|
||||
|
||||
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;
|
||||
localStorage.setItem('userData', JSON.stringify(user));
|
||||
sessionStorage.setItem('userData', JSON.stringify(user));
|
||||
setToken(token);
|
||||
this.token = token;
|
||||
|
||||
@@ -34,7 +39,7 @@ const useUserStore = defineStore(
|
||||
getInfo() {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const userData = localStorage.getItem('userData');
|
||||
const userData = sessionStorage.getItem('userData');
|
||||
|
||||
if (!userData) {
|
||||
return reject(new Error('未找到用户数据'));
|
||||
|
||||
@@ -3,8 +3,6 @@ import Cookies from "js-cookie";
|
||||
|
||||
const TokenKey = "token";
|
||||
|
||||
const ExpiresInKey = "Meta-Enterprise-Expires-In";
|
||||
|
||||
export function getToken() {
|
||||
return Cookies.get(TokenKey);
|
||||
}
|
||||
@@ -17,14 +15,3 @@ export function removeToken() {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -122,9 +122,7 @@ service.interceptors.response.use(
|
||||
case 200:
|
||||
case 201:
|
||||
return Promise.resolve(responseData);
|
||||
|
||||
case 401:
|
||||
console.log('未授权', responseData)
|
||||
return Promise.resolve(responseData);
|
||||
// return handleUnauthorized().then(() => {
|
||||
// return Promise.reject({ code: 401, message: '未授权' });
|
||||
|
||||
@@ -2,6 +2,7 @@ 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();
|
||||
@@ -11,7 +12,7 @@ let canvasInstance = null;
|
||||
|
||||
// 获取本地缓存 userData
|
||||
function getLocalUserData() {
|
||||
const dataStr = localStorage.getItem('userData');
|
||||
const dataStr = sessionStorage.getItem('userData');
|
||||
if (!dataStr) return null;
|
||||
try {
|
||||
return JSON.parse(dataStr);
|
||||
@@ -28,8 +29,7 @@ export const WhiteboardSync = {
|
||||
canvasInstance = canvas;
|
||||
|
||||
const localUser = getLocalUserData();
|
||||
const localUid = localUser?.user?.uid;
|
||||
|
||||
const localUid = localUser?.uid;
|
||||
try {
|
||||
// 先连接 MQTT
|
||||
await mqttClient.connect(meterStore.getSudid());
|
||||
@@ -44,8 +44,8 @@ export const WhiteboardSync = {
|
||||
// 订阅当前房间
|
||||
const topic = `xSynergy/ROOM/${roomUid}/whiteboard/#`;
|
||||
mqttClient.subscribe(topic, async (shapeData) => {
|
||||
// console.log(shapeData, 'shapeData++格式装换')
|
||||
const shapeDataNew = decode(shapeData);
|
||||
const shapeDataNew = JSON.parse(shapeData.toString())
|
||||
// const shapeDataNew = decode(message);
|
||||
// console.log(shapeDataNew, '格式解码')
|
||||
try {
|
||||
isRemote = true;
|
||||
@@ -55,6 +55,7 @@ export const WhiteboardSync = {
|
||||
if (res.meta.code === 200) {
|
||||
canvasInstance.addShape(res.data.shapes);
|
||||
} else {
|
||||
ElMessage.error("获取历史数据失败");
|
||||
console.error("获取历史数据失败");
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -82,6 +83,7 @@ export const WhiteboardSync = {
|
||||
try {
|
||||
await getWhiteboardShapes(shape, roomUid);
|
||||
} catch (err) {
|
||||
ElMessage.error("提交形状失败");
|
||||
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 v-if="!status" class="meeting-container">
|
||||
<!-- 视频容器 -->
|
||||
<div class="video-layout" :class="{ 'screen-sharing-active': hasActiveScreenShare }">
|
||||
<!-- 左侧共享屏幕区域 -->
|
||||
<div class="screen-share-area" v-if="hasActiveScreenShare">
|
||||
<div class="video-layout" :class="{ 'screen-sharing-active': hasActiveScreenShare || isWhiteboardActive}">
|
||||
<!-- 左侧共享屏幕/白板区域 -->
|
||||
<div class="screen-share-area" v-if="hasActiveScreenShare || isWhiteboardActive">
|
||||
<div class="screen-share-header">
|
||||
<h3>共享屏幕</h3>
|
||||
<h3>{{ isWhiteboardActive ? '共享白板' : '共享屏幕' }}</h3>
|
||||
<div class="sharing-user">
|
||||
由 {{ screenSharingUser }} 共享
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<!-- 屏幕共享视频 -->
|
||||
<video
|
||||
@@ -40,9 +50,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 右侧视频区域 -->
|
||||
<div class="participants-area" :class="{ 'with-screen-share': hasActiveScreenShare }">
|
||||
<div class="participants-area" :class="{ 'with-screen-share': hasActiveScreenShare || isWhiteboardActive }">
|
||||
<div class="participants-header">
|
||||
<h3>会议名称:测试会议名称</h3>
|
||||
<h3>会议名称:{{ roomName }}</h3>
|
||||
<h3>参会者 ({{ participantCount }})</h3>
|
||||
</div>
|
||||
<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>
|
||||
{{ microphoneEnabled ? '关闭麦克风' : '开启麦克风' }}
|
||||
</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>
|
||||
{{ 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 @click="leaveRoom" type="warning" class="control-btn leave-btn" size="large">
|
||||
<i class="el-icon-switch-button"></i>
|
||||
@@ -145,12 +171,14 @@
|
||||
|
||||
<script setup>
|
||||
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 { Room, RoomEvent, ParticipantEvent, Track } from "livekit-client";
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRoomStore } from '@/stores/modules/room.js'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import tabulaRase from '@/views/custom/tabulaRase/index.vue'
|
||||
import { mqttClient } from "@/utils/mqtt.js";
|
||||
|
||||
const userStore = useUserStore()
|
||||
const roomStore = useRoomStore()
|
||||
@@ -162,6 +190,8 @@ const wsURL = "wss://meeting.cnsdt.com:443";
|
||||
|
||||
const hostUid = ref('')
|
||||
const status = ref(true);
|
||||
const roomName = ref('')
|
||||
const roomId = ref('')
|
||||
|
||||
// 音视频相关引用和数据
|
||||
const localVideo = ref(null);
|
||||
@@ -181,6 +211,20 @@ const screenSharingUser = ref('');
|
||||
const activeScreenShareTrack = 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(() => {
|
||||
return remoteParticipants.value.size + 1; // 包括自己
|
||||
@@ -194,6 +238,18 @@ const hasActiveScreenShare = computed(() => {
|
||||
return screenSharingUser.value !== '' || isScreenSharing.value;
|
||||
});
|
||||
|
||||
// 添加一个计算属性来显示屏幕共享状态提示
|
||||
const screenShareStatusText = computed(() => {
|
||||
if (isGlobalScreenSharing.value) {
|
||||
if (globalScreenSharingUser.value === hostUid.value) {
|
||||
return '您正在共享屏幕';
|
||||
} else {
|
||||
return `${globalScreenSharingUser.value} 正在共享屏幕`;
|
||||
}
|
||||
}
|
||||
return '暂无屏幕共享';
|
||||
});
|
||||
|
||||
// 创建 Room 实例
|
||||
const room = new Room({
|
||||
adaptiveStream: true,
|
||||
@@ -220,11 +276,209 @@ const room = new Room({
|
||||
audioEncoding: {
|
||||
maxBitrate: 64000, // 64kbps for audio
|
||||
},
|
||||
dtx: true, // 不连续传输,节省带宽
|
||||
// dtx: 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) {
|
||||
if (!el) return;
|
||||
@@ -286,7 +540,7 @@ function setupRoomListeners() {
|
||||
.on(RoomEvent.ParticipantConnected, handleParticipantConnected)
|
||||
.on(RoomEvent.ParticipantDisconnected, handleParticipantDisconnected)
|
||||
.on(RoomEvent.LocalTrackPublished, handleLocalTrackPublished)
|
||||
.on(RoomEvent.LocalTrackUnpublished, handleLocalTrackUnpublished)
|
||||
.on(RoomEvent.LocalTrackUnpublished, handleLocalTrackUnpublished)//
|
||||
.on(RoomEvent.TrackMuted, handleTrackMuted)
|
||||
.on(RoomEvent.TrackUnmuted, handleTrackUnmuted)
|
||||
.on(RoomEvent.ActiveSpeakersChanged, handleActiveSpeakersChanged)
|
||||
@@ -296,11 +550,9 @@ function setupRoomListeners() {
|
||||
|
||||
// 事件处理函数
|
||||
async function handleConnected() {
|
||||
console.log("成功连接到房间:", room.name);
|
||||
await initMqtt();
|
||||
status.value = false;
|
||||
|
||||
ElMessage.success('已成功连接到房间');
|
||||
|
||||
// 初始化现有远程参与者
|
||||
room.remoteParticipants.forEach(participant => {
|
||||
addRemoteParticipant(participant);
|
||||
@@ -310,16 +562,16 @@ async function handleConnected() {
|
||||
});
|
||||
|
||||
// 自动开启麦克风
|
||||
try {
|
||||
await enableMicrophone();
|
||||
ElMessage.success('麦克风已自动开启');
|
||||
} catch (error) {
|
||||
console.warn('自动开启麦克风失败:', error);
|
||||
}
|
||||
// try {
|
||||
// await enableMicrophone();
|
||||
// ElMessage.success('麦克风已自动开启');
|
||||
// } catch (error) {
|
||||
// console.warn('自动开启麦克风失败:', error);
|
||||
// }
|
||||
}
|
||||
|
||||
function handleDisconnected(reason) {
|
||||
console.log("断开连接:", reason);
|
||||
|
||||
status.value = true;
|
||||
cameraEnabled.value = false;
|
||||
microphoneEnabled.value = false;
|
||||
@@ -334,13 +586,11 @@ function handleDisconnected(reason) {
|
||||
}
|
||||
|
||||
function handleReconnected() {
|
||||
console.log("已重新连接");
|
||||
ElMessage.success('已重新连接到房间');
|
||||
}
|
||||
|
||||
// 处理轨道订阅事件
|
||||
function handleTrackSubscribed(track, publication, participant) {
|
||||
console.log("轨道已订阅:", track.kind, "轨道来源:", publication.source, "来自:", participant.identity);
|
||||
|
||||
if (track) {
|
||||
if (track.kind === Track.Kind.Video) {
|
||||
@@ -350,6 +600,9 @@ function handleTrackSubscribed(track, publication, participant) {
|
||||
// 如果是屏幕共享,更新屏幕共享状态
|
||||
if (publication.source === Track.Source.ScreenShare) {
|
||||
updateScreenShareState(participant, track);
|
||||
// 设置全局屏幕共享用户
|
||||
globalScreenSharingUser.value = participant.identity;
|
||||
isGlobalScreenSharing.value = true;
|
||||
}
|
||||
|
||||
// 立即附加到视频元素(如果元素已存在)
|
||||
@@ -365,6 +618,7 @@ function handleTrackSubscribed(track, publication, participant) {
|
||||
updateParticipantTracks(participant);
|
||||
}
|
||||
|
||||
|
||||
function handleTrackUnsubscribed(track, publication, participant) {
|
||||
// 移除对应的轨道信息
|
||||
if (track.kind === Track.Kind.Video) {
|
||||
@@ -375,6 +629,11 @@ function handleTrackUnsubscribed(track, publication, participant) {
|
||||
// 如果是屏幕共享,更新屏幕共享状态
|
||||
if (publication.source === Track.Source.ScreenShare) {
|
||||
updateScreenShareState(participant, null);
|
||||
// 清除全局屏幕共享状态
|
||||
if (globalScreenSharingUser.value === participant.identity) {
|
||||
globalScreenSharingUser.value = '';
|
||||
isGlobalScreenSharing.value = false;
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
ElMessage.info(`用户离开: ${participant.identity}`);
|
||||
}
|
||||
@@ -415,6 +680,11 @@ function handleLocalTrackPublished(publication) {
|
||||
// 本地用户开始屏幕共享
|
||||
screenSharingUser.value = room.localParticipant.identity;
|
||||
activeScreenShareTrack.value = publication.track;
|
||||
|
||||
// 设置全局屏幕共享状态
|
||||
globalScreenSharingUser.value = room.localParticipant.identity;
|
||||
isGlobalScreenSharing.value = true;
|
||||
|
||||
if (screenShareVideo.value) {
|
||||
attachTrackToVideo(screenShareVideo.value, publication.track);
|
||||
}
|
||||
@@ -434,8 +704,14 @@ function handleLocalTrackUnpublished(publication) {
|
||||
} else if (publication.source === Track.Source.ScreenShare) {
|
||||
// 本地用户停止屏幕共享
|
||||
if (screenSharingUser.value === room.localParticipant.identity) {
|
||||
isScreenSharing.value = false;
|
||||
screenSharingUser.value = '';
|
||||
activeScreenShareTrack.value = null;
|
||||
// 清除全局屏幕共享状态
|
||||
if (globalScreenSharingUser.value === room.localParticipant.identity) {
|
||||
globalScreenSharingUser.value = '';
|
||||
isGlobalScreenSharing.value = false;
|
||||
}
|
||||
if (screenShareVideo.value && screenShareVideo.value.srcObject) {
|
||||
screenShareVideo.value.srcObject = null;
|
||||
}
|
||||
@@ -490,7 +766,6 @@ function handleActiveSpeakersChanged(speakers) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 处理数据接收事件
|
||||
function handleDataReceived(payload, participant, kind) {
|
||||
try {
|
||||
@@ -505,7 +780,6 @@ function handleDataReceived(payload, participant, kind) {
|
||||
function handleConnectionStateChanged(state) {
|
||||
console.log('连接状态改变:', state);
|
||||
}
|
||||
|
||||
// 更新屏幕共享状态
|
||||
function updateScreenShareState(participant, track) {
|
||||
if (track) {
|
||||
@@ -528,7 +802,6 @@ function updateScreenShareState(participant, track) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 参与者管理函数
|
||||
function addRemoteParticipant(participant) {
|
||||
if (!participant || participant.identity === room.localParticipant?.identity) {
|
||||
@@ -561,7 +834,6 @@ function removeRemoteParticipant(participant) {
|
||||
audioElementsMap.value.delete(identity);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新参与者轨道信息
|
||||
function updateParticipantTrack(participant, source, track) {
|
||||
const data = remoteParticipants.value.get(participant.identity);
|
||||
@@ -605,7 +877,6 @@ function removeParticipantAudioTrack(participant) {
|
||||
data.audioEnabled = false;
|
||||
remoteParticipants.value.set(participant.identity, { ...data });
|
||||
}
|
||||
|
||||
// 附加轨道到视频元素
|
||||
function attachTrackToVideo(videoElement, track) {
|
||||
if (!videoElement || !track) return;
|
||||
@@ -618,7 +889,6 @@ function attachTrackToVideo(videoElement, track) {
|
||||
console.error('附加轨道到视频元素失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 附加轨道到音频元素
|
||||
function attachTrackToAudio(audioElement, track) {
|
||||
if (!audioElement || !track) return;
|
||||
@@ -634,7 +904,6 @@ function attachTrackToAudio(audioElement, track) {
|
||||
console.error('附加轨道到音频元素失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 附加轨道到参与者的视频元素
|
||||
function attachTrackToParticipantVideo(identity, source, track) {
|
||||
const videoElements = videoElementsMap.value.get(identity);
|
||||
@@ -645,7 +914,6 @@ function attachTrackToParticipantVideo(identity, source, track) {
|
||||
attachTrackToVideo(videoElement, track);
|
||||
}
|
||||
}
|
||||
|
||||
// 附加轨道到参与者的音频元素
|
||||
function attachTrackToParticipantAudio(identity, track) {
|
||||
const audioElement = audioElementsMap.value.get(identity);
|
||||
@@ -653,7 +921,6 @@ function attachTrackToParticipantAudio(identity, track) {
|
||||
attachTrackToAudio(audioElement, track);
|
||||
}
|
||||
}
|
||||
|
||||
// 从参与者的视频元素分离轨道
|
||||
function detachTrackFromParticipantVideo(identity, source) {
|
||||
const videoElements = videoElementsMap.value.get(identity);
|
||||
@@ -664,7 +931,6 @@ function detachTrackFromParticipantVideo(identity, source) {
|
||||
videoElement.srcObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 从参与者的音频元素分离轨道
|
||||
function detachTrackFromParticipantAudio(identity) {
|
||||
const audioElement = audioElementsMap.value.get(identity);
|
||||
@@ -731,7 +997,6 @@ function handleVideoLoaded(identity, type) {
|
||||
function handleScreenShareLoaded() {
|
||||
console.log('屏幕共享视频加载完成');
|
||||
}
|
||||
|
||||
// 视频轨道处理函数
|
||||
function attachLocalVideoTrack(track) {
|
||||
if (localVideo.value && track) {
|
||||
@@ -745,7 +1010,6 @@ function attachLocalVideoTrack(track) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 媒体控制函数
|
||||
async function enableCamera() {
|
||||
try {
|
||||
@@ -799,8 +1063,7 @@ async function toggleCamera() {
|
||||
}, 200);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('切换摄像头失败:', error);
|
||||
ElMessage.error('切换摄像头失败: ' + error.message);
|
||||
errorHandling(error,'摄像头');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -839,6 +1102,14 @@ async function toggleMicrophone() {
|
||||
if (microphoneEnabled.value) {
|
||||
await room.localParticipant.setMicrophoneEnabled(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('麦克风已关闭');
|
||||
} else {
|
||||
// 确保有音频输入设备权限
|
||||
@@ -864,32 +1135,80 @@ async function toggleMicrophone() {
|
||||
}, 500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('切换麦克风失败:', error);
|
||||
ElMessage.error('切换麦克风失败: ' + error.message);
|
||||
errorHandling(error,'麦克风');
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
try {
|
||||
if(isWhiteboardActive.value){
|
||||
ElMessage.error('请先关闭白板');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已经有其他用户在共享屏幕
|
||||
if (!isScreenSharing.value && isGlobalScreenSharing.value && globalScreenSharingUser.value !== hostUid.value) {
|
||||
ElMessage.error(`当前 ${globalScreenSharingUser.value} 正在共享屏幕,请等待其结束后再共享`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isScreenSharing.value) {
|
||||
await room.localParticipant.setScreenShareEnabled(false);
|
||||
isScreenSharing.value = false;
|
||||
// 清除全局屏幕共享状态
|
||||
if (globalScreenSharingUser.value === hostUid.value) {
|
||||
globalScreenSharingUser.value = '';
|
||||
isGlobalScreenSharing.value = false;
|
||||
}
|
||||
ElMessage.info('屏幕共享已停止');
|
||||
} else {
|
||||
await room.localParticipant.setScreenShareEnabled(true);
|
||||
isScreenSharing.value = true;
|
||||
// 设置全局屏幕共享状态
|
||||
globalScreenSharingUser.value = hostUid.value;
|
||||
isGlobalScreenSharing.value = true;
|
||||
ElMessage.success('屏幕共享已开始');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('切换屏幕共享失败:', error);
|
||||
ElMessage.error('切换屏幕共享失败: ' + error.message);
|
||||
errorHandling(error,'屏幕共享');
|
||||
}
|
||||
}
|
||||
|
||||
function handleScreenShareEnded() {
|
||||
console.log('用户通过浏览器控件停止了屏幕共享');
|
||||
isScreenSharing.value = false;
|
||||
ElMessage.info('屏幕共享已停止');
|
||||
|
||||
// 移除事件监听器
|
||||
room.localParticipant.off('screenShareEnded', handleScreenShareEnded);
|
||||
}
|
||||
|
||||
async function joinRoomBtn() {
|
||||
try {
|
||||
const res = await getRoomToken({max_participants: 20});
|
||||
if(res.meta.code != 200){
|
||||
ElMessage.error(res.meta.message);
|
||||
return;
|
||||
}
|
||||
const token = res.data.access_token;
|
||||
roomName.value = res.data.room.name
|
||||
if (!token) {
|
||||
throw new Error('获取 token 失败');
|
||||
}
|
||||
@@ -906,8 +1225,23 @@ async function joinRoomBtn() {
|
||||
// 离开会议函数
|
||||
async function leaveRoom() {
|
||||
try {
|
||||
// 如果白板正在运行,先退出白板
|
||||
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)
|
||||
console.log(res,'离开房间成功')
|
||||
// 停止屏幕共享(如果正在共享)
|
||||
if (isScreenSharing.value) {
|
||||
await room.localParticipant.setScreenShareEnabled(false);
|
||||
@@ -930,13 +1264,16 @@ async function leaveRoom() {
|
||||
|
||||
} catch (error) {
|
||||
console.error('离开会议失败:', error);
|
||||
ElMessage.error('离开会议失败: ' + error.message);
|
||||
ElMessage.error('离开会议失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 重置房间状态函数
|
||||
function resetRoomState() {
|
||||
// 重置本地状态
|
||||
globalScreenSharingUser.value = '';
|
||||
isGlobalScreenSharing.value = false;
|
||||
isWhiteboardActive.value = false;
|
||||
cameraEnabled.value = false;
|
||||
microphoneEnabled.value = false;
|
||||
isScreenSharing.value = false;
|
||||
@@ -944,6 +1281,9 @@ function resetRoomState() {
|
||||
status.value = true;
|
||||
hostUid.value = '';
|
||||
|
||||
// 断开MQTT连接
|
||||
mqttClient.disconnect();
|
||||
|
||||
// 重置远程参与者
|
||||
remoteParticipants.value.clear();
|
||||
videoElementsMap.value.clear();
|
||||
@@ -992,6 +1332,11 @@ function resetRoomState() {
|
||||
|
||||
// 在组件卸载时也清理资源
|
||||
onUnmounted(() => {
|
||||
if (isWhiteboardActive.value && whiteboardRef.value && whiteboardRef.value.cleanup) {
|
||||
whiteboardRef.value.cleanup();
|
||||
}
|
||||
// 清理MQTT
|
||||
mqttClient.disconnect();
|
||||
if (room && room.state === 'connected') {
|
||||
leaveRoom();
|
||||
}
|
||||
@@ -999,14 +1344,14 @@ onUnmounted(() => {
|
||||
|
||||
onMounted(async () => {
|
||||
// 确保在连接前请求音频权限
|
||||
try {
|
||||
// 预请求音频权限
|
||||
await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
console.log('音频权限已获取');
|
||||
} catch (error) {
|
||||
console.warn('音频权限获取失败:', error);
|
||||
ElMessage.warning('请允许麦克风权限以使用音频功能');
|
||||
}
|
||||
// try {
|
||||
// // 预请求音频权限
|
||||
// await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
// console.log('音频权限已获取');
|
||||
// } catch (error) {
|
||||
// console.warn('音频权限获取失败:', error);
|
||||
// ElMessage.warning('请允许麦克风权限以使用音频功能');
|
||||
// }
|
||||
|
||||
if(route.query.type == '1'){
|
||||
await joinRoomBtn()
|
||||
@@ -1014,23 +1359,47 @@ onMounted(async () => {
|
||||
// 邀请用户参与房间
|
||||
await getInvite(room.name,{user_uids:[roomStore.detailUid], participant_role: "participant"})
|
||||
} else {
|
||||
const userInfo = await userStore.getInfo()
|
||||
hostUid.value = userInfo.uid
|
||||
// 确保 hostUid 更新到模板
|
||||
await nextTick();
|
||||
// const userInfo = await userStore.getInfo()
|
||||
// hostUid.value = userInfo.uid
|
||||
const res = await getTokenApi(route.query.room_uid)
|
||||
if(res.meta.code == 200){
|
||||
const token = res.data.access_token;
|
||||
hostUid.value = res.data.user_uid
|
||||
await nextTick();
|
||||
setupRoomListeners();
|
||||
await room.connect(wsURL, token, {
|
||||
autoSubscribe: true,
|
||||
});
|
||||
}else{
|
||||
ElMessage.error(res.meta.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<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 {
|
||||
margin-left: 8px;
|
||||
@@ -1490,6 +1859,9 @@ body {
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.whiteboard-container {
|
||||
height: 300px; /* 在移动端限制白板高度 */
|
||||
}
|
||||
.form-container {
|
||||
margin: 20px auto;
|
||||
padding: 24px;
|
||||
|
||||
@@ -305,9 +305,9 @@ const getList = async () => {
|
||||
* 通讯录 人员信息树
|
||||
*/
|
||||
const HandleLoadNode = async (node, resolve) => {
|
||||
if(node.level === 0){
|
||||
if(node?.level === 0){
|
||||
loadNode(resolve)
|
||||
}else if(node.level === 1){
|
||||
}else if(node?.level === 1){
|
||||
loadNode(resolve,node.data.directory_uid)
|
||||
}
|
||||
}
|
||||
@@ -317,7 +317,13 @@ const loadNode = async(resolve,id)=>{
|
||||
// state.leftListLoading = true
|
||||
if(!id){
|
||||
let res = await getDirectories({level:1})
|
||||
resolve(res.data)
|
||||
if(res.meta.code == 401){
|
||||
emit('showLogin', true)
|
||||
return
|
||||
}else{
|
||||
resolve(res.data)
|
||||
}
|
||||
|
||||
}else{
|
||||
let res = await getDirectoriesUsers(id,{directory_uuid:id})
|
||||
resolve(res.data)
|
||||
@@ -383,7 +389,7 @@ watch(
|
||||
|
||||
if (newValue == 1) {
|
||||
state.isShow = false
|
||||
getList()
|
||||
// getList()
|
||||
} else {
|
||||
HandleLoadNode()
|
||||
}
|
||||
|
||||
@@ -1,266 +1,273 @@
|
||||
<template>
|
||||
<div class="app-container" v-loading="load" :element-loading-text="loadText">
|
||||
<el-row :gutter="6" style="padding: 0 10px; background: #fefefe">
|
||||
<el-col :xs="24" :sm="24" :md="8" :lg="6">
|
||||
<leftTab
|
||||
@updateDetail="updateDetail"
|
||||
@updateTab="updateTab"
|
||||
:loading="!detail?.appId && !detail?.userId && isShow"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="24" :md="16" :lg="18">
|
||||
<div
|
||||
class="right-content"
|
||||
|
||||
>
|
||||
<!-- v-loading="!detail?.appId && !detail?.userId && isShow" -->
|
||||
<div class="right-content-title">
|
||||
{{ tabValue == 1 ? '协作信息' : '员工信息' }}
|
||||
</div>
|
||||
<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 v-if="showLogin" style="height:100%">
|
||||
<!-- 登录界面 -->
|
||||
<Login @loginSuccess="handleLoginSuccess" />
|
||||
</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>
|
||||
|
||||
<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 { getInfo } from '@/api/login.js'
|
||||
import { getStatusApi } from '@/api/conferencingRoom.js'
|
||||
@@ -269,6 +276,8 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRoomStore } from '@/stores/modules/room'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { mqttClient } from "@/utils/mqtt.js";
|
||||
import { getToken } from '@/utils/auth.js'
|
||||
import Login from "@/components/Login/index.vue";
|
||||
const roomStore = useRoomStore()
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
@@ -285,6 +294,17 @@ const state = reactive({
|
||||
inviteDialog: false,
|
||||
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) => {
|
||||
return !obj || Object.keys(obj).length === 0
|
||||
@@ -294,7 +314,7 @@ const isEmptyObject = (obj) => {
|
||||
const clickInitiate = () => {
|
||||
let userData = null
|
||||
try {
|
||||
userData = JSON.parse(localStorage.getItem('userData')) || null
|
||||
userData = JSON.parse(sessionStorage.getItem('userData')) || null
|
||||
} catch (e) {
|
||||
console.error('解析 userData 失败:', e)
|
||||
}
|
||||
@@ -404,7 +424,6 @@ const clickRefuseJoin = async () => {
|
||||
const processingSocket = (message) => {
|
||||
if (message) {
|
||||
state.socketInformation = JSON.parse(message)
|
||||
console.log(state.socketInformation,'state.socketInformation')
|
||||
state.inviteDialog = true
|
||||
showNotification(state.socketInformation)
|
||||
}
|
||||
@@ -430,6 +449,11 @@ const showNotification = (data) => {
|
||||
// 暴露给模板
|
||||
const { detail, weekName, tabValue, isShow, load, loadText, isLinkKnow, socketInformation, inviteDialog, cooperation } = toRefs(state)
|
||||
onMounted(async () => {
|
||||
if(!getToken()){
|
||||
showLogin.value = true;
|
||||
}else{
|
||||
showLogin.value = false;
|
||||
}
|
||||
await mqttClient.connect(`room${Math.random().toString(16).substr(2, 8)}`);
|
||||
const res = await userStore.getInfo()
|
||||
const topic = `xSynergy/ROOM/+/rooms/${res.uid}`;
|
||||
|
||||
@@ -27,7 +27,6 @@ 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";
|
||||
@@ -35,6 +34,17 @@ import Login from "@/components/Login/index.vue";
|
||||
import Canvas from "@/core/index.js";
|
||||
import { getInfo } from "@/api/login";
|
||||
|
||||
const props = defineProps({
|
||||
roomId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
userId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const showLogin = ref(false); // 是否显示登录页面
|
||||
const hasJoined = ref(false); // 是否加入白板
|
||||
const canvas = ref(null);
|
||||
@@ -80,47 +90,21 @@ function initWhiteboard() {
|
||||
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";
|
||||
|
||||
const roomUid = route.query.room_uid || props.roomId || "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");
|
||||
console.log(res, "用户信息校验")
|
||||
if (res.meta.code === 401) {
|
||||
showLogin.value = true;
|
||||
} else {
|
||||
@@ -143,8 +127,8 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
}
|
||||
@@ -159,9 +143,7 @@ onUnmounted(() => {
|
||||
/* 白板容器 */
|
||||
.whiteboard-wrapper {
|
||||
position: relative;
|
||||
width: 90vw;
|
||||
max-width: 1280px;
|
||||
aspect-ratio: 16 / 9;
|
||||
width: 72vw;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
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,8 +40,10 @@
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { watch, ref, getCurrentInstance, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElNotification } from 'element-plus'
|
||||
import { ElNotification,ElMessage } from 'element-plus'
|
||||
import { getInfo } from "@/api/login";
|
||||
import CryptoJS from 'crypto-js';
|
||||
import { useMeterStore } from '@/stores/modules/meter'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
@@ -49,6 +51,9 @@ const route = useRoute()
|
||||
const { proxy } = getCurrentInstance()
|
||||
const showLogin = ref(false)
|
||||
|
||||
const meterStore = useMeterStore()
|
||||
|
||||
|
||||
const redirect = ref(undefined);
|
||||
const loginView = ref(false)
|
||||
|
||||
@@ -73,14 +78,30 @@ function handleLogin() {
|
||||
proxy.$refs.loginRef.validate((valid) => {
|
||||
if (valid) {
|
||||
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的登录方法
|
||||
userStore
|
||||
.login({
|
||||
password: loginForm.value.password,
|
||||
// password: loginForm.value.password,
|
||||
password: ciphertext,
|
||||
username: loginForm.value.username,
|
||||
})
|
||||
.then(async (res) => {
|
||||
const userInfo = JSON.parse(localStorage.getItem('userData'))
|
||||
const userInfo = JSON.parse(sessionStorage.getItem('userData'))
|
||||
router.push({
|
||||
path: '/coordinate',
|
||||
})
|
||||
@@ -99,6 +120,16 @@ function handleLogin() {
|
||||
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 请求浏览器的通知权限
|
||||
* @returns {*}
|
||||
@@ -113,6 +144,7 @@ function requestNotificationPermission() {
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
meterStore.initUdid()
|
||||
try {
|
||||
loginView.value = true
|
||||
const res = await getInfo("self");
|
||||
@@ -125,8 +157,8 @@ onMounted(async () => {
|
||||
// query: { room_uid: 'nxst-ok4j' }
|
||||
// })
|
||||
router.push({
|
||||
path: '/coordinate',
|
||||
})
|
||||
path: '/coordinate',
|
||||
})
|
||||
}
|
||||
loginView.value = false
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user