This commit is contained in:
parent
77578dd8a8
commit
a5e58c2898
17
main.js
17
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) => {
|
||||
// 设置最小窗口尺寸
|
||||
|
|
|
|||
|
|
@ -16,5 +16,9 @@ window.electron = {
|
|||
// 复制文字
|
||||
setWriteText: (text) => {
|
||||
return ipcRenderer.invoke('setWriteText', text)
|
||||
},
|
||||
// 加入房间通知
|
||||
joinNotification: (user) => {
|
||||
ipcRenderer.invoke('joinNotification', user)
|
||||
}
|
||||
}
|
||||
21
src/App.tsx
21
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 (
|
||||
<>
|
||||
<Routes>
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ const User: React.FC = () => {
|
|||
<Input
|
||||
placeholder="请输入用户名"
|
||||
prefix={<SearchOutlined style={{ color: 'white' }} />}
|
||||
value={list.searchKeywod}
|
||||
onChange={(e) => {
|
||||
setList({
|
||||
...list,
|
||||
|
|
@ -156,7 +157,7 @@ const User: React.FC = () => {
|
|||
<Column title="角色" dataIndex="roleName" key="roleName" />
|
||||
<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) => (
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<any>();
|
||||
const invitingPersonnelRef = useRef<any>();
|
||||
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 ? <div key={index} className='drag'>
|
||||
{item.isShow ? <div key={index + item.id} className='drag'>
|
||||
<div>
|
||||
<div><img src={ImageUrl.avatar} alt="" /></div>
|
||||
<span>
|
||||
|
|
@ -906,6 +911,7 @@ const Meeting: React.FC = () => {
|
|||
</div>
|
||||
</Modal>
|
||||
<SpeakerModeModal ref={speakerModeModalRef} />
|
||||
<InvitingPersonnelModal ref={invitingPersonnelRef} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export interface IElectronAPI {
|
|||
setViewStatus: (status: 'quit' | 'maximize' | 'minimize' | 'unmaximize') => void;
|
||||
getIsMaximized: () => Promise<boolean>;
|
||||
setWriteText: (text: string) => void;
|
||||
joinNotification: (data: { name: string, body: string }) => void
|
||||
}
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue