WGShare.Client.Electron/src/page/Meeting/index.tsx

717 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import styles from '@/page/Meeting/index.module.scss'
import { useEffect, useRef, useState } from "react";
import Operation from '@/components/Operation';
import { Button, Input, Popover, Modal, Checkbox, message, Table, Pagination } from "antd";
import { DeleteOutlined, LoadingOutlined, ProfileOutlined, ReloadOutlined, SearchOutlined, VerticalAlignBottomOutlined } from '@ant-design/icons';
import { useLocation, useNavigate } from 'react-router-dom';
import { thumbImageBufferToBase64 } from '@/utils/package/base64'
import { storage } from '@/utils';
import { GetRoomFile, PostRoomFile, DeleteRoomFile, GetRoomUpFileurl, GetRoomFileDwUrl, GetRoomUser } from '@/api/Meeting';
import axios from 'axios';
import ImageUrl from '@/utils/package/imageUrl'
import agora from '@/utils/package/agora'
import StupWizard from '@/components/StupWizard';
import { onInvoke, onSignalr } from '@/utils/package/signalr';
import dayjs from 'dayjs';
import durationPlugin from 'dayjs/plugin/duration';
dayjs.extend(durationPlugin);
const { Column } = Table
const Meeting: React.FC = () => {
const navigate = useNavigate();
const { state } = useLocation();
const stupWizardRef = useRef<any>();
const [statusList, setStatusList] = useState({
userList: false,
userChatList: false,
})
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [isSharedScreenModal, setIsSharedScreenModal] = useState(false);
const [isInit, setIsInit] = useState(true);
const [user, setUser] = useState<any>({});
const [showRowSelection, setShowRowSelection] = useState(false);
const [isSharedFilesModel, setIsSharedFilesModel] = useState(false);
const [sharedScreenList, setSharedScreenList] = useState<any>([]);
const [sharedScreenItem, setSharedScreenItem] = useState<any>('');
const [textMsg, setTextMsg] = useState('');
const [footerList, setFooterList] = useState([
[
{
title: '关闭声音',
icon: ImageUrl.icon22,
iconActive: ImageUrl.icon22Active,
active: false,
},
{
title: '关闭视频',
icon: ImageUrl.icon23,
iconActive: ImageUrl.icon23Active,
active: false,
},
],
[
{
title: '共享屏幕',
icon: ImageUrl.icon24,
active: false,
},
{
title: '共享文件',
icon: ImageUrl.icon25,
active: false,
},
{
title: '邀请人员',
icon: ImageUrl.icon26,
active: false,
},
{
title: '录制',
icon: ImageUrl.icon27,
iconActive: ImageUrl.icon27Active,
active: false,
},
{
title: '设置向导',
icon: ImageUrl.icon28,
active: false,
},
{
title: '结束',
icon: ImageUrl.icon29,
active: false,
},
],
[
{
title: '成员列表',
icon: ImageUrl.icon30,
active: false,
},
{
title: '聊天',
icon: ImageUrl.icon31,
active: false,
},
],
])
const [footerListIndex, setFooterListIndex] = useState<any>({
itemIndex: 0,
rowIndex: 0,
});
const [fileList, setFileList] = useState({
data: [],
keyword: '',
total: 0,
pageIndex: 1,
pageSize: 10,
})
const [roomUserList, setRoomUserList] = useState<any>([])
const [chatList, setChatList] = useState<any>([])
const [currentVideoId, setCurrentVideoId] = useState('')
let [currentSeconds, setCurrentSeconds] = useState(0)
const [currentEffective, setCurrentEffective] = useState(0)
const [list] = useState<number[]>([1, 2, 3, 4, 5, 6, 7])
const [open, setOpen] = useState(false)
useEffect(() => {
let time = null as any;
if (isInit) {
let userInfo = JSON.parse(storage.getItem('user') as string)
agora.init()
agora.setJoinChannel({
channelId: state.channelId,
userid: userInfo.account,
token: state.token,
})
setUser(userInfo)
setIsInit(false)
window.addEventListener('customStorageChange', handleCustomStorageChange);
time = setInterval(() => {
let effectiveTypeLength = ['slow-2g', '2g', '3g', '4g'].indexOf((navigator as any).connection.effectiveType)
if (effectiveTypeLength >= 0) {
setCurrentEffective(effectiveTypeLength + 1)
}
setCurrentSeconds(currentSeconds++)
}, 1000)
} else {
getRoomFile()
}
return () => {
window.removeEventListener('customStorageChange', handleCustomStorageChange);
clearInterval(time)
};
}, [fileList.pageIndex]);
useEffect(() => {
roomUserList.forEach((item: any) => {
let dom = document.getElementById(`video-${item.account}`) as HTMLElement
if (!dom.getAttribute('load')) {
dom.setAttribute('load', 'true')
agora.setVideo({
account: Number(item.account),
view: dom,
channelId: state.channelId,
})
}
});
}, [roomUserList]);
useEffect(() => {
onSignalr((item: any) => {
setChatList((newChatList: any) => [...newChatList, item])
})
}, [])
useEffect(() => {
console.log(currentVideoId);
}, [currentVideoId])
// 加入房间时间
const changeCurrentSeconds = () => {
const duration = dayjs.duration(currentSeconds, '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 changeStatusList = async (row: any, itemIndex: number, rowIndex: number): Promise<void> => {
const footerListTemplate = [...footerList]
setFooterListIndex({
itemIndex,
rowIndex,
})
switch (row.title) {
case '成员列表':
setStatusList({
userList: true,
userChatList: false,
})
break;
case '聊天':
setStatusList({
userList: false,
userChatList: true,
})
break;
case '共享屏幕':
getDesktopCapturerVideo()
setIsSharedScreenModal(true)
break;
case '停止共享':
agora.stopScreenCapture()
footerListTemplate[itemIndex][rowIndex].title = '共享屏幕'
break;
case '关闭声音':
footerListTemplate[itemIndex][rowIndex].title = '开启声音'
footerListTemplate[itemIndex][rowIndex].active = true
setFooterList(footerListTemplate)
agora.muteLocalAudioStream(true)
break;
case '开启声音':
footerListTemplate[itemIndex][rowIndex].title = '关闭声音'
footerListTemplate[itemIndex][rowIndex].active = false
setFooterList(footerListTemplate)
agora.muteLocalAudioStream(false)
break;
case '关闭视频':
footerListTemplate[itemIndex][rowIndex].title = '开启视频'
footerListTemplate[itemIndex][rowIndex].active = true
setFooterList(footerListTemplate)
agora.muteLocalVideoStream(true)
break;
case '开启视频':
footerListTemplate[itemIndex][rowIndex].title = '关闭视频'
footerListTemplate[itemIndex][rowIndex].active = false
setFooterList(footerListTemplate)
agora.muteLocalVideoStream(false)
break;
case '设置向导':
stupWizardRef.current.changeIsStupWizard()
break;
case '录制':
footerListTemplate[itemIndex][rowIndex].title = '录制中'
footerListTemplate[itemIndex][rowIndex].active = true
setFooterList(footerListTemplate)
agora.startRecording()
break;
case '录制中':
footerListTemplate[itemIndex][rowIndex].title = '录制'
footerListTemplate[itemIndex][rowIndex].active = false
setFooterList(footerListTemplate)
agora.stopRecording()
break;
case '共享文件':
await getRoomFile()
setIsSharedFilesModel(true)
break;
}
}
// 分享屏幕
const clickSharedScreen = async (): Promise<void> => {
let data = sharedScreenList.find((item: any) => item.sourceId === sharedScreenItem.sourceId)
if (data) {
const footerListTemplate = [...footerList]
footerListTemplate[footerListIndex.itemIndex][footerListIndex.rowIndex].title = '停止共享'
setIsSharedScreenModal(false)
agora.setDesktopCapturerVideo(sharedScreenItem)
} else {
message.error('请选择应用!')
}
}
// 获取桌面可共享屏幕的引用
const getDesktopCapturerVideo = (): void => {
agora.getDesktopCapturerVideo().then((res: any) => {
if (sharedScreenList.length !== res.length) {
res.forEach((item: any) => {
if (item.thumbImage.buffer) {
item.thumbnailUrl = thumbImageBufferToBase64(item.thumbImage)
}
if (item.iconImage.buffer) {
item.iconDataUrl = thumbImageBufferToBase64(item.iconImage)
}
})
setSharedScreenList(res)
}
})
};
// 获取共享文件列表
const getRoomFile = async (): Promise<void> => {
await GetRoomFile({
pageIndex: fileList.pageIndex,
pageSize: fileList.pageSize,
keyword: fileList.keyword,
roomId: state.roomId
}).then(res => {
if (res.code === 200) {
setFileList({
...fileList,
data: res.data.items.map((item: any) => {
return {
...item,
key: item.id,
}
}),
total: res.data.total
})
}
})
}
// 获取房间用户
const getRoomUser = async (): Promise<void> => {
await GetRoomUser(state.channelId).then(res => {
if (res.code === 200) {
setRoomUserList(res.data)
}
})
}
const handleCustomStorageChange = (e: any): void => {
if (e.key === 'isJoin') {
if (e.value) {
onInvoke('joinChannel', {
roomNum: state.channelId
})
getRoomUser()
} else {
onInvoke('levelChannel', {
roomNum: state.channelId
})
}
} else if (e.key === 'isRemotJoin') {
setTimeout(() => {
getRoomUser()
}, 1000)
}
};
// 聊天发送
const sendMsg = (): void => {
if (textMsg) {
onInvoke('sendChannelMsg', {
roomNum: state.channelId,
msg: textMsg,
})
setChatList((newChatList: any) => [...newChatList, {
uid: state.uid,
userName: state.userName,
message: textMsg,
}])
setTextMsg('')
} else {
message.success('请输入内容!')
}
}
return (
<>
<div className={styles.meeting}>
<div className={styles.meetingHeader}>
<div>
<div>
{currentEffective >= 1 ? <span></span> : null}
{currentEffective >= 2 ? <span></span> : null}
{currentEffective >= 3 ? <span></span> : null}
{currentEffective >= 4 ? <span></span> : null}
</div>
<div>{changeCurrentSeconds()}</div>
</div>
<div>{state.channelId}</div>
<div className='drag'>
<div className={styles.meetingGrayButton}></div>
<Operation></Operation>
</div>
</div>
<div className={styles.meetingContent}>
<div className={styles.meetingContentBody}>
<div className={styles.meetingContentBodyLeft}>
<div className={`${styles.meetingContentSwiper} drag`}>
{roomUserList.map((item: any, index: number) =>
<div
className={styles.meetingContentSwiperCard}
key={index}
onClick={() => {
setCurrentVideoId(item.account)
}}
>
<div className={styles.meetingContentSwiperCardVdeio} id={`video-${item.account}`}>
<div className={styles.meetingContentSwiperCardVdeioLoading}>
<LoadingOutlined style={{ color: 'white', fontSize: '30px' }} />
</div>
</div>
{meetingContentUser(item)}
</div>
)}
</div>
<div className={`${styles.meetingContentVideo} drag`}>
<div className={styles.meetingContentVideoDom}></div>
{/* {meetingContentUser()} */}
</div>
</div>
{
(statusList.userList || statusList.userChatList) ? (
<div className={styles.meetingContentBodyRight}>
{statusList.userList ?
<div className={styles.meetingUserList}>
<div className={styles.meetingUserListTitle}>
<span></span>
<img src={ImageUrl.icon18} alt="" className='drag' onClick={() => {
setStatusList({
userList: false,
userChatList: false,
})
}} />
</div>
<Input placeholder="请输入用户名" className='drag' prefix={<SearchOutlined style={{ color: 'white' }} />} />
<div className={styles.meetingUserListContent}>
{list.map((item: number) =>
<div key={item} className='drag'>
<div>
<div><img src={ImageUrl.avatar} alt="" /></div>
<span><span style={{ color: '#02B188', marginLeft: '4px' }}></span></span>
</div>
<div>
<img src={ImageUrl.icon22} alt="" />
<img src={ImageUrl.icon23} alt="" />
</div>
</div>
)}
</div>
<div className={`${styles.meetingUserListFooter} drag`}>
<div></div>
<div></div>
</div>
</div>
:
<div className={styles.meetingUserChat}>
<div className={styles.meetingUserChatTitle}>
<span></span>
<img src={ImageUrl.icon18} alt="" className='drag' onClick={() => {
setStatusList({
userList: false,
userChatList: false,
})
}} />
</div>
<div className={styles.meetingUserChatContent}>
{chatList.map((item: any, index: number) =>
<div
key={index}
className={`${item.uid !== state.uid ? styles.meetingUserChatContentLeft : styles.meetingUserChatContentRight} drag`}>
<div>
<div><img src={ImageUrl.avatar} alt="" /></div>
<span>{item.userName}</span>
</div>
<div>{item.message}</div>
</div>
)}
</div>
<div className={`${styles.meetingUserChatInput} drag`}>
<Input.TextArea placeholder="请输入消息" value={textMsg} style={{ flexGrow: 1 }} onChange={(e) => {
setTextMsg(e.target.value)
}} onPressEnter={sendMsg}></Input.TextArea>
<Button type="primary" className='m-ant-btn' style={{ flexShrink: 0, marginTop: '4px' }} onClick={sendMsg}></Button>
</div>
</div>
}
</div>
) : null
}
</div>
<div className={styles.meetingContentFooter}>
{footerList.map((item, itemIndex) => {
return (
<div key={itemIndex}>
{item.map((row, rowIndex) => {
return (
row.title === '结束' ?
<Popover key={rowIndex}
content={
<div className='meetingContentFooterPopover'>
<div onClick={() => {
agora.leaveChannel()
agora.stopScreenCapture()
navigate(-1)
}}></div>
<div onClick={() => {
agora.leaveChannel()
agora.stopScreenCapture()
navigate(-1)
}}></div>
<div onClick={() => { setOpen(false) }}></div>
</div>
}
title=""
trigger="click"
open={open}
onOpenChange={() => setOpen(true)}
>
<div className='drag'>
<img src={row.active ? row.iconActive : row.icon} alt="" />
<span>{row.title}</span>
</div>
</Popover> :
<div className='drag' onClick={() => changeStatusList(row, itemIndex, rowIndex)} key={rowIndex}>
<img src={row.active ? row.iconActive : row.icon} alt="" />
<span>{row.title}</span>
</div>
)
})}
</div>
)
})}
</div>
</div>
</div>
<Modal title="共享屏幕" open={isSharedScreenModal} footer={null} closable={false} centered width={'40vw'}>
<div className={styles.sharedScreenModal}>
<div>
{sharedScreenList.map((item: any, index: number) => {
return (
<div
className={sharedScreenItem.sourceId === item.sourceId ? styles.active : ''}
key={index}
onClick={() => {
setSharedScreenItem(item)
}}>
{item.iconDataUrl ? <img src={item.iconDataUrl} alt="" /> : ''}
<div><img src={item.thumbnailUrl} alt="" /></div>
<span>{item.sourceTitle}</span>
</div>
)
})}
</div>
<div>
<Checkbox onChange={() => {
}}></Checkbox>
<div>
<Button type="primary" onClick={() => { setIsSharedScreenModal(false) }} style={{ backgroundColor: '#31353A', marginRight: '14px' }}></Button>
<Button type="primary" className='m-ant-btn' onClick={() => clickSharedScreen()}></Button>
</div>
</div>
</div>
</Modal>
<StupWizard ref={stupWizardRef} />
<Modal
title="共享文件"
open={isSharedFilesModel}
footer={null}
centered
width={'60vw'}
onCancel={() => setIsSharedFilesModel(false)}
maskClosable
>
<div>
<div className={styles.sharedFilesModel}>
<div>
<span>{fileList.total}</span>
<div style={{ color: 'white' }}>
<Input
placeholder="搜索"
style={{ width: '200px' }}
prefix={<SearchOutlined style={{ color: 'white' }} />}
onChange={(e) => {
setFileList({
...fileList,
keyword: e.target.value
})
}}
onBlur={() => {
if (fileList.pageIndex === 1) {
getRoomFile()
} else {
setFileList({
...fileList,
pageIndex: 1
})
}
}}
/>
<ReloadOutlined title='刷新' onClick={() => {
if (fileList.pageIndex === 1) {
getRoomFile()
} else {
setFileList({
...fileList,
pageIndex: 1
})
}
}} />
<ProfileOutlined title={showRowSelection ? '取消框选' : '显示框选'} onClick={() => {
setShowRowSelection(!showRowSelection)
}} style={{ color: showRowSelection ? '#5575F2' : 'white' }} />
{showRowSelection ? <DeleteOutlined title='删除' onClick={() => {
if (selectedRowKeys.length) {
DeleteRoomFile(selectedRowKeys).then(res => {
if (res.code === 200) {
message.success('删除成功!')
getRoomFile()
}
})
} else {
message.error('请选择文件!')
}
}} /> : null}
<Button type="primary" style={{ backgroundColor: '#31353A' }}
onClick={() => {
const file = document.createElement("input") as any;
file.type = "file";
file.onchange = async () => {
const fileInfo = file.files[0];
const fileType = fileInfo.name.split('.');
const fileTypeName = fileType[fileType.length - 1];
await GetRoomUpFileurl(state.channelId, fileTypeName).then(async res => {
const formData = new FormData();
formData.append("name", fileInfo.name);
formData.append("OSSAccessKeyId", res.data.ossAccessKeyId);
formData.append("key", res.data.key);
formData.append("policy", res.data.policy);
formData.append("signature", res.data.signature);
formData.append("success_action_status", res.data.success_action_status);
formData.append("file", fileInfo);
await axios.post(res.data.host, formData, {
headers: {
"Content-Type": "multipart/form-data",
"Authorization": `Bearer ${user.token}`
},
withCredentials: false
})
await PostRoomFile({
fileUrl: res.data.key,
size: fileInfo.size,
fileName: fileInfo.name,
roomId: state.roomId
})
getRoomFile()
})
};
file.click();
}}
></Button>
</div>
</div>
<div>
<Table
size={'small'}
rowSelection={showRowSelection ? {
selectedRowKeys,
onChange: (newSelectedRowKeys: React.Key[]) => {
setSelectedRowKeys(newSelectedRowKeys);
}
} : undefined}
dataSource={fileList.data}
pagination={false}
scroll={{ y: '40vh' }}
style={{ width: '100%' }}
>
<Column title="文件" dataIndex="fileName" key="fileName" width={140} />
<Column title="更新时间" dataIndex="modifyTime" key="modifyTime" width={200} />
<Column title="大小" render={(item) => (
<>
<span>{item.size / 1024 > 1000 ? (item.size / (1024 * 1024)).toFixed(2) + 'MB' : (item.size / 1024).toFixed(2) + 'KB'}</span>
</>
)} />
<Column title="上传者" dataIndex="userName" key="userName" />
<Column title="下载次数"
render={(item) => (
<>
<span>{item.downloadCount}</span>
</>
)}
/>
<Column title="操作" render={(item) => (
<>
<VerticalAlignBottomOutlined title='下载' style={{ color: '#5575F2', cursor: 'pointer' }} onClick={() => {
GetRoomFileDwUrl(item.fileUrl, item.id).then(res => {
if (res.code === 200) {
const downloadLink = document.createElement("a");
downloadLink.href = res.data;
downloadLink.download = item.fileName;
downloadLink.click();
getRoomFile()
}
})
}} />
{/* <FolderOutlined title='文件' style={{ color: '#FFA000', cursor: 'pointer' }} /> */}
</>
)} />
</Table>
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '10px' }}>
<Pagination size="small" total={fileList.total} onChange={(e) => {
setFileList({
...fileList,
pageIndex: e
})
}} pageSize={fileList.pageSize} current={fileList.pageIndex} hideOnSinglePage={true} />
</div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
</div>
</div>
</Modal>
</>
)
}
const meetingContentUser = (item: any) => {
return (
<>
<div className={styles.meetingContentUser}>
{item.roleId === '1' ? <div className={styles.meetingContentUserRole}>
<img src={ImageUrl.icon32} alt="" />
</div> : null}
<div className={styles.meetingContentUserName}>
<img src={ImageUrl.icon22} alt="" />
<span>{item.userName}</span>
</div>
</div>
</>
)
}
export default Meeting