Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

前端录屏组件和管理端录屏回放模块 #59

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61,044 changes: 61,044 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,16 @@
"file-loader": "^6.2.0",
"fs-extra": "^9.0.1",
"html-webpack-plugin": "4.5.0",
"html2canvas": "^1.4.1",
"i18next": "^20.4.0",
"i18next-browser-languagedetector": "^6.1.3",
"identity-obj-proxy": "3.0.0",
"jest": "26.6.0",
"jest-circus": "26.6.0",
"jest-resolve": "26.6.0",
"jest-watch-typeahead": "0.6.1",
"js-md5": "^0.7.3",
"mini-css-extract-plugin": "0.11.3",
"nginx": "^1.0.8",
"node-sass": "^6.0.1",
"optimize-css-assets-webpack-plugin": "5.0.4",
"pnp-webpack-plugin": "1.6.4",
Expand Down Expand Up @@ -207,5 +208,8 @@
"jest-watch-typeahead/testname"
],
"resetMocks": true
},
"devDependencies": {
"jest-circus": "^29.7.0"
}
}
2 changes: 1 addition & 1 deletion src/Component/problemSet/PSLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,4 @@ const mapDispatchToProps = (dispatch: Dispatch<any>) => ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(withTranslation()(withRouter(PSLayout)))
)(withTranslation()(withRouter(PSLayout)))
82 changes: 82 additions & 0 deletions src/Component/screenrecord/ScreenRecord.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useEffect } from 'react';
import cApi from "Utils/API/c-api";
import { useSelector } from "react-redux";

const ScreenshotComponent = (props: any) => {
const userInfo = useSelector((state: any) => state.UserReducer?.userInfo);

const bs_id = props.match.params.problemSetId;
const u_name = userInfo.username;
const u_id = parseInt(userInfo.userId, 10);
const token = generateToken(userInfo.userid);

useEffect(() => {
let mediaRecorder: MediaRecorder;
let recordedBlobs: Blob[] = [];

const startRecording = async () => {
try {
const displayStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
mediaRecorder = new MediaRecorder(displayStream, { mimeType: 'video/webm' });

mediaRecorder.ondataavailable = (event) => {
if (event.data && event.data.size > 0) {
recordedBlobs.push(event.data);
}
};

mediaRecorder.onstop = async () => {
const blob = new Blob(recordedBlobs, { type: 'video/webm' });
const formData = new FormData();
formData.append('token', token);
formData.append('video', blob, 'recordedVideo.webm');

const data: any = await cApi.addFrame(formData);
console.log(data);
if (data === "无此视频记录") {
const recordData = {
bs_id,
u_name,
u_id,
token,
};
await cApi.addRecord(recordData);
await cApi.addFrame(formData);
} else if (data === "视频正在导出") {
console.log('视频正在导出');
}
};

mediaRecorder.start(1000);
} catch (error) {
console.error('Error capturing screen:', error);
}
};

startRecording();

const intervalId = setInterval(() => {
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
startRecording(); // Restart the recording
}
}, 10000);

return () => {
if (mediaRecorder) {
mediaRecorder.stop();
}
clearInterval(intervalId);
};
}, [bs_id, u_name, u_id, token]);

return null;
};

export default ScreenshotComponent;

function generateToken(username: string) {
const date = new Date();
const formattedDate = date.toISOString().replace(/[-:.TZ]/g, '').substring(0, 14);
return `${username}#${formattedDate}`;
}
141 changes: 141 additions & 0 deletions src/Component/screenrecord/UserListButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import React, { useState } from 'react';
import { Button, message, Modal, Space, Table } from 'antd';
import mApi from "Utils/API/m-api";

interface VideoListProps {
psid: number;
token: string;
onRefresh: () => void;
}

const UserListButton: React.FC<VideoListProps> = ({ psid, onRefresh}) => {
const [videoList, setVideoList] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [isModalVisible, setIsModalVisible] = useState(false);

const fetchVideoList = async () => {
setLoading(true);
try {
const data = await mApi.getVideoList({ bs_id: psid });
// 假设响应格式是 { data: any[] }
if (data && Array.isArray(data)) {
setVideoList(data);
setIsModalVisible(true); // 显示模态框
} else {
// 如果响应格式不正确或数据不是数组,则抛出错误
throw new Error('Invalid data format or data is not an array');
}
} finally {
setLoading(false);
}
};

const handleDownload = async (record: any) => {
try {
const blob:any = await mApi.getVideo(record.token)

const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
// 修改视频文件名为 用户名_开始时间.mp4
const videoName = `${record.u_name}_${record.start_time.replace(/:/g, '-')}.mp4`;
link.setAttribute('download', videoName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);

message.success('视频下载成功');
} catch (error: any) {
message.error(`视频下载失败: ${error.message}`);
}
};

const handleDelete = async (token: string) => {
await mApi.deleteVideo({token:token});
message.success('视频删除成功');
setVideoList(videoList.filter((video) => video.token !== token));
};

const handleDeleteAll = async () => {
await mApi.deleteAll({ bs_id: psid });
message.success('所有视频记录删除成功');
setVideoList([]);
};

const columns = [
{
title: '用户名',
dataIndex: 'u_name',
key: 'u_name',
},
{
title: '开始时间',
dataIndex: 'start_time',
key: 'start_time',
},
{
title: '最后修改时间',
dataIndex: 'modify_time',
key: 'modify_time',
},
{
title: '操作',
key: 'action',
render: (text: any, record: any) => (
<Space size="middle">
<Button onClick={() => handleDownload(record)}>下载</Button>
<Button onClick={() => handleDelete(record.token)}>删除</Button>
</Space>
),
},
];

const handleOk = () => {
setIsModalVisible(false);
onRefresh();
};

const modalFooter = (
<Button type="primary" onClick={handleOk}>
确定
</Button>
);

return (
<>
<Button type="primary" onClick={fetchVideoList}>
查看视频记录
</Button>
{loading && <div>加载中...</div>}
<Modal
title={
<div>
视频记录
<Button
style={{ float: 'right' }}
onClick={handleDeleteAll}
>
删除所有视频
</Button>
</div>
}
visible={isModalVisible}
onOk={handleOk}
width={1000}
closable={false}
footer={modalFooter}
>
<Table
columns={columns}
dataSource={videoList}
rowKey={(record) => record.u_id}
pagination={false}
bordered
/>
</Modal>
</>
);
};

export default UserListButton;
11 changes: 10 additions & 1 deletion src/Config/router/routerM.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
NotificationOutlined,
ReconciliationOutlined,
UsergroupAddOutlined,
UserOutlined
UserOutlined,
VideoCameraOutlined
} from "@ant-design/icons";

import {lazy} from "react";
Expand Down Expand Up @@ -219,4 +220,12 @@ export const routerM: IRouter[] = [
// },
// ]
// }
{
id: 10,
path: UrlPrefix + "/manage/replay",
title_i18n: "录屏回放",
exact: true,
icon: <VideoCameraOutlined/>,
component: lazy(() => import('../../Pages/Manage/MReplay'))
},
]
Loading