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

745 lines
28 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, useState } from "react";
import Operation from '@/components/Operation';
import { Navigation, Pagination } from 'swiper/modules';
import { Swiper, SwiperSlide } from 'swiper/react';
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
import { Button, Input, Popover, Modal, Checkbox, message, Select, Slider, Table, Pagination as AntdPagination } from "antd";
import { DeleteOutlined, FolderOutlined, 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 } from '@/api/Meeting';
import axios from 'axios';
const { Column } = Table
const Meeting: React.FC = () => {
const navigate = useNavigate();
const { state } = useLocation();
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 [isStupWizard, setIsStupWizard] = useState(false);
const [showRowSelection, setShowRowSelection] = useState(false);
const [isSharedFilesModel, setIsSharedFilesModel] = useState(false);
const [sharedScreenList, setSharedScreenList] = useState<any>([]);
const [sharedScreenItem, setSharedScreenItem] = useState<any>('');
const [footerList, setFooterList] = useState([
[
{
title: '关闭声音',
icon: '/src/assets/icon22',
active: false,
},
{
title: '关闭视频',
icon: '/src/assets/icon23',
active: false,
},
],
[
{
title: '共享屏幕',
icon: '/src/assets/icon24',
active: false,
},
{
title: '共享文件',
icon: '/src/assets/icon25',
active: false,
},
{
title: '邀请人员',
icon: '/src/assets/icon26',
active: false,
},
{
title: '录制',
icon: '/src/assets/icon27',
active: false,
},
{
title: '设置向导',
icon: '/src/assets/icon28',
active: false,
},
{
title: '结束',
icon: '/src/assets/icon29',
active: false,
},
],
[
{
title: '成员列表',
icon: '/src/assets/icon30',
active: false,
},
{
title: '聊天',
icon: '/src/assets/icon31',
active: false,
},
],
])
const [footerListIndex, setFooterListIndex] = useState<any>({
itemIndex: 0,
rowIndex: 0,
});
const [audioDeviceManager, setAudioDeviceManager] = useState<any>({
currentDevices: [],
currentDevice: {},
currentVolume: 0,
});
const [fileList, setFileList] = useState({
data: [],
keyword: '',
total: 0,
pageIndex: 1,
pageSize: 10,
})
const [stepsStatus, setStepsStatus] = useState<boolean>(true);
const [isVideoLoad, setIsVideoLoad] = useState<boolean>(false);
const [list] = useState<number[]>([1, 2, 3, 4, 5, 6, 7])
const [open, setOpen] = useState(false)
const [videoID, setVideoID] = useState('')
useEffect(() => {
if (isInit) {
setUser(JSON.parse(storage.getItem('user') as string))
// window.electron.setJoinChannel({
// channelId: state.channelId,
// userid: user.userName,
// token: state.token,
// })
setIsInit(false)
} else {
getRoomFile()
}
}, [fileList.pageIndex]);
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 '停止共享':
window.electron.stopScreenCapture()
footerListTemplate[itemIndex][rowIndex].title = '共享屏幕'
break;
case '关闭声音':
footerListTemplate[itemIndex][rowIndex].title = '开启声音'
footerListTemplate[itemIndex][rowIndex].active = true
setFooterList(footerListTemplate)
window.electron.muteLocalAudioStream(true)
break;
case '开启声音':
footerListTemplate[itemIndex][rowIndex].title = '关闭声音'
footerListTemplate[itemIndex][rowIndex].active = false
setFooterList(footerListTemplate)
window.electron.muteLocalAudioStream(false)
break;
case '关闭视频':
footerListTemplate[itemIndex][rowIndex].title = '开启视频'
footerListTemplate[itemIndex][rowIndex].active = true
setFooterList(footerListTemplate)
window.electron.muteLocalVideoStream(true)
break;
case '开启视频':
footerListTemplate[itemIndex][rowIndex].title = '关闭视频'
footerListTemplate[itemIndex][rowIndex].active = false
setFooterList(footerListTemplate)
window.electron.muteLocalVideoStream(false)
break;
case '设置向导':
getAudioMediaList()
window.electron.startRecordingDeviceTest(200)
setIsStupWizard(true)
break;
case '录制':
footerListTemplate[itemIndex][rowIndex].title = '录制中'
footerListTemplate[itemIndex][rowIndex].active = true
setFooterList(footerListTemplate)
window.electron.startRecording()
break;
case '录制中':
footerListTemplate[itemIndex][rowIndex].title = '录制'
footerListTemplate[itemIndex][rowIndex].active = false
setFooterList(footerListTemplate)
window.electron.stopRecording()
break;
case '共享文件':
await getRoomFile()
setIsSharedFilesModel(true)
break;
}
}
const changeVdeio = async (bool: boolean): Promise<void> => {
if (bool) {
} else {
let data = sharedScreenList.find((item: any) => item.sourceId === sharedScreenItem.sourceId)
if (data) {
const footerListTemplate = [...footerList]
footerListTemplate[footerListIndex.itemIndex][footerListIndex.rowIndex].title = '停止共享'
setIsSharedScreenModal(false)
window.electron.setDesktopCapturerVideo(sharedScreenItem)
setVideoID(window.electron.getVideoId())
} else {
message.error('请选择应用!')
}
}
}
const getDesktopCapturerVideo = (): void => {
window.electron.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 getAudioMediaList = (): void => {
const { currentDevices, currentDevice, currentVolume } = window.electron.getAudioMediaList();
setAudioDeviceManager({
currentDevices: currentDevices.map((row: any) => {
return {
value: row.deviceId,
label: row.deviceName
}
}),
currentDevice: currentDevice.deviceId,
currentVolume,
})
}
const getRoomFile = async (): Promise<void> => {
await GetRoomFile({
pageIndex: fileList.pageIndex,
pageSize: fileList.pageSize,
keyword: fileList.keyword,
roomId: state.channelId
}).then(res => {
if (res.code === 200) {
setFileList({
...fileList,
data: res.data.items.map((item: any) => {
return {
...item,
key: item.id,
}
}),
total: res.data.total
})
}
})
}
return (
<>
<div className={styles.meeting}>
<div className={styles.meetingHeader}>
<div>
<div>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div>00:13:45</div>
</div>
<div>2323235</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`}>
<Swiper
loop={false}
centeredSlides={true}
modules={[Navigation, Pagination]}
spaceBetween={20}
slidesPerView={6}
navigation
pagination={false}
scrollbar={{ draggable: true }}
onSwiper={(swiper: any) => {
swiper.on('click', (e: any) => {
swiper.slideTo(e.clickedIndex)
})
}}
onSlideChange={() => { }}
>
{list.map((item, index) =>
<SwiperSlide key={item}>
<div className={styles.meetingContentSwiperCard}>
<div className={styles.meetingContentSwiperCardVdeio} id={`video-${index}`}></div>
{meetingContentUser()}
</div>
</SwiperSlide>
)}
</Swiper>
</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="/src/assets/icon18.png" 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="/src/assets/avatar.png" alt="" /></div>
<span><span style={{ color: '#02B188', marginLeft: '4px' }}></span></span>
</div>
<div>
<img src="/src/assets/icon22.png" alt="" />
<img src="/src/assets/icon23.png" 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="/src/assets/icon18.png" alt="" className='drag' onClick={() => {
setStatusList({
userList: false,
userChatList: false,
})
}} />
</div>
<div className={styles.meetingUserChatContent}>
{list.map((item: number) =>
<div key={item} className={`${styles.meetingUserChatContentLeft} drag`}>
<div>
<div><img src="/src/assets/avatar.png" alt="" /></div>
<span></span>
</div>
<div></div>
</div>
)}
</div>
<div className={`${styles.meetingUserChatInput} drag`}>
<Input.TextArea placeholder="请输入消息" style={{ flexGrow: 1 }}></Input.TextArea>
<Button type="primary" className='m-ant-btn' style={{ flexShrink: 0, marginTop: '4px' }}></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={() => {
window.electron.leaveChannel()
window.electron.stopScreenCapture()
navigate(-1)
}}></div>
<div onClick={() => {
window.electron.leaveChannel()
window.electron.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.icon + '-active.png' : row.icon + '.png'} alt="" />
<span>{row.title}</span>
</div>
</Popover> :
<div className='drag' onClick={() => changeStatusList(row, itemIndex, rowIndex)} key={rowIndex}>
<img src={row.active ? row.icon + '-active.png' : row.icon + '.png'} 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={() => changeVdeio(false)}></Button>
</div>
</div>
</div>
</Modal>
<Modal title="设置向导" open={isStupWizard} footer={null} closable={false} centered width={'40vw'}>
<div className={styles.stupWizard}>
<div>
<div>{stepsStatus ? '音频设置向导' : '视频测试'}</div>
<div>
<div>
<span></span>
<Select
options={audioDeviceManager.currentDevices} style={{ flexGrow: 1 }}
value={audioDeviceManager.currentDevice} onChange={(e) => {
setAudioDeviceManager({
...audioDeviceManager,
currentDevice: e
})
window.electron.setRecordingDevice(e);
getAudioMediaList()
}} />;
<audio src="" id='startAudio'></audio>
</div>
{stepsStatus ? <div>
<span></span>
<Slider min={0} max={255} value={audioDeviceManager.currentVolume} onChange={(e) => {
setAudioDeviceManager({
...audioDeviceManager,
currentVolume: e
})
window.electron.setRecordingDeviceVolume(e)
}} style={{ flexGrow: 1 }} />
</div> :
<div>
<span></span>
<video id='startPreview'
poster={'/src/assets/error.png'}
style={{ width: '226px', height: '136px', backgroundColor: 'black' }}>
</video>
</div>
}
</div>
{stepsStatus ? <div>
<span></span>
<div>
<img src="/src/assets/icon33.png" alt="" />
<div>
<img src="/src/assets/icon34.png" alt="" />
<div id='recordingDeviceTest'>
<img src="/src/assets/icon35.png" alt="" />
</div>
</div>
</div>
</div> : null}
</div>
<div>
{stepsStatus ? <div>
<Button type="primary" className='m-ant-btn' onClick={() => {
let audio = document.getElementById('startAudio') as any;
if (audio.srcObject) {
const tracks = audio.srcObject.getTracks();
tracks.forEach((track: any) => {
track.stop();
});
audio.srcObject = null;
}
setStepsStatus(false)
window.electron.startPreview().then((res: boolean) => {
setIsVideoLoad(res)
})
}}></Button>
</div> :
<div>
<Button
type="primary"
style={{ backgroundColor: '#31353A', marginRight: '14px' }}
onClick={() => {
if (isVideoLoad) {
setIsVideoLoad(false)
window.electron.stopAudioDeviceLoopbackTest()
setStepsStatus(true)
window.electron.startRecordingDeviceTest(200)
} else {
message.error('视频加载中!')
}
}}
></Button>
<Button
type="primary"
className='m-ant-btn'
onClick={() => {
if (isVideoLoad) {
window.electron.stopAudioDeviceLoopbackTest()
window.electron.setRecordingDeviceVolume(audioDeviceManager.currentVolume)
setIsStupWizard(false)
setStepsStatus(true)
setIsVideoLoad(false)
} else {
message.error('视频加载中!')
}
}}
>
</Button>
</div>}
</div>
</div>
</Modal>
<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.channelId
})
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).then(res => {
if (res.code === 200) {
window.electron.dwFile(res.data)
}
})
}} />
{/* <FolderOutlined title='文件' style={{ color: '#FFA000', cursor: 'pointer' }} /> */}
</>
)} />
</Table>
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '10px' }}>
<AntdPagination 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 = () => {
return (
<>
<div className={styles.meetingContentUser}>
<div className={styles.meetingContentUserRole}>
<img src="/src/assets/icon32.png" alt="" />
</div>
<div className={styles.meetingContentUserName}>
<img src="/src/assets/icon22.png" alt="" />
<span></span>
</div>
</div>
</>
)
}
export default Meeting