This commit is contained in:
yj 2024-07-19 15:43:15 +08:00
parent 77578dd8a8
commit a5e58c2898
14 changed files with 376 additions and 15 deletions

17
main.js
View File

@ -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') const path = require('node:path')
app.allowRendererProcessReuse = false; app.allowRendererProcessReuse = false;
let mainWindow = null; let mainWindow = null;
@ -94,6 +94,16 @@ function createWindow() {
mainWindow.focus(); 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', () => { app.on('ready', () => {
createWindow() createWindow()
createTray() createTray()
@ -146,6 +156,11 @@ app.on('ready', () => {
clipboard.writeText(text) clipboard.writeText(text)
}); });
// 加入房间通知
ipcMain.handle('joinNotification', (event, user) => {
createNotification(user)
});
// 设置桌面应用基础属性 // 设置桌面应用基础属性
ipcMain.handle('setMainWindowSize', (event, config) => { ipcMain.handle('setMainWindowSize', (event, config) => {
// 设置最小窗口尺寸 // 设置最小窗口尺寸

View File

@ -16,5 +16,9 @@ window.electron = {
// 复制文字 // 复制文字
setWriteText: (text) => { setWriteText: (text) => {
return ipcRenderer.invoke('setWriteText', text) return ipcRenderer.invoke('setWriteText', text)
},
// 加入房间通知
joinNotification: (user) => {
ipcRenderer.invoke('joinNotification', user)
} }
} }

View File

@ -10,6 +10,7 @@ import Meeting from '@/page/Meeting/index'
import NotFound from '@/page/NotFound/index' import NotFound from '@/page/NotFound/index'
import { storage } from '@/utils' import { storage } from '@/utils'
import { Spin } from "antd"; import { Spin } from "antd";
import { onSignalr, offSignalr } from "@/utils/package/signalr";
const App: React.FC = () => { const App: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -19,7 +20,7 @@ const App: React.FC = () => {
}); });
const [spinning, setSpinning] = useState(false); const [spinning, setSpinning] = useState(false);
useEffect(() => { useEffect(() => {
if (storage.getItem('TOKEN')) { if (storage.getItem('user')) {
try { try {
window.electron.setMainWindowSize({ window.electron.setMainWindowSize({
width: 1200, width: 1200,
@ -57,7 +58,21 @@ const App: React.FC = () => {
window.removeEventListener('customStorageChange', handleCustomStorageChange); 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 => { const handleResize = (): void => {
setWindowSize({ setWindowSize({
width: window.innerWidth, width: window.innerWidth,
@ -75,6 +90,8 @@ const App: React.FC = () => {
setSpinning(Boolean(e.value)) setSpinning(Boolean(e.value))
} }
}; };
return ( return (
<> <>
<Routes> <Routes>

View File

@ -1,8 +1,9 @@
import { request } from '@/utils' import { request } from '@/utils'
export const GetUserList = (data: { pageIndex: number, pageSize: number, searchKeywod: string }) => export const GetUserList = (data: any) =>
request({ request({
url: `/user/list?pageIndex=${data.pageIndex}&pageSize=${data.pageSize}&searchKeywod=${data.searchKeywod}`, url: `/user/list`,
method: 'get' method: 'get',
data
}) })
export const PostUser = (data: any) => export const PostUser = (data: any) =>

View File

@ -86,3 +86,10 @@ export const GetSyncView = (roomNum: string, type: string) =>
url: `/room/sync-view?roomNum=${roomNum}&type=${type}`, url: `/room/sync-view?roomNum=${roomNum}&type=${type}`,
method: 'get' method: 'get'
}) })
export const PostRoomInvite = (roomId: string, data: any) =>
request({
url: `/room/invite?roomId=${roomId}`,
method: 'post',
data
})

View File

@ -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;
}
}

View File

@ -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<any>({
data: [],
searchKeywod: '',
total: 0,
pageIndex: 1,
pageSize: 5,
})
const [checkedList, setCheckedList] = useState<any>([])
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<void> => {
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 (
<>
<Modal
title="邀请成员"
open={isInvitingPersonnelModal}
footer={null}
onCancel={() => setIsInvitingPersonnelModal(false)}
centered
width={'560px'}
>
<div className={styles.invitingPersonnelModal}>
<div className={styles.invitingPersonnelModalContent}>
<div className={styles.invitingPersonnelModalContentLeft}>
<Input
placeholder="请输入成员名称"
style={{ width: '100%', flexShrink: 0 }}
prefix={<SearchOutlined style={{ color: 'white' }} />}
value={list.searchKeywod}
onChange={(e) => {
setList({
...list,
searchKeywod: e.target.value,
})
}}
onPressEnter={() => {
if (list.pageIndex === 1) {
getUserList()
} else {
setList({
...list,
pageIndex: 1
})
}
}}
/>
<Checkbox.Group
style={{ flexShrink: 0, margin: '10px 0' }}
options={operation.options}
value={operation.optionsValue}
onChange={changeOptionsValue}
/>
<div className={styles.invitingPersonnelModalContentLeftUserList}>
{list.data.length ? list.data.map((item: any, index: number) => <div key={item.id}>
<div >
<Checkbox onChange={(e) => {
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}></Checkbox>
<div><img src={ImageUrl.avatar} alt="" /></div>
<span>{item.userName}</span>
</div>
<div style={{ color: item.isOnline ? '#02B188' : 'rgb(221 11 11)' }}>{item.isOnline ? '在线' : '离线'}</div>
</div>) : <span style={{ display: 'block', textAlign: 'center', color: 'white', padding: '30px 0' }}></span>}
</div>
<Pagination size="small" total={list.total} style={{ flexShrink: 0, margin: '10px 0 0' }} onChange={(e) => {
setList({
...list,
pageIndex: e
})
}} pageSize={list.pageSize} current={list.pageIndex} hideOnSinglePage={true} />
</div>
<div className={styles.invitingPersonnelModalContentRight}>
<span></span>
<div>
{checkedList.map((item: any, index: number) => <div key={item.id + index}>
<div><img src={ImageUrl.avatar} alt="" /></div>
<span>{item.userName}</span>
</div>)}
</div>
</div>
</div>
<div className={styles.invitingPersonnelModalFooter}>
<Button type="primary" onClick={() => { setIsInvitingPersonnelModal(false) }} style={{ backgroundColor: '#31353A', marginRight: '14px' }}></Button>
<Button type="primary" className='m-ant-btn' onClick={() => {
if (checkedList.length) {
PostRoomInvite(state.roomId, checkedList.map((item: any) => item.id))
} else {
message.error('请选择人员')
}
}}></Button>
</div>
</div>
</Modal>
</>
)
})
export default InvitingPersonnelModal

View File

@ -124,7 +124,8 @@ const Index: React.FC = () => {
state: { state: {
channelId: item.roomNum, channelId: item.roomNum,
token: res, token: res,
roomId: item.id roomId: item.id,
roomName: item.roomName,
} }
}) })
} }
@ -263,7 +264,8 @@ const Index: React.FC = () => {
state: { state: {
channelId: joinRoomFrom, channelId: joinRoomFrom,
token, token,
roomId: res.data.id roomId: res.data.id,
roomName: res.data.roomName,
} }
}) })
} }

View File

@ -118,6 +118,7 @@ const User: React.FC = () => {
<Input <Input
placeholder="请输入用户名" placeholder="请输入用户名"
prefix={<SearchOutlined style={{ color: 'white' }} />} prefix={<SearchOutlined style={{ color: 'white' }} />}
value={list.searchKeywod}
onChange={(e) => { onChange={(e) => {
setList({ setList({
...list, ...list,
@ -156,7 +157,7 @@ const User: React.FC = () => {
<Column title="角色" dataIndex="roleName" key="roleName" /> <Column title="角色" dataIndex="roleName" key="roleName" />
<Column title="在线状态" render={(item) => ( <Column title="在线状态" render={(item) => (
<> <>
<div style={{ color: '#02B188' }}>{item.account}</div> <div style={{ color: item.isOnline ? '#02B188' : 'rgb(221 11 11)' }}>{item.isOnline ? '在线' : '离线'}</div>
</> </>
)} /> )} />
<Column title="操作" render={(item) => ( <Column title="操作" render={(item) => (

View File

@ -6,6 +6,7 @@ import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn' import 'dayjs/locale/zh-cn'
import { storage } from '@/utils'; import { storage } from '@/utils';
import ImageUrl from '@/utils/package/imageUrl' import ImageUrl from '@/utils/package/imageUrl'
import { startSignalr } from '@/utils/package/signalr';
dayjs.locale('zh-cn'); dayjs.locale('zh-cn');
type navListType = { type navListType = {
title: string; title: string;
@ -48,9 +49,8 @@ const Home: React.FC = () => {
}) })
useEffect(() => { useEffect(() => {
const user = JSON.parse(storage.getItem('user') as string); const user = JSON.parse(storage.getItem('user') as string);
if (user) {
setUserInfo(user) setUserInfo(user)
} startSignalr()
const updateTime = () => { const updateTime = () => {
setDateInfo({ setDateInfo({
work: dayjs().format('ddd'), work: dayjs().format('ddd'),

View File

@ -6,7 +6,6 @@ import { Input, Button, Checkbox, message } from "antd"
import { storage } from '@/utils' import { storage } from '@/utils'
import { GetCheckUser, PostLogin } from '@/api/Login' import { GetCheckUser, PostLogin } from '@/api/Login'
import * as CryptoJS from 'crypto-js'; import * as CryptoJS from 'crypto-js';
import { startSignalr } from '@/utils/package/signalr'
import ImageUrl from '@/utils/package/imageUrl' import ImageUrl from '@/utils/package/imageUrl'
const Login: React.FC = () => { const Login: React.FC = () => {
@ -127,7 +126,6 @@ const Login: React.FC = () => {
} }
navigate('/home') navigate('/home')
startSignalr()
} }
}) })
} }

View File

@ -15,12 +15,14 @@ import { onInvoke, onSignalr, offSignalr } from '@/utils/package/signalr';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import durationPlugin from 'dayjs/plugin/duration'; import durationPlugin from 'dayjs/plugin/duration';
import { VideoSourceType } from 'agora-electron-sdk'; import { VideoSourceType } from 'agora-electron-sdk';
import InvitingPersonnelModal from '@/components/InvitingPersonnelModal';
dayjs.extend(durationPlugin); dayjs.extend(durationPlugin);
const { Column } = Table const { Column } = Table
const Meeting: React.FC = () => { const Meeting: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { state } = useLocation(); const { state } = useLocation();
const speakerModeModalRef = useRef<any>(); const speakerModeModalRef = useRef<any>();
const invitingPersonnelRef = useRef<any>();
const [statusList, setStatusList] = useState({ const [statusList, setStatusList] = useState({
userList: false, userList: false,
userChatList: false, userChatList: false,
@ -270,6 +272,9 @@ const Meeting: React.FC = () => {
break; break;
case '设置向导': case '设置向导':
break;
case '邀请人员':
invitingPersonnelRef.current.changeInvitingPersonnelModal()
break; break;
case '录制': case '录制':
if (currentVideoId === user.account) { if (currentVideoId === user.account) {
@ -561,7 +566,7 @@ const Meeting: React.FC = () => {
{roomUserList.map((item: any, index: number) => { {roomUserList.map((item: any, index: number) => {
return ( return (
<> <>
{item.isShow ? <div key={index} className='drag'> {item.isShow ? <div key={index + item.id} className='drag'>
<div> <div>
<div><img src={ImageUrl.avatar} alt="" /></div> <div><img src={ImageUrl.avatar} alt="" /></div>
<span> <span>
@ -906,6 +911,7 @@ const Meeting: React.FC = () => {
</div> </div>
</Modal> </Modal>
<SpeakerModeModal ref={speakerModeModalRef} /> <SpeakerModeModal ref={speakerModeModalRef} />
<InvitingPersonnelModal ref={invitingPersonnelRef} />
</> </>
) )
} }

1
src/render.d.ts vendored
View File

@ -4,6 +4,7 @@ export interface IElectronAPI {
setViewStatus: (status: 'quit' | 'maximize' | 'minimize' | 'unmaximize') => void; setViewStatus: (status: 'quit' | 'maximize' | 'minimize' | 'unmaximize') => void;
getIsMaximized: () => Promise<boolean>; getIsMaximized: () => Promise<boolean>;
setWriteText: (text: string) => void; setWriteText: (text: string) => void;
joinNotification: (data: { name: string, body: string }) => void
} }
declare global { declare global {
interface Window { interface Window {

View File

@ -46,6 +46,12 @@ export const onSignalr = (callBack: Function) => {
type type
}) })
}); });
connection.on("Invitation", (roomNum: string, roomName: string, InviterName: string) => {
callBack({
key: 'Invitation',
roomNum, roomName, InviterName
})
});
} }
} }
export const offSignalr = () => { export const offSignalr = () => {
@ -54,6 +60,7 @@ export const offSignalr = () => {
connection.off('RefreshUserList'); connection.off('RefreshUserList');
connection.off('Operation'); connection.off('Operation');
connection.off('ForceExitRoom'); connection.off('ForceExitRoom');
connection.off('Invitation');
} }
} }
export const onInvoke = async (str: string, data: any) => { export const onInvoke = async (str: string, data: any) => {