diff --git a/main.js b/main.js index 6989ce7..5d85ed3 100644 --- a/main.js +++ b/main.js @@ -1,4 +1,4 @@ -const { app, BrowserWindow, screen, Tray, nativeImage, Menu, ipcMain, clipboard, dialog, webFrame } = require('electron'); +const { app, BrowserWindow, screen, Tray, nativeImage, Menu, ipcMain, clipboard, dialog, webFrame, Notification } = require('electron'); const path = require('node:path') app.allowRendererProcessReuse = false; let mainWindow = null; @@ -94,6 +94,16 @@ function createWindow() { mainWindow.focus(); } +function createNotification(user) { + const notification = new Notification({ + title: `${user.name} 邀请你加入`, + body: user.body, + icon: path.join(`${__dirname}/src/assets/avatar.png`) + }); + notification.show(); + mainWindow.focus(); +} + app.on('ready', () => { createWindow() createTray() @@ -146,6 +156,11 @@ app.on('ready', () => { clipboard.writeText(text) }); + // 加入房间通知 + ipcMain.handle('joinNotification', (event, user) => { + createNotification(user) + }); + // 设置桌面应用基础属性 ipcMain.handle('setMainWindowSize', (event, config) => { // 设置最小窗口尺寸 diff --git a/preload.js b/preload.js index 5538ed9..9622aec 100644 --- a/preload.js +++ b/preload.js @@ -16,5 +16,9 @@ window.electron = { // 复制文字 setWriteText: (text) => { return ipcRenderer.invoke('setWriteText', text) + }, + // 加入房间通知 + joinNotification: (user) => { + ipcRenderer.invoke('joinNotification', user) } } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 1579eed..13672b1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import Meeting from '@/page/Meeting/index' import NotFound from '@/page/NotFound/index' import { storage } from '@/utils' import { Spin } from "antd"; +import { onSignalr, offSignalr } from "@/utils/package/signalr"; const App: React.FC = () => { const navigate = useNavigate(); @@ -19,7 +20,7 @@ const App: React.FC = () => { }); const [spinning, setSpinning] = useState(false); useEffect(() => { - if (storage.getItem('TOKEN')) { + if (storage.getItem('user')) { try { window.electron.setMainWindowSize({ width: 1200, @@ -57,7 +58,21 @@ const App: React.FC = () => { window.removeEventListener('customStorageChange', handleCustomStorageChange); }; }, []); - + useEffect(() => { + onSignalr((item: any) => { + switch (item.key) { + case 'Invitation': + window.electron.joinNotification({ + body: item.roomName, + name: item.InviterName, + }) + break; + } + }) + return () => { + offSignalr() + } + }, []) const handleResize = (): void => { setWindowSize({ width: window.innerWidth, @@ -75,6 +90,8 @@ const App: React.FC = () => { setSpinning(Boolean(e.value)) } }; + + return ( <> diff --git a/src/api/Home/User/index.ts b/src/api/Home/User/index.ts index 03d6c3b..7a8d9e0 100644 --- a/src/api/Home/User/index.ts +++ b/src/api/Home/User/index.ts @@ -1,8 +1,9 @@ import { request } from '@/utils' -export const GetUserList = (data: { pageIndex: number, pageSize: number, searchKeywod: string }) => +export const GetUserList = (data: any) => request({ - url: `/user/list?pageIndex=${data.pageIndex}&pageSize=${data.pageSize}&searchKeywod=${data.searchKeywod}`, - method: 'get' + url: `/user/list`, + method: 'get', + data }) export const PostUser = (data: any) => diff --git a/src/api/Meeting/index.ts b/src/api/Meeting/index.ts index 8fe0e15..fa81669 100644 --- a/src/api/Meeting/index.ts +++ b/src/api/Meeting/index.ts @@ -86,3 +86,10 @@ export const GetSyncView = (roomNum: string, type: string) => url: `/room/sync-view?roomNum=${roomNum}&type=${type}`, method: 'get' }) + +export const PostRoomInvite = (roomId: string, data: any) => + request({ + url: `/room/invite?roomId=${roomId}`, + method: 'post', + data + }) \ No newline at end of file diff --git a/src/components/InvitingPersonnelModal/index.module.scss b/src/components/InvitingPersonnelModal/index.module.scss new file mode 100644 index 0000000..1447ee2 --- /dev/null +++ b/src/components/InvitingPersonnelModal/index.module.scss @@ -0,0 +1,122 @@ +.invitingPersonnelModal { + .invitingPersonnelModalContent { + height: 382px; + display: flex; + + .invitingPersonnelModalContentLeft { + height: 100%; + background-color: #181A1D; + width: 50%; + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + box-sizing: border-box; + + .invitingPersonnelModalContentLeftUserList { + flex-grow: 1; + overflow-y: auto; + width: 100%; + + >div { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; + + >div:nth-child(1) { + display: flex; + align-items: center; + + >div:nth-child(2) { + width: 36px; + height: 36px; + overflow: hidden; + border-radius: 50%; + margin: 0 10px; + + >img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + >span { + font-size: 14px; + color: white; + } + } + + >div:nth-child(2) { + font-size: 14px; + margin-left: 10px; + } + + &:last-child { + margin-bottom: 0; + } + } + } + } + + .invitingPersonnelModalContentRight { + height: 100%; + background-color: #101317; + width: 50%; + display: flex; + flex-direction: column; + padding: 20px; + box-sizing: border-box; + + >span { + font-size: 16px; + color: white; + flex-shrink: 0; + margin-bottom: 10px; + } + + >div { + flex-grow: 1; + overflow-y: auto; + + >div { + display: flex; + align-items: center; + margin-bottom: 10px; + + >div { + width: 36px; + height: 36px; + overflow: hidden; + border-radius: 50%; + margin: 0 10px; + + >img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + >span { + font-size: 14px; + color: white; + } + + &:last-child { + margin-bottom: 0; + } + } + + } + } + } + + .invitingPersonnelModalFooter { + display: flex; + align-items: center; + justify-content: center; + margin-top: 10px; + } +} \ No newline at end of file diff --git a/src/components/InvitingPersonnelModal/index.tsx b/src/components/InvitingPersonnelModal/index.tsx new file mode 100644 index 0000000..51487ec --- /dev/null +++ b/src/components/InvitingPersonnelModal/index.tsx @@ -0,0 +1,180 @@ +import styles from '@/components/InvitingPersonnelModal/index.module.scss' +import { Button, Checkbox, Input, Modal, Pagination, message } from 'antd'; +import { useState, useImperativeHandle, forwardRef, useEffect } from "react"; +import ImageUrl from '@/utils/package/imageUrl'; +import { SearchOutlined } from '@ant-design/icons'; +import { GetUserList } from '@/api/Home/User'; +import { useLocation } from 'react-router-dom'; +import { PostRoomInvite } from '@/api/Meeting'; +const InvitingPersonnelModal = forwardRef((props: any, ref: any) => { + useImperativeHandle(ref, () => ({ + changeInvitingPersonnelModal: () => { + setIsInvitingPersonnelModal(true) + } + })) + const { state } = useLocation(); + const [isInvitingPersonnelModal, setIsInvitingPersonnelModal] = useState(false); + const [operation, setOperation] = useState<{ + options: { label: string; value: number }[]; + optionsValue: number[]; + }>({ + options: [ + { label: '在线', value: 1 }, + { label: '不在线', value: 2 }, + ], + optionsValue: [1, 2] + }); + const [list, setList] = useState({ + data: [], + searchKeywod: '', + total: 0, + pageIndex: 1, + pageSize: 5, + }) + const [checkedList, setCheckedList] = useState([]) + + useEffect(() => { + getUserList() + }, [list.pageIndex]); + + useEffect(() => { + if (list.pageIndex === 1) { + getUserList() + } else { + setList({ + ...list, + pageIndex: 1 + }) + } + }, [operation.optionsValue]); + + // 设置勾选 + const changeOptionsValue = (checkedValues: number[]): void => { + setOperation({ + ...operation, + optionsValue: checkedValues, + }) + }; + // 获取用户列表 + const getUserList = async (): Promise => { + await GetUserList({ + pageIndex: list.pageIndex, + pageSize: list.pageSize, + searchKeywod: list.searchKeywod, + isOnline: operation.optionsValue.length === 1 ? operation.optionsValue[0] === 1 ? true : false : '', + }).then(res => { + if (res.code === 200) { + setList({ + ...list, + total: res.data.total, + data: res.data.items.map((item: any) => { + return { + ...item, + checked: checkedList.find((checkedItem: any) => checkedItem.id === item.id) ? true : false, + } + }), + }) + } + }) + } + return ( + <> + setIsInvitingPersonnelModal(false)} + centered + width={'560px'} + > +
+
+
+ } + value={list.searchKeywod} + onChange={(e) => { + setList({ + ...list, + searchKeywod: e.target.value, + }) + }} + onPressEnter={() => { + if (list.pageIndex === 1) { + getUserList() + } else { + setList({ + ...list, + pageIndex: 1 + }) + } + }} + /> + +
+ {list.data.length ? list.data.map((item: any, index: number) =>
+
+ { + const newData = [...list.data] as any; + newData[index].checked = e.target.checked + setList({ + ...list, + data: newData, + }) + setCheckedList((checkedList: any) => { + if (newData[index].checked) { + return [...checkedList, newData[index]] + } else { + checkedList.splice(checkedList.findIndex((item: any) => item.id === newData[index].id), 1); + return checkedList + } + }) + }} defaultChecked={item.checked}> +
+ {item.userName} +
+
{item.isOnline ? '在线' : '离线'}
+
) : 暂无数据} +
+ { + setList({ + ...list, + pageIndex: e + }) + }} pageSize={list.pageSize} current={list.pageIndex} hideOnSinglePage={true} /> +
+
+ 已选成员 +
+ {checkedList.map((item: any, index: number) =>
+
+ {item.userName} +
)} +
+
+
+
+ + +
+
+
+ + ) +}) + + +export default InvitingPersonnelModal \ No newline at end of file diff --git a/src/page/Home/Index/index.tsx b/src/page/Home/Index/index.tsx index c45ba87..af194bd 100644 --- a/src/page/Home/Index/index.tsx +++ b/src/page/Home/Index/index.tsx @@ -124,7 +124,8 @@ const Index: React.FC = () => { state: { channelId: item.roomNum, token: res, - roomId: item.id + roomId: item.id, + roomName: item.roomName, } }) } @@ -263,7 +264,8 @@ const Index: React.FC = () => { state: { channelId: joinRoomFrom, token, - roomId: res.data.id + roomId: res.data.id, + roomName: res.data.roomName, } }) } diff --git a/src/page/Home/User/index.tsx b/src/page/Home/User/index.tsx index 0efe0c4..67da669 100644 --- a/src/page/Home/User/index.tsx +++ b/src/page/Home/User/index.tsx @@ -118,6 +118,7 @@ const User: React.FC = () => { } + value={list.searchKeywod} onChange={(e) => { setList({ ...list, @@ -156,7 +157,7 @@ const User: React.FC = () => { ( <> -
{item.account}
+
{item.isOnline ? '在线' : '离线'}
)} /> ( diff --git a/src/page/Home/index.tsx b/src/page/Home/index.tsx index 6546d75..64bedfb 100644 --- a/src/page/Home/index.tsx +++ b/src/page/Home/index.tsx @@ -6,6 +6,7 @@ import dayjs from 'dayjs'; import 'dayjs/locale/zh-cn' import { storage } from '@/utils'; import ImageUrl from '@/utils/package/imageUrl' +import { startSignalr } from '@/utils/package/signalr'; dayjs.locale('zh-cn'); type navListType = { title: string; @@ -48,9 +49,8 @@ const Home: React.FC = () => { }) useEffect(() => { const user = JSON.parse(storage.getItem('user') as string); - if (user) { - setUserInfo(user) - } + setUserInfo(user) + startSignalr() const updateTime = () => { setDateInfo({ work: dayjs().format('ddd'), diff --git a/src/page/Login/index.tsx b/src/page/Login/index.tsx index a3578c2..6c425a8 100644 --- a/src/page/Login/index.tsx +++ b/src/page/Login/index.tsx @@ -6,7 +6,6 @@ import { Input, Button, Checkbox, message } from "antd" import { storage } from '@/utils' import { GetCheckUser, PostLogin } from '@/api/Login' import * as CryptoJS from 'crypto-js'; -import { startSignalr } from '@/utils/package/signalr' import ImageUrl from '@/utils/package/imageUrl' const Login: React.FC = () => { @@ -127,7 +126,6 @@ const Login: React.FC = () => { } navigate('/home') - startSignalr() } }) } diff --git a/src/page/Meeting/index.tsx b/src/page/Meeting/index.tsx index 82973e5..9e5afb9 100644 --- a/src/page/Meeting/index.tsx +++ b/src/page/Meeting/index.tsx @@ -15,12 +15,14 @@ import { onInvoke, onSignalr, offSignalr } from '@/utils/package/signalr'; import dayjs from 'dayjs'; import durationPlugin from 'dayjs/plugin/duration'; import { VideoSourceType } from 'agora-electron-sdk'; +import InvitingPersonnelModal from '@/components/InvitingPersonnelModal'; dayjs.extend(durationPlugin); const { Column } = Table const Meeting: React.FC = () => { const navigate = useNavigate(); const { state } = useLocation(); const speakerModeModalRef = useRef(); + const invitingPersonnelRef = useRef(); const [statusList, setStatusList] = useState({ userList: false, userChatList: false, @@ -270,6 +272,9 @@ const Meeting: React.FC = () => { break; case '设置向导': + break; + case '邀请人员': + invitingPersonnelRef.current.changeInvitingPersonnelModal() break; case '录制': if (currentVideoId === user.account) { @@ -561,7 +566,7 @@ const Meeting: React.FC = () => { {roomUserList.map((item: any, index: number) => { return ( <> - {item.isShow ?
+ {item.isShow ?
@@ -906,6 +911,7 @@ const Meeting: React.FC = () => {
+ ) } diff --git a/src/render.d.ts b/src/render.d.ts index 33b6b62..49c45ae 100644 --- a/src/render.d.ts +++ b/src/render.d.ts @@ -4,6 +4,7 @@ export interface IElectronAPI { setViewStatus: (status: 'quit' | 'maximize' | 'minimize' | 'unmaximize') => void; getIsMaximized: () => Promise; setWriteText: (text: string) => void; + joinNotification: (data: { name: string, body: string }) => void } declare global { interface Window { diff --git a/src/utils/package/signalr.ts b/src/utils/package/signalr.ts index 083dab6..1f23682 100644 --- a/src/utils/package/signalr.ts +++ b/src/utils/package/signalr.ts @@ -46,6 +46,12 @@ export const onSignalr = (callBack: Function) => { type }) }); + connection.on("Invitation", (roomNum: string, roomName: string, InviterName: string) => { + callBack({ + key: 'Invitation', + roomNum, roomName, InviterName + }) + }); } } export const offSignalr = () => { @@ -54,6 +60,7 @@ export const offSignalr = () => { connection.off('RefreshUserList'); connection.off('Operation'); connection.off('ForceExitRoom'); + connection.off('Invitation'); } } export const onInvoke = async (str: string, data: any) => {