diff --git a/main.js b/main.js index db2a312..af61aba 100644 --- a/main.js +++ b/main.js @@ -52,23 +52,6 @@ class AppWindow extends BrowserWindow { }); } } - -function showWindow() { - // 如果主窗口已经存在但被最小化了,则恢复显示 - if (mainWindow && mainWindow.isMinimized()) { - mainWindow.show(); - } - // 如果主窗口已存在但不是焦点窗口,则将其置为焦点 - if (mainWindow && !mainWindow.isFocused()) { - mainWindow.show(); - mainWindow.focus(); - } - // 如果主窗口还没有被创建,则创建它 - if (!mainWindow) { - createWindow(); - } -} - function quit() { app.quit() } @@ -80,7 +63,7 @@ function createTray() { const contextMenu = Menu.buildFromTemplate([ { label: '打开', click: () => { - showWindow() + mainWindow.webContents.send('isOpenWindows'); }, // icon: iconPath, }, @@ -100,11 +83,7 @@ function createTray() { tray.setToolTip('智汇享'); tray.setContextMenu(contextMenu); tray.on('click', () => { - if (mainWindow.isVisible()) { - mainWindow.hide() - } else { - mainWindow.show() - } + mainWindow.webContents.send('isOpenWindows'); }); } @@ -113,11 +92,6 @@ function createWindow() { mainWindow.focus(); } const additionalData = { myKey: 'myValue' } -// 退出房间 -app.on('will-quit', async (event) => { - await mainWindow.webContents.send('quitAndInstall'); -}); - app.on('ready', () => { const gotTheLock = app.requestSingleInstanceLock(additionalData) if (gotTheLock) { @@ -161,7 +135,6 @@ app.on('ready', () => { mainWindow.on('move', () => { // 如果是全屏自动恢复到上次窗口大小 if (isMaximized) { - mainWindow.setResizable(true) mainWindow.unmaximize() isMaximized = false; } @@ -177,10 +150,8 @@ app.on('ready', () => { break; case 'maximize': mainWindow.maximize() - mainWindow.setResizable(false) break; case 'unmaximize': - mainWindow.setResizable(true) mainWindow.unmaximize() break; case 'minimize': @@ -189,6 +160,10 @@ app.on('ready', () => { case 'hide': mainWindow.hide() break; + case 'show': + mainWindow.show() + mainWindow.focus(); + break; } }); // 导出是否全屏 @@ -199,6 +174,10 @@ app.on('ready', () => { ipcMain.handle('getVersion', () => { return app.getVersion(); }); + // 获取窗口是否显示 + ipcMain.handle('isVisible', () => { + return mainWindow.isVisible(); + }); // 获取共享屏幕列表 ipcMain.handle('getSources', async () => { return await desktopCapturer.getSources({ @@ -210,9 +189,12 @@ app.on('ready', () => { clipboard.writeText(text) }); // 退出 - ipcMain.handle('quit', async (event) => { - await mainWindow.webContents.send('quitAndInstall'); - quit() + ipcMain.handle('quit', async (event, bool) => { + if (bool) { + quit() + } else { + await mainWindow.webContents.send('quitAndInstall'); + } }); // 加入房间通知 ipcMain.handle('joinNotification', (event, user) => { @@ -273,10 +255,7 @@ app.on('ready', () => { // 设置窗口尺寸 mainWindow.setSize(config.width, config.height) // 设置窗口位置使其居中于当前屏幕 - const display = screen.getDisplayMatching({ ...mainWindow.getBounds() }); - const x = Math.round((display.workArea.width - mainWindow.getSize()[0]) / 2); - const y = Math.round((display.workArea.height - mainWindow.getSize()[1]) / 2); - mainWindow.setPosition(x, y); + mainWindowCenter() }); // 写入注册表 ipcMain.handle('setRegistry', (event, uuid) => { @@ -319,19 +298,90 @@ app.on('ready', () => { width: config.width, height: config.height, }) - child.loadURL(config.url) + if (env === 'development') { + // 开发 + child.loadURL(config.url) + } else { + // 测试 | 生产 + child.loadURL(`file://${path.join(__dirname, './dist/index.html')}#/${config.key}`); + } childWindow[config.key] = child child.once('ready-to-show', () => { - child.show() - if (config.key === 'shareScreenWindow') { - const display = screen.getDisplayMatching({ ...child.getBounds() }); - const x = Math.round((display.workArea.width - child.getSize()[0]) / 2); - child.setPosition(x, 0); - child.setResizable(false) - child.setMovable(false) - mainWindow.setPosition(-999999, -999999); + if (config.show) { + childWindow[config.key].show() } + childWindow[config.key].setAlwaysOnTop(true, 'screen-saver') + childWindow[config.key].setSkipTaskbar(true) + windowOperation(config) }) + child.webContents.on('before-input-event', (event, input) => { + // if (env === 'development') { + if (input.key === 'F12') { + child.webContents.openDevTools() + } + // } + }); + }); + // 关闭子窗口 + ipcMain.handle('closeChildWindow', (event, key) => { + if (key === 'shareScreenWindow') { + for (const k in childWindow) { + if (childWindow[k]) { + childWindow[k].close() + childWindow[k] = "" + } + } + mainWindowCenter() + } else { + childWindow[key].close() + childWindow[key] = "" + } + }); + // 设置子窗口 + ipcMain.handle('setChildWindow', (event, config) => { + switch (config.key) { + case 'shareScreenWindow': + childWindow[config.key].setBounds({ width: config.width }) + break; + case 'chatSmallWindow': + childWindow[config.key].setBounds({ height: config.height }) + break; + case 'noticeWindow': + childWindow[config.key].setBounds({ width: config.width, height: config.height }) + break; + } + }); + // 隐藏显示子窗口 + ipcMain.handle('setChildWindowShow', (event, config) => { + if (config.key === 'noticeWindow') { + if (config.bool) { + childWindow[config.key].show() + } else { + if (childWindow[config.key].isVisible()) { + childWindow[config.key].hide() + } + } + } else { + if (childWindow[config.key].isVisible()) { + childWindow[config.key].hide() + } else { + childWindow[config.key].show() + } + } + }); + // 隐藏主窗口 + ipcMain.handle('mainWindowHide', () => { + mainWindowHide() + }); + // 居中主窗口 + ipcMain.handle('mainWindowCenter', () => { + mainWindowCenter() + }); + // 窗口通信 + ipcMain.handle('windowHandleMessage', (event, data) => { + if (childWindow[data.key]) { + childWindow[data.key].webContents.send('windowHandleMessageCallBack', data) + } }); } }); @@ -401,3 +451,42 @@ function cancleDownloadUpdate() { function quitAndInstall() { autoUpdater.quitAndInstall(); } + +function windowOperation(config) { + const child = childWindow[config.key]; + const display = screen.getDisplayMatching({ ...child.getBounds() }); + const { width, height } = display.size + let x, y; + child.setResizable(false) + switch (config.key) { + case 'shareScreenWindow': + x = Math.round((display.workArea.width - child.getSize()[0]) / 2); + child.setPosition(x, 0); + mainWindowHide() + break; + case 'chatSmallWindow': + y = height - child.getSize()[1]; + child.setPosition(40, y - 200); + break; + case 'currentSpeakUserWindow': + x = width - child.getSize()[0]; + child.setPosition(x - 40, 40); + break; + case 'noticeWindow': + x = width - child.getSize()[0]; + y = height - child.getSize()[1]; + child.setPosition(x, y - 80); + break; + } +} +// 主窗口居中 +function mainWindowCenter() { + const display = screen.getDisplayMatching({ ...mainWindow.getBounds() }); + const x = Math.round((display.workArea.width - mainWindow.getSize()[0]) / 2); + const y = Math.round((display.workArea.height - mainWindow.getSize()[1]) / 2); + mainWindow.setPosition(x, y); +} +// 主窗口隐藏 +function mainWindowHide() { + mainWindow.setPosition(-999999, -999999); +} \ No newline at end of file diff --git a/preload.js b/preload.js index adecf8a..ba34b71 100644 --- a/preload.js +++ b/preload.js @@ -21,6 +21,10 @@ window.electron = { getVersion: () => { return ipcRenderer.invoke('getVersion') }, + // 获取窗口是否显示 + isVisible: () => { + return ipcRenderer.invoke('isVisible') + }, // 获取共享屏幕列表 getSources: () => { return ipcRenderer.invoke('getSources') @@ -38,8 +42,8 @@ window.electron = { ipcRenderer.on('onQuit', callback) }, // 退出房间 - quit: () => { - return ipcRenderer.invoke('quit') + quit: (bool) => { + return ipcRenderer.invoke('quit', bool) }, // 监听更新 onUpdate: (callback) => { @@ -49,6 +53,10 @@ window.electron = { quitAndInstall: (callback) => { ipcRenderer.on('quitAndInstall', callback) }, + // 点击任务栏图标是否打开窗口 + isOpenWindows: (callback) => { + ipcRenderer.on('isOpenWindows', callback) + }, // 通知下载最新的包 onDownload: (type) => { ipcRenderer.invoke('updateDownload', type) @@ -77,4 +85,32 @@ window.electron = { createChildWindow: (config) => { ipcRenderer.invoke('createChildWindow', config) }, + // 关闭子窗口 + closeChildWindow: (key) => { + ipcRenderer.invoke('closeChildWindow', key) + }, + // 设置子窗口 + setChildWindow: (config) => { + ipcRenderer.invoke('setChildWindow', config) + }, + // 隐藏主窗口 + setChildWindowShow: (config) => { + ipcRenderer.invoke('setChildWindowShow', config) + }, + // 隐藏主窗口 + mainWindowHide: () => { + ipcRenderer.invoke('mainWindowHide') + }, + // 居中主窗口 + mainWindowCenter: () => { + ipcRenderer.invoke('mainWindowCenter') + }, + // 窗口通信传参 + windowHandleMessage: (data) => { + ipcRenderer.invoke('windowHandleMessage', data) + }, + // 窗口通信回调 + windowHandleMessageCallBack: (callback) => { + ipcRenderer.on('windowHandleMessageCallBack', callback) + }, } diff --git a/src/App.tsx b/src/App.tsx index c6d6195..82e43fc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,7 +18,13 @@ import { agora } from "@/utils/package/agora"; import QuitTips from "@/components/QuitTips"; import { GetLeave } from "@/api/Meeting"; import path from "path"; -import ShareScreenWindow from "./page/ShareScreenWindow"; +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 CurrentSpeakUserWindow from "@/page/Meeting/CurrentSpeakUserWindow"; +import NoticeWindow from "@/page/Meeting/NoticeWindow"; +import { getKeyOpenChildWindow, setKeyOpenChildWindow } from "./utils/package/public"; const fs = require('fs').promises; const { exec } = require('child_process'); const App: React.FC = () => { @@ -33,7 +39,8 @@ const App: React.FC = () => { }); const [spinning, setSpinning] = useState(false); const [isState, setIsState] = useState(true); - if (location.hash.indexOf('shareScreenWindow') == -1) { + const urlHashArr = ['#/userListWindow', '#/shareScreenWindow', '#/chatSmallWindow', '#/chatBigWindow', '#/currentSpeakUserWindow', '#/noticeWindow'] + if (urlHashArr.indexOf(location.hash) == -1) { useEffect(() => { let userInfo = JSON.parse(storage.getItem('user') as string) let loginInfo = JSON.parse(storage.getItem('login') as string) @@ -97,7 +104,22 @@ const App: React.FC = () => { storage.setItem('setting', JSON.stringify(setting)) }) window.electron.quitAndInstall(async (_e: any) => { - leaveChannel() + let bool = await window.electron.isVisible() + if (bool) { + storage.setItem('quitMeeting', true) + window.electron.setViewStatus('show') + } + }) + window.electron.isOpenWindows(async (_e: any) => { + let bool = await window.electron.isVisible() + if (location.hash.indexOf('/meeting') === -1) { + window.electron.setViewStatus(bool ? 'hide' : 'show') + } else { + let shareScreenWindow = await getKeyOpenChildWindow('shareScreenWindow') + if (!shareScreenWindow) { + window.electron.setViewStatus(bool ? 'hide' : 'show') + } + } }) }, []) useEffect(() => { @@ -120,22 +142,26 @@ const App: React.FC = () => { closeSetting: 'hide', //关闭按钮设置 isAINoiseReduction: true, //是否开启ai降噪 aINoiseReduction: 1, // 降噪模式 + isRecordingTips: true, //是否开启录制提示 })) } + if (!storage.getItem('openChildWindow')) { + storage.setItem('openChildWindow', JSON.stringify({})) + } }, []) useEffect(() => { if (isState) { setIsState(false) window.electron.onQuit(async () => { if (location.hash.indexOf('/login') === 1) { - window.electron.quit() + window.electron.quit(location.hash.indexOf('/meeting') === -1) } else { if (storage.getItem('isTips') === 'true') { const setting = JSON.parse(storage.getItem('setting') as string) if (setting.closeSetting === 'hide') { window.electron.setViewStatus(setting.closeSetting) } else { - window.electron.quit() + window.electron.quit(location.hash.indexOf('/meeting') === -1) } } else { quitTipsRef.current.changeModal() @@ -208,6 +234,20 @@ const App: React.FC = () => { }; const leaveChannel = async (bool?: boolean): Promise => { if (location.hash.indexOf('/meeting') === 1) { + window.electron.closeChildWindow('shareScreenWindow') + setKeyOpenChildWindow('shareScreenWindow', false) + window.electron.setViewStatus('show') + window.electron.getWindowSize().then((res: any) => { + window.electron.setMainWindowSize({ + width: Math.ceil(res.width / 1.5), + height: Math.ceil(res.height / 1.3), + }) + window.electron.getIsMaximized().then((b: boolean) => { + if (!b) { + window.electron.setViewStatus('maximize') + } + }) + }) const data = JSON.parse(localStorage.stateInfo); if (!bool) { await GetLeave({ @@ -243,6 +283,11 @@ const App: React.FC = () => { } /> } /> } /> + } /> + } /> + } /> + } /> + } /> } /> diff --git a/src/assets/icon50.png b/src/assets/icon50.png new file mode 100644 index 0000000..9914d40 Binary files /dev/null and b/src/assets/icon50.png differ diff --git a/src/assets/icon51.png b/src/assets/icon51.png new file mode 100644 index 0000000..1b36b17 Binary files /dev/null and b/src/assets/icon51.png differ diff --git a/src/components/EquipmentManagement/index.tsx b/src/components/EquipmentManagement/index.tsx index cf20fb2..2872586 100644 --- a/src/components/EquipmentManagement/index.tsx +++ b/src/components/EquipmentManagement/index.tsx @@ -1,15 +1,21 @@ import styles from '@/components/EquipmentManagement/index.module.scss' +import { getKeyOpenChildWindow } from '@/utils/package/public'; import { onInvoke } from '@/utils/package/signalr'; import { Button, Modal, Select, Slider, message } from 'antd'; import { useState, useImperativeHandle, forwardRef } from "react"; -const EquipmentManagement = forwardRef((_props: any, ref: any) => { +const EquipmentManagement = forwardRef((props: any, ref: any) => { useImperativeHandle(ref, () => ({ changeModal: async (uid: string, userName: string) => { setCallerUid(uid) setDeviceInfo({}) - await onInvoke('getDrivers', { - uid - }) + let isOpen = await getKeyOpenChildWindow('shareScreenWindow') + if (isOpen) { + props.getDriver?.(uid) + } else { + await onInvoke('getDrivers', { + uid + }) + } setUserName(userName) setEquipmentManagementModal(true) }, @@ -21,6 +27,9 @@ const EquipmentManagement = forwardRef((_props: any, ref: any) => { const [callerUid, setCallerUid] = useState('') const [deviceInfo, setDeviceInfo] = useState({}) const [userName, setUserName] = useState({}) + const handleWindowsChange = async (): Promise => { + setEquipmentManagementModal(false) + } return ( <> { centered width={'500px'} onCancel={() => { - setEquipmentManagementModal(false) + handleWindowsChange() }}>
@@ -83,16 +92,24 @@ const EquipmentManagement = forwardRef((_props: any, ref: any) => {
+ onClick={() => handleWindowsChange()}>取消
diff --git a/src/components/JoinSetting/index.tsx b/src/components/JoinSetting/index.tsx index 6a11b2a..af7fa60 100644 --- a/src/components/JoinSetting/index.tsx +++ b/src/components/JoinSetting/index.tsx @@ -12,20 +12,26 @@ import { role } from '@/config/role'; let time = null as any; const JoinSetting = forwardRef((_props: any, ref: any) => { useImperativeHandle(ref, () => ({ - changeModal: (roomNum: string = '') => { + changeModal: async (roomNum: string = '') => { let userInfo = JSON.parse(storage.getItem('user') as string) setUser(userInfo) setJoinRoomSettingModal(true) + if (location.hash.indexOf('/meeting') === -1) { + await agora.init() + } setJoinRoomSettingForm((res: any) => { - res.forEach((item: any) => { - item.active = false + res.forEach(async (item: any, index: number) => { + if (index === 0 && role.ID.includes(userInfo.roleId)) { + await agora.getAudioMediaList().then(res => { + item.active = res.ecordingList.length ? true : false + }) + } else { + item.active = false + } }); return res }) setRoomNumber(roomNum) - if (location.hash.indexOf('/meeting') === -1) { - agora.init() - } getDeviceList() } })) diff --git a/src/components/Operation/index.tsx b/src/components/Operation/index.tsx index 2ebc3dd..fa55f90 100644 --- a/src/components/Operation/index.tsx +++ b/src/components/Operation/index.tsx @@ -1,7 +1,7 @@ import styles from '@/components/Operation/index.module.scss' import ImageUrl from '@/utils/package/imageUrl'; import { useEffect, useState } from "react"; -type OperationKeyType = 'minimize' | 'quit' | 'maximize' | 'unmaximize' | 'hide'; +type OperationKeyType = 'minimize' | 'quit' | 'maximize' | 'unmaximize' | 'hide' | 'show'; type OperationType = { icon: string; key: OperationKeyType; @@ -46,7 +46,11 @@ const Operation: React.FC = () => { key: 'quit', title: '关闭', onClick: (key: OperationKeyType) => { - window.electron.setViewStatus(key) + if (location.hash.indexOf('/meeting') === -1) { + window.electron.setViewStatus(key) + } else { + window.electron.quit(false) + } }, show: true, },]) diff --git a/src/components/QuitTips/index.tsx b/src/components/QuitTips/index.tsx index 4eaecdd..3ace7e8 100644 --- a/src/components/QuitTips/index.tsx +++ b/src/components/QuitTips/index.tsx @@ -3,7 +3,7 @@ import { storage } from '@/utils'; import { InfoCircleOutlined } from '@ant-design/icons'; import { Button, Checkbox, Modal, Radio } from 'antd'; import { useState, useImperativeHandle, forwardRef } from "react"; -type OperationKeyType = 'minimize' | 'quit' | 'maximize' | 'unmaximize' | 'hide'; +type OperationKeyType = 'minimize' | 'quit' | 'maximize' | 'unmaximize' | 'hide' | 'show'; const QuitTips = forwardRef((props: any, ref: any) => { useImperativeHandle(ref, () => ({ changeModal: () => { @@ -52,7 +52,7 @@ const QuitTips = forwardRef((props: any, ref: any) => { + + + +
+ 录制设置 +
+ { + setting.isRecordingTips = e.target.checked; storage.setItem('setting', JSON.stringify(setting)) - setFilePath(e.target.value) - }} - /> - - + setIsRecordingTips(e.target.checked) + }} checked={isRecordingTips}>开启入会录制提示 +
diff --git a/src/components/UserVideo/index.tsx b/src/components/UserVideo/index.tsx index 3a5cfc1..de403fe 100644 --- a/src/components/UserVideo/index.tsx +++ b/src/components/UserVideo/index.tsx @@ -60,7 +60,7 @@ const UserVideo: React.FC = () => { useEffect(() => { userList.forEach(async (item: any) => { - await agora.destroyRendererByConfig(Number('1' + item.screenShareId)) + await agora.destroyRendererByConfig(Number('1' + item.screenShareId), state.channelId + 'a') await agora.setupRemoteVideoEx({ uid: Number('1' + item.screenShareId), view: document.getElementById(`video-${item.screenShareId}`), diff --git a/src/page/Meeting/ChatBigWindow/index.module.scss b/src/page/Meeting/ChatBigWindow/index.module.scss new file mode 100644 index 0000000..081b30e --- /dev/null +++ b/src/page/Meeting/ChatBigWindow/index.module.scss @@ -0,0 +1,133 @@ +.chatBigWindow { + height: 100%; + width: 100%; + box-sizing: border-box; + display: flex; + flex-direction: column; + box-sizing: border-box; + padding: 10px 0; + + .chatBigWindowTitle { + background-color: #16191E; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: space-between; + box-sizing: border-box; + padding: 10px 10px; + + >span { + color: #EEEEEE; + font-size: 18px; + } + + >img { + cursor: pointer; + } + } + + .chatBigWindowContent { + flex-grow: 1; + height: 0px; + overflow-y: auto; + background-color: rgb(31, 33, 37); + padding: 10px; + + .chatBigWindowContentLeft { + display: flex; + flex-direction: column; + align-items: flex-start; + + >div:nth-child(1) { + display: flex; + align-items: center; + + >span { + font-size: 14px; + color: #F3F3F5; + margin-left: 4px; + } + + >div {} + } + + >div:nth-child(2) { + background-color: #5575F2; + color: #F3F3F5; + max-width: 266px; + padding: 10px; + box-sizing: border-box; + border-radius: 0 25px 25px 25px; + margin: 10px 0 10px 40px; + font-size: 14px; + } + } + + .chatBigWindowContentRight { + display: flex; + flex-direction: column; + align-items: flex-end; + + >div:nth-child(1) { + display: flex; + align-items: center; + flex-direction: row-reverse; + + >span { + font-size: 14px; + color: #F3F3F5; + } + + >div { + margin-left: 4px; + } + } + + >div:nth-child(2) { + background-color: #464E6B; + color: #F3F3F5; + max-width: 266px; + padding: 10px; + box-sizing: border-box; + border-radius: 25px 0 25px 25px; + margin: 10px 40px 10px 0; + font-size: 14px; + } + } + } + + .chatBigWindowButton { + display: flex; + flex-shrink: 0; + flex-wrap: wrap; + padding: 10px; + box-sizing: border-box; + background-color: rgb(31, 33, 37); + + >button { + margin: 4px 4px 0; + } + } + + .chatBigWindowInput { + flex-shrink: 0; + padding: 10px 10px; + background-color: #16191E; + display: flex; + flex-direction: column; + align-items: flex-end; + } +} + +.chatBigWindowInputPopover { + display: flex; + flex-direction: column; + + >button { + margin-bottom: 4px; + + &:last-child { + margin-bottom: 0; + } + } +} \ No newline at end of file diff --git a/src/page/Meeting/ChatBigWindow/index.tsx b/src/page/Meeting/ChatBigWindow/index.tsx new file mode 100644 index 0000000..e87418b --- /dev/null +++ b/src/page/Meeting/ChatBigWindow/index.tsx @@ -0,0 +1,284 @@ +import styles from '@/page/Meeting/ChatBigWindow/index.module.scss' +import ImageUrl from '@/utils/package/imageUrl'; +import { useEffect, useState, useRef } from "react"; +import { storage } from '@/utils'; +import { Button, Input, Modal, Popover } from 'antd'; +import { role } from '@/config/role'; +import { GetRoomUserItem } from '@/api/Meeting'; +import Avatar from '@/components/Avatar'; +import dayjs from 'dayjs'; +import { ExclamationCircleFilled } from '@ant-design/icons'; +import EquipmentManagement from '@/components/EquipmentManagement'; +const { confirm } = Modal; +const ChatBigWindow: React.FC = () => { + const [inputValue, setInputValue] = useState('') + const [chatLists, setChatLists] = useState([]) + const [user, setUser] = useState({}); + const [roomUserItem, setRoomUserItem] = useState(null) + const [commonlyChatList] = useState([ + '能听到我说话吗?', + '听得到', + '听不到', + '我要发言', + ]) + const equipmentManagementRef = useRef(); + const userInfo = JSON.parse(storage.getItem('user') as string) + const stateInfo = JSON.parse(storage.getItem('stateInfo') as string) + const channel = new BroadcastChannel('meeting_channel'); + useEffect(() => { + setUser(userInfo) + channel.onmessage = function (event) { + const { type, showDriverList } = event.data; + switch (type) { + case 'showDriverList': + equipmentManagementRef.current.setData(showDriverList) + break; + } + } + window.electron.windowHandleMessageCallBack((_e: any, data: any) => { + setChatLists(data.parmes.chatList) + }) + channel.postMessage({ + type: 'chatBigWindowSendChannelMsg', + chatBigWindowSendChannelMsg: { + msg: '', + } + }); + return () => { + channel.close(); + } + }, []); + useEffect(() => { + const chatBigWindowView = document.getElementById('chatBigWindowView') as HTMLElement; + if (chatBigWindowView) { + chatBigWindowView.scrollTop = chatBigWindowView.scrollHeight; + } + }, [chatLists]); + + return ( + <> +
+
+ 聊天 + { + window.electron.setChildWindowShow({ + key: 'chatBigWindow', + }) + }} /> +
+
+ {chatLists.map((item: any, index: number) => +
+ {role.ID.includes(user.roleId) ? { + if (e) { + GetRoomUserItem(stateInfo.channelId, item.uid).then((res: any) => { + if (res.code === 200) { + setRoomUserItem(res.data) + } + }) + } else { + setRoomUserItem(null) + } + }} + content={ + roomUserItem ?
+ {roomUserItem.isRoomManager || role.ID.includes(roomUserItem.roleId) ? : null} + {roomUserItem.uid !== user.uid && !role.ID.includes(roomUserItem.roleId) ? : null} + {roomUserItem.isRoomManager ? : null} + {roomUserItem.isRoomManager ? : null} + {roomUserItem.uid !== user.uid ? : null} + {roomUserItem.uid !== user.uid ? : null} +
:
用户不在房间内
+ }> +
+
+ {item.uid !== user.uid ? + {item.userName} {dayjs(item.timestamp).format('HH:mm:ss')} : + {dayjs(item.timestamp).format('HH:mm:ss')} {item.userName} + } + +
+
:
+
+ {item.uid !== user.uid ? + {item.userName}{dayjs(item.timestamp).format('HH:mm:ss')} : + {dayjs(item.timestamp).format('HH:mm:ss')} {item.userName} + } +
} +
{item.message}
+
+ )} +
+
+ { + commonlyChatList.map((item: string, index: number) => { + return + }) + } +
+
+ { + setInputValue(e.target.value) + }} + autoSize={{ minRows: 3, maxRows: 3 }} /> + +
+
+ { + channel.postMessage({ + type: 'userListWindowEquipmentManagement', + userListWindowEquipmentManagement: { + uid + } + }); + }} setDriver={(data: any) => { + channel.postMessage({ + type: 'userListWindowSetEquipmentManagement', + userListWindowSetEquipmentManagement: data + }); + }} /> + + ) +} + +export default ChatBigWindow diff --git a/src/page/Meeting/ChatSmallWindow/index.module.scss b/src/page/Meeting/ChatSmallWindow/index.module.scss new file mode 100644 index 0000000..5f5f3e9 --- /dev/null +++ b/src/page/Meeting/ChatSmallWindow/index.module.scss @@ -0,0 +1,96 @@ +.chatSmallWindow { + color: white; + width: 100%; + display: flex; + flex-direction: column; + height: 100%; + position: relative; + + >div:nth-child(1) { + flex-grow: 1; + overflow-y: hidden; + max-height: 80%; + padding: 0 4px; + box-sizing: border-box; + display: flex; + background-color: rgba(40, 40, 44, .4); + flex-direction: column-reverse; + + .chatSmallWindowContentLeft { + display: flex; + flex-direction: column; + align-items: flex-start; + + >div { + background-color: #1e232baf; + padding: 4px; + box-sizing: border-box; + border-radius: 0 15px 15px 15px; + margin: 0 0 4px 0; + font-size: 12px; + display: flex; + + >span:nth-child(1) { + white-space: nowrap; + color: #ff970f; + } + + >span:nth-child(2) { + color: #fff; + } + } + } + + .chatSmallWindowContentRight { + display: flex; + flex-direction: column; + align-items: flex-end; + + >div { + background-color: #464e6b55; + color: black; + padding: 4px; + box-sizing: border-box; + border-radius: 15px 0 15px 15px; + margin: 0 0 4px 0; + font-size: 14px; + display: flex; + flex-direction: row-reverse; + + >span:nth-child(1) { + white-space: nowrap; + } + } + } + } + + >div:nth-child(2) { + flex-shrink: 0; + opacity: 0.4; + + &:hover { + opacity: 1; + } + } + + >div:nth-child(3) { + position: absolute; + left: 50%; + top: 0px; + transform: translate(-50%, 0); + display: flex; + align-items: center; + margin: 0 auto; + background-color: rgba(40, 40, 44, .2); + padding: 0 6px; + cursor: pointer; + + &:hover { + background-color: rgba(40, 40, 44, 1); + } + + >span { + font-size: 12px; + } + } +} \ No newline at end of file diff --git a/src/page/Meeting/ChatSmallWindow/index.tsx b/src/page/Meeting/ChatSmallWindow/index.tsx new file mode 100644 index 0000000..4cd2d41 --- /dev/null +++ b/src/page/Meeting/ChatSmallWindow/index.tsx @@ -0,0 +1,101 @@ +import styles from '@/page/Meeting/ChatSmallWindow/index.module.scss' +import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons'; +import { Input } from 'antd'; +import { useEffect, useState } from "react"; +const ChatSmallWindow: React.FC = () => { + const [inputValue, setInputValue] = useState('') + const [chatLists, setChatLists] = useState([]) + const [isExpand, setIsExpand] = useState(false) + const channel = new BroadcastChannel('meeting_channel'); + useEffect(() => { + window.electron.windowHandleMessageCallBack((_e: any, data: any) => { + setChatLists((newChatList: any) => { + data.parmes.chatListIten.timer = setTimeout(() => { + removeItemByIndex(); + }, 10000); + return [data.parmes.chatListIten, ...newChatList] + }) + }) + return () => { + channel.close(); + } + }, []); + useEffect(() => { + const chatSmallWindowView = document.getElementById('chatSmallWindowView') as HTMLElement; + if (chatSmallWindowView) { + chatSmallWindowView.scrollTop = chatSmallWindowView.scrollHeight; + } + }, [chatLists]); + const removeItemByIndex = () => { + setChatLists((res: any) => { + return res.slice(0, -1) + }) + } + + return ( + <> +
+
+ {chatLists.map((item: any, index: number) => +
+
+ {item.userName}: + {item.message} +
+
+ )} +
+
+ { + setInputValue(e.target.value) + }} + onPressEnter={() => { + if (inputValue) { + channel.postMessage({ + type: 'chatSmallWindowSendChannelMsg', + chatSmallWindowSendChannelMsg: { + msg: inputValue, + } + }); + setInputValue('') + } + }} + suffix={ + { + if (inputValue) { + channel.postMessage({ + type: 'chatSmallWindowSendChannelMsg', + chatSmallWindowSendChannelMsg: { + msg: inputValue, + } + }); + setInputValue('') + } + }} + >发送 + + } + /> +
+
{ + setIsExpand(!isExpand) + window.electron.setChildWindow({ + height: isExpand ? 150 : 150 / 2, + key: 'chatSmallWindow', + }) + }}> + {isExpand ? '展开' : '收起'} + {isExpand ? : } +
+
+ + ) +} + +export default ChatSmallWindow diff --git a/src/page/Meeting/CurrentSpeakUserWindow/index.module.scss b/src/page/Meeting/CurrentSpeakUserWindow/index.module.scss new file mode 100644 index 0000000..4b7bf32 --- /dev/null +++ b/src/page/Meeting/CurrentSpeakUserWindow/index.module.scss @@ -0,0 +1,19 @@ +.currentSpeakUserWindow { + color: white; + width: 100%; + display: flex; + flex-direction: column; + height: 100%; + background-color: #16191E; + padding: 4px; + box-sizing: border-box; + + >div { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + max-width: 100%; + width: fit-content; + font-size: 14px; + } +} \ No newline at end of file diff --git a/src/page/Meeting/CurrentSpeakUserWindow/index.tsx b/src/page/Meeting/CurrentSpeakUserWindow/index.tsx new file mode 100644 index 0000000..64fbf8a --- /dev/null +++ b/src/page/Meeting/CurrentSpeakUserWindow/index.tsx @@ -0,0 +1,36 @@ +import styles from '@/page/Meeting/CurrentSpeakUserWindow/index.module.scss' +import { useEffect, useState } from "react"; +const CurrentSpeakUserWindow: React.FC = () => { + const [inputValue, setInputValue] = useState('') + const channel = new BroadcastChannel('meeting_channel'); + useEffect(() => { + let time: NodeJS.Timeout; + time = setInterval(() => { + channel.postMessage({ + type: 'currentSpeakUserWindowGetUserName' + }) + }, 1000) + window.electron.windowHandleMessageCallBack((_e: any, data: any) => { + if (data.parmes.currentSpeakUser.length) { + setInputValue(data.parmes.currentSpeakUser.join(';')) + } else { + setInputValue('') + } + }) + return () => { + clearInterval(time) + } + }, []); + + return ( + <> +
+
+ {`正在说话: ${inputValue}`} +
+
+ + ) +} + +export default CurrentSpeakUserWindow diff --git a/src/page/Meeting/NoticeWindow/index.module.scss b/src/page/Meeting/NoticeWindow/index.module.scss new file mode 100644 index 0000000..2264327 --- /dev/null +++ b/src/page/Meeting/NoticeWindow/index.module.scss @@ -0,0 +1,6 @@ +.noticeWindow { + height: 100%; + width: 100%; + box-sizing: border-box; + background-color: transparent; +} \ No newline at end of file diff --git a/src/page/Meeting/NoticeWindow/index.tsx b/src/page/Meeting/NoticeWindow/index.tsx new file mode 100644 index 0000000..61d4c56 --- /dev/null +++ b/src/page/Meeting/NoticeWindow/index.tsx @@ -0,0 +1,100 @@ +import styles from '@/page/Meeting/NoticeWindow/index.module.scss' +import { Button, notification } from 'antd'; +import { useEffect } from "react"; + +const NoticeWindow: React.FC = () => { + const [api, contextHolder] = notification.useNotification({ + stack: { + threshold: 3 + } + }); + const channel = new BroadcastChannel('meeting_channel'); + useEffect(() => { + window.electron.setChildWindowShow({ + key: 'noticeWindow', + bool: false + }) + channel.onmessage = function (event) { + const { type, noticeItem } = event.data; + switch (type) { + case 'noticeItem': + api.open({ + message: '', + description:
+ {noticeItem.uname}申请发言 +
+ + +
+
, + onClose: () => { + const dom = document.getElementsByClassName('ant-notification') + if (dom.length === 0) { + window.electron.setChildWindowShow({ + key: 'noticeWindow', + bool: false + }) + } + }, + duration: 10, + placement: 'bottomRight', + showProgress: true, + pauseOnHover: false, + }); + break; + } + } + return () => { + channel.close(); + }; + }, []); + return ( + <> +
+ {contextHolder} +
+ + ) +} + +export default NoticeWindow diff --git a/src/page/Meeting/ShareScreenWindow/index.module.scss b/src/page/Meeting/ShareScreenWindow/index.module.scss new file mode 100644 index 0000000..d93e050 --- /dev/null +++ b/src/page/Meeting/ShareScreenWindow/index.module.scss @@ -0,0 +1,85 @@ +.shareScreenWindow { + color: white; + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + + .shareScreenWindowTitle { + font-size: 12px; + flex-shrink: 0; + padding: 4px; + box-sizing: border-box; + background-color: lighten(#07090B, 4%); + display: flex; + justify-content: space-between; + align-items: center; + + >span:nth-child(2) { + cursor: pointer; + background-color: #FF5219; + padding: 1px 10px; + border-radius: 2px; + font-size: 12px; + } + } + + .shareScreenWindowContent { + flex-grow: 1; + align-items: center; + justify-content: space-between; + background-color: #07090B; + display: flex; + + .shareScreenWindowContentList { + display: flex; + flex-grow: 1; + justify-content: space-between; + + >div { + display: flex; + flex-direction: column; + align-items: center; + cursor: pointer; + width: calc(100% / 6); + + >div { + position: relative; + + >img { + height: 24px; + width: 24px; + } + + >div { + position: absolute; + left: 0; + bottom: 0; + background-repeat: no-repeat; + background-position: bottom center; + background-size: cover; + width: 100%; + height: 0%; + } + } + + >span { + font-size: 10px; + } + } + } + } + + .shareScreenWindowExpand { + display: flex; + align-items: center; + margin: 0 auto; + background-color: lighten(#07090B, 4%); + padding: 0 6px; + cursor: pointer; + + >span { + font-size: 12px; + } + } +} \ No newline at end of file diff --git a/src/page/Meeting/ShareScreenWindow/index.tsx b/src/page/Meeting/ShareScreenWindow/index.tsx new file mode 100644 index 0000000..5a29c8a --- /dev/null +++ b/src/page/Meeting/ShareScreenWindow/index.tsx @@ -0,0 +1,212 @@ +import { GetRoomUser } from '@/api/Meeting'; +import { role } from '@/config/role'; +import styles from '@/page/Meeting/ShareScreenWindow/index.module.scss' +import { storage } from '@/utils'; +import ImageUrl from '@/utils/package/imageUrl'; +import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons'; +import { Button } from 'antd'; +import dayjs from 'dayjs'; +import { useEffect, useState } from "react"; +const ShareScreenWindow: React.FC = () => { + const [footerLists, setFooterLists] = useState([ + { + title: '静音', + icon: ImageUrl.icon22, + iconActive: ImageUrl.icon22Active, + active: false, + select: false, + }, + { + title: '关闭视频', + icon: ImageUrl.icon23, + iconActive: ImageUrl.icon23Active, + active: false, + select: false, + }, + { + title: '成员', + icon: ImageUrl.icon30, + iconSelect: ImageUrl.icon30Select, + active: false, + select: false, + }, + { + title: '聊天', + icon: ImageUrl.icon31, + iconSelect: ImageUrl.icon31Select, + active: false, + select: false, + }, + { + title: '录制', + icon: ImageUrl.icon27, + iconSelect: ImageUrl.icon27Select, + iconActive: ImageUrl.icon27Active, + active: false, + select: false, + }, + ]) + const [timeStr, setTimeStr] = useState(0) + const [isExpand, setIsExpand] = useState(false) + const [roomUserLists, setRoomUserLists] = useState([]) + const channel = new BroadcastChannel('meeting_channel'); + const userInfo = JSON.parse(storage.getItem('user') as string) + let timeout: NodeJS.Timeout; + useEffect(() => { + getRoomUser() + channel.onmessage = function (event) { + let { type, time } = event.data; + switch (type) { + case 'time': + setTimeStr(time) + timeout = setInterval(() => { + setTimeStr(time++) + }, 1000) + break; + } + } + channel.postMessage({ + type: 'shareScreenWindowGetTime' + }); + window.electron.windowHandleMessageCallBack((_e: any, data: any) => { + switch (data.parmes.type) { + case 'currentSpeakUserMe': + let domMe = document.getElementById(`micr-item-${userInfo.uid}`) as HTMLDivElement; + if (domMe) { + domMe.style.height = `${data.parmes.currentSpeakUserMe}%` + } + break; + case 'footerList': + const footerListTemplate = [...footerLists]; + footerListTemplate[0].title = data.parmes.footerList[0][0].active ? '解除静音' : '静音'; + footerListTemplate[0].active = data.parmes.footerList[0][0].active; + footerListTemplate[1].title = data.parmes.footerList[0][1].active ? '开启视频' : '关闭视频'; + footerListTemplate[1].active = data.parmes.footerList[0][1].active; + footerListTemplate[4].title = data.parmes.footerList[1][3].active ? '录制中' : '录制'; + footerListTemplate[4].active = data.parmes.footerList[1][3].active; + setFooterLists(footerListTemplate) + break; + case 'roomUserList': + setRoomUserLists(data.parmes.roomUserList) + break; + } + }) + return () => { + clearInterval(timeout) + channel.close(); + }; + }, []); + const changeCurrentSeconds = (time: number): string => { + const duration = dayjs.duration(time, 'seconds'); + const hours = duration.hours(); // 整数小时 + const minutes = duration.minutes(); // 整数分钟 + const secondsRemaining = duration.seconds(); // 剩余的秒数 + return `${hours > 9 ? hours : '0' + hours}:${minutes > 9 ? minutes : '0' + minutes}:${secondsRemaining > 9 ? secondsRemaining : '0' + secondsRemaining}` + } + // 获取房间用户 + const getRoomUser = async (): Promise => { + const data = JSON.parse(storage.getItem('stateInfo') as string) + await GetRoomUser(data.channelId).then(res => { + if (res.code === 200) { + res.data.forEach((item: any) => { + item.isShow = true; + item.isRoom = true; + item.isAdmin = role.ID.includes(item.roleId) || item.isRoomManager + }) + setRoomUserLists(res.data) + channel.postMessage({ + type: 'shareScreenWindowGetFooterLists', + }); + } + }) + } + // 底部按钮点击效果 + const changeFooterListSelect = (item: any, index: number, bool: boolean): void => { + let arr = ['静音', '解除静音', '关闭视频', '开启视频'] + if (arr.indexOf(item.title) === -1) { + const footerListTemplate = [...footerLists] + footerListTemplate[index].select = bool; + setFooterLists(footerListTemplate) + } + } + return ( + <> +
+
+ {changeCurrentSeconds(timeStr)} 共享中 + {isExpand ? { + channel.postMessage({ + type: 'shareScreenWindowClose', + shareScreenWindowClose: timeStr + }); + }}>结束共享 : 结束共享} +
+ {isExpand ? null :
+
+ {footerLists.map((item: any, index: number) => { + return ( +
changeFooterListSelect(item, index, true)} + onMouseUp={() => changeFooterListSelect(item, index, false)} + onMouseLeave={() => changeFooterListSelect(item, index, false)} + onClick={async () => { + switch (item.title) { + case '静音': + case '解除静音': + case '关闭视频': + case '开启视频': + case '录制': + case '录制中': + channel.postMessage({ + type: 'shareScreenWindowfooterListsTitle', + shareScreenWindowfooterListsTitle: item.title + }); + break; + case '聊天': + window.electron.setChildWindowShow({ + key: 'chatBigWindow', + }) + break; + case '成员': + window.electron.setChildWindowShow({ + key: 'userListWindow', + }) + break; + } + }} + key={index}> +
+ {!item.active ?
+
: ''} + {item.select ? : } +
+ {item.title}{item.title === '成员' ? `(${roomUserLists.filter((item: any) => item.isRoom).length})` : ''} +
+ ) + })} +
+ +
} +
{ + setIsExpand(!isExpand) + window.electron.setChildWindow({ + width: isExpand ? 440 : 440 / 2, + key: 'shareScreenWindow', + }) + }}> + {isExpand ? '展开' : '收起'} + {isExpand ? : } +
+
+ + ) +} + +export default ShareScreenWindow diff --git a/src/page/Meeting/UserListWindow/index.module.scss b/src/page/Meeting/UserListWindow/index.module.scss new file mode 100644 index 0000000..2cc7677 --- /dev/null +++ b/src/page/Meeting/UserListWindow/index.module.scss @@ -0,0 +1,120 @@ +.userListWindow { + height: 100%; + width: 100%; + box-sizing: border-box; + background-color: #16191E; + display: flex; + flex-direction: column; + box-sizing: border-box; + padding: 10px 0; + + >div { + margin-bottom: 10px; + + &:last-child { + margin-bottom: 0px; + } + } + + .userListWindowTitle { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: space-between; + box-sizing: border-box; + padding: 0 10px; + + >span { + color: #EEEEEE; + font-size: 18px; + } + + >img { + cursor: pointer; + } + } + + .userListWindowContent { + flex-grow: 1; + height: 0px; + overflow-y: auto; + + >div { + display: flex; + align-items: center; + justify-content: space-between; + position: relative; + box-sizing: border-box; + padding: 4px 10px; + + >div:nth-child(1) { + display: flex; + align-items: center; + + >span { + font-size: 14px; + color: #F3F3F5; + margin-left: 4px; + } + + >div { + flex-shrink: 0; + } + } + + >div:nth-child(2) { + display: flex; + align-items: center; + + >div { + height: 30px; + width: 30px; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + + >img { + width: 20px; + } + + &:hover { + background-color: rgba(0, 0, 0, 0.5); + } + } + } + + &:hover { + background-color: rgb(52, 52, 52); + } + } + } + + .userListWindowFooter { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + + >div { + font-size: 14px; + cursor: pointer; + background-color: #31353A; + color: #EEEEEE; + width: 104px; + height: 30px; + line-height: 30px; + text-align: center; + border-radius: 5px; + + &:hover { + background-color: lighten(#31353A, 4%); + } + + &:active { + background-color: darken(#31353A, 4%); + } + } + } +} \ No newline at end of file diff --git a/src/page/Meeting/UserListWindow/index.tsx b/src/page/Meeting/UserListWindow/index.tsx new file mode 100644 index 0000000..14fd1d9 --- /dev/null +++ b/src/page/Meeting/UserListWindow/index.tsx @@ -0,0 +1,212 @@ +import { role } from '@/config/role'; +import styles from '@/page/Meeting/UserListWindow/index.module.scss' +import ImageUrl from '@/utils/package/imageUrl'; +import { EllipsisOutlined, ExclamationCircleFilled, SearchOutlined } from '@ant-design/icons'; +import { Button, Input, Modal, Popover } from 'antd'; +import Avatar from '@/components/Avatar'; +import { useEffect, useState, useRef } from "react"; +import { storage } from '@/utils'; +import EquipmentManagement from '@/components/EquipmentManagement'; +const { confirm } = Modal; + +const UserListWindow: React.FC = () => { + const [userSearchValue, setUserSearchValue] = useState('') + const [user, setUser] = useState({}); + const [roomUserList, setRoomUserList] = useState([]) + const equipmentManagementRef = useRef(); + const channel = new BroadcastChannel('meeting_channel'); + const userInfo = JSON.parse(storage.getItem('user') as string) + useEffect(() => { + setUser(userInfo) + channel.onmessage = function (event) { + const { type, showDriverList } = event.data; + switch (type) { + case 'showDriverList': + equipmentManagementRef.current.setData(showDriverList) + break; + } + } + channel.postMessage({ + type: 'userListWindowGetRoomUserList' + }); + window.electron.windowHandleMessageCallBack((_e: any, data: any) => { + switch (data.parmes.type) { + case 'roomUserList': + setRoomUserList(data.parmes.roomUserList) + break; + } + }) + return () => { + channel.close(); + } + }, []); + return ( + <> +
+
+ 成员列表 + { + window.electron.setChildWindowShow({ + key: 'userListWindow', + }) + }} /> +
+
+ } + value={userSearchValue} + onChange={(e) => { + setUserSearchValue(e.target.value) + const newRoomUserList = [...roomUserList] + newRoomUserList.forEach(row => { + if (e.target.value) { + if (row.userName.indexOf(e.target.value) !== -1) { + row.isShow = true; + } else { + row.isShow = false; + } + } else { + row.isShow = true; + } + }); + setRoomUserList(newRoomUserList) + }} + /> +
+
+ {roomUserList.map((item: any, index: number) => { + return ( + item.isShow && item.isRoom ?
+
+
+ + {item.userName}{item.uid === user.uid ? '(我)' : ''} + {role.ID.includes(item.roleId) || item.isRoomManager ? + + {role.ID.includes(item.roleId) ? '管理员' : '发言人'} + + : null} + +
+
+ {role.ID.includes(item.roleId) || item.isRoomManager ?
+ { + channel.postMessage({ + type: 'userListWindowPostOpenMicr', + userListWindowPostOpenMicr: { + enableMicr: !item.enableMicr, + uid: item.uid + } + }); + }} title={item.enableMicr ? '静音' : '解除静音'} /> +
: null} + {role.ID.includes(item.roleId) || item.isRoomManager ?
+ { + channel.postMessage({ + type: 'userListWindowPostOpenCamera', + userListWindowPostOpenCamera: { + enableCamera: !item.enableCamera, + uid: item.uid + } + }); + }} title={item.enableCamera ? '关闭视频' : '开启视频'} /> +
: null} + {item.uid !== user.uid && role.ID.includes(user.roleId) ?
+ + + +
+ }> + + +
: null} + {item.uid !== user.uid && !role.ID.includes(item.roleId) && role.ID.includes(user.roleId) ?
{ + if (item.isRoomManager) { + channel.postMessage({ + type: 'userListWindowDeleteRoomManager', + userListWindowDeleteRoomManager: { + uid: item.uid + } + }); + } else { + channel.postMessage({ + type: 'userListWindowPostRoomManager', + userListWindowPostRoomManager: { + uid: item.uid + } + }); + } + }}> + {!item.isRoomManager ? + : + } +
: null} +
+
: null + ) + } + )} +
+
+
{ + channel.postMessage({ + type: 'userListWindowAllPostOpenMicr' + }); + }}>全员静音
+
+ + { + channel.postMessage({ + type: 'userListWindowEquipmentManagement', + userListWindowEquipmentManagement: { + uid + } + }); + }} setDriver={(data: any) => { + channel.postMessage({ + type: 'userListWindowSetEquipmentManagement', + userListWindowSetEquipmentManagement: data + }); + }} /> + + ) +} + +export default UserListWindow diff --git a/src/page/Meeting/index.module.scss b/src/page/Meeting/index.module.scss index 262c256..cbb972d 100644 --- a/src/page/Meeting/index.module.scss +++ b/src/page/Meeting/index.module.scss @@ -937,6 +937,7 @@ width: 144px; height: 94px; box-sizing: border-box; + border: 5px solid transparent; >img { width: 100%; @@ -953,14 +954,16 @@ &:hover { >div { - border: 1px solid #EBEBEB; + border: 5px solid yellow; + box-sizing: border-box; } } } .active { >div { - border: 1px solid #EBEBEB; + border: 5px solid yellow; + box-sizing: border-box; } } } diff --git a/src/page/Meeting/index.tsx b/src/page/Meeting/index.tsx index d2a6964..5b77b7f 100644 --- a/src/page/Meeting/index.tsx +++ b/src/page/Meeting/index.tsx @@ -14,7 +14,7 @@ import { agora } from '@/utils/package/agora' import { onInvoke, onSignalr, offSignalr } from '@/utils/package/signalr'; import dayjs from 'dayjs'; import durationPlugin from 'dayjs/plugin/duration'; -import { AudioVolumeInfo, ConnectionChangedReasonType, ConnectionStateType, QualityType, RtcConnection, RtcStats, UserOfflineReasonType, VideoSourceType, VideoStreamType } from 'agora-electron-sdk'; +import { AudioVolumeInfo, ConnectionChangedReasonType, ConnectionStateType, LocalVideoStreamReason, LocalVideoStreamState, QualityType, RtcConnection, RtcStats, UserOfflineReasonType, VideoSourceType, VideoStreamType } from 'agora-electron-sdk'; import Avatar from '@/components/Avatar'; import SharedFilesModel from '@/components/SharedFilesModel'; import StupWizard from '@/components/StupWizard'; @@ -22,6 +22,7 @@ import EquipmentManagement from '@/components/EquipmentManagement'; import UserVideo from '@/components/UserVideo'; import { role } from '@/config/role'; import { fixWebmDuration } from "webm-duration-fix-buffer"; +import { getKeyOpenChildWindow, setKeyOpenChildWindow } from '@/utils/package/public'; const { confirm } = Modal; const { exec } = require('child_process'); const fs = require('fs').promises; @@ -41,6 +42,7 @@ const Meeting: React.FC = () => { userVideo: false, }) const [isSharedScreenModal, setIsSharedScreenModal] = useState(false); + const [quitMeetingModal, setQuitMeetingModal] = useState(false); const [user, setUser] = useState({}); const [sharedScreenList, setSharedScreenList] = useState([]); const [sharedScreenItem, setSharedScreenItem] = useState(''); @@ -48,17 +50,17 @@ const Meeting: React.FC = () => { const [footerList, setFooterList] = useState([ [ { - title: '静音', + title: '解除静音', icon: ImageUrl.icon22, iconActive: ImageUrl.icon22Active, - active: false, + active: true, select: false, }, { - title: '关闭视频', + title: '开启视频', icon: ImageUrl.icon23, iconActive: ImageUrl.icon23Active, - active: false, + active: true, select: false, }, { @@ -142,6 +144,7 @@ const Meeting: React.FC = () => { rowIndex: 0, }); const [roomUserList, setRoomUserList] = useState([]) + const [_speackUid, setSpeackUid] = useState([]) const [chatList, setChatList] = useState([]) const [currentVideoId, setCurrentVideoId] = useState('') let [currentSeconds, setCurrentSeconds] = useState(0) @@ -151,7 +154,8 @@ const Meeting: React.FC = () => { text: '网络质量极好。' }) const [networkOther, setNetworkOther] = useState({}) - const [isComputerAudio, setIsComputerAudio] = useState(true) + const [isComputerAudio, setIsComputerAudio] = useState(false) + const [_isScreenCapture, setIsScreenCapture] = useState(false) const [isFluencyPriority, setIsFluencyPriority] = useState(false) const [open, setOpen] = useState(false) const [modeOpen, setModeOpen] = useState(false) @@ -184,46 +188,265 @@ const Meeting: React.FC = () => { const [observer, setObserver] = useState() let userInfo = JSON.parse(storage.getItem('user') as string) const msgTips = '您不是管理员或发言人,无法开启此功能!' + const channel = new BroadcastChannel('meeting_channel'); useEffect(() => { let time: NodeJS.Timeout; - // let getDesktopCapturerVideoTime: NodeJS.Timeout; setUser(userInfo) window.electron.getIsMaximized().then((res: boolean) => { if (!res) { window.electron.setViewStatus('maximize') } }) + setKeyOpenChildWindow('shareScreenWindow', false) setMeetingMode('StandardMode'); agoraInit() storage.setItem('noViewChatList', 0) window.addEventListener('customStorageChange', handleCustomStorageChange); const container = document.getElementById('videoView') as HTMLElement; container.addEventListener('wheel', handleWheelChange); + channel.onmessage = async function (event) { + const { + type, + shareScreenWindowfooterListsTitle, + shareScreenWindowClose, + userListWindowPostOpenMicr, + userListWindowPostOpenCamera, + userListWindowDeleteRoomManager, + userListWindowPostRoomManager, + userListWindowGetRoomKickout, + userListWindowEquipmentManagement, + userListWindowSetEquipmentManagement, + chatSmallWindowSendChannelMsg, + chatBigWindowSetAllUserLook, + chatBigWindowDeleteRoomManager, + chatBigWindowPostRoomManager, + chatBigWindowPostOpenMicr, + chatBigWindowPostOpenCamera, + chatBigWindowGetRoomKickout, + chatBigWindowSendChannelMsg, + noticeWindowPostRoomManager + } = event.data; + switch (type) { + case 'shareScreenWindowGetTime': + setCurrentSeconds((res => { + channel.postMessage({ + type: 'time', + time: res, + }); + return res + })) + break; + case 'shareScreenWindowClose': + setCurrentSeconds(shareScreenWindowClose) + await stopScreenCapture() + await allUserLook(userInfo.uid, userInfo.userName) + break; + case 'shareScreenWindowfooterListsTitle': + switch (shareScreenWindowfooterListsTitle) { + case '静音': + case '解除静音': + changeStatusList({ + title: shareScreenWindowfooterListsTitle + }, 0, 1) + break; + case '关闭视频': + case '开启视频': + changeStatusList({ + title: shareScreenWindowfooterListsTitle + }, 0, 2) + break; + case '录制': + case '录制中': + changeStatusList({ + title: shareScreenWindowfooterListsTitle + }, 1, 3) + break; + } + break; + case 'shareScreenWindowGetFooterLists': + setFooterList((res: any) => { + window.electron.windowHandleMessage({ + key: 'shareScreenWindow', + parmes: { + footerList: res, + type: 'footerList' + } + }) + return res + }) + break; + case 'userListWindowEquipmentManagement': + await onInvoke('getDrivers', { + uid: userListWindowEquipmentManagement.uid, + }) + break; + case 'userListWindowSetEquipmentManagement': + await onInvoke('setDrivers', { + uid: userListWindowSetEquipmentManagement.uid, + driversJsonString: userListWindowSetEquipmentManagement.driversJsonString + }) + break; + case 'userListWindowPostOpenMicr': + postOpenMicr(userListWindowPostOpenMicr.enableMicr, userListWindowPostOpenMicr.uid) + break; + case 'userListWindowPostOpenCamera': + postOpenCamera(userListWindowPostOpenCamera.enableCamera, userListWindowPostOpenCamera.uid) + break; + case 'userListWindowDeleteRoomManager': + DeleteRoomManager({ + roomId: state.roomId, + roomNum: state.channelId, + userId: userListWindowDeleteRoomManager.uid + }) + break; + case 'userListWindowPostRoomManager': + postRoomManager({ + roomId: state.roomId, + roomNum: state.channelId, + userId: userListWindowPostRoomManager.uid + }) + break; + case 'userListWindowGetRoomUserList': + setRoomUserList(((res: any) => { + window.electron.windowHandleMessage({ + key: 'userListWindow', + parmes: { + roomUserList: res, + type: 'roomUserList' + } + }) + return res + })) + break; + break; + case 'userListWindowGetRoomKickout': + GetRoomKickout(state.channelId, userListWindowGetRoomKickout.uid) + break; + case 'userListWindowAllPostOpenMicr': + postOpenMicr(false, userInfo.id, true) + break; + case 'chatSmallWindowSendChannelMsg': + sendMsg(chatSmallWindowSendChannelMsg.msg) + break; + case 'chatBigWindowSetAllUserLook': + setAllUserLook(chatBigWindowSetAllUserLook.roomUserItem) + break; + case 'chatBigWindowDeleteRoomManager': + DeleteRoomManager({ + roomId: state.roomId, + roomNum: state.channelId, + userId: chatBigWindowDeleteRoomManager.uid + }) + break; + case 'chatBigWindowPostRoomManager': + postRoomManager({ + roomId: state.roomId, + roomNum: state.channelId, + userId: chatBigWindowPostRoomManager.uid + }) + break; + case 'chatBigWindowPostOpenMicr': + postOpenMicr(chatBigWindowPostOpenMicr.enableMicr, chatBigWindowPostOpenMicr.uid) + break; + case 'chatBigWindowPostOpenCamera': + postOpenCamera(chatBigWindowPostOpenCamera.enableCamera, chatBigWindowPostOpenCamera.uid) + break; + case 'chatBigWindowGetRoomKickout': + GetRoomKickout(state.channelId, chatBigWindowGetRoomKickout.uid) + break; + case 'chatBigWindowSendChannelMsg': + if (chatBigWindowSendChannelMsg.msg) { + sendMsg(chatBigWindowSendChannelMsg.msg) + } else[ + setChatList((res: any) => { + window.electron.windowHandleMessage({ + key: 'chatBigWindow', + parmes: { + chatList: res, + } + }) + return res + }) + ] + break; + case 'noticeWindowPostRoomManager': + postRoomManager({ + roomId: state.roomId, + roomNum: state.channelId, + userId: noticeWindowPostRoomManager.uid + }) + break; + case 'currentSpeakUserWindowGetUserName': + setSpeackUid((uids: any) => { + const usernames: string[] = []; + setRoomUserList((res: any) => { + uids.forEach((uid: any) => { + const user = res.find((item: any) => item.uid == uid); + if (user) { + usernames.push(user.userName); + } + }) + window.electron.windowHandleMessage({ + key: 'currentSpeakUserWindow', + parmes: { + currentSpeakUser: usernames, + } + }) + return res + }); + return [] + }) + break; + } + } time = setInterval(() => { - setCurrentSeconds(currentSeconds++) + setCurrentSeconds(currentSeconds => { + return currentSeconds += 1 + }) }, 1000) - // getDesktopCapturerVideoTime = setInterval(() => { - // setSharedScreenItem((i: any) => { - // if (i && i.type === 0) { - // agora.getDesktopCapturerVideo({ width: 0, height: 0 }, { width: 0, height: 0 }, false).then(res => { - // if (res.length) { - // let row = res.find((item: any) => item.sourceId === i.sourceId) - // if (!row) { - // stopScreenCapture() - // setSharedScreenItem('') - // allUserLook(userInfo.uid, userInfo.userName) - // } - // } - // }) - // } - // return i - // }) - // }, 3000) + // 首次加载图标更新 + const firstFooterList = [...footerList] + firstFooterList[0][0].title = state.enableMicr ? '静音' : '解除静音' + firstFooterList[0][0].active = !state.enableMicr + firstFooterList[0][1].title = state.enableCamera ? '关闭视频' : '开启视频' + firstFooterList[0][1].active = !state.enableCamera + agora.muteLocalVideoStream(userInfo, state.enableMicr, state.enableCamera) + setFooterList(firstFooterList) + setTimeout(async () => { + const setting = await JSON.parse(storage.getItem('setting') as string); + const stateInfo = await JSON.parse(storage.getItem('stateInfo') as string); + if (stateInfo && setting.isRecordingTips && !recorder) { + setRecorder((data: any) => { + if (!data) { + confirm({ + title: '提示', + icon: , + content: `是否录制本次会议?`, + centered: true, + okText: '确定', + cancelText: '取消', + async onOk() { + if (stateInfo) { + changeStatusList({ + title: '录制' + }, 1, 3) + } else { + message.error('当前不在会议室!') + } + }, + onCancel() { + } + }) + } + return data + }) + } + }, 10000); return () => { window.removeEventListener('customStorageChange', handleCustomStorageChange); window.removeEventListener('wheel', handleWheelChange); clearInterval(time) - // clearInterval(getDesktopCapturerVideoTime) + channel.close(); }; }, []); @@ -262,6 +485,17 @@ const Meeting: React.FC = () => { } }, [currentEffective]); + useEffect(() => { + if (chatList.length) { + window.electron.windowHandleMessage({ + key: 'chatBigWindow', + parmes: { + chatList, + } + }) + } + }, [chatList]); + useEffect(() => { let currentVideoUserItem = roomUserList.find((item: any) => item.uid === currentVideoId || item.screenShareId === currentVideoId) if (currentVideoUserItem) { @@ -312,6 +546,12 @@ const Meeting: React.FC = () => { setNoViewChatList(storageNoViewChatList) } setChatList((newChatList: any) => [...newChatList, item]) + window.electron.windowHandleMessage({ + key: 'chatSmallWindow', + parmes: { + chatListIten: item, + } + }) setStatusList((res: any) => { if (!res.userChatList) { api.open({ @@ -407,14 +647,14 @@ const Meeting: React.FC = () => { message.success(`操作成功`) await agora.updateChannelMediaOptions(item.user.isRoomManager) await postOpenMicrApi(item.user.isRoomManager, userInfo.uid, false) - await postOpenCameraApi(item.user.isRoomManager, userInfo.uid) + await postOpenCameraApi(false, userInfo.uid) // 不管身份如何改变都关闭摄像头 await stopScreenCapture() } else { message.success(`${item.user.userName}已结束发言`) } } else { if (item.user.uid === userInfo.uid) { - if (!item.user.isRoomManager) { + if (item.user.isRoomManager) { await agora.allLeaveChannelEx() } message.success(`管理员${item.user.isRoomManager ? '设置' : '取消'}您为发言人`) @@ -426,7 +666,7 @@ const Meeting: React.FC = () => { postOpenMicrApi(item.user.isRoomManager, userInfo.uid, false) } else { postOpenMicrApi(item.user.isRoomManager, userInfo.uid, false) - postOpenCameraApi(item.user.isRoomManager, userInfo.uid) + postOpenCameraApi(false, userInfo.uid) } return '' }) @@ -440,59 +680,73 @@ const Meeting: React.FC = () => { break; // 申请发言 case 'ApplyToSpeak': - api.open({ - message: '', - description:
- {item.uname}申请发言 -
- - + -
-
, - duration: 10, - placement: 'bottomRight', - showProgress: true, - pauseOnHover: false, - }); + }} + style={{ backgroundColor: '#EC3C3C', marginLeft: '14px' }} + >拒绝 + + , + duration: 10, + placement: 'bottomRight', + showProgress: true, + pauseOnHover: false, + }); + } + return bool + }) break; // 管理员查看随机用户 case 'Watch': @@ -560,7 +814,12 @@ const Meeting: React.FC = () => { case 'ShowDriverList': if (item.driversJsonString) { const data = JSON.parse(item.driversJsonString); - equipmentManagementRef.current.setData(data) + const isOpen = await getKeyOpenChildWindow('shareScreenWindow') + if (isOpen) { + channel.postMessage({ type: 'showDriverList', showDriverList: data }) + } else { + equipmentManagementRef.current.setData(data) + } } break; } @@ -639,6 +898,27 @@ const Meeting: React.FC = () => { observerObject.observe(element); }); } + window.electron.windowHandleMessage({ + key: 'shareScreenWindow', + parmes: { + footerList, + type: 'footerList' + } + }) + window.electron.windowHandleMessage({ + key: 'shareScreenWindow', + parmes: { + roomUserList, + type: 'roomUserList' + } + }) + window.electron.windowHandleMessage({ + key: 'userListWindow', + parmes: { + roomUserList, + type: 'roomUserList' + } + }) return () => { elements.forEach(element => { observer?.unobserve(element); @@ -701,18 +981,30 @@ const Meeting: React.FC = () => { } }, onAudioVolumeIndication: async (speakers: AudioVolumeInfo[]) => { - speakers.forEach((item: any) => { - let domMe = document.getElementById(`micr-item-${userInfo.uid}`); - let dom = document.getElementById(`micr-${item.uid ? item.uid : userInfo.uid}`); - if (dom) { - const percentage = (item.volume / 255) * 100 - dom.style.height = `${percentage}%` - } - if (domMe && !item.uid) { - const percentage = (item.volume / 255) * 100 - domMe.style.height = `${percentage}%` - } - }); + if (speakers.length) { + setSpeackUid((res: any) => { + return [...new Set([...res, ...(speakers.filter((item: any) => item.volume)).map(item => item.uid || userInfo.uid)])] + }) + speakers.forEach((item: any) => { + let domMe = document.getElementById(`micr-item-${userInfo.uid}`); + let dom = document.getElementById(`micr-${item.uid ? item.uid : userInfo.uid}`); + if (dom) { + const percentage = (item.volume / 255) * 100 + dom.style.height = `${percentage}%` + } + if (domMe && !item.uid) { + const percentage = (item.volume / 255) * 100 + domMe.style.height = `${percentage}%` + window.electron.windowHandleMessage({ + key: 'shareScreenWindow', + parmes: { + currentSpeakUserMe: percentage, + type: 'currentSpeakUserMe' + } + }) + } + }); + } }, onNetworkQuality: async (_connection: RtcConnection, remoteUid: number, _txQuality: QualityType, rxQuality: QualityType) => { if (remoteUid === 0) { @@ -748,6 +1040,18 @@ const Meeting: React.FC = () => { message.error('网络断开,请检查网络') } }, + onLocalVideoStateChanged: async (_source: VideoSourceType, _state: LocalVideoStreamState, reason: LocalVideoStreamReason) => { + if (reason === 12) { + setIsScreenCapture(bool => { + if (bool) { + stopScreenCapture() + setSharedScreenItem('') + allUserLook(userInfo.uid, userInfo.userName) + } + return bool + }) + } + } }) if (state.enableCamera) { await agora.startCameraCapture() @@ -847,8 +1151,19 @@ const Meeting: React.FC = () => { setRoomUserList((res: any) => { let userItem = res.find((row: any) => row.uid === item.user.uid) if (userItem) { - for (const key in item.user) { - userItem[key] = item.user[key]; + for (const keys in item.user) { + if (keys !== 'enableCamera' && keys !== 'enableMicr') { + userItem[keys] = item.user[keys]; + } + } + if (key === 'OperCamera') { + userItem.enableCamera = item.user.enableCamera; + if (userItem.uid === userInfo.uid) { + userItem.enableCamera ? agora.startCameraCapture() : agora.stopCameraCapture() + } + } + if (key === 'OperMicr') { + userItem.enableMicr = item.user.enableMicr; } userItem.isAdmin = role.ID.includes(item.user.roleId) || item.user.isRoomManager; refreshVideoView(userItem) @@ -1075,12 +1390,12 @@ const Meeting: React.FC = () => { await allUserLook(userInfo.uid, userInfo.userName) break; case '静音': - await postOpenMicr(false, user.uid) + await postOpenMicr(false, userInfo.uid) break; case '解除静音': await getUserRoomInfo().then(async (res) => { if (res) { - await postOpenMicr(true, user.uid) + await postOpenMicr(true, userInfo.uid) } else { if (!isClicked) { setCurrentRequestSpeakType('audio') @@ -1092,12 +1407,12 @@ const Meeting: React.FC = () => { }) break; case '关闭视频': - await postOpenCamera(false, user.uid) + await postOpenCamera(false, userInfo.uid) break; case '开启视频': await getUserRoomInfo().then(async (res) => { if (res) { - await postOpenCamera(true, user.uid) + await postOpenCamera(true, userInfo.uid) } else { if (!isClicked) { setCurrentRequestSpeakType('video') @@ -1259,6 +1574,52 @@ const Meeting: React.FC = () => { setIsSharedScreenModal(false) await agora.setDesktopCapturerVideo(sharedScreenItem, isComputerAudio, isFluencyPriority) await allUserLook(user.screenShareId, user.userName) + const isOpen = await getKeyOpenChildWindow('shareScreenWindow') + setIsScreenCapture(true) + window.electron.setViewStatus('hide') + if (!isOpen) { + window.electron.createChildWindow({ + url: location.origin + `/#/noticeWindow`, + width: 388, + height: 150, + key: 'noticeWindow', + show: true, + }) + window.electron.createChildWindow({ + url: location.origin + `/#/shareScreenWindow`, + width: 400, + height: 80, + key: 'shareScreenWindow', + show: true, + }) + window.electron.createChildWindow({ + url: location.origin + `/#/chatSmallWindow`, + width: 200, + height: 150, + key: 'chatSmallWindow', + show: true, + }) + window.electron.createChildWindow({ + url: location.origin + `/#/currentSpeakUserWindow`, + width: 200, + height: 30, + key: 'currentSpeakUserWindow', + show: true, + }) + window.electron.createChildWindow({ + url: location.origin + `/#/chatBigWindow`, + width: 540, + height: 640, + key: 'chatBigWindow', + }) + window.electron.createChildWindow({ + url: location.origin + `/#/userListWindow`, + width: 440, + height: 540, + key: 'userListWindow', + }) + setKeyOpenChildWindow('shareScreenWindow', true) + } } else { message.error('请选择应用!') } @@ -1267,7 +1628,9 @@ const Meeting: React.FC = () => { const getDesktopCapturerVideo = (): void => { agora.getDesktopCapturerVideo({ width: 300, height: 300 }, { width: 300, height: 300 }, true).then((res: any) => { res.forEach((item: any, index: number) => { - item.sourceTitle = '桌面' + (index + 1) + if (item.type === 1) { + item.sourceTitle = '桌面' + (index + 1) + } if (item.thumbImage.buffer) { item.thumbnailUrl = thumbImageBufferToBase64(item.thumbImage) } @@ -1301,6 +1664,25 @@ const Meeting: React.FC = () => { agora.stopScreenCapture() footerListTemplate[1][0].title = '共享屏幕' setFooterList(footerListTemplate) + window.electron.closeChildWindow('shareScreenWindow') + setKeyOpenChildWindow('shareScreenWindow', false) + setIsScreenCapture(bool => { + if (bool) { + window.electron.setViewStatus('show') + window.electron.getWindowSize().then((res: any) => { + window.electron.setMainWindowSize({ + width: Math.ceil(res.width / 1.5), + height: Math.ceil(res.height / 1.3), + }) + window.electron.getIsMaximized().then((b: boolean) => { + if (!b) { + window.electron.setViewStatus('maximize') + } + }) + }) + } + return false + }) } // 获取房间用户 const getRoomUser = async (): Promise => { @@ -1325,6 +1707,11 @@ const Meeting: React.FC = () => { case 'meetingMode': setMeetingMode(e.value) break; + case 'quitMeeting': + if (e.value) { + setQuitMeetingModal(true) + } + break; case 'reconnect': if (e.value == true) { await onInvoke('joinChannel', { @@ -1342,6 +1729,12 @@ const Meeting: React.FC = () => { userId: userInfo.uid }) } + setIsScreenCapture(bool => { + if (bool) { + allUserLook(userItem.screenShareId, userItem.userName) + } + return bool + }) return res }) @@ -1357,12 +1750,19 @@ const Meeting: React.FC = () => { roomNum: state.channelId, msg: msg, }) - setChatList((newChatList: any) => [...newChatList, { - uid: user.uid, - userName: user.userName, + 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, + } + }) setTextMsg(''); chatScrollBotton() } else { @@ -1783,7 +2183,7 @@ const Meeting: React.FC = () => { }) } }} - >{item.isRoomManager ? '取消发言人' : '设为发言人'} : null} + >{item.isRoomManager ? '取消发言' : '允许发言'} : null} {item.isRoomManager ? : null} +
}> - + : null} + {item.uid !== user.uid && !role.ID.includes(item.roleId) && role.ID.includes(user.roleId) ?
{ + if (item.isRoomManager) { + DeleteRoomManager({ + roomId: state.roomId, + roomNum: state.channelId, + userId: item.uid + }) + } else { + postRoomManager({ + roomId: state.roomId, + roomNum: state.channelId, + userId: item.uid + }) + } + }}> + {!item.isRoomManager ? + : + + } +
: null} : null ) @@ -2092,7 +2488,7 @@ const Meeting: React.FC = () => { } }) }} - >{roomUserItem.isRoomManager ? '取消发言人' : '设为发言人'} : null} + >{roomUserItem.isRoomManager ? '取消发言' : '允许发言'} : null} {roomUserItem.isRoomManager ? - - - - ) -} - -export default ShareScreenWindow diff --git a/src/render.d.ts b/src/render.d.ts index 2a0cc6d..3be8a2c 100644 --- a/src/render.d.ts +++ b/src/render.d.ts @@ -2,7 +2,7 @@ export interface IElectronAPI { setMainWindowSize: (config: any) => void; getWindowSize: () => any; - setViewStatus: (status: 'quit' | 'maximize' | 'minimize' | 'unmaximize' | 'hide') => void; + setViewStatus: (status: 'quit' | 'maximize' | 'minimize' | 'unmaximize' | 'hide' | 'show') => void; getIsMaximized: () => Promise; setWriteText: (text: string) => void; onQuit: (callBack: Function) => void; @@ -12,13 +12,22 @@ export interface IElectronAPI { selectFilePath: (data?: any) => void onFilePath: (callBack: Function) => void; getSources: () => any; - quit: () => any; + quit: (bool) => any; downFile: (callBack: Function) => void; quitAndInstall: (callBack: Function) => void; + isOpenWindows: (callBack: Function) => void; getVersion: () => Promise; + isVisible: () => Promise; setRegistry: (uuid: string) => any; getRegistry: () => any; createChildWindow: (config: any) => void; + setChildWindow: (config: any) => void; + setChildWindowShow: (config: any) => void; + closeChildWindow: (key: string) => void; + mainWindowCenter: () => any; + mainWindowHide: () => any; + windowHandleMessage: (data: any) => {} + windowHandleMessageCallBack: (callBack: Function) => void; } declare global { diff --git a/src/utils/package/agora.ts b/src/utils/package/agora.ts index 7819346..4c45306 100644 --- a/src/utils/package/agora.ts +++ b/src/utils/package/agora.ts @@ -15,7 +15,9 @@ import { AudioVolumeInfo, UserOfflineReasonType, ConnectionStateType, - ConnectionChangedReasonType + ConnectionChangedReasonType, + LocalVideoStreamReason, + LocalVideoStreamState } from "agora-electron-sdk"; import { GetRoomRtcToken, GetAgoraConf } from "@/api/Home/Index"; import { storage } from '@/utils'; @@ -38,7 +40,9 @@ export const agora = { await rtcEngine.initialize({ appId: data, }); - await agora.setDeviceManager(bool) + if (bool) { + await agora.setDeviceManager() + } } }, // 获取rtcEngine @@ -46,7 +50,7 @@ export const agora = { return rtcEngine }, // 获取当前设备是否存在不存在就获取默认设备 - setDeviceManager: async (bool: boolean = false) => { + setDeviceManager: async () => { const setting = await JSON.parse(storage.getItem('setting') as string) // 摄像头 if (setting.videoDeviceId) { @@ -99,19 +103,17 @@ export const agora = { } setTimeout(async () => { storage.setItem('setting', JSON.stringify(setting)) - if (bool) { - const setting = await JSON.parse(storage.getItem('setting') as string) - if (setting.videoDeviceId) agora.setVideoDeviceManager(setting.videoDeviceId) //指定摄像头头采集设备 - if (setting.playBackDeviceId) agora.setPlaybackDevice(setting.playBackDeviceId) //指定播放设备 - if (setting.playBackVolume) agora.setPlaybackDeviceVolume(setting.playBackVolume) // 设置播放设备音量 - if (setting.ecordingDeviceId) agora.setRecordingDevice(setting.ecordingDeviceId) // 设置音频采集设备 - if (setting.ecordingVolume) agora.setRecordingDeviceVolume(setting.ecordingVolume) // 设置音频设备音量 - if (setting.isAINoiseReduction) agora.setAINSMode(setting.isAINoiseReduction, setting.aINoiseReduction) // 设置ai降噪 - } + const settingData = await JSON.parse(storage.getItem('setting') as string) + if (settingData.videoDeviceId) agora.setVideoDeviceManager(settingData.videoDeviceId) //指定摄像头头采集设备 + if (settingData.playBackDeviceId) agora.setPlaybackDevice(settingData.playBackDeviceId) //指定播放设备 + if (settingData.playBackVolume) agora.setPlaybackDeviceVolume(settingData.playBackVolume) // 设置播放设备音量 + if (settingData.ecordingDeviceId) agora.setRecordingDevice(settingData.ecordingDeviceId) // 设置音频采集设备 + if (settingData.ecordingVolume) agora.setRecordingDeviceVolume(settingData.ecordingVolume) // 设置音频设备音量 + if (settingData.isAINoiseReduction) agora.setAINSMode(settingData.isAINoiseReduction, settingData.aINoiseReduction) // 设置ai降噪 }, 1000); }, // 事件回调 - registerEventHandler: ({ onJoinChannelSuccess, onUserJoined, onUserOffline, onAudioVolumeIndication, onNetworkQuality, onRtcStats, onConnectionStateChanged }: any) => { + registerEventHandler: ({ onJoinChannelSuccess, onUserJoined, onUserOffline, onAudioVolumeIndication, onNetworkQuality, onRtcStats, onConnectionStateChanged, onLocalVideoStateChanged }: any) => { rtcEngine.registerEventHandler({ // 监听本地用户加入频道事件 onJoinChannelSuccess: async (connection: RtcConnection, elapsed: number) => { @@ -153,6 +155,10 @@ export const agora = { onConnectionStateChanged: async (connection: RtcConnection, state: ConnectionStateType, reason: ConnectionChangedReasonType) => { await onConnectionStateChanged?.(connection, state, reason) }, + // 本地视频状态发生改变回调。 + onLocalVideoStateChanged: async (source: VideoSourceType, state: LocalVideoStreamState, reason: LocalVideoStreamReason) => { + await onLocalVideoStateChanged?.(source, state, reason) + }, }); }, // 获取视图模式 @@ -165,7 +171,7 @@ export const agora = { }, // 本地加入 setupLocalVideo: async (item: any) => { - if (item.view?.childNodes.length === 1) { + if (item.view?.childNodes.length === 1 || item.type) { await rtcEngine.setupLocalVideo({ renderMode: agora.getRrenderMode(item.uid), sourceType: item.sourceType, @@ -232,7 +238,7 @@ export const agora = { }, // 加入频道 joinChannel: async () => { - await rtcEngine.enableAudioVolumeIndication(100, 1, true) + await rtcEngine.enableAudioVolumeIndication(100, 3, true) await rtcEngine.joinChannel(option.token, option.channelId, option.uid); await rtcEngine.setDualStreamModeEx( SimulcastStreamMode.EnableSimulcastStream, @@ -323,8 +329,8 @@ export const agora = { rtcEngine.muteRemoteVideoStream(uid, mute) }, // 销毁视频渲染dom - destroyRendererByConfig: async (uid: number) => { - await rtcEngine.destroyRendererByConfig(VideoSourceType.VideoSourceRemote, option.channelId + 'a', uid); + destroyRendererByConfig: async (uid: number, channelId: string) => { + await rtcEngine.destroyRendererByConfig(VideoSourceType.VideoSourceRemote, channelId, uid); }, // ai降噪 setAINSMode: async (enabled: boolean, mode: AudioAinsMode) => { @@ -388,7 +394,7 @@ export const agora = { }, // 桌面捕获音频和视频的媒体源的信息 getDesktopCapturerVideo: async (thumbSize: any, iconSize: any, includeScreen: boolean) => { - return await rtcEngine.getScreenCaptureSources(thumbSize, iconSize, includeScreen).filter((item: any) => item.type === 1) + return await rtcEngine.getScreenCaptureSources(thumbSize, iconSize, includeScreen) }, // 共享屏幕采集 setDesktopCapturerVideo: async (targetSource: any, isComputerAudio: boolean, isFluencyPriority: boolean) => { @@ -414,8 +420,6 @@ export const agora = { {}, { windowFocus: true, - enableHighLight: true, - highLightColor: 0xFF99CC00, ...data } ); @@ -425,8 +429,6 @@ export const agora = { {}, { windowFocus: true, - enableHighLight: true, - highLightColor: 0xFF99CC00, ...data } ); diff --git a/src/utils/package/imageUrl.ts b/src/utils/package/imageUrl.ts index 0eaccc4..e0c31e2 100644 --- a/src/utils/package/imageUrl.ts +++ b/src/utils/package/imageUrl.ts @@ -71,6 +71,8 @@ import icon47Active from '@/assets/icon47-active.png' import icon48 from '@/assets/icon48.png' import icon48Select from '@/assets/icon48-select.png' import icon49 from '@/assets/icon49.png' +import icon50 from '@/assets/icon50.png' +import icon51 from '@/assets/icon51.png' export default { loading, icon, @@ -145,4 +147,6 @@ export default { icon48, icon48Select, icon49, + icon50, + icon51, } \ No newline at end of file diff --git a/src/utils/package/public.ts b/src/utils/package/public.ts new file mode 100644 index 0000000..6a22ec2 --- /dev/null +++ b/src/utils/package/public.ts @@ -0,0 +1,15 @@ +import storage from "./storage"; +export const setKeyOpenChildWindow = async (key: string, bool: boolean) => { + const openChildWindow = await JSON.parse(storage.getItem('openChildWindow') as string) + openChildWindow[key] = bool; + if (key === 'shareScreenWindow' && !bool) { + for (const k in openChildWindow) { + openChildWindow[k] = false + } + } + storage.setItem('openChildWindow', JSON.stringify(openChildWindow)) +}; +export const getKeyOpenChildWindow = async (key: string): Promise => { + const openChildWindow = await JSON.parse(storage.getItem('openChildWindow') as string) + return openChildWindow[key] +}; diff --git a/src/utils/styles/App.scss b/src/utils/styles/App.scss index 3b5551f..1c1cf96 100644 --- a/src/utils/styles/App.scss +++ b/src/utils/styles/App.scss @@ -376,6 +376,7 @@ $pagination-hover-background-color: #5575F2; .ant-message { -webkit-app-region: no-drag; } + // ant-spin-fullscreen .ant-spin-fullscreen { z-index: 10000; @@ -383,4 +384,8 @@ $pagination-hover-background-color: #5575F2; .ant-picker-dropdown { -webkit-app-region: no-drag; +} + +.ant-tabs { + -webkit-app-region: no-drag; } \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 8d788dd..f52346c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -70,7 +70,9 @@ export default defineConfig({ AudioVolumeInfo, UserOfflineReasonType, ConnectionStateType, - ConnectionChangedReasonType + ConnectionChangedReasonType, + LocalVideoStreamState, + LocalVideoStreamReason } = require("agora-electron-sdk") export { createAgoraRtcEngine, @@ -89,7 +91,9 @@ export default defineConfig({ AudioVolumeInfo, UserOfflineReasonType, ConnectionStateType, - ConnectionChangedReasonType + ConnectionChangedReasonType, + LocalVideoStreamState, + LocalVideoStreamReason } `, })