yangjie #49

Merged
yangqiang merged 71 commits from yangjie into master 2025-03-10 14:21:26 +08:00
50 changed files with 1085 additions and 442 deletions

View File

@ -1,6 +0,0 @@
#基础API 绝对的
VITE_BASE_URL_API = 'http://192.168.2.9:5192'
#当前IP 相对的
VITE_BASE_CURRENT_API = '.'
#开发环境
VITE_ENV = 'development'

View File

@ -1,6 +0,0 @@
#基础API 绝对的
VITE_BASE_URL_API = 'https://meeting-api.23544.com/pc'
#当前IP 相对的
VITE_BASE_CURRENT_API = '.'
#生产环境
VITE_ENV = 'production'

View File

@ -1,6 +0,0 @@
#基础API 绝对的
VITE_BASE_URL_API = 'https://meeting-api.23544.com/pc'
#当前IP 相对的
VITE_BASE_CURRENT_API = '.'
#测试环境
VITE_ENV = 'xy'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

16
build/install.nsh Normal file
View File

@ -0,0 +1,16 @@
!macro customFinishPage
AutoCloseWindow true
Function StartApp
${if} ${isUpdated}
StrCpy $1 "--updated"
${else}
StrCpy $1 ""
${endif}
${StdUtils.ExecShellAsUser} $0 "$launchLink" "open" "$1"
FunctionEnd
Function .onInstSuccess
Call StartApp
FunctionEnd
!macroend

57
config/development.json Normal file
View File

@ -0,0 +1,57 @@
{
"appId": "agora.io.ElectronApiExample",
"asar": true,
"asarUnpack": [
"node_modules/agora-electron-sdk"
],
"buildDependenciesFromSource": true,
"compression": "normal",
"productName": "智汇享",
"publish": [
{
"provider": "generic",
"url": "http://192.168.2.9:8827"
}
],
"files": [
"!*.log"
],
"win": {
"icon": "build/start.ico",
"requestedExecutionLevel": "highestAvailable",
"target": [
{
"target": "nsis",
"arch": [
"ia32"
]
}
]
},
"directories": {
"output": "electron"
},
"extraResources": [
{
"from": "src/assets/virtualBackground",
"to": "images",
"filter": [
"**/*"
]
}
],
"nsis": {
"oneClick": false,
"installerIcon": "build/start.ico",
"uninstallerIcon": "build/start.ico",
"installerHeaderIcon": "build/start.ico",
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"deleteAppDataOnUninstall": true,
"shortcutName": "智汇享",
"allowElevation": true,
"perMachine": true,
"include": "build/install.nsh"
}
}

View File

@ -42,15 +42,16 @@
],
"nsis": {
"oneClick": false,
"installerIcon": "build/install.ico",
"uninstallerIcon": "build/install.ico",
"installerHeaderIcon": "build/install.ico",
"installerIcon": "build/start.ico",
"uninstallerIcon": "build/start.ico",
"installerHeaderIcon": "build/start.ico",
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"deleteAppDataOnUninstall": true,
"shortcutName": "智汇享",
"allowElevation": true,
"perMachine": true
"perMachine": true,
"include": "build/install.nsh"
}
}

View File

@ -51,6 +51,7 @@
"deleteAppDataOnUninstall": true,
"shortcutName": "湖北襄阳四中教研平台",
"allowElevation": true,
"perMachine": true
"perMachine": true,
"include": "build/install.nsh"
}
}

111
main.js
View File

@ -11,6 +11,7 @@ const {
crashReporter,
desktopCapturer,
powerSaveBlocker,
net
} = require('electron');
const path = require('node:path')
const updateJs = require('./src/utils/package/update')
@ -18,16 +19,15 @@ const fs = require('fs');
const Registry = require('winreg');
const { autoUpdater, CancellationToken } = require('electron-updater');
const signalR = require('@microsoft/signalr');
const { setTimeout, setInterval, clearTimeout, clearInterval } = require('timers');
const { setTimeout, setInterval } = require('timers');
const cancellationToken = new CancellationToken()
app.allowRendererProcessReuse = false;
let mainWindow = null;
let childWindow = {}
let isMaximized = false;
let env;
let env = 'development'; //development production xy
let regKey;
let connection = null;
let envStr;
let startNumber = 0;
let buildStatus = false; //true 打包开发版本 false 本地开发
powerSaveBlocker.start('prevent-display-sleep')
@ -53,16 +53,15 @@ class AppWindow extends BrowserWindow {
const finalConfig = { ...basicConfig, ...config };
super(finalConfig);
if (env === 'development') {
// 开发
if (buildStatus) {
this.loadFile(path.resolve(__dirname, './dist/index.html'));
this.loadURL('http://192.168.2.9:8827/');
} else {
this.loadURL('http://localhost:3000');
}
} else {
// 测试 | 生产
this.loadFile(path.resolve(__dirname, './dist/index.html'));
this.loadURL('https://meeting-api.23544.com/')
}
// this.loadFile(path.resolve(__dirname, './dist/index.html'))
this.once('ready-to-show', () => {
this.show();
});
@ -72,11 +71,20 @@ function quit() {
app.quit()
}
let tray;
// 检查网络状态
function checkNetworkStatus() {
if (!net.isOnline()) {
dialog.showErrorBox(`${env === 'xy' ? '湖北襄阳四中教研平台' : '智汇享'}-网络连接错误', '当前无网络连接,请检查您的网络设置。`);
app.quit();
return false;
}
return true;
}
function createTray() {
const iconPath = `${__dirname}/src/assets/${updateJs.getIcon(envStr)}.png`;
const iconPath = `${__dirname}/src/assets/${updateJs.getIcon(env)}.png`;
const trayIcon = nativeImage.createFromPath(iconPath);
tray = new Tray(trayIcon);
tray.setToolTip(updateJs.getTitle(envStr));
tray.setToolTip(updateJs.getTitle(env));
tray.on('click', () => {
mainWindow.webContents.send('isOpenWindows');
});
@ -95,6 +103,10 @@ function createWindow() {
}
const additionalData = { myKey: 'myValue' }
app.on('ready', () => {
// 检查网络状态
if (!checkNetworkStatus()) {
return;
}
// const gotTheLock = true
const gotTheLock = app.requestSingleInstanceLock(additionalData)
if (gotTheLock) {
@ -103,15 +115,14 @@ app.on('ready', () => {
uploadToServer: false,
ignoreSystemCrashHandler: false
})
env = process.argv.find((arg) => arg.startsWith('--env='))?.split('=')[1];
if (env === 'development') {
Object.defineProperty(app, 'isPackaged', {
get() {
return true
}
})
autoUpdater.updateConfigPath = path.join('latest.yml')
}
// if (!buildStatus) {
// Object.defineProperty(app, 'isPackaged', {
// get() {
// return true
// }
// })
// autoUpdater.updateConfigPath = path.join('latest.yml')
// }
createWindow()
regKey = new Registry({
hive: Registry.HKCU,
@ -149,12 +160,11 @@ app.on('ready', () => {
isMaximized = true;
}
});
ipcMain.handle('setEnv', (event, str) => {
envStr = str;
ipcMain.handle('startLoad', (event) => {
if (startNumber === 0) {
updateHandle() // 检查更新
setInterval(() => {
updateHandle() // 每一小时检查更新
autoUpdater.checkForUpdates()
}, 1000 * 60 * 60)
createTray()
startNumber++
@ -162,7 +172,7 @@ app.on('ready', () => {
});
// 更新
ipcMain.handle('updateHandle', () => {
updateHandle()
autoUpdater.checkForUpdates()
});
// socket
ipcMain.handle('startSignalr', (event, user) => {
@ -191,6 +201,8 @@ app.on('ready', () => {
connection.off('JoinChannelCallback');
connection.off('ExitSharedScreen');
connection.off('SetSpeaker');
connection.off('ReceivedOperation');
connection.off('StopedSharedScreen');
}
});
ipcMain.handle('onStop', (event) => {
@ -210,7 +222,7 @@ app.on('ready', () => {
await connection.invoke(str, data.roomNum, data.msg)
break;
case 'sendOper':
await connection.invoke(str, data.roomNum, data.type)
await connection.invoke(str, data.roomNum, data.contentString)
break;
case 'getDrivers':
// 获取某个人的设备列表
@ -236,6 +248,10 @@ app.on('ready', () => {
// 发言人设置成功
await connection.invoke(str, data)
break;
case 'sendOper2User':
// 扩展参数
await connection.invoke(str, data.uid, data.contentString)
break;
}
});
ipcMain.handle('onOtherSignalr', (event) => {
@ -266,10 +282,10 @@ app.on('ready', () => {
})
});
// 扩展操作
connection.on("Operation", (type) => {
connection.on("Operation", (contentString) => {
mainWindow.webContents.send('onSignalr', {
key: 'Operation',
type
contentString
})
});
// 移出会议
@ -411,6 +427,20 @@ app.on('ready', () => {
RoomManagerInputDTO
})
});
// 扩展参数
connection.on("ReceivedOperation", (contentString) => {
mainWindow.webContents.send('onSignalr', {
key: 'ReceivedOperation',
contentString
})
});
// 共享人取消共享屏幕
connection.on("StopedSharedScreen", (ScreenShareId) => {
mainWindow.webContents.send('onSignalr', {
key: 'StopedSharedScreen',
ScreenShareId
})
});
}
});
// 放大缩小退出窗口
@ -452,6 +482,10 @@ app.on('ready', () => {
ipcMain.handle('getVersion', () => {
return app.getVersion();
});
// 获取环境
ipcMain.handle('getEnv', () => {
return env;
});
// 获取窗口是否显示
ipcMain.handle('isVisible', () => {
return mainWindow.isVisible();
@ -487,6 +521,11 @@ app.on('ready', () => {
downloadUpdate()
} else if (data === '2') { // 下载完成 点击安装
quitAndInstall()
} else if (data === '3') { // 打开弹窗
let message = JSON.stringify({
type: '3',
})
sendUpdateMessage(message)
}
});
// 选择文件夹
@ -604,16 +643,18 @@ app.on('ready', () => {
width: config.width,
height: config.height,
})
if (envStr === 'development') {
if (env === 'development') {
// 开发
if (buildStatus) {
child.loadURL(`file://${path.join(__dirname, './dist/index.html')}#/${config.key}`);
child.loadURL(`http://192.168.2.9:8827/#/${config.key}`);
// child.loadURL(`file://${path.join(__dirname, './dist/index.html')}#/${config.key}`);
} else {
child.loadURL(config.url)
}
} else {
// 测试 | 生产
child.loadURL(`file://${path.join(__dirname, './dist/index.html')}#/${config.key}`);
child.loadURL(`https://meeting-api.23544.com/#/${config.key}`);
// child.loadURL(`file://${path.join(__dirname, './dist/index.html')}#/${config.key}`);
}
child.hookWindowMessage(278, function (e) {
child.setEnabled(false);//窗口禁用
@ -715,7 +756,6 @@ app.on('ready', () => {
});
// 检测更新在你想要检查更新的时候执行renderer事件触发后的操作自行编写
function updateHandle() {
autoUpdater.checkForUpdates()
// autoUpdater.checkForUpdatesAndNotify().catch();
const message = {
error: '检查更新出错',
@ -723,11 +763,11 @@ function updateHandle() {
updateAva: '检测到新版本,正在下载……',
updateNotAva: '已经是最新版本,不用更新'
}
autoUpdater.setFeedURL(updateJs.getUpdateUrl(envStr))
autoUpdater.setFeedURL(updateJs.getUpdateUrl(env))
autoUpdater.autoDownload = false // 不自动下载安装包
autoUpdater.autoInstallOnAppQuit = false // 不自动安装
autoUpdater.on('error', function (error) {
sendUpdateMessage(message.error)
sendUpdateMessage(error)
})
autoUpdater.on('checking-for-update', function () {
sendUpdateMessage(message.checking)
@ -735,9 +775,14 @@ function updateHandle() {
autoUpdater.on('update-available', function (info) {
let messageStr = JSON.stringify({ type: '0' })
sendUpdateMessage(messageStr)
mainWindow.webContents.send('changeLocalStorage', {
isUpdate: true,
});
})
autoUpdater.on('update-not-available', function (info) {
mainWindow.webContents.send('changeLocalStorage', {
isUpdate: false,
});
})
// 更新下载进度事件
autoUpdater.on('download-progress', function (progressObj) {
@ -810,7 +855,7 @@ function mainWindowCenter() {
const startSignalr = async (user) => {
connection = new signalR.HubConnectionBuilder()
.withUrl(`${envStr === 'development' ? 'http://192.168.2.9:5192' : 'https://meeting-api.23544.com/pc'}/session-manage`, {
.withUrl(`${env === 'development' ? 'http://192.168.2.9:5192' : 'https://meeting-api.23544.com/pc'}/session-manage`, {
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
accessTokenFactory: () => user.token

7
package-lock.json generated
View File

@ -1,17 +1,17 @@
{
"name": "WGShare.Metting",
"version": "0.4.8",
"version": "0.7.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "WGShare.Metting",
"version": "0.4.8",
"version": "0.7.1",
"dependencies": {
"@ant-design/icons": "^5.3.7",
"@microsoft/signalr": "^8.0.0",
"@types/node": "^20.14.9",
"agora-electron-sdk": "^4.4.0",
"agora-electron-sdk": "4.4.0",
"animate.css": "^4.1.1",
"antd": "^5.18.2",
"axios": "^1.7.2",
@ -19,6 +19,7 @@
"dayjs": "^1.11.11",
"electron-squirrel-startup": "^1.0.1",
"electron-updater": "^6.2.1",
"js-yaml": "^4.1.0",
"os": "^0.1.2",
"path": "^0.12.7",
"postcss-px-to-viewport-8-plugin": "^1.2.5",

View File

@ -1,21 +1,17 @@
{
"name": "WGShare.Metting",
"private": true,
"version": "0.7.1",
"version": "0.8.0",
"main": "main.js",
"authors": "yj",
"description": "智汇享",
"scripts": {
"dev": "concurrently \"electron . --env=development\" \"cross-env BROWSER=none vite\"",
"prod": "concurrently \"electron . --env=production\" \"cross-env BROWSER=none vite\"",
"xy": "concurrently \"electron . --env=xy\" \"cross-env BROWSER=none vite\"",
"build": "vite build --mode development",
"build:prod": "vite build --mode production",
"build:xy": "vite build --mode xy",
"dev": "concurrently \"electron .\" \"cross-env BROWSER=none vite\"",
"build": "vite build",
"preview": "vite preview",
"build:dev-win": "vite build --mode development & electron-builder -w --config=./config/build.json",
"build:prod-win": "vite build --mode production & electron-builder -w --config=./config/build.json",
"build:prod-win-xy": "vite build --mode xy & electron-builder -w --config=./config/xy.json"
"build:dev": "vite build & electron-builder -w --config=./config/development.json",
"build:prod": "vite build & electron-builder -w --config=./config/production.json",
"build:xy": "vite build & electron-builder -w --config=./config/xy.json"
},
"agora_electron": {
"platform": "win32",
@ -34,6 +30,7 @@
"dayjs": "^1.11.11",
"electron-squirrel-startup": "^1.0.1",
"electron-updater": "^6.2.1",
"js-yaml": "^4.1.0",
"os": "^0.1.2",
"path": "^0.12.7",
"postcss-px-to-viewport-8-plugin": "^1.2.5",

View File

@ -51,6 +51,10 @@ window.electron = {
getVersion: () => {
return ipcRenderer.invoke('getVersion')
},
// 获取环境
getEnv: () => {
return ipcRenderer.invoke('getEnv')
},
// 获取窗口是否显示
isVisible: () => {
return ipcRenderer.invoke('isVisible')
@ -87,9 +91,9 @@ window.electron = {
isOpenWindows: (callback) => {
ipcRenderer.on('isOpenWindows', callback)
},
// 设置环境变量
setEnv: (str) => {
ipcRenderer.invoke('setEnv', str)
// 首次加载
startLoad: () => {
ipcRenderer.invoke('startLoad')
},
// 更新
updateHandle: () => {
@ -134,7 +138,7 @@ window.electron = {
ipcRenderer.invoke('createChildWindow', {
url: location.origin + `/#/noticeWindow`,
width: 388,
height: 150,
height: 180,
key: 'noticeWindow',
})
ipcRenderer.invoke('createChildWindow', {

View File

@ -8,20 +8,21 @@ import Login from '@/page/Login/index'
import Meeting from '@/page/Meeting/index'
import NotFound from '@/page/NotFound/index'
import { storage } from '@/utils'
import { message, Spin } from "antd";
import { message, Modal, Spin } from "antd";
import JoinMeetingModal from "@/components/JoinMeetingModal";
import UpdateModal from "@/components/UpdateModal";
import * as CryptoJS from 'crypto-js';
import { PostLogin } from "@/api/Login";
import { GetCheckOnline, PostLogin } from "@/api/Login";
import { agora } from "@/utils/package/agora";
import QuitTips from "@/components/QuitTips";
import { GetLeave } from "@/api/Meeting";
import ShareScreenWindow from "@/page/Meeting/ShareScreenWindow";
import UserListWindow from "@/page/Meeting/UserListWindow";
import ChatSmallWindow from "@/page/Meeting/ChatSmallWindow";
import ChatBigWindow from "@/page/Meeting/ChatBigWindow";
import NoticeWindow from "@/page/Meeting/NoticeWindow";
import { getKeyOpenChildWindow, getTitle, setKeyOpenChildWindow, storageSeeting } from "./utils/package/public";
import { ExclamationCircleFilled } from "@ant-design/icons";
const { confirm } = Modal;
const fs = require('fs').promises;
const { exec } = require('child_process');
const App: React.FC = () => {
@ -41,20 +42,43 @@ const App: React.FC = () => {
useEffect(() => {
let userInfo = JSON.parse(storage.getItem('user') as string)
let loginInfo = JSON.parse(storage.getItem('login') as string)
window.electron.setEnv(import.meta.env.VITE_ENV);
const login = () => {
PostLogin({
account: loginInfo.account,
pwd: CryptoJS.MD5(loginInfo.password).toString(CryptoJS.enc.Hex)
}).then(async (res) => {
if (res.code === 200) {
storage.setItem('user', JSON.stringify(res.data))
storage.setItem('userLogin', true)
toSrc('/home')
await window.electron.startSignalr(res.data)
} else {
toSrc('/login')
}
})
}
if (userInfo && !userInfo.isAnonymous) {
if (loginInfo && loginInfo.isAutoLogin) {
PostLogin({
account: loginInfo.account,
pwd: CryptoJS.MD5(loginInfo.password).toString(CryptoJS.enc.Hex)
}).then(async (res) => {
if (res.code === 200) {
storage.setItem('user', JSON.stringify(res.data))
storage.setItem('userLogin', true)
toSrc('/home')
await window.electron.startSignalr(res.data)
} else {
toSrc('/login')
GetCheckOnline(loginInfo.account).then(req => {
if (req.code === 200) {
if (req.data) {
confirm({
title: '提示',
icon: <ExclamationCircleFilled />,
content: `账号已在其他地方登录,是否强制登陆?`,
centered: true,
okText: '确定',
cancelText: '取消',
async onOk() {
login()
},
onCancel() {
toSrc('/login')
}
})
} else {
login()
}
}
})
} else {
@ -79,6 +103,7 @@ const App: React.FC = () => {
};
}, []);
useEffect(() => {
window.electron.startLoad();
window.electron.downFile(async (_e: any, data: any) => {
const response = await fetch(data.filePath);
const arrayBuffer = await response.arrayBuffer();
@ -127,9 +152,7 @@ const App: React.FC = () => {
}, [])
useEffect(() => {
window.electron.onUpdate((_e: any, data: any) => {
if (location.hash.indexOf('/meeting') === -1) {
updateModalRef.current.changeModal(data)
}
updateModalRef.current.changeModal(data)
})
if (!storage.getItem('setting')) {
storage.setItem('setting', JSON.stringify(storageSeeting))
@ -164,16 +187,24 @@ const App: React.FC = () => {
if (location.href.indexOf('/login') !== -1) {
window.electron.onStop()
}
if (location.hash && location.hash.indexOf('/meeting') === -1) {
window.electron.updateHandle()
}
message.destroy('cameraTemporarily')
}, [navigate])
}
useEffect(() => {
document.addEventListener('keydown', (event) => {
if (event.key === 'F11') {
document.addEventListener('keydown', async (event) => {
if (event.keyCode == 122) {
event.preventDefault();
} else if (((event.ctrlKey && event.keyCode == 82) || event.keyCode == 116)) {
let env = await window.electron.getEnv()
if (env !== 'development') {
event.preventDefault();
}
}
});
document.getElementsByTagName('title')[0].innerText = getTitle(import.meta.env.VITE_ENV)
getTitle()
}, [])
const handleResize = (): void => {
setWindowSize({
@ -200,8 +231,10 @@ const App: React.FC = () => {
if (item.msg) {
message.error(item.msg)
}
await leaveChannel(true)
toSrc('/login')
await leaveChannel()
setTimeout(() => {
toSrc('/login')
}, 5000);
break;
}
})
@ -223,7 +256,7 @@ const App: React.FC = () => {
}
})
};
const leaveChannel = async (bool?: boolean): Promise<void> => {
const leaveChannel = async (): Promise<void> => {
if (location.hash.indexOf('/meeting') === 1) {
window.electron.closeChildWindow('shareScreenWindow')
setKeyOpenChildWindow('shareScreenWindow', false)
@ -240,11 +273,9 @@ const App: React.FC = () => {
})
})
const data = JSON.parse(localStorage.stateInfo);
if (!bool) {
await GetLeave({
roomNum: data.channelId,
})
}
await window.electron.onInvoke('levelChannel', {
roomNum: data.channelId
})
await agora.leaveChannel()
}
};
@ -260,12 +291,8 @@ const App: React.FC = () => {
storage.removeItem('user')
navigate('/login')
}
} else if (e.key === 'reconnect') {
if (e.value == true) {
if (location.hash.indexOf('/meeting') === -1) {
window.electron.updateHandle()
}
}
} else if (e.key === 'env') {
}
};
return (

View File

@ -22,4 +22,10 @@ export const PostAnonLogin = (data: any) =>
url: `/auth/anon-login`,
method: 'post',
data,
})
export const GetCheckOnline = (account: string) =>
request({
url: `/auth/check-online?account=${account}`,
method: 'get'
})

View File

@ -170,4 +170,15 @@ export const PostSharedScreen = (roomNum: string) =>
request({
url: `/room/shared-screen?roomNum=${roomNum}`,
method: 'post'
})
export const PostStopSharedScreen = (roomNum: string) =>
request({
url: `/room/stop-shared-screen?roomNum=${roomNum}`,
method: 'post'
})
export const PostHomeVerLog = (data: any) =>
request({
url: `/home/ver-log`,
method: 'post',
data
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 712 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 838 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 600 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 921 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

View File

@ -0,0 +1,48 @@
import ImageUrl from '@/utils/package/imageUrl';
import { Empty, Popover } from 'antd';
import { GetQrcode } from '@/api/Home/Index';
import { memo, useImperativeHandle, forwardRef, useState } from "react";
const Code = forwardRef((props: any, ref: any) => {
useImperativeHandle(ref, () => ({
getData: () => {
}
}))
const [baseImage, setBaseImage] = useState('');
const [roomNum, setRoomNum] = useState(props.roomNum);
return (
<>
<Popover
placement="bottom"
onOpenChange={async (e: boolean) => {
setBaseImage('')
if (e) {
let env = await window.electron.getEnv()
GetQrcode(roomNum, env === 'development' ? 'trial' : 'release').then(res => {
if (res.code === 200) {
setBaseImage(res.data)
}
})
}
}}
content={
baseImage ? <div>
<img style={{ width: '200px', margin: '0 auto' }} src={`data:image/png;base64,${baseImage}`} alt="" />
<div style={{ color: 'white', textAlign: 'center', fontSize: '16px', marginTop: '10px' }}>
<span></span><br />
<span></span>
</div>
</div> : <div>
<Empty description={'二维码加载中,请稍后'} />
</div>
}
>
<div title='小程序'>
<img src={ImageUrl.icon55} alt="" style={{ width: '16px' }} />
</div>
</Popover>
</>
)
})
export default memo(Code)

View File

@ -9,6 +9,7 @@ import Avatar from '@/components/Avatar';
import { useNavigate } from 'react-router-dom';
import { agora } from '@/utils/package/agora';
import { role } from '@/config/role';
import { PostHomeVerLog } from '@/api/Meeting';
const { setInterval, clearInterval } = require('timers');
let time = null as any;
const JoinSetting = forwardRef((_props: any, ref: any) => {
@ -201,6 +202,13 @@ const JoinSetting = forwardRef((_props: any, ref: any) => {
GetRoomInfo(roomNumber).then(async (res) => {
if (res.code === 200) {
await agora.release()
await window.electron.getVersion().then(async req => {
await PostHomeVerLog({
version: req,
platformType: 1,
roomNum: roomNumber,
})
})
navigate(`/meeting`, {
state: {
channelId: roomNumber,

View File

@ -34,6 +34,9 @@ const StupWizard = forwardRef((_props: any, ref: any) => {
}
}
storage.setItem('setting', JSON.stringify(setting))
},
getStupWizardModal: () => {
return isStupWizard
}
}))
const [list, setList] = useState([
@ -130,9 +133,11 @@ const StupWizard = forwardRef((_props: any, ref: any) => {
})
const CurrencyComponents = () => {
const [optionsValue, setOperationValue] = useState<'hide' | 'quit'>('hide');
const [voiceStimulation, setVoiceStimulation] = useState(true);
const setting = JSON.parse(storage.getItem('setting') as string)
useEffect(() => {
setOperationValue(setting.closeSetting)
setVoiceStimulation(setting.voiceStimulation)
}, []);
return (
<>
@ -151,6 +156,33 @@ const CurrencyComponents = () => {
<Radio value={'hide'}>退</Radio>
</Radio.Group>
</div>
<div>
<span> <Popover
content={
<span
style={{
color: 'white'
}}>
</span>
}
title=""
>
<QuestionCircleOutlined style={{
color: 'white',
cursor: 'pointer',
marginRight: '10px'
}} />
</Popover></span>
<Radio.Group onChange={(e: any) => {
setting.voiceStimulation = e.target.value;
storage.setItem('setting', JSON.stringify(setting))
setVoiceStimulation(e.target.value)
}} style={{ flexShrink: 0, margin: '10px 0' }} value={voiceStimulation}>
<Radio value={true}></Radio>
<Radio value={false}></Radio>
</Radio.Group>
</div>
</div>
</div>
</div>
@ -213,23 +245,25 @@ const VideoComponents = () => {
}, [darkLightEnhancement]);
useEffect(() => {
if (typeof virtualBackground.sourceIndex === 'number') {
if (import.meta.env.VITE_ENV === 'development') {
window.electron.getAppPath().then((res: string) => {
const imagePath = path.join(res, 'src', 'assets', 'virtualBackground', `${virtualBackground.sourceIndex + 1}.png`);
window.electron.getEnv().then(res=>{
if (res === 'development') {
window.electron.getAppPath().then((res: string) => {
const imagePath = path.join(res, 'src', 'assets', 'virtualBackground', `${virtualBackground.sourceIndex + 1}.png`);
agora.enableVirtualBackground(virtualBackground.isVirtualBackground, {
source: imagePath,
background_source_type: 2,
color: Number(virtualBackground.color),
})
})
} else {
const imagePath = path.join((process as any).resourcesPath, 'images', `${virtualBackground.sourceIndex + 1}.png`);
agora.enableVirtualBackground(virtualBackground.isVirtualBackground, {
source: imagePath,
background_source_type: 2,
color: Number(virtualBackground.color),
})
})
} else {
const imagePath = path.join((process as any).resourcesPath, 'images', `${virtualBackground.sourceIndex + 1}.png`);
agora.enableVirtualBackground(virtualBackground.isVirtualBackground, {
source: imagePath,
background_source_type: 2,
color: Number(virtualBackground.color),
})
}
}
})
} else {
agora.enableVirtualBackground(virtualBackground.isVirtualBackground, {
background_source_type: 1,
@ -696,7 +730,18 @@ const AudioComponents = () => {
ecordingVolume: e,
})
}} disabled={!audioDeviceManager.ecordingItem} />
{/* || audioDeviceManager.autoEcordingVolume */}
</div>
{/* <div style={{ marginBottom: '10px' }}>
<Checkbox onChange={async (e) => {
setting.autoEcordingVolume = e.target.checked;
storage.setItem('setting', JSON.stringify(setting))
setAudioDeviceManager({
...audioDeviceManager,
autoEcordingVolume: e.target.checked
})
}} checked={audioDeviceManager.autoEcordingVolume}></Checkbox>
</div> */}
<div>
<div>
<Checkbox checked={audioDeviceManager.isAINoiseReduction} onChange={(e) => {
@ -729,16 +774,6 @@ const AudioComponents = () => {
</Radio.Group>
</div>
</div>
{/* <div>
<Checkbox onChange={async (e) => {
setting.autoEcordingVolume = e.target.checked;
storage.setItem('setting', JSON.stringify(setting))
setAudioDeviceManager({
...audioDeviceManager,
autoEcordingVolume: e.target.checked
})
}} checked={audioDeviceManager.autoEcordingVolume}></Checkbox>
</div> */}
</div>
<div>
<div className={styles.audioComponentsSelect}>

View File

@ -1,45 +1,106 @@
import styles from '@/components/UpdateModal/index.module.scss'
import ImageUrl from '@/utils/package/imageUrl';
import { getUpdateUrl } from '@/utils/package/public';
import { getUpdateUrl, isVersion } from '@/utils/package/public';
import { ExclamationCircleFilled } from '@ant-design/icons';
import { Button, Flex, Modal, Progress } from 'antd';
import { forwardRef, useImperativeHandle, useState, memo } from "react";
import { forwardRef, useImperativeHandle, useState, memo, useEffect } from "react";
const { confirm } = Modal;
const UpdateModal = forwardRef((props: any, ref: any) => {
const UpdateModal = forwardRef((_props: any, ref: any) => {
useImperativeHandle(ref, () => ({
changeModal: (data: any) => {
let dataJson = JSON.parse(data)
getContent()
if (dataJson.type === '0') { // 打开弹窗
setIsUpdateModal(true)
} else if (dataJson.type === '1') { // 下载中 返回进度值
setProgress(dataJson.value.toFixed(2))
} else if (dataJson.type === '2') { // 下载完成
setProgress(100)
if (JSON.stringify(data).includes('Error') || JSON.stringify(data).includes('{}')) {
setIsError(res => {
if (res) {
setIsError(false)
confirm({
keyboard: false,
title: '提示',
icon: <ExclamationCircleFilled />,
content: `更新失败,请尝试手动更新!`,
centered: true,
okText: '立即更新',
wrapClassName: 'hideCancelText',
cancelText: '',
async onOk() {
isVersion((bool: boolean, req: any) => {
if (bool && req) {
window.electron.getEnv().then(res => {
location.href = `${getUpdateUrl(res)}/${req.path}`
})
}
})
},
})
}
return res
})
} else {
try {
let dataJson = JSON.parse(data)
if (dataJson.type === '0') { // 打开弹窗
setProgress(res => {
if (res) {
} else {
window.electron.onDownload('1')
}
return res
})
} else if (dataJson.type === '1') { // 下载中 返回进度值
if (dataJson.value === 100 && location.hash.indexOf('/meeting') === -1) {
setIsUpdateModal(true)
getContent()
}
setProgress(dataJson.value.toFixed(2))
} else if (dataJson.type === '2') { // 下载完成
setProgress(100)
if (location.hash.indexOf('/meeting') === -1) {
setIsUpdateModal(true)
getContent()
}
} else if (dataJson.type === '3') {
if (location.hash.indexOf('/meeting') === -1) {
setIsUpdateModal(true)
getContent()
}
}
} catch (error) {
}
}
}
}))
const [isUpdateModal, setIsUpdateModal] = useState(false);
const [progress, setProgress] = useState(0); // 下载进度值
const [updateContent, setUpdateContent] = useState('') // 版本更新内容
const [env, setEnv] = useState('')
const [_isError, setIsError] = useState(true)
useEffect(() => {
window.electron.getEnv().then(res => {
setEnv(res)
})
}, [])
function getContent() {
fetch(`${getUpdateUrl(import.meta.env.VITE_ENV)}/update.txt?t=${+new Date()}`) // 配置服务器地址
.then(async response => {
if (response.status === 200) {
return setUpdateContent(await response.text())
}
throw new Error('Network response was not ok.');
})
.then(textContent => {
})
.catch(error => {
});
window.electron.getEnv().then(res => {
fetch(`${getUpdateUrl(res)}/update.txt?t=${+new Date()}`) // 配置服务器地址
.then(async response => {
if (response.status === 200) {
return setUpdateContent(await response.text())
}
throw new Error('Network response was not ok.');
})
.then(_textContent => {
})
.catch(_error => {
});
})
}
function closeModal() {
if (progress != 100) {
window.electron.onDownload('0') // 取消下载
}
// if (progress != 100) {
// window.electron.onDownload('0') // 取消下载
// }
setIsUpdateModal(false)
}
@ -49,12 +110,12 @@ const UpdateModal = forwardRef((props: any, ref: any) => {
title=""
open={isUpdateModal}
footer={null}
// onCancel={() => closeModal()}
onCancel={() => closeModal()}
centered
width={'400px'}
className='modal-padding'
maskClosable={false}
closeIcon={false}
closeIcon={progress === 100 ? false : true}
>
<div className={styles.isUpdateModal} style={{ backgroundImage: `url(${ImageUrl.icon7})` }}>
<div className={styles.remarks} dangerouslySetInnerHTML={{ __html: updateContent }}>
@ -67,7 +128,7 @@ const UpdateModal = forwardRef((props: any, ref: any) => {
style={{ width: '100%', height: '40px', marginBottom: '10px' }}
className={`m-ant-btn`}
></Button>
{import.meta.env.VITE_ENV === "development" ? <div className={styles.button2} onClick={() => setIsUpdateModal(false)}></div> : null}
{env === "development" ? <div className={styles.button2} onClick={() => setIsUpdateModal(false)}></div> : null}
</div> : progress < 100 ?
<div style={{ margin: '20px 0' }}>
{progress}%

View File

@ -47,6 +47,10 @@ const UserName = forwardRef((props: any, ref: any) => {
onClick={async () => {
const stateInfo = await JSON.parse(storage.getItem('stateInfo') as string);
if (info.userName) {
if (info.userName.length > 10) {
message.error('用户名称最多10个字')
return
}
PutAlterUname({
nickName: info.userName,
roomNum: stateInfo.channelId,

View File

@ -121,14 +121,6 @@
font-size: 14px;
}
}
>div:nth-child(2) {
cursor: pointer;
>img {
width: 16px;
}
}
}
>div:nth-child(2) {

View File

@ -2,7 +2,7 @@ import styles from '@/page/Home/Index/index.module.scss'
import { useEffect, useState, useRef } from "react";
import Operation from '@/components/Operation';
import { Button, Input, Modal, Pagination, Empty, message, Popover, Popconfirm, DatePicker, Select, Radio } from "antd";
import { GetRoom, PostRoom, GetCheckoutRoomNum, GetRoomRtcToken, DeleteRoom, GetRecord, PostRoomInfo, GetQrcode } from '@/api/Home/Index';
import { GetRoom, PostRoom, GetCheckoutRoomNum, GetRoomRtcToken, DeleteRoom, GetRecord, PostRoomInfo } from '@/api/Home/Index';
import ImageUrl from '@/utils/package/imageUrl'
import { ExclamationCircleFilled, ReloadOutlined } from '@ant-design/icons';
import JoinSetting from '@/components/JoinSetting';
@ -14,6 +14,10 @@ import dayjs from 'dayjs';
import StupWizard from '@/components/StupWizard';
import { GetSubDpList } from '@/api/Home/User';
import FeedBackModel from '@/components/FeedBackModel';
import { PostHomeVerLog } from '@/api/Meeting';
import Code from '@/components/Code';
import { isVersion } from "@/utils/package/public";
const { setInterval, clearInterval } = require('timers');
const fs = require('fs').promises;
const { exec } = require('child_process');
@ -46,7 +50,6 @@ const Index: React.FC = () => {
const [timeData, setTimeData] = useState<any>([]);
const [isCreateRoom, setIsCreateRoom] = useState<boolean>(false);
const [allowAnonymous, setAllowAnonymous] = useState(true);
const [baseImage, setBaseImage] = useState('');
const userInfo = JSON.parse(storage.getItem('user') as string)
useEffect(() => {
setUser(userInfo)
@ -245,35 +248,7 @@ const Index: React.FC = () => {
<span>{item.roomNum}</span>
<img src={ImageUrl.icon10} alt="" />
</div>
<Popover
placement="bottom"
onOpenChange={(e: boolean) => {
setBaseImage('')
if (e) {
GetQrcode(item.roomNum, import.meta.env.VITE_ENV === 'development' ? 'trial' : 'release').then(res => {
if (res.code === 200) {
setBaseImage(res.data)
}
})
}
}}
content={
baseImage ? <div>
<img style={{ width: '200px', margin: '0 auto' }} src={`data:image/png;base64,${baseImage}`} alt="" />
<div style={{ color: 'white', textAlign: 'center', fontSize: '16px', marginTop: '10px' }}>
<span></span><br />
<span></span>
</div>
</div> : <div>
<Empty description={'二维码加载中,请稍后'} />
</div>
}
>
<div title='小程序'>
<img src={ImageUrl.icon55} alt="" />
</div>
</Popover>
<Code roomNum={item.roomNum}></Code>
</div>
<div>
{role.ID.includes(userInfo.roleId) ? <Popover
@ -335,28 +310,43 @@ const Index: React.FC = () => {
<Button type="primary"
iconPosition={'end'}
onClick={async () => {
if (role.ID.includes(userInfo.roleId)) {
joinSettingRef.current.changeModal(item.roomNum)
} else {
storage.setItem('loading', true)
postRefresh(() => {
getRoomRtcToken(item.roomNum, (options: any) => {
if (options) {
navigate(`/meeting`, {
state: {
channelId: item.roomNum,
token: options.token,
tokenA: options.tokenA,
roomId: item.id,
roomName: item.roomName,
enableMicr: false,
enableCamera: false,
storage.setItem('loading', true)
isVersion((bool: boolean) => {
storage.setItem('loading', false)
if (bool) {
window.electron.onDownload('3')
} else {
if (role.ID.includes(userInfo.roleId)) {
joinSettingRef.current.changeModal(item.roomNum)
} else {
storage.setItem('loading', true)
postRefresh(() => {
getRoomRtcToken(item.roomNum, async (options: any) => {
if (options) {
await window.electron.getVersion().then(async req => {
await PostHomeVerLog({
version: req,
platformType: 1,
roomNum: item.roomNum,
})
})
navigate(`/meeting`, {
state: {
channelId: item.roomNum,
token: options.token,
tokenA: options.tokenA,
roomId: item.id,
roomName: item.roomName,
enableMicr: false,
enableCamera: false,
}
})
}
})
}
})
})
}
})
}
}
})
}}
icon={<img src={ImageUrl.icon9} alt="" />}
className='m-ant-btn'>

View File

@ -458,6 +458,9 @@ const User: React.FC = () => {
if (!addUserFrom.UserName && isCreateUser !== 'batch') {
return message.error('请输入用户名称!')
}
if (addUserFrom.UserName.length > 10) {
return message.error('用户名称最多10个字')
}
if (addUserFrom.year === '') {
return message.error('请输入届!')
}

View File

@ -120,8 +120,26 @@
@else if $i ==4 {
flex-shrink: 0;
color: #ccc;
font-size: 16px;
>div:nth-child(1) {
color: #ccc;
font-size: 16px;
display: flex;
align-items: center;
>span:nth-child(2) {
background-color: red;
font-size: 12px;
padding: 0px 4px;
border-radius: 10px;
margin-left: 4px;
}
}
>div:nth-child(2) {
width: 100%;
margin-top: 10px;
}
}
@else if $i ==5 {

View File

@ -1,7 +1,7 @@
import styles from '@/page/Home/index.module.scss'
import { useEffect, useState, useRef } from "react";
import { Outlet, useNavigate } from 'react-router-dom';
import { Popconfirm, Popover } from 'antd';
import { Button, Popconfirm, Popover } from 'antd';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn'
import { storage } from '@/utils';
@ -45,6 +45,7 @@ const Home: React.FC = () => {
]);
const [userInfo, setUserInfo] = useState<any>({})
const [version, setVersion] = useState<string>('')
const [update, setUpdate] = useState(false)
const [dateInfo, setDateInfo] = useState<{
work: string;
time: string;
@ -67,11 +68,18 @@ const Home: React.FC = () => {
})
};
const timer = setInterval(updateTime, 1000);
window.addEventListener('customStorageChange', handleCustomStorageChange);
return () => {
window.removeEventListener('customStorageChange', handleCustomStorageChange);
clearInterval(timer);
};
}, []);
const handleCustomStorageChange = (e: any): void => {
if (e.key === 'isUpdate') {
setUpdate(e.value)
}
};
const changtNavList = (index: number, bool?: boolean): void => {
const newNavList = [...navList];
if (typeof bool === 'boolean') {
@ -126,14 +134,22 @@ const Home: React.FC = () => {
)
})}
</div>
<div>
{version}
<div className='drag'>
<div>
<span>:{version}</span>
{update ? <span>new</span> : null}
</div>
{update ? <div>
<Button type="primary" className='m-ant-btn' style={{ width: '100%' }} onClick={() => {
window.electron.onDownload('3')
}}></Button>
</div> : null}
</div>
<div>
<Popover
placement="right"
content={
<img style={{ width: '400px' }} src={'https://meeting-api.23544.com/meeting/update/ddq.png'} alt="" />
<img style={{ width: '400px' }} src={`https://meeting-api.23544.com/meeting/update/ddq.png?t=${+new Date()}`} alt="" />
}
>
<div className='drag' title='反馈建议'>

View File

@ -4,11 +4,14 @@ import { useEffect, useState } from "react";
import { useNavigate } from 'react-router-dom';
import { Input, Button, Checkbox, message, Modal } from "antd"
import { storage } from '@/utils'
import { GetCheckUser, PostAnonLogin, PostLogin } from '@/api/Login'
import { GetCheckOnline, GetCheckUser, PostAnonLogin, PostLogin } from '@/api/Login'
import * as CryptoJS from 'crypto-js';
import ImageUrl from '@/utils/package/imageUrl'
import { v4 as uuidv4 } from 'uuid';
import { GetCheckoutRoomNum, GetRoomInfo, GetRoomRtcToken } from '@/api/Home/Index';
import { ExclamationCircleFilled } from '@ant-design/icons';
import { isVersion } from '@/utils/package/public';
const { confirm } = Modal;
const Login: React.FC = () => {
const navigate = useNavigate();
const [accountPasswordStatus, setAccountPasswordStatus] = useState<boolean>(false);
@ -37,13 +40,16 @@ const Login: React.FC = () => {
roomNum: '',
})
const [nameModal, setNameModal] = useState(false)
const [env, setEnv] = useState('')
useEffect(() => {
window.electron.setMainWindowSize({
width: 752,
height: 520,
key: 'login'
})
window.electron.getEnv().then(res => {
setEnv(res)
})
if (storage.getItem('login')) {
const login = JSON.parse(storage.getItem('login') as string);
const data = {
@ -100,7 +106,32 @@ const Login: React.FC = () => {
}
GetCheckUser(operation.account).then(res => {
if (res.code === 200) {
res.data ? setAccountPasswordStatus(true) : message.error('账号不存在!')
if (res.data) {
GetCheckOnline(operation.account).then(req => {
if (req.code === 200) {
if (req.data) {
confirm({
title: '提示',
icon: <ExclamationCircleFilled />,
content: `账号已在其他地方登录,是否强制登陆?`,
centered: true,
okText: '确定',
cancelText: '取消',
async onOk() {
setAccountPasswordStatus(true)
},
onCancel() {
}
})
} else {
setAccountPasswordStatus(true)
}
}
})
} else {
message.error('账号不存在!')
}
}
})
}
@ -173,7 +204,7 @@ const Login: React.FC = () => {
<>
<div className={styles.login}>
<div className={styles.loginBg}>
<img src={import.meta.env.VITE_ENV === 'xy' ? ImageUrl.icon53 : ImageUrl.icon1} alt="" />
{env ? <img src={env === 'xy' ? ImageUrl.icon53 : ImageUrl.icon1} alt="" /> : null}
</div>
<div className={styles.loginContent}>
<div>
@ -315,47 +346,58 @@ const Login: React.FC = () => {
if (!anonInfo.nickName) {
return message.error('请输入参会昵称!')
}
if (anonInfo.nickName.length > 10) {
return message.error('参会昵称最多10个字')
}
storage.setItem('loading', true)
PostAnonLogin(anonInfo).then(async (res) => {
if (res.code == 200) {
storage.setItem('user', JSON.stringify(res.data))
storage.setItem('userLogin', true)
await window.electron.startSignalr(res.data)
getRoomRtcToken(anonInfo.roomNum, (options: any) => {
if (options) {
GetRoomInfo(anonInfo.roomNum).then(async (res) => {
if (res.code === 200) {
setTimeout(() => {
isVersion((bool: boolean) => {
storage.setItem('loading', false)
if (bool) {
window.electron.onDownload('3')
} else {
storage.setItem('loading', true)
PostAnonLogin(anonInfo).then(async (res) => {
if (res.code == 200) {
storage.setItem('user', JSON.stringify(res.data))
storage.setItem('userLogin', true)
await window.electron.startSignalr(res.data)
getRoomRtcToken(anonInfo.roomNum, (options: any) => {
if (options) {
GetRoomInfo(anonInfo.roomNum).then(async (res) => {
if (res.code === 200) {
setTimeout(() => {
storage.setItem('loading', false)
window.electron.getWindowSize().then((res: any) => {
window.electron.setMainWindowSize({
width: Math.ceil(res.width / 1.5),
height: Math.ceil(res.height / 1.3),
})
})
navigate(`/meeting`, {
state: {
channelId: anonInfo.roomNum,
token: options.token,
tokenA: options.tokenA,
roomId: res.data.id,
roomName: res.data.roomName,
enableMicr: false,
enableCamera: false,
}
})
}, 2000)
} else {
storage.setItem('loading', false)
}
}).catch(() => {
storage.setItem('loading', false)
window.electron.getWindowSize().then((res: any) => {
window.electron.setMainWindowSize({
width: Math.ceil(res.width / 1.5),
height: Math.ceil(res.height / 1.3),
})
})
navigate(`/meeting`, {
state: {
channelId: anonInfo.roomNum,
token: options.token,
tokenA: options.tokenA,
roomId: res.data.id,
roomName: res.data.roomName,
enableMicr: false,
enableCamera: false,
}
})
}, 2000)
} else {
storage.setItem('loading', false)
})
}
}).catch(() => {
storage.setItem('loading', false)
})
}
}).catch(() => {
storage.setItem('loading', false)
})
}
}).catch(() => {
storage.setItem('loading', false)
})
}}></Button>
</div>

View File

@ -46,6 +46,8 @@
font-size: 14px;
color: #F3F3F5;
margin-left: 4px;
display: flex;
align-items: center;
}
>div {}
@ -77,6 +79,8 @@
>span {
font-size: 14px;
color: #F3F3F5;
display: flex;
align-items: center;
}
>div {

View File

@ -206,19 +206,30 @@ const ChatBigWindow: React.FC = () => {
></Button> : null}
</div> : <div style={{ color: 'white' }}></div>
}>
<div>
<div title={item.userName}>
<div><Avatar name={item.userName} /></div>
{item.uid !== user.uid ?
<span>{item.userName} <span style={{ fontSize: '12px', color: '#ccc', marginLeft: '4px' }}>{dayjs(item.timestamp).format('HH:mm:ss')}</span></span> :
<span> <span style={{ fontSize: '12px', color: '#ccc', marginRight: '4px' }}>{dayjs(item.timestamp).format('HH:mm:ss')} </span>{item.userName}</span>
<span>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '150px' }}>{item.userName}</span>
<span style={{ fontSize: '12px', color: '#ccc', marginLeft: '4px' }}>{dayjs(item.timestamp).format('HH:mm:ss')}</span>
</span> :
<span>
<span style={{ fontSize: '12px', color: '#ccc', marginRight: '4px' }}>{dayjs(item.timestamp).format('HH:mm:ss')}</span>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '150px' }}>{item.userName}</span>
</span>
}
</div>
</Popover> : <div>
</Popover> : <div title={item.userName}>
<div><Avatar name={item.userName} /></div>
{item.uid !== user.uid ?
<span>{item.userName}<span style={{ fontSize: '12px', color: '#ccc', marginLeft: '4px' }}>{dayjs(item.timestamp).format('HH:mm:ss')}</span></span> :
<span><span style={{ fontSize: '12px', color: '#ccc', marginRight: '4px' }}>{dayjs(item.timestamp).format('HH:mm:ss')} </span>{item.userName}</span>
<span>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '150px' }}>{item.userName}</span>
<span style={{ fontSize: '12px', color: '#ccc', marginLeft: '4px' }}>{dayjs(item.timestamp).format('HH:mm:ss')}</span>
</span> :
<span>
<span style={{ fontSize: '12px', color: '#ccc', marginRight: '4px' }}>{dayjs(item.timestamp).format('HH:mm:ss')}</span>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '150px' }}>{item.userName}</span>
</span>
}
</div>}
<div>{item.message}</div>

View File

@ -31,7 +31,7 @@
display: flex;
>span:nth-child(1) {
white-space: nowrap;
word-break: break-all;
color: #ff970f;
}

View File

@ -21,7 +21,10 @@ const NoticeWindow: React.FC = () => {
api.open({
message: '',
description: <div>
<span style={{ fontSize: '16px' }}>{noticeItem.uname}</span>
<div style={{ fontSize: '16px' }}>
<div style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={noticeItem.uname}>{noticeItem.uname}</div>
<div></div>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end' }} className='drag'>
<Button
type="primary"

View File

@ -202,7 +202,7 @@ const ShareScreenWindow: React.FC = () => {
}}
key={index}>
<div>
{!item.active ? <div style={{ backgroundImage: `url(${ImageUrl.icon49})` }} id={`micr-item-${userInfo.uid}`}>
{!item.active ? <div style={{ backgroundImage: `url(${ImageUrl.icon49})`, transition: '0.18s' }} id={`micr-item-${userInfo.uid}`}>
</div> : ''}
{item.select ? <img src={item.iconSelect} alt="" /> : <img src={item.active ? item.iconActive : item.icon} alt="" />}
</div>

View File

@ -303,7 +303,7 @@
// 演讲者模式
.meetingContentBodyLeftSpeakerMode {
width: 18%;
width: 270px;
overflow-y: auto;
height: 100%;
@ -312,7 +312,7 @@
}
.meetingContentSwiperCard {
height: calc(100% / 6);
height: 160px;
}
}
@ -379,7 +379,7 @@
right: 0;
bottom: 0;
height: 100% !important;
width: calc(100% - 18%) !important;
width: calc(100% - 270px) !important;
z-index: 2;
}
@ -390,7 +390,7 @@
.meetingContentSwiperCard {
height: 160px;
width: calc(100% / 6);
width: 270px;
border-radius: 10px;
overflow: hidden;
position: relative;
@ -444,8 +444,8 @@
color: white;
border: 1px white solid;
font-size: 20px;
width: 30px;
height: 30px;
width: 25px;
height: 25px;
display: flex;
justify-content: center;
align-items: center;
@ -549,6 +549,8 @@
font-size: 14px;
color: #F3F3F5;
margin-left: 4px;
display: flex;
align-items: center;
}
>div {
@ -660,6 +662,8 @@
font-size: 14px;
color: #F3F3F5;
margin-left: 4px;
display: flex;
align-items: center;
}
}
@ -689,6 +693,8 @@
>span {
font-size: 14px;
color: #F3F3F5;
display: flex;
align-items: center;
}
>div {
@ -798,7 +804,7 @@
}
>img {
height: 50px;
height: 35px;
}
>span {
@ -820,8 +826,8 @@
}
>label {
height: 50px;
height: 50px;
height: 35px;
height: 35px;
cursor: pointer;
position: relative;

View File

@ -8,12 +8,12 @@ import { SearchOutlined, EllipsisOutlined, ExclamationCircleFilled, FullscreenEx
import { useLocation, useNavigate } from 'react-router-dom';
import { thumbImageBufferToBase64 } from '@/utils/package/base64'
import { storage } from '@/utils';
import { GetRoomUser, PostOpenMicr, GetSharedScreen, PostOpenCamera, GetLeaveAll, PostRoomManager, DeleteRoomManager, GetRoomKickout, GetShowUser, PostShowUser, PostMuteAll, GetRoomUserItem, GetApplySpeak, PostSharedScreen } from '@/api/Meeting';
import { GetRoomUser, PostOpenMicr, GetSharedScreen, PostOpenCamera, GetLeaveAll, PostRoomManager, DeleteRoomManager, GetRoomKickout, GetShowUser, PostShowUser, PostMuteAll, GetRoomUserItem, GetApplySpeak, PostSharedScreen, PostStopSharedScreen } from '@/api/Meeting';
import ImageUrl from '@/utils/package/imageUrl'
import { agora } from '@/utils/package/agora'
import dayjs from 'dayjs';
import durationPlugin from 'dayjs/plugin/duration';
import { AudioVolumeInfo, ConnectionChangedReasonType, ConnectionStateType, LocalVideoStreamReason, LocalVideoStreamState, RenderModeType, RtcConnection, RtcStats, UserOfflineReasonType, VideoSourceType, VideoStreamType } from 'agora-electron-sdk';
import { AudioVolumeInfo, ConnectionChangedReasonType, ConnectionStateType, LocalVideoStreamReason, LocalVideoStreamState, RenderModeType, RtcConnection, RtcStats, StreamPublishState, UserOfflineReasonType, VideoSourceType, VideoStreamType } from 'agora-electron-sdk';
import Avatar from '@/components/Avatar';
import SharedFilesModel from '@/components/SharedFilesModel';
import StupWizard from '@/components/StupWizard';
@ -26,6 +26,7 @@ import MeetingDisconnected from '@/components/MeetingDisconnected';
import SingIn from '@/components/SingIn';
import UserName from '@/components/UserName';
import { GetRoomRtcToken } from '@/api/Home/Index';
import Code from '@/components/Code';
const { setTimeout, setInterval, clearTimeout, clearInterval } = require('timers');
const { confirm } = Modal;
const { exec } = require('child_process');
@ -158,6 +159,8 @@ const Meeting: React.FC = () => {
itemIndex: 0,
rowIndex: 0,
});
const [_audioStatus, setAudioStatus] = useState<StreamPublishState>(1);
const [_videoStatus, setVideoStatus] = useState<StreamPublishState>(1);
const [roomUserList, setRoomUserList] = useState<any>([])
const [_speackUid, setSpeackUid] = useState<any>([])
const [currentSpeakUser, setCurrentSpeakUser] = useState<any>([])
@ -215,6 +218,7 @@ const Meeting: React.FC = () => {
});
const [isVideoFullScreen, setIsVideoFullScreen] = useState<boolean>(false)
const [observer, setObserver] = useState<IntersectionObserver>()
const [_activeSpeaker, setActiveSpeaker] = useState('')
let userInfo = JSON.parse(storage.getItem('user') as string)
const msgTips = '您不是管理员或发言人,无法开启此功能!'
const channel = new BroadcastChannel('meeting_channel');
@ -272,7 +276,7 @@ const Meeting: React.FC = () => {
case 'shareScreenWindowClose':
setCurrentSeconds(shareScreenWindowClose)
await stopScreenCapture()
await allUserLook(userInfo.uid, userInfo.userName)
await allUserLook(userInfo.uid, userInfo.userName, true)
break;
case 'shareScreenWindowfooterListsTitle':
switch (shareScreenWindowfooterListsTitle) {
@ -719,7 +723,7 @@ const Meeting: React.FC = () => {
break;
// 扩展操作
case 'Operation':
switch (item.type) {
switch (item.contentString) {
}
break;
@ -782,6 +786,14 @@ const Meeting: React.FC = () => {
break;
// 发言人用户信息刷新
case 'ManagerRefresh':
if (!item.user.isRoomManager) {
setCurrentVideoId((res: any) => {
if (res === String(item.user.uid)) {
getShowUser()
}
return res
})
}
setAllUserListData('ManagerRefresh', item, async () => {
if (item.user.uid === item.uid) {
if (item.user.uid === userInfo.uid) {
@ -821,73 +833,14 @@ const Meeting: React.FC = () => {
break;
// 申请发言
case 'ApplyToSpeak':
setIsScreenCapture(bool => {
if (bool) {
window.electron.setChildWindowShow({
key: 'noticeWindow',
bool: true
})
channel.postMessage({
type: 'noticeItem',
noticeItem: item
});
} else {
api.open({
message: '',
description: <div>
<span style={{ fontSize: '16px' }}>{item.uname}</span>
<div style={{ display: 'flex', justifyContent: 'flex-end' }} className='drag'>
<Button
type="primary"
className='m-ant-btn'
onClick={async (e: any) => {
let i = e.nativeEvent.path;
if (i) {
i.forEach((i: any) => {
if (i.className === 'ant-notification-notice ant-notification-notice-closable') {
i.childNodes.forEach((row: any) => {
if (row.className === 'ant-notification-notice-close') {
row.click()
postRoomManager({
roomId: state.roomId,
roomNum: state.channelId,
userId: item.uid
})
}
})
}
})
}
}}
></Button>
<Button
type="primary"
onClick={(e: any) => {
let item = e.nativeEvent.path;
if (item) {
item.forEach((item: any) => {
if (item.className === 'ant-notification-notice ant-notification-notice-closable') {
item.childNodes.forEach((row: any) => {
if (row.className === 'ant-notification-notice-close') {
row.click()
}
})
}
})
}
}}
style={{ backgroundColor: '#EC3C3C', marginLeft: '14px' }}
></Button>
</div>
</div>,
duration: 10,
placement: 'bottomRight',
showProgress: true,
pauseOnHover: false,
});
}
return bool
window.electron.setChildWindowShow({
key: 'noticeWindow',
bool: true
})
channel.postMessage({
type: 'noticeItem',
noticeItem: item
});
break;
// 管理员查看随机用户
case 'Watch':
@ -932,7 +885,7 @@ const Meeting: React.FC = () => {
}
}),
playBackDeviceId: res[1].playBackList.find((row: any) => row.deviceId === setting.playBackDeviceId) ? setting.playBackDeviceId : res[1].playBackList.length ? res[1].playBackList[0].deviceId : null,
ecordingVolume: res[1].ecordingVolume
ecordingVolume: setting.ecordingVolume
}
window.electron.onInvoke('sendDrivers', {
uid: item.callerUid,
@ -1023,10 +976,35 @@ const Meeting: React.FC = () => {
return res
})
break;
// 共享
// 设置发言人
case 'SetSpeaker':
window.electron.onInvoke('SetSpeakerCallback', item.RoomManagerInputDTO)
break;
// 扩展参数
case 'ReceivedOperation':
try {
const temp = JSON.parse(item.contentString)
if (temp.type === 'audio') {
await PostOpenMicr({
roomNum: temp.roomNum,
uid: temp.uid,
enableMicr: temp.enableMicr
})
} else {
await PostOpenCamera({
roomNum: temp.roomNum,
uid: temp.uid,
enableCamera: temp.enableCamera
})
}
} catch (error) {
}
break;
// 共享人取消共享屏幕
case 'StopedSharedScreen':
setVideoUser()
break;
}
})
return () => {
@ -1182,6 +1160,61 @@ const Meeting: React.FC = () => {
return () => clearTimeout(timer);
}, [isClickedMediaSteam]);
// useEffect(() => {
// let timer: NodeJS.Timeout | string = '';
// if (audioStatus === 1 && videoStatus === 1) {
// if (timer) {
// clearTimeout(timer)
// timer = ''
// }
// timer = setTimeout(() => {
// setIsShare((req: any) => {
// if (!req) {
// setRoomUserList((res: any) => {
// let userItem = res.find((item: any) => item.uid === userInfo.uid)
// if (!role.ID.includes(userInfo.roleId) && userItem && userItem.isRoomManager) {
// DeleteRoomManager({
// roomId: state.roomId,
// roomNum: state.channelId,
// userId: userInfo.uid
// })
// confirm({
// title: '提示',
// icon: <ExclamationCircleFilled />,
// content: `由于您长时间未发言,已自动取消发言权限,是否重新申请发言?`,
// centered: true,
// okText: '确定',
// cancelText: '取消',
// async onOk() {
// GetApplySpeak(state.channelId).then(res => {
// if (res.code === 200) {
// setIsClicked(true);
// message.success('申请发言成功')
// }
// })
// },
// onCancel() {
// }
// })
// }
// clearTimeout(timer)
// timer = ''
// return res
// })
// }
// return req
// })
// }, 1000 * 60 * 5);
// } else {
// if (timer) {
// clearTimeout(timer)
// timer = ''
// }
// }
// return () => timer ? clearTimeout(timer) : '';
// }, [audioStatus, videoStatus]);
useEffect(() => {
let timer: NodeJS.Timeout | undefined;
if (timer) {
@ -1313,6 +1346,32 @@ const Meeting: React.FC = () => {
channelId: connection.channelId,
renderMode: RenderModeType.RenderModeFit
})
setCurrentVideoId((res: any) => {
if (res === String(remoteUid)) {
let dom: any;
setCurrentLookUserStatus(req => {
switch (req) {
case 1:
dom = document.getElementById(`video-source-camera-primary`) as HTMLElement
break;
case 2:
dom = document.getElementById(`video-source-screen`) as HTMLElement;
break;
case 3:
dom = document.getElementById(`video-source-remote-screen`) as HTMLElement
break;
case 4:
dom = document.getElementById(`video-source-remote-camera`) as HTMLElement
break;
}
if (dom && dom.childNodes.length === 1) {
renderVideo(String(remoteUid))
}
return req
})
}
return res
})
}, 1000);
}
}
@ -1369,6 +1428,20 @@ const Meeting: React.FC = () => {
} else if (state === 3) {
meetingDisconnectedRef.current.changeModal(false)
setIsAgoraDisconnected(false)
} else if (state === 5) {
confirm({
keyboard: false,
title: '提示',
icon: <ExclamationCircleFilled />,
content: `重连失败,请退出房间重试!`,
centered: true,
okText: '退出',
wrapClassName: 'hideCancelText',
cancelText: '',
async onOk() {
leaveChannel()
},
})
}
},
onConnectionLost: () => {
@ -1388,18 +1461,33 @@ const Meeting: React.FC = () => {
if (bool) {
stopScreenCapture()
setSharedScreenItem('')
allUserLook(userInfo.uid, userInfo.userName)
allUserLook(userInfo.uid, userInfo.userName, true)
}
return bool
})
} else if (reason === 3 || reason === 4) {
message.error({
content: <div><span style={{ color: '#606fc7', cursor: 'pointer' }} onClick={() => {
content: <div><span style={{ color: '#606fc7', cursor: 'pointer' }} onClick={() => {
stupWizardRef.current.changeModal(1);
}}></span></div>,
duration: 60,
duration: 15,
key: 'cameraTemporarily'
});
} else if (_state === 3) {
if (!stupWizardRef.current.getStupWizardModal()) {
await PostOpenCamera({
roomNum: state.channelId,
uid: userInfo.uid,
enableCamera: false
})
message.error({
content: <div><span style={{ color: '#606fc7', cursor: 'pointer' }} onClick={() => {
stupWizardRef.current.changeModal(1);
}}></span></div>,
duration: 15,
key: 'cameraTemporarily'
});
}
}
},
onTokenPrivilegeWillExpire: async (connection: RtcConnection, _token: string) => {
@ -1411,6 +1499,23 @@ const Meeting: React.FC = () => {
})
}
})
},
onVideoPublishStateChanged: (_source: VideoSourceType, _channel: string, _oldState: StreamPublishState, newState: StreamPublishState, _elapseSinceLastState: number) => {
setVideoStatus(newState)
},
onAudioPublishStateChanged: (_channel: string, _oldState: StreamPublishState, newState: StreamPublishState, _elapseSinceLastState: number) => {
setAudioStatus(newState)
},
onActiveSpeaker: (_connection: RtcConnection, uid: number) => {
if (String(uid).length !== 9) {
setActiveSpeaker(String(uid))
}
setIsShare((res: any) => {
if (!res && String(uid).length !== 9) {
renderVideo(String(uid))
}
return res
})
}
})
if (state.enableCamera) {
@ -1664,6 +1769,20 @@ const Meeting: React.FC = () => {
})
}
}
const setVideoUser = () => {
setRoomUserList((newChatList: any) => {
setActiveSpeaker(res => {
let item = newChatList.find((item: any) => item.uid === res)
if (item && item.isRoom && item.isAdmin) {
renderVideo(res)
} else {
getShowUser()
}
return res
})
return newChatList
})
}
// 加入房间时间
const changeCurrentSeconds = (): string => {
const duration = dayjs.duration(currentSeconds, 'seconds');
@ -1731,8 +1850,8 @@ const Meeting: React.FC = () => {
if (res) {
GetSharedScreen(state.channelId).then(req => {
if (req.code === 200) {
if (res.data) {
setIsShare(res.data)
if (req.data) {
setIsShare(req.data)
}
getDesktopCapturerVideo()
setIsSharedScreenModal(true)
@ -1753,7 +1872,7 @@ const Meeting: React.FC = () => {
}
})
if (row.title === '停止共享') {
await allUserLook(userInfo.uid, userInfo.userName)
await allUserLook(userInfo.uid, userInfo.userName, true)
}
break;
case '静音':
@ -2020,8 +2139,12 @@ const Meeting: React.FC = () => {
})
};
// 设置全员看谁
const allUserLook = async (uid: string, name: string): Promise<void> => {
await PostShowUser(state.channelId, uid, name)
const allUserLook = async (uid: string, name: string, bool?: boolean): Promise<void> => {
if (bool) {
await PostStopSharedScreen(state.channelId)
} else {
await PostShowUser(state.channelId, uid, name)
}
}
// 设置发言人
const postRoomManager = async (data: any): Promise<void> => {
@ -2083,7 +2206,25 @@ const Meeting: React.FC = () => {
item.isRoom = true;
item.isAdmin = role.ID.includes(item.roleId) || item.isRoomManager
})
setRoomUserList(res.data)
setRoomUserList((req: any) => {
if (req.length) {
let arr: any = []
res.data.forEach((item: any) => {
let userItem = req.find((row: any) => row.uid == item.uid);
if (userItem) {
userItem.enableCamera = item.enableCamera;
userItem.enableMicr = item.enableMicr;
userItem.isRoomManager = item.isRoomManager;
userItem.isAdmin = role.ID.includes(item.roleId) || item.isRoomManager;
} else {
arr.push(item)
}
});
return [...req, ...arr]
} else {
return res.data
}
})
getUserRoomInfo().then(async (res) => {
await agora.updateChannelMediaOptions(res ? true : false)
changeAgoraDevice()
@ -2115,7 +2256,7 @@ const Meeting: React.FC = () => {
enableCamera: !storeDevice[0][1].active,
isRoomManager: userItem ? userItem.isRoomManager : false,
})
await getShowUser()
setVideoUser()
if (userItem.isRoomManager) {
await postOpenMicr(!storeDevice[0][0].active, userInfo.uid)
await postOpenCamera(!storeDevice[0][1].active, userInfo.uid)
@ -2147,25 +2288,28 @@ const Meeting: React.FC = () => {
const sendMsg = (text?: string): void => {
let msg = text ? text : textMsg;
if (msg) {
window.electron.onInvoke('sendChannelMsg', {
roomNum: state.channelId,
msg: msg,
})
let item = {
uid: userInfo.uid,
userName: userInfo.userName,
message: msg,
timestamp: +new Date()
}
setChatList((newChatList: any) => [...newChatList, item])
window.electron.windowHandleMessage({
key: 'chatSmallWindow',
parmes: {
chatListIten: item,
setRoomUserList((res: any) => {
let row = res.find((item: any) => item.uid === userInfo.uid)
window.electron.onInvoke('sendChannelMsg', {
roomNum: state.channelId,
msg: msg,
})
let item = {
uid: userInfo.uid,
userName: row.userName,
message: msg,
timestamp: +new Date()
}
setChatList((newChatList: any) => [...newChatList, item])
window.electron.windowHandleMessage({
key: 'chatSmallWindow',
parmes: {
chatListIten: item,
}
})
chatScrollBotton()
return res
})
setTextMsg('');
chatScrollBotton()
} else {
message.error('请输入内容!')
}
@ -2213,8 +2357,6 @@ const Meeting: React.FC = () => {
}
})
}
// 开关麦克风
const postOpenMicrApi = async (enableMicr: boolean, uid: string, isAll: boolean, isMessage: boolean = false): Promise<void> => {
if (isAll) {
@ -2223,11 +2365,20 @@ const Meeting: React.FC = () => {
enableMicr
})
} else {
await PostOpenMicr({
roomNum: state.channelId,
await window.electron.onInvoke('sendOper2User', {
uid,
enableMicr
contentString: JSON.stringify({
roomNum: state.channelId,
uid,
enableMicr,
type: 'audio'
})
})
// await PostOpenMicr({
// roomNum: state.channelId,
// uid,
// enableMicr
// })
}
if (isMessage) {
// message.success('操作成功')
@ -2268,11 +2419,20 @@ const Meeting: React.FC = () => {
} else {
await agora.stopCameraCapture();
}
await PostOpenCamera({
roomNum: state.channelId,
await window.electron.onInvoke('sendOper2User', {
uid,
enableCamera
contentString: JSON.stringify({
roomNum: state.channelId,
uid,
enableCamera,
type: 'video'
})
})
// await PostOpenCamera({
// roomNum: state.channelId,
// uid,
// enableCamera
// })
if (isMessage) {
// message.success('操作成功')
}
@ -2376,7 +2536,7 @@ const Meeting: React.FC = () => {
</svg>
</div> //右
} else {
return <div className={`${styles.speakerModeIcon} drag`} style={{ left: 'calc(18% - 4px)' }} title='收起' onClick={() => setIsVideoFullScreen(true)}>
return <div className={`${styles.speakerModeIcon} drag`} style={{ left: 'calc(270px - 4px)' }} title='收起' onClick={() => setIsVideoFullScreen(true)}>
<svg width="12" height="22" viewBox="0 0 12 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.9355 1.45949L10.7759 0.299805L0.0636029 11.0143L10.7759 21.7275L11.9355 20.5679L2.38107 11.0136L11.9355 1.45949Z" />
</svg>
@ -2415,6 +2575,14 @@ const Meeting: React.FC = () => {
}
message.success('操作成功')
}
// 判断是否出现滚动条
const hasScrollbar = () => {
let element = document.getElementById('videoView') as HTMLDivElement
if (element) {
return element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth;
}
return false
}
// 移出房间
const getRoomKickout = async (channelId: string, uid: string, userName: string): Promise<void> => {
confirm({
@ -2601,7 +2769,9 @@ const Meeting: React.FC = () => {
</Popover>}
<div>{changeCurrentSeconds()}</div>
</div>
<div>{state.channelId} {state.roomName}</div>
<div style={{ display: 'flex', alignItems: 'center' }}>{state.channelId} {state.roomName}
<span className='drag' style={{ marginTop: '2px', marginLeft: '4px' }}><Code roomNum={state.channelId}></Code></span>
</div>
<div className='drag'>
<Popover
content={
@ -2769,13 +2939,13 @@ const Meeting: React.FC = () => {
</div> : null)
}
)}
{isAdmin > 6 ? <div>
{hasScrollbar() ? <div>
{meetingMode === "StandardMode" ? <div className={`${styles.meetingContentSwiperCaret}`} style={{ left: '20px', top: '66px' }} onClick={() => {
const container = document.getElementById('videoView') as HTMLElement;
container.scrollLeft -= 100
}}>
<CaretLeftOutlined />
</div> : <div className={`${styles.meetingContentSwiperCaret}`} style={{ left: '8.2%', top: '20px' }} onClick={() => {
</div> : <div className={`${styles.meetingContentSwiperCaret}`} style={{ left: '115px', top: '20px' }} onClick={() => {
const container = document.getElementById('videoView') as HTMLElement;
container.scrollTop -= 100
}}>
@ -2786,7 +2956,7 @@ const Meeting: React.FC = () => {
container.scrollLeft += 100
}}>
<CaretRightOutlined />
</div> : <div className={`${styles.meetingContentSwiperCaret}`} style={{ left: '8.2%', bottom: '20px' }} onClick={() => {
</div> : <div className={`${styles.meetingContentSwiperCaret}`} style={{ left: '115px', bottom: '20px' }} onClick={() => {
const container = document.getElementById('videoView') as HTMLElement;
container.scrollTop += 100
}}>
@ -2900,7 +3070,7 @@ const Meeting: React.FC = () => {
<div>
<div><Avatar name={item.userName} /></div>
<span>
{item.userName}{item.uid === user.uid ? '(我)' : ''}
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: role.ID.includes(item.roleId) || item.isRoomManager ? '50px' : '80px' }} title={item.userName}>{item.userName}</span>{item.uid === user.uid ? '(我)' : ''}
{role.ID.includes(item.roleId) || item.isRoomManager ?
<span style={{ color: '#02B188', marginLeft: '4px' }}>
{role.ID.includes(item.roleId) ? '管理员' : '发言人'}
@ -3120,19 +3290,30 @@ const Meeting: React.FC = () => {
></Button> : null}
</div> : <div style={{ color: 'white' }}></div>
}>
<div>
<div title={item.userName}>
<div><Avatar name={item.userName} /></div>
{item.uid !== user.uid ?
<span>{item.userName} <span style={{ fontSize: '12px', color: '#ccc', marginLeft: '4px' }}>{dayjs(item.timestamp).format('HH:mm:ss')}</span></span> :
<span> <span style={{ fontSize: '12px', color: '#ccc', marginRight: '4px' }}>{dayjs(item.timestamp).format('HH:mm:ss')} </span>{item.userName}</span>
<span>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '150px' }}>{item.userName}</span>
<span style={{ fontSize: '12px', color: '#ccc', marginLeft: '4px' }}>{dayjs(item.timestamp).format('HH:mm:ss')}</span>
</span> :
<span>
<span style={{ fontSize: '12px', color: '#ccc', marginRight: '4px' }}>{dayjs(item.timestamp).format('HH:mm:ss')}</span>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '150px' }}>{item.userName}</span>
</span>
}
</div>
</Popover> : <div>
</Popover> : <div title={item.userName}>
<div><Avatar name={item.userName} /></div>
{item.uid !== user.uid ?
<span>{item.userName}<span style={{ fontSize: '12px', color: '#ccc', marginLeft: '4px' }}>{dayjs(item.timestamp).format('HH:mm:ss')}</span></span> :
<span><span style={{ fontSize: '12px', color: '#ccc', marginRight: '4px' }}>{dayjs(item.timestamp).format('HH:mm:ss')} </span>{item.userName}</span>
<span>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '150px' }}>{item.userName}</span>
<span style={{ fontSize: '12px', color: '#ccc', marginLeft: '4px' }}>{dayjs(item.timestamp).format('HH:mm:ss')}</span>
</span> :
<span>
<span style={{ fontSize: '12px', color: '#ccc', marginRight: '4px' }}>{dayjs(item.timestamp).format('HH:mm:ss')}</span>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '150px' }}>{item.userName}</span>
</span>
}
</div>}
<div>{item.message}</div>
@ -3157,7 +3338,10 @@ const Meeting: React.FC = () => {
<Input.TextArea placeholder="请输入消息" value={textMsg} style={{ flexGrow: 1 }} onChange={(e) => {
setTextMsg(e.target.value)
}}></Input.TextArea>
<Button type="primary" className='m-ant-btn' style={{ flexShrink: 0, marginTop: '4px' }} onClick={() => sendMsg()}></Button>
<Button type="primary" className='m-ant-btn' style={{ flexShrink: 0, marginTop: '4px' }} onClick={() => {
sendMsg()
setTextMsg('');
}}></Button>
</div>
</div>
:
@ -3327,7 +3511,7 @@ const Meeting: React.FC = () => {
key={rowIndex}>
<label>
<img src={row.active ? row.iconActive : row.icon} alt="" />
{!row.active ? <div style={{ backgroundImage: `url(${ImageUrl.icon49})` }} id={`micr-item-${userInfo.uid}`}>
{!row.active ? <div style={{ backgroundImage: `url(${ImageUrl.icon49})`, transition: '0.18s' }} id={`micr-item-${userInfo.uid}`}>
</div> : ''}
</label>
<span>{row.title}</span>
@ -3494,7 +3678,7 @@ const meetingContentUser = (item: any, bool?: boolean) => {
<>
<div className={styles.meetingContentUser}>
<div className={styles.meetingContentUserName}>
{role.ID.includes(item.roleId) || item.isRoomManager ?
{role.ID.includes(item.roleId) ?
<div style={{ background: role.ID.includes(item.roleId) ? '#FDC229' : '#3F51B5' }}>
<img src={ImageUrl.icon32} alt="" />
</div> : null}
@ -3502,7 +3686,7 @@ const meetingContentUser = (item: any, bool?: boolean) => {
bool ? !item.enableMicr ? <img src={item.enableMicr ? ImageUrl.icon22 : ImageUrl.icon22Active} alt="" /> : '' :
<label>
<img src={item.enableMicr ? ImageUrl.icon22 : ImageUrl.icon22Active} alt="" />
{item.enableMicr ? <div style={{ backgroundImage: `url(${ImageUrl.icon49})` }} id={`micr-${item.uid}`}>
{item.enableMicr ? <div style={{ backgroundImage: `url(${ImageUrl.icon49})`, transition: '0.18s' }} id={`micr-${item.uid}`}>
</div> : ''}
</label>
}

3
src/render.d.ts vendored
View File

@ -24,9 +24,10 @@ export interface IElectronAPI {
downFile: (callBack: Function) => void;
quitAndInstall: (callBack: Function) => void;
isOpenWindows: (callBack: Function) => void;
setEnv: (str: string) => any;
startLoad: () => any;
updateHandle: () => any;
getVersion: () => Promise<string>;
getEnv: () => Promise<string>;
isVisible: () => Promise<string>;
setRegistry: (uuid: string) => any;
getRegistry: () => any;

View File

@ -1,3 +1,4 @@
declare module 'react-dom/client';
declare module 'crypto-js';
declare module 'js-yaml';
declare module 'uuid';

View File

@ -21,7 +21,8 @@ import {
ColorEnhanceOptions,
LowlightEnhanceOptions,
VirtualBackgroundSource,
AudienceLatencyLevelType
AudienceLatencyLevelType,
StreamPublishState
} from "agora-electron-sdk";
import { GetRoomRtcToken, GetAgoraConf } from "@/api/Home/Index";
import { storage } from '@/utils';
@ -124,23 +125,25 @@ export const agora = {
if (settingData.darkLightEnhancement) agora.setLowlightEnhanceOptions(settingData.darkLightEnhancement.isDarkLightEnhancement, settingData.darkLightEnhancement)
if (settingData.virtualBackground) {
if (typeof settingData.virtualBackground.sourceIndex === 'number') {
if (import.meta.env.VITE_ENV === 'development') {
window.electron.getAppPath().then((res: string) => {
const imagePath = path.join(res, 'src', 'assets', 'virtualBackground', `${settingData.virtualBackground.sourceIndex + 1}.png`);
window.electron.getEnv().then(res => {
if (res === 'development') {
window.electron.getAppPath().then((res: string) => {
const imagePath = path.join(res, 'src', 'assets', 'virtualBackground', `${settingData.virtualBackground.sourceIndex + 1}.png`);
agora.enableVirtualBackground(settingData.virtualBackground.isVirtualBackground, {
source: imagePath,
background_source_type: 2,
color: Number(settingData.virtualBackground.color),
})
})
} else {
const imagePath = path.join((process as any).resourcesPath, 'images', `${settingData.virtualBackground.sourceIndex + 1}.png`);
agora.enableVirtualBackground(settingData.virtualBackground.isVirtualBackground, {
source: imagePath,
background_source_type: 2,
color: Number(settingData.virtualBackground.color),
})
})
} else {
const imagePath = path.join((process as any).resourcesPath, 'images', `${settingData.virtualBackground.sourceIndex + 1}.png`);
agora.enableVirtualBackground(settingData.virtualBackground.isVirtualBackground, {
source: imagePath,
background_source_type: 2,
color: Number(settingData.virtualBackground.color),
})
}
}
})
} else {
agora.enableVirtualBackground(settingData.virtualBackground.isVirtualBackground, {
background_source_type: 1,
@ -151,7 +154,7 @@ export const agora = {
}, 1000);
},
// 事件回调
registerEventHandler: ({ onJoinChannelSuccess, onUserJoined, onUserOffline, onAudioVolumeIndication, onRtcStats, onConnectionStateChanged, onLocalVideoStateChanged, onConnectionLost, onTokenPrivilegeWillExpire }: any) => {
registerEventHandler: ({ onJoinChannelSuccess, onUserJoined, onUserOffline, onAudioVolumeIndication, onRtcStats, onConnectionStateChanged, onLocalVideoStateChanged, onConnectionLost, onTokenPrivilegeWillExpire, onActiveSpeaker, onVideoPublishStateChanged, onAudioPublishStateChanged }: any) => {
rtcEngine.registerEventHandler({
// 监听本地用户加入频道事件
onJoinChannelSuccess: async (connection: RtcConnection, elapsed: number) => {
@ -166,17 +169,13 @@ export const agora = {
await onUserOffline?.(connection, remoteUid, reason)
},
// // 视频发布状态改变回调
// onVideoPublishStateChanged: (source: any, channel: any, oldState: any, newState: any, elapseSinceLastState: any) => {
// if (newState === 1) {
// }
// },
onVideoPublishStateChanged: (source: VideoSourceType, channel: string, oldState: StreamPublishState, newState: StreamPublishState, elapseSinceLastState: number) => {
onVideoPublishStateChanged?.(source, channel, oldState, newState, elapseSinceLastState)
},
// // 音频发布状态改变回调
// onAudioPublishStateChanged: (channel: any, oldState: any, newState: any, elapseSinceLastState: any) => {
// if (newState === 1) {
// }
// },
onAudioPublishStateChanged: (channel: string, oldState: StreamPublishState, newState: StreamPublishState, elapseSinceLastState: number) => {
onAudioPublishStateChanged?.(channel, oldState, newState, elapseSinceLastState)
},
// // 用户音量提示回调。
onAudioVolumeIndication: async (_connection: RtcConnection, speakers: AudioVolumeInfo[], _speakerNumber: number, _totalVolume: number) => {
await onAudioVolumeIndication?.(speakers)
@ -200,6 +199,13 @@ export const agora = {
// Token 即将在 30s 内过期回调。
onTokenPrivilegeWillExpire: (connection: RtcConnection, token: string) => {
onTokenPrivilegeWillExpire?.(connection, token)
},
// 监测到远端最活跃用户回调。
onActiveSpeaker: (connection: RtcConnection, uid: number) => {
const setting = JSON.parse(storage.getItem('setting') as string);
if (setting.voiceStimulation) {
onActiveSpeaker?.(connection, uid)
}
}
});
},
@ -289,7 +295,7 @@ export const agora = {
},
// 加入频道
joinChannel: async () => {
await rtcEngine.enableAudioVolumeIndication(100, 3, true)
await rtcEngine.enableAudioVolumeIndication(200, 3, true)
await rtcEngine.joinChannel(option.token, option.channelId, option.uid);
await rtcEngine.setDualStreamModeEx(
SimulcastStreamMode.EnableSimulcastStream,
@ -302,6 +308,7 @@ export const agora = {
},
{ channelId: option.channelId, localUid: Number(option.uid) }
);
await rtcEngine.setAudioScenario(8)
},
// 更新频道配置
updateChannelMediaOptions: async (bool: boolean) => {
@ -441,8 +448,8 @@ export const agora = {
startCameraCapture: async (bool: boolean = false) => {
await rtcEngine.startCameraCapture(VideoSourceType.VideoSourceCamera, {
format: {
width: bool ? 160 : 1280,
height: bool ? 160 : 720,
width: bool ? 160 : 1920,
height: bool ? 160 : 1080,
fps: 15,
}
})
@ -472,7 +479,7 @@ export const agora = {
}
let data = {
frameRate: isFluencyPriority ? 30 : 15,
frameRate: isFluencyPriority ? 15 : 7,
dimensions: {
width: 1920,
height: 1080,

View File

@ -1,5 +1,7 @@
import path from "path";
import storage from "./storage";
import axios from "axios";
import yaml from 'js-yaml'
export const setKeyOpenChildWindow = async (key: string, bool: boolean) => {
const openChildWindow = await JSON.parse(storage.getItem('openChildWindow') as string)
openChildWindow[key] = bool;
@ -27,6 +29,7 @@ export const storageSeeting: any = {
shareFilesPath: path.resolve(__dirname, '../../Downloads/') + '\\', //共享文件保存路径
isShareSavePath: true, //是否下载钱询问每个文件保存的位置
closeSetting: 'hide', //关闭按钮设置
voiceStimulation: true, //语音激励
isAINoiseReduction: true, //是否开启ai降噪
aINoiseReduction: 1, // 降噪模式
isRecordingTips: true, //是否开启录制提示
@ -59,15 +62,61 @@ export const getUpdateUrl = (env: string) => {
switch (env) {
case 'xy':
return 'https://meeting-api.23544.com/meeting/xysz'
case 'development':
return 'http://192.168.2.9:8827'
default:
return 'https://meeting-api.23544.com/meeting/update'
}
}
export const getTitle = (env: string) => {
export const getTitle = async () => {
let env = await window.electron.getEnv()
let str;
switch (env) {
case 'xy':
return '湖北襄阳四中教研平台'
str = '湖北襄阳四中教研平台'
break;
case 'development':
str = '智汇享'
break;
default:
return '智汇享'
str = '智汇享'
break;
}
document.getElementsByTagName('title')[0].innerText = str
}
export const compareVersions = (version1: string, version2: string): number => {
const v1Parts = version1.split('.').map(Number);
const v2Parts = version2.split('.').map(Number);
const maxLength = Math.max(v1Parts.length, v2Parts.length);
for (let i = 0; i < maxLength; i++) {
const v1 = v1Parts[i] || 0;
const v2 = v2Parts[i] || 0;
if (v1 > v2) {
return 1;
} else if (v1 < v2) {
return -1;
}
}
return 0;
}
export const isVersion = (callBack: Function) => {
window.electron.getEnv().then(res => {
axios.get(`${getUpdateUrl(res)}/latest.yml`).then(res => {
if (res.status === 200 && res.data) {
const data = yaml.load(res.data); // 解析 YAML 内容
window.electron.getVersion().then(req => {
if (compareVersions(data.version, req) == 1) {
callBack(true, data)
} else {
callBack(false, data)
}
})
} else {
callBack(false)
}
}).catch(() => {
callBack(false)
})
})
}

View File

@ -3,6 +3,8 @@ module.exports = {
switch (env) {
case 'xy':
return 'https://meeting-api.23544.com/meeting/xysz'
case 'development':
return 'http://192.168.2.9:8827'
default:
return 'https://meeting-api.23544.com/meeting/update'
}
@ -11,6 +13,8 @@ module.exports = {
switch (env) {
case 'xy':
return '湖北襄阳四中教研平台'
case 'development':
return '智汇享'
default:
return '智汇享'
}
@ -19,6 +23,8 @@ module.exports = {
switch (env) {
case 'xy':
return 'icon54'
case 'development':
return 'icon'
default:
return 'icon'
}

View File

@ -1,10 +1,10 @@
import { AxiosRequestConfig, AxiosResponse } from 'axios'
import Request from './request'
import { constant } from '@/config'
let baseURL = !location.hostname.includes('meeting-api.23544.com') ? 'http://192.168.2.9:5192' : 'https://meeting-api.23544.com/pc'
// 实例化
const req = new Request({
baseURL: import.meta.env.VITE_BASE_URL_API,
baseURL,
timeout: constant.CONFIG_REQUEST_TIMEOUT_TIME as number,
interceptors: {
// 请求拦截器
@ -22,5 +22,4 @@ const request = (config: any) => {
}
return req.request<any>(config)
}
export default request

View File

@ -77,6 +77,9 @@ class Request {
case 403:
updatePostRefresh()
break
case 502:
message.error('网络已断开,请检查网络状态')
break
default:
message.error(err.message)
break;

View File

@ -76,7 +76,9 @@ export default defineConfig({
ColorEnhanceOptions,
LowlightEnhanceOptions,
VirtualBackgroundSource,
AudienceLatencyLevelType
AudienceLatencyLevelType,
StreamPublishState,
IMediaEngine
} = require("agora-electron-sdk")
export {
createAgoraRtcEngine,
@ -101,7 +103,9 @@ export default defineConfig({
ColorEnhanceOptions,
LowlightEnhanceOptions,
VirtualBackgroundSource,
AudienceLatencyLevelType
AudienceLatencyLevelType,
StreamPublishState,
IMediaEngine
}
`,
})