├── .gitignore ├── .npmrc ├── .babelrc ├── img ├── 1.png ├── 2.png ├── 3.png ├── 4.jpg ├── 5.png ├── 6.png ├── 7.png ├── bg.jpg ├── logo.png └── my-favicon.ico ├── src ├── style │ ├── interviewer │ │ ├── interviewManage.less │ │ ├── interviewEntrance.less │ │ ├── showTest.less │ │ ├── examInform.css │ │ ├── modify.less │ │ ├── examReport.css │ │ ├── add.less │ │ ├── interviewRoom.css │ │ └── testAlone.less │ ├── candidate │ │ ├── markdownEditor.css │ │ ├── code.css │ │ ├── countDown.less │ │ ├── showTests.less │ │ ├── candidateExam.css │ │ ├── drawer.css │ │ └── program.less │ ├── basicInform.less │ ├── login │ │ └── login.less │ └── basic.less ├── useRedux │ ├── constant.ts │ ├── actions │ │ └── showExam.tsx │ ├── store.ts │ └── reducers │ │ └── showExam.tsx ├── api │ ├── modules │ │ ├── test.ts │ │ ├── candidate.ts │ │ ├── interview.ts │ │ ├── paper.ts │ │ └── user.ts │ └── index.ts ├── index.tsx ├── common │ ├── components │ │ ├── footer.tsx │ │ ├── interviewer │ │ │ ├── dropdownMenu.tsx │ │ │ ├── wangeditor.tsx │ │ │ ├── wangeditor2.tsx │ │ │ ├── examReport.tsx │ │ │ ├── paper.tsx │ │ │ └── tabler.tsx │ │ ├── candidate │ │ │ ├── countdown.tsx │ │ │ ├── markdownEditor.tsx │ │ │ ├── testAlone.tsx │ │ │ ├── codeEditor.tsx │ │ │ └── programInform.tsx │ │ ├── navbar.tsx │ │ ├── Socket.tsx │ │ ├── breadcrumbs.tsx │ │ ├── header.tsx │ │ ├── webrtcCopy.tsx │ │ └── webrtc.tsx │ ├── types.ts │ ├── const.ts │ └── utils.ts ├── index.html ├── pages │ ├── candidate │ │ ├── watchTest.tsx │ │ ├── showTests.tsx │ │ └── index.tsx │ └── interviewer │ │ ├── index.tsx │ │ ├── interview │ │ ├── showTest.tsx │ │ ├── entrance.tsx │ │ ├── manage.tsx │ │ └── room.tsx │ │ ├── consult │ │ ├── lookOver.tsx │ │ ├── showExam.tsx │ │ └── examInform.tsx │ │ └── edit │ │ ├── add.tsx │ │ ├── modify.tsx │ │ └── show.tsx └── App.tsx ├── images.d.ts ├── deploy ├── config.js └── index.js ├── webpack ├── webpack.prod.js ├── webpack.dev.js └── webpack.common.js ├── tsconfig.json ├── 视频通话技术方案.md ├── README.md ├── package.json └── problem.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package-lock.json -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry = 'https://registry.npm.taobao.org' -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | } 4 | -------------------------------------------------------------------------------- /img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HCYETY/react-ts-coding_web/HEAD/img/1.png -------------------------------------------------------------------------------- /img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HCYETY/react-ts-coding_web/HEAD/img/2.png -------------------------------------------------------------------------------- /img/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HCYETY/react-ts-coding_web/HEAD/img/3.png -------------------------------------------------------------------------------- /img/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HCYETY/react-ts-coding_web/HEAD/img/4.jpg -------------------------------------------------------------------------------- /img/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HCYETY/react-ts-coding_web/HEAD/img/5.png -------------------------------------------------------------------------------- /img/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HCYETY/react-ts-coding_web/HEAD/img/6.png -------------------------------------------------------------------------------- /img/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HCYETY/react-ts-coding_web/HEAD/img/7.png -------------------------------------------------------------------------------- /img/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HCYETY/react-ts-coding_web/HEAD/img/bg.jpg -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HCYETY/react-ts-coding_web/HEAD/img/logo.png -------------------------------------------------------------------------------- /img/my-favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HCYETY/react-ts-coding_web/HEAD/img/my-favicon.ico -------------------------------------------------------------------------------- /src/style/interviewer/interviewManage.less: -------------------------------------------------------------------------------- 1 | .site-layout{ 2 | .interviewButton{ 3 | margin: 10px; 4 | } 5 | } -------------------------------------------------------------------------------- /src/useRedux/constant.ts: -------------------------------------------------------------------------------- 1 | export const GET_EXAM: string = 'getExam'; 2 | export const GET_EMAIL: string = 'getEmail'; 3 | export const GET_PROGRAM_EXAM: string = 'getProgramExam'; -------------------------------------------------------------------------------- /images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' 2 | declare module '*.png' 3 | declare module '*.jpg' 4 | declare module '*.jpeg' 5 | declare module '*.gif' 6 | declare module '*.bmp' 7 | declare module '*.tiff' 8 | -------------------------------------------------------------------------------- /src/useRedux/actions/showExam.tsx: -------------------------------------------------------------------------------- 1 | // import { GET_EXAM } from 'useRedux/constant'; 2 | // import store from 'redux/' 3 | // export const getExam = examName => ({ 4 | // type: GET_EXAM, 5 | // data: examName 6 | // }) -------------------------------------------------------------------------------- /src/style/candidate/markdownEditor.css: -------------------------------------------------------------------------------- 1 | .editor{ 2 | display: flex; 3 | } 4 | .editor-textarea{ 5 | height: 40px; 6 | } 7 | .editor-bottom{ 8 | display: flex; 9 | height: 10px; 10 | line-height: 10px; 11 | } -------------------------------------------------------------------------------- /deploy/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 服务器相关配置 3 | */ 4 | const server_list = { 5 | host: '', 6 | port: 22, 7 | username: 'root', 8 | password: '', 9 | path: '/usr/local/nginx/html/dist' 10 | } 11 | 12 | module.exports = server_list; -------------------------------------------------------------------------------- /src/api/modules/test.ts: -------------------------------------------------------------------------------- 1 | import { post } from 'api/index'; 2 | 3 | // 添加试题接口 4 | export function addTest(data: any) { 5 | return post('/add_test', data); 6 | } 7 | // 获取试题接口 8 | export function showTest(data?: { paper?: string; test?: string; }) { 9 | return post('/show_test', data); 10 | } -------------------------------------------------------------------------------- /src/style/interviewer/interviewEntrance.less: -------------------------------------------------------------------------------- 1 | .interviewCheck{ 2 | background: white; 3 | border-radius: 10px; 4 | box-shadow: 0 2px 8px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,.1); 5 | padding: 40px; 6 | position: absolute; 7 | top: 50%; 8 | left: 50%; 9 | transform: translate(-30%, -70%); 10 | } -------------------------------------------------------------------------------- /src/useRedux/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers } from 'redux'; 2 | import { composeWithDevTools } from 'redux-devtools-extension'; 3 | import showExam from 'useRedux/reducers/showExam'; 4 | 5 | const allReducer = combineReducers({ 6 | exam: showExam 7 | }) 8 | 9 | export default createStore(showExam, composeWithDevTools()); -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from 'src/App'; 4 | import { Provider } from 'react-redux'; 5 | import store from 'useRedux/store'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('app') 12 | ) 13 | -------------------------------------------------------------------------------- /src/api/modules/candidate.ts: -------------------------------------------------------------------------------- 1 | import { post } from 'api/index'; 2 | 3 | // 查找候选人信息接口 4 | export function search(data?: any) { 5 | return post('/search', data); 6 | } 7 | // 提交候选人信息接口 8 | export function submit(data: any) { 9 | return post('/submit', data); 10 | } 11 | // 评论区留言接口 12 | export function comment(data?: any) { 13 | return post('/comment', data); 14 | } -------------------------------------------------------------------------------- /src/common/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Layout } from 'antd'; 3 | const { Footer } = Layout; 4 | import 'style/basic.less'; 5 | 6 | export default class Foot extends React.PureComponent{ 7 | render() { 8 | return( 9 | 12 | ) 13 | } 14 | } -------------------------------------------------------------------------------- /src/style/candidate/code.css: -------------------------------------------------------------------------------- 1 | .breakpoints { 2 | background: red; 3 | width: 10px !important; 4 | height: 10px !important; 5 | left: 0px !important; 6 | top: 3px; 7 | border-radius: 5px; 8 | } 9 | 10 | .breakpoints-fake { 11 | background: red; 12 | width: 10px !important; 13 | height: 10px !important; 14 | left: 0px !important; 15 | top: 3px; 16 | border-radius: 5px; 17 | } 18 | -------------------------------------------------------------------------------- /src/style/candidate/countDown.less: -------------------------------------------------------------------------------- 1 | .time-box{ 2 | display: flex; 3 | flex-grow: 1; 4 | .time{ 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-items: center; 9 | width: 25%; 10 | height: 75%; 11 | background: white; 12 | border-radius: 10px; 13 | margin: 10px; 14 | .times{ 15 | font-size: 3em; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/style/interviewer/showTest.less: -------------------------------------------------------------------------------- 1 | .inform{ 2 | margin: 15px; 3 | } 4 | .inform-top{ 5 | display: flex; 6 | justify-content: space-between; 7 | } 8 | .inform-status{ 9 | margin: 5px; 10 | display: flex; 11 | } 12 | .inform-content{ 13 | overflow: hidden; 14 | } 15 | .inform-bottom{ 16 | display: flex; 17 | justify-content: space-between; 18 | margin-top: 10px 0px; 19 | border-bottom: 1px solid red; 20 | margin-bottom: 10px; 21 | } -------------------------------------------------------------------------------- /webpack/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const commonConfig = require('./webpack.common.js'); 3 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 4 | 5 | const devConfig = merge(commonConfig, { 6 | mode: 'production', 7 | devtool: 'none', 8 | stats: { 9 | timings: true, 10 | builtAt: true 11 | }, 12 | plugins: [ 13 | new BundleAnalyzerPlugin({ analyzerPort: 8001 }) 14 | ], 15 | }) 16 | 17 | module.exports = devConfig; -------------------------------------------------------------------------------- /deploy/index.js: -------------------------------------------------------------------------------- 1 | const client = require('scp2'); 2 | const server = require('./config.js'); 3 | const [ , , host, password] = process.argv; 4 | server.host = host; 5 | server.password = password; 6 | 7 | client.scp('dist/', { 8 | port: server.port, 9 | host: server.host, 10 | username: server.username, 11 | password: server.password, 12 | path: server.path 13 | }, function(err) { 14 | if (err) { 15 | console.log('文件上传失败', err) 16 | } else { 17 | console.log('文件上传成功'); 18 | } 19 | }) -------------------------------------------------------------------------------- /src/style/basicInform.less: -------------------------------------------------------------------------------- 1 | .paper{ 2 | display: inline-block; 3 | } 4 | 5 | .level{ 6 | display: inline-block; 7 | margin-left: 70px; 8 | width: 100px; 9 | 10 | .easy{ 11 | color: rgba(0,175,155,1); 12 | } 13 | .middle{ 14 | color: rgba(255,184,0,1); 15 | } 16 | .hard{ 17 | color: rgba(255,45,85,1); 18 | } 19 | } 20 | 21 | .time{ 22 | display: inline-block; 23 | margin-left: 70px; 24 | } 25 | 26 | .tags{ 27 | display: inline-block; 28 | margin-left: 70px; 29 | width: 100px; 30 | } -------------------------------------------------------------------------------- /src/common/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "_@types_react@16.14.21@@types/react"; 2 | 3 | export interface testObj { 4 | test_name: string; 5 | level: string; 6 | tags: string[]; 7 | test: string; 8 | } 9 | interface arrayoBJ { 10 | path: string; 11 | breadcrumbName: string; 12 | } 13 | export interface Route { 14 | path: string; 15 | breadcrumbName: string; 16 | component: ReactNode; 17 | children?: Array<{ 18 | path: string; 19 | breadcrumbName: string; 20 | component: ReactNode; 21 | }>; 22 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 实习项目 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | -------------------------------------------------------------------------------- /src/pages/candidate/watchTest.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // import 'style/candidate/programInform.less'; 4 | import ProgramInform from 'common/components/candidate/programInform'; 5 | 6 | export default class App extends React.Component { 7 | 8 | 9 | 10 | render() { 11 | 12 | return( 13 |
14 |
15 | 16 |
17 | 18 |
19 | 你好 20 |
21 |
22 | ) 23 | } 24 | } -------------------------------------------------------------------------------- /src/style/interviewer/examInform.css: -------------------------------------------------------------------------------- 1 | .exam-inform-box{ 2 | padding: 20px; 3 | } 4 | 5 | .site-content-top{ 6 | height: 100px; 7 | display: flex; 8 | justify-content: space-between; 9 | } 10 | .site-content-top-left{ 11 | display: grid; 12 | justify-content: start; 13 | } 14 | .site-content-top-right{ 15 | margin: 10px; 16 | display: flex; 17 | } 18 | .nodo{ 19 | color: rgb(33,40,53); 20 | } 21 | .doing{ 22 | color: rgba(255,184,0,1); 23 | } 24 | .done{ 25 | color: rgba(0,175,155,1); 26 | } 27 | /* .site-content-top-left-top{ 28 | font-size: 20px; 29 | margin: 10px; 30 | } */ -------------------------------------------------------------------------------- /src/useRedux/reducers/showExam.tsx: -------------------------------------------------------------------------------- 1 | import { GET_EMAIL, GET_EXAM, GET_PROGRAM_EXAM } from "useRedux/constant"; 2 | 3 | const initState = { exam: 'hello'}; 4 | 5 | export default function examReducer(preState = initState, action: { type: string; lookExam: string; lookEmail: string; programExam: string; }) { 6 | switch(action.type) { 7 | case GET_EXAM: 8 | return { lookExam: action.lookExam }; 9 | case GET_EMAIL: 10 | return { lookEmail: action.lookEmail }; 11 | case GET_PROGRAM_EXAM: 12 | return { programExam: action.programExam }; 13 | default: 14 | return preState; 15 | } 16 | } -------------------------------------------------------------------------------- /src/api/modules/interview.ts: -------------------------------------------------------------------------------- 1 | import { post } from 'api/index'; 2 | 3 | // 创建面试间接口 4 | export function createInterview(data: any) { 5 | return post('/create_interview', data); 6 | } 7 | // 查询面试间信息接口 8 | export function findInterview(data?: { interviewer?: string, candidate?: string, isInterviewer?: boolean, findArr?: any, cookie?: string }) { 9 | return post('/find_interview', data); 10 | } 11 | // 删除面试间信息接口 12 | export function deleteInterview(data: any) { 13 | return post('/delete_interview', data); 14 | } 15 | // 提交面试结果接口 16 | export function submitInterview(data: { submitArr: any }) { 17 | return post('/submit_interview', data); 18 | } -------------------------------------------------------------------------------- /src/style/interviewer/modify.less: -------------------------------------------------------------------------------- 1 | .site-card-border-less-wrapper{ 2 | width: 100%; 3 | height: 100%; 4 | padding: 20px; 5 | background: #ececec; 6 | 7 | .paper{ 8 | display: inline-block; 9 | width: 520px; 10 | } 11 | .time{ 12 | display: inline-block; 13 | margin-right: 70px; 14 | } 15 | 16 | .site-card-divide{ 17 | border-bottom: 1px solid rgb(202, 200, 200); 18 | margin-bottom: 20px; 19 | padding-bottom: 15px; 20 | } 21 | 22 | .choice-time{ 23 | display: flex; 24 | } 25 | .choice-time-button{ 26 | border: none; 27 | color: #1890ff; 28 | background: white; 29 | } 30 | } -------------------------------------------------------------------------------- /src/style/interviewer/examReport.css: -------------------------------------------------------------------------------- 1 | .content{ 2 | margin: 15px; 3 | border-radius: 10px; 4 | box-shadow: 0 2px 8px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,.1); 5 | background: white; 6 | } 7 | 8 | .report{ 9 | display: grid; 10 | grid-template-columns: 1fr 1fr; 11 | border-bottom: 1px solid #000; 12 | padding: 20px; 13 | } 14 | .report-content-right{ 15 | margin-left: 20px; 16 | } 17 | .report-content-right-top{ 18 | width: 250px; 19 | padding: 10px; 20 | border: 1px solid black; 21 | border-radius: 10px; 22 | display: grid; 23 | } 24 | .report-content-right-content{ 25 | background: #c0c0c0; 26 | margin: 20px 0px; 27 | } -------------------------------------------------------------------------------- /webpack/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const { merge } = require('webpack-merge'); 3 | const path = require('path'); 4 | const commonConfig = require('./webpack.common.js'); 5 | 6 | const devConfig = merge(commonConfig, { 7 | mode: 'development', 8 | devtool: "cheap-module-eval-source-map", 9 | plugins: [ 10 | new webpack.HotModuleReplacementPlugin(), 11 | ], 12 | devServer: { 13 | contentBase: path.join(__dirname, 'dist'), 14 | compress: true, 15 | port: 3000, 16 | hot: true, 17 | open: true, 18 | progress: true, 19 | historyApiFallback: true 20 | } 21 | }) 22 | 23 | module.exports = devConfig; 24 | -------------------------------------------------------------------------------- /src/api/modules/paper.ts: -------------------------------------------------------------------------------- 1 | import { post } from 'api/index'; 2 | 3 | // 获取试卷接口 4 | export function showPaper(data?: { paper?: string; cookie?: string; interviewer?: boolean }) { 5 | return post('/paper', data); 6 | } 7 | // 新建试卷接口 8 | export function addPaper(data: { cookie: string; values: any; }) { 9 | return post('/add_paper', data); 10 | } 11 | // 删除试卷接口 12 | export function deletePaper(data: number[]) { 13 | return post('/delete_paper', data); 14 | } 15 | // 修改试卷接口 16 | export function modifyPaper(data: any) { 17 | return post('/modify_paper', data); 18 | } 19 | // 批阅试卷接口 20 | export function lookOver(data?: any) { 21 | return post('/look_over', data); 22 | } -------------------------------------------------------------------------------- /src/common/components/interviewer/dropdownMenu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Select, Menu, Dropdown, Button, Popover, } from 'antd'; 3 | import { DownOutlined } from '@ant-design/icons'; 4 | 5 | export default class DropdownMenu extends React.PureComponent{ 6 | render() { 7 | const text = Title; 8 | const content = ( 9 |
10 |

Content

11 |

Content

12 |
13 | ); 14 | 15 | 16 | return( 17 |
18 | 19 | 20 | 21 |
22 | ) 23 | } 24 | } -------------------------------------------------------------------------------- /src/pages/interviewer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { Layout } from 'antd'; 3 | const { Content } = Layout; 4 | 5 | import Navbar from 'common/components/navbar'; 6 | import Foot from 'common/components/footer'; 7 | 8 | export default class Interviewer extends PureComponent{ 9 | render() { 10 | return( 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | Bill is a cat. 19 |
20 |
21 | 22 | 23 |
24 |
25 | ) 26 | } 27 | } -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { REQUESTIP } from 'common/const'; 3 | 4 | axios.defaults.withCredentials = true; 5 | 6 | export function generateHttpApi(method: 'get' | 'post') { 7 | return async (url: string, params?: any) => { 8 | const data = method === 'get' ? { 9 | params 10 | } : { 11 | data: params 12 | }; 13 | url = REQUESTIP + url; 14 | try { 15 | const response = await axios({ 16 | url, 17 | method, 18 | ...data, 19 | }); 20 | return response.data; 21 | } catch (error) { 22 | return await Promise.reject(error); 23 | } 24 | } 25 | } 26 | 27 | export const get = generateHttpApi('get'); 28 | export const post = generateHttpApi('post'); -------------------------------------------------------------------------------- /src/api/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { post } from 'api/index'; 2 | 3 | // 向邮箱发送验证码 4 | export function sendEmail(data: { email: any; }) { 5 | return post('/email', data); 6 | } 7 | // 登录接口 8 | export function testLogin(data: { email?: string; cypher?: string; cookie?: string }) { 9 | return post('/login', data); 10 | } 11 | // 注册接口 12 | export function testRegister(data: { email: string; cypher: string; captcha: string; identity: number}) { 13 | return post('/register', data); 14 | } 15 | // 退出登录接口 16 | export function logout(data: { cookie: string }) { 17 | return post('/logout', data); 18 | } 19 | // 查找所有面试官或候选人的邮箱接口 20 | export function searchEmail(data?: { cookie?: string; interviewer?: boolean }) { 21 | return post('/search_email', data); 22 | } -------------------------------------------------------------------------------- /src/style/candidate/showTests.less: -------------------------------------------------------------------------------- 1 | html, body{ 2 | background: #ececec; 3 | 4 | .tests-box{ 5 | display: flex; 6 | .test-box{ 7 | flex: 0.65; 8 | border-radius: 10px; 9 | box-shadow: 0 2px 8px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,.1); 10 | width: 60%; 11 | // height: 100%; 12 | margin: 30px; 13 | border: 1px solid rgba(0,10,32,.05); 14 | background: white; 15 | float: left; 16 | } 17 | 18 | .inform-box{ 19 | flex: 0.35; 20 | display: flex; 21 | flex-direction: column; 22 | margin-left: 20px; 23 | 24 | .inform-box-countdown{ 25 | // background-color: skyblue; 26 | margin: 20px; 27 | font-size: 2em; 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "../dist/", 4 | "noImplicitAny": false, 5 | "module": "esnext", 6 | "target": "es5", 7 | "experimentalDecorators": true, 8 | "jsx": "react", 9 | "allowJs": true, 10 | "suppressImplicitAnyIndexErrors": true, 11 | "esModuleInterop": true, 12 | "moduleResolution": "node", 13 | "baseUrl": "./", 14 | "paths": { 15 | "img/*": ["./img/*"], 16 | "src/*": ["./src/*"], 17 | "api/*": ["./src/api/*"], 18 | "pages/*": ["./src/pages/*"], 19 | "common/*": ["./src/common/*"], 20 | "useRedux/*": ["./src/useRedux/*"], 21 | "style/*": ["./src/style/*"], 22 | } 23 | }, 24 | "include": [ 25 | "./src/*", 26 | "./images.d.ts", 27 | "./src/Sendmail.js" 28 | ], 29 | "exclude": [ 30 | "./node_modules" 31 | ] 32 | } -------------------------------------------------------------------------------- /src/style/interviewer/add.less: -------------------------------------------------------------------------------- 1 | .site-layout{ 2 | .site-layout-content{ 3 | 4 | .form-button-right { 5 | margin: 0px 10px 10px 0px; 6 | float: right; 7 | } 8 | 9 | // .drawer{ 10 | // display: flex; 11 | 12 | // .drawer-left{ 13 | // flex: 1; 14 | // } 15 | // .drawer-right{ 16 | // flex: 1; 17 | // background-color: skyblue; 18 | // } 19 | // } 20 | 21 | // .card-right{ 22 | // .dynamic-delete-button { 23 | // position: relative; 24 | // top: 4px; 25 | // margin: 0 8px; 26 | // color: #999; 27 | // font-size: 24px; 28 | // cursor: pointer; 29 | // transition: all 0.3s; 30 | // display: inline; 31 | // } 32 | // .dynamic-delete-button:hover { 33 | // color: #777; 34 | // } 35 | // .dynamic-delete-button[disabled] { 36 | // cursor: not-allowed; 37 | // opacity: 0.5; 38 | // } 39 | // } 40 | } 41 | } -------------------------------------------------------------------------------- /src/style/interviewer/interviewRoom.css: -------------------------------------------------------------------------------- 1 | .box{ 2 | display: flex; 3 | margin: 15px; 4 | overflow: hidden; 5 | } 6 | .box-left{ 7 | width: 75%; 8 | background: white; 9 | box-shadow: 0 2px 8px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,.1); 10 | } 11 | .program{ 12 | display: flex; 13 | } 14 | .program-left{ 15 | width: 30%; 16 | padding: 10px; 17 | /* background: red; */ 18 | align-items: center; 19 | justify-content: center; 20 | overflow: auto; 21 | } 22 | .program-left-before{ 23 | display: grid; 24 | justify-content: center; 25 | align-content: center; 26 | } 27 | .program-left-after{ 28 | display: flex; 29 | justify-content: space-between; 30 | } 31 | .program-inform{ 32 | overflow: auto; 33 | } 34 | .program-right{ 35 | width: 70%; 36 | background: skyblue; 37 | } 38 | .program-right-after{ 39 | overflow: auto; 40 | } 41 | 42 | .box-right{ 43 | width: 25%; 44 | background: rgb(23, 33, 51);; 45 | } 46 | .box-right-show-inform{ 47 | width: 100%; 48 | height: 200px; 49 | background: yellow; 50 | overflow-y: auto; 51 | } -------------------------------------------------------------------------------- /视频通话技术方案.md: -------------------------------------------------------------------------------- 1 | ## 需求背景 2 | 面试官和候选人在同一个面试间进行一对一面试,面试官或者候选人可能需要进行视频通话或者语音通话进行实时交流。 3 | 4 | ## 设计方案 5 | 使用 WebRTC 进行网络通信, 6 | 7 | ## 实现流程 8 | ### webRTC大致流程 9 | 获取本地媒体流放到video的src中 10 | AB两点连接需要创建双方各自的 RTCPeerConnection 11 | A点创建offer后设置本地视频setLocalDescription 12 | A点发送带有本地sdp的offer给B点 13 | B点收到offer并根据offer的sdp设置远端视频setRemoteDescription 14 | B点创建answer后设置本地视频setLocalDescription 15 | B点发送带有本地sdp的answer给A 16 | A点收到answer并根据answer的sdp设置远端视频setRemoteDescription 17 | 至此AB两点都设置了各自的本地和远端视频,点对点视频通话完成 18 | ### 白话翻译:张三和李四视频通信过程 19 | 首先张三和李四各自都有自己的本地视频标签video和远端视频标签video 20 | 张三创建自己本地视频流并放到自己(张三)本地视频标签video中 21 | 张三将自己(张三)的本地视频流发给李四 22 | 李四收到张三的本地视频流并将其放到自己(李四)远端视频标签video中 23 | 李四创建自己的本地视频流放到自己(李四)本地视频标签video中 24 | 李四将自己(李四)的本地视频流发给张三 25 | 张三收到李四的本地视频流并将其放到自己(张三)远端视频标签video中 26 | 这样张三和李四就可以视频通信了 27 | 28 | ## 场景推演 29 | 1. 只有一个用户进入面试间后,该用户立即发起视频通话。 30 | > 31 | 2. 两个用户同时发起或挂断视频通话。 32 | > 33 | 3. 一个用户在不挂断的前提下直接离开面试间。 34 | > 35 | 4. 一个用户在未经同意下连续多次发起视频通话。 36 | > 弹出一个对话框,防止用户点击“视频通话”按钮。 37 | 5. 不允许第三个人进入面试间并且发起视频通话等操作。 38 | 39 | 40 | 41 | 每个需求: 42 | - 要做什么 43 | - 技术选型 44 | - 遇到的困难 45 | - 怎么解决 46 | - 收获了什么 -------------------------------------------------------------------------------- /src/style/interviewer/testAlone.less: -------------------------------------------------------------------------------- 1 | .exam-box{ 2 | height: 100px; 3 | text-align: center; 4 | margin: 10px 10px 0px 10px; 5 | border-bottom: 1px solid rgba(53, 54, 56, 0.05); 6 | display: grid; 7 | grid-template-columns: 2fr repeat(3,1fr); 8 | grid-template-rows: repeat(1,minmax(0,1fr)); 9 | justify-items: start; 10 | align-items: center; 11 | padding: 0.5rem 0.75rem; 12 | 13 | .exam-box-left{ 14 | overflow: hidden; 15 | text-align: left; 16 | } 17 | 18 | .easy{ 19 | color: rgba(0,175,155,1); 20 | } 21 | .middle{ 22 | color: rgba(255,184,0,1); 23 | } 24 | .hard{ 25 | color: rgba(255,45,85,1); 26 | } 27 | 28 | .exam-box-right{ 29 | justify-self: end; 30 | .exam-status{ 31 | width: 100%; 32 | height: 35px; 33 | line-height: 35px; 34 | border-radius: 3px; 35 | color: #007AFF; 36 | background-color: rgba(0,10,32,.05); 37 | float: right; 38 | padding: 0 0.75rem; 39 | font-size: 15px; 40 | } 41 | .exam-status-finish{ 42 | color: rgb(46, 196, 46); 43 | background: none; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/style/login/login.less: -------------------------------------------------------------------------------- 1 | .app-container { 2 | height: 100vh; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: center; 6 | align-items: center; 7 | .app-background { 8 | background: url("img/bg.jpg") center no-repeat; 9 | width: 100%; 10 | height: 100%; 11 | background-size: cover; 12 | position: absolute; 13 | } 14 | 15 | .app-header { 16 | position: fixed; 17 | left: 0; 18 | top: 0; 19 | width: 100%; 20 | height: 80px; 21 | background-color: rgb(60, 194, 138); 22 | z-index: 6; 23 | img { 24 | width: 50px; 25 | height: 50px; 26 | margin-left: 50px; 27 | margin-top: 15px; 28 | } 29 | h1 { 30 | position: absolute; 31 | left: 110px; 32 | top: 17px; 33 | font-size: 30px; 34 | } 35 | } 36 | 37 | .app-content{ 38 | position: absolute; 39 | top: 150px; 40 | left: auto; 41 | width: 360px; 42 | padding: 20px; 43 | background-color: white; 44 | border-radius: 10px; 45 | 46 | .login-form-forgot{ 47 | float: right; 48 | } 49 | 50 | .login-form-button, .register-form-button{ 51 | width: 100%; 52 | margin-bottom: 10px; 53 | } 54 | 55 | .testLogin-form-button{ 56 | width: 70%; 57 | margin: 15px auto; 58 | } 59 | } 60 | } 61 | 62 | @keyframes rotate { 63 | 0% { 64 | transform: rotate(0deg); 65 | } 66 | 100% { 67 | transform: rotate(360deg); 68 | } 69 | } -------------------------------------------------------------------------------- /src/common/components/interviewer/wangeditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import E from 'wangeditor'; 3 | import PubSub from 'pubsub-js'; 4 | // import { inject, observer } from 'mobx-react' 5 | // import { withRouter } from 'react-router-dom' 6 | 7 | // @withRouter @inject('appStore') @observer 8 | 9 | interface Props{ 10 | 11 | } 12 | interface state{ 13 | editorContent: string, 14 | } 15 | 16 | let editor: E = null; 17 | 18 | export default class Wangeditor extends Component { 19 | constructor(props:Props) { 20 | super(props); 21 | this.state = { 22 | editorContent:'', 23 | }; 24 | } 25 | 26 | componentDidMount() { 27 | editor = new E('#div1'); 28 | editor.config.placeholder = '请输入面试题的相关信息' 29 | editor.config.menus = [ 30 | 'head', // 标题 31 | 'bold', // 粗体 32 | 'fontSize', // 字号 33 | 'fontName', // 字体 34 | 'italic', // 斜体 35 | 'list', // 序列 36 | 'justify', // 对齐方式 37 | 'link', // 插入链接 38 | 'image', // 插入图片 39 | 'table', // 表格 40 | 'quote', // 引用 41 | 'code', // 插入代码 42 | ] 43 | editor.config.uploadImgShowBase64 = true; 44 | editor.config.uploadImgMaxLength = 5; 45 | editor.config.uploadImgParams = { 46 | fileBytes: '', 47 | // maxBytes: 204800, 48 | // thumbHeight: 120, 49 | // thumbWidth: 120 50 | } 51 | editor.config.onchange = () => { 52 | PubSub.publish('testInform', { test: editor.txt.html() }) 53 | } 54 | PubSub.subscribe('modifyTest', (_, data) => { 55 | editor.txt.html(data.test); 56 | }); 57 | editor.create() 58 | }; 59 | 60 | componentWillUnmount() { 61 | // 销毁编辑器 62 | editor.destroy() 63 | editor = null 64 | } 65 | 66 | render() { 67 | return ( 68 |
69 | ); 70 | } 71 | } -------------------------------------------------------------------------------- /src/style/candidate/candidateExam.css: -------------------------------------------------------------------------------- 1 | html, body{ 2 | background-color: #f5f5f5; 3 | } 4 | .card-container > .ant-tabs-card { 5 | padding: 24px; 6 | overflow: hidden; 7 | background: #f5f5f5; 8 | } 9 | .card-container p { 10 | margin: 0; 11 | } 12 | .card-container > .ant-tabs-card .ant-tabs-content { 13 | height: 100%; 14 | margin-top: -16px; 15 | } 16 | .card-container > .ant-tabs-card .ant-tabs-content > .ant-tabs-tabpane { 17 | padding: 16px; 18 | background: #fff; 19 | } 20 | .card-container > .ant-tabs-card > .ant-tabs-nav::before { 21 | display: none; 22 | } 23 | .card-container > .ant-tabs-card .ant-tabs-tab, 24 | [data-theme='compact'] .card-container > .ant-tabs-card .ant-tabs-tab { 25 | background: transparent; 26 | border-color: transparent; 27 | } 28 | .card-container > .ant-tabs-card .ant-tabs-tab-active, 29 | [data-theme='compact'] .card-container > .ant-tabs-card .ant-tabs-tab-active { 30 | background: #fff; 31 | border-color: #fff; 32 | } 33 | /* #components-tabs-demo-card-top .code-box-demo { 34 | padding: 24px; 35 | overflow: hidden; 36 | background: #f5f5f5; 37 | } */ 38 | [data-theme='compact'] .card-container > .ant-tabs-card .ant-tabs-content { 39 | height: 120px; 40 | margin-top: -8px; 41 | } 42 | [data-theme='dark'] .card-container > .ant-tabs-card .ant-tabs-tab { 43 | background: transparent; 44 | border-color: transparent; 45 | } 46 | [data-theme='dark'] #components-tabs-demo-card-top .code-box-demo { 47 | background: #000; 48 | } 49 | [data-theme='dark'] .card-container > .ant-tabs-card .ant-tabs-content > .ant-tabs-tabpane { 50 | background: #141414; 51 | } 52 | [data-theme='dark'] .card-container > .ant-tabs-card .ant-tabs-tab-active { 53 | background: #141414; 54 | border-color: #141414; 55 | } -------------------------------------------------------------------------------- /src/common/components/interviewer/wangeditor2.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import E from 'wangeditor'; 3 | import PubSub from 'pubsub-js'; 4 | // import { inject, observer } from 'mobx-react' 5 | // import { withRouter } from 'react-router-dom' 6 | 7 | // @withRouter @inject('appStore') @observer 8 | 9 | interface Props{ 10 | 11 | } 12 | interface state{ 13 | editorContent:string 14 | } 15 | 16 | let editor: E = null; 17 | 18 | export default class Wangeditors extends Component { 19 | constructor(props:Props) { 20 | super(props); 21 | this.state = { 22 | editorContent:'' 23 | }; 24 | } 25 | 26 | componentDidMount() { 27 | editor = new E('#div2'); 28 | editor.config.placeholder = '请输入面试题的相关信息' 29 | editor.config.menus = [ 30 | 'head', // 标题 31 | 'bold', // 粗体 32 | 'fontSize', // 字号 33 | 'fontName', // 字体 34 | 'italic', // 斜体 35 | 'underline', // 下划线 36 | 'strikeThrough', // 删除线 37 | 'list', // 序列 38 | 'justify', // 对齐方式 39 | 'image', // 插入图片 40 | 'table', // 表格 41 | 'video', // 插入视频 42 | 'link', // 插入链接 43 | 'quote', // 引用 44 | 'code', // 插入代码 45 | ] 46 | editor.config.uploadImgShowBase64 = true; 47 | editor.config.uploadImgMaxLength = 5; 48 | editor.config.uploadImgParams = { 49 | fileBytes: '', 50 | // maxBytes: 204800, 51 | // thumbHeight: 120, 52 | // thumbWidth: 120 53 | } 54 | editor.config.onchange = () => { 55 | PubSub.publish('testAnswer', { test: editor.txt.html() }) 56 | } 57 | PubSub.subscribe('modifyAnswer', (_, data) => { 58 | editor.txt.html(data.answer); 59 | }); 60 | editor.create() 61 | }; 62 | 63 | componentWillUnmount() { 64 | // 销毁编辑器 65 | editor.destroy() 66 | editor = null 67 | } 68 | 69 | render() { 70 | return ( 71 |
72 | ); 73 | } 74 | } -------------------------------------------------------------------------------- /src/pages/interviewer/interview/showTest.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Tag } from 'antd'; 3 | import { StarOutlined, StarFilled } from '@ant-design/icons'; 4 | import 'style/interviewer/showTest.less'; 5 | import { getExamLevel } from 'src/common/utils'; 6 | import { testObj } from 'common/types'; 7 | 8 | interface Prop { 9 | inform: testObj; 10 | getTest: any; 11 | } 12 | 13 | interface State { 14 | 15 | } 16 | 17 | export default class ShowTest extends React.Component { 18 | 19 | setTest = () => { 20 | const { inform } = this.props; 21 | this.props.getTest(inform); 22 | } 23 | 24 | render() { 25 | const { inform } = this.props; 26 | 27 | return( 28 |
29 |
30 |
31 | { inform.test_name } 32 |
33 |
34 | 35 | 36 |
37 |
38 |
39 |
{ inform.level }
40 |
41 | { 42 | inform.tags.map(item => { 43 | return( 44 | { item } 45 | ) 46 | }) 47 | } 48 |
49 |
50 |
54 |
55 |
56 |
57 | 共考核次/通过率 58 |
59 |
60 | 61 |
62 |
63 |
64 | ) 65 | } 66 | } -------------------------------------------------------------------------------- /src/style/basic.less: -------------------------------------------------------------------------------- 1 | *{ 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | .all-header-div{ 7 | height: 55px; 8 | } 9 | .all-header { 10 | width: 100%; 11 | height: 55px; 12 | background-color: white; 13 | position: fixed; 14 | top: 0; 15 | left: 0; 16 | z-index: 5; 17 | // margin-left: 200px; 18 | display: flex; 19 | .all-header-logo{ 20 | width: 200px; 21 | display: flex; 22 | img { 23 | width: 35px; 24 | height: 35px; 25 | margin: 5px 15px 5px 30px; 26 | } 27 | h1 { 28 | margin: 10px; 29 | font-size: 20px; 30 | } 31 | } 32 | .all-header-breadcrumb{ 33 | width: 100%; 34 | margin: 20px 0px; 35 | .all-header-breadcrumb-font{ 36 | width: 1em; 37 | height: 1em; 38 | line-height: 1em; 39 | } 40 | } 41 | 42 | #modeCheckBox { 43 | display: none; 44 | } 45 | #modeCheckBox:checked + .all-header { 46 | background-color: black; 47 | color: white; 48 | transition: all 1s; 49 | } 50 | #modeBtn { 51 | font-size: 2rem; 52 | float: right; 53 | } 54 | #modeBtn:hover{ 55 | cursor: pointer; 56 | } 57 | #modeBtn::after { 58 | content: '🌞'; 59 | } 60 | #modeCheckBox:checked + #modeBtn::after { 61 | content: '🌜'; 62 | } 63 | 64 | .all-header-avatar-box{ 65 | cursor: pointer; 66 | margin: 5px 10px; 67 | .all-header-avatar{ 68 | background-color: #87d068; 69 | float: right; 70 | align-items: center; 71 | } 72 | } 73 | } 74 | 75 | .all-left-menu{ 76 | min-height: 100vh; 77 | overflow: auto; 78 | width: 200px; 79 | height: 100vh; 80 | position: fixed; 81 | left: 0; 82 | top: 0; 83 | z-index: 10; 84 | .all-left-logo{ 85 | height: 32px; 86 | margin: 23px; 87 | background: rgba(255, 255, 255, 0.3); 88 | } 89 | } 90 | 91 | .all-bottom-font{ 92 | text-align: center; 93 | } 94 | 95 | .site-layout{ 96 | margin-left: 200px; 97 | // margin-top: 55px; 98 | .site-layout-content{ 99 | margin: 10px 16px; 100 | } 101 | } 102 | 103 | // .candidate-site-layout{ 104 | // margin-top: 55px; 105 | // } -------------------------------------------------------------------------------- /src/common/components/candidate/countdown.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'style/candidate/countDown.less'; 3 | import { getDays } from 'common/utils'; 4 | 5 | export default class CountDown extends React.Component { 6 | 7 | state = { 8 | days: 1, 9 | hours: 1, 10 | minutes: 1, 11 | seconds: 1, 12 | } 13 | 14 | componentDidMount = () => { 15 | // 获取更新后的 this.props.over ,这里使用 setTimeout ,或许有更好的方法? 16 | setTimeout(() => { 17 | if (this.props.over === true) { 18 | this.setState({ 19 | days: 0, 20 | hours: 0, 21 | minutes: 0, 22 | seconds: 0, 23 | }) 24 | } else { 25 | this.countDown(); 26 | } 27 | }, 500); 28 | }; 29 | 30 | countDown = () => { 31 | this.timer = setInterval(() => { 32 | const nowtime = new Date().getTime(); 33 | const endtime = this.props.endTime; 34 | const { days, hours, minutes, seconds } = this.state; 35 | this.setState({ 36 | days: getDays(nowtime, endtime, 1), 37 | hours: getDays(nowtime, endtime, 2), 38 | minutes: getDays(nowtime, endtime, 3), 39 | seconds: getDays(nowtime, endtime, 4), 40 | }) 41 | if (days===0 && hours===0 && minutes===0 && seconds===0) { 42 | this.props.submitPaper(); 43 | } 44 | }, 1000); 45 | } 46 | timer: NodeJS.Timeout; 47 | componentWillUnmount() { 48 | clearInterval(this.timer); 49 | } 50 | 51 | render() { 52 | const { days, hours, minutes, seconds } = this.state; 53 | 54 | return( 55 |
56 | 57 |
58 |
{ days }
59 |

60 |
61 | 62 |
63 |
{ hours }
64 |

65 |
66 | 67 |
68 |
{ minutes }
69 |

70 |
71 | 72 |
73 |
{ seconds }
74 |

75 |
76 | 77 |
78 | ) 79 | } 80 | } -------------------------------------------------------------------------------- /src/common/components/navbar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from "react-router-dom"; 3 | import { Menu } from 'antd'; 4 | import { 5 | RobotOutlined, 6 | TeamOutlined, 7 | AppstoreOutlined, 8 | ProfileOutlined, 9 | ReadOutlined, 10 | } from '@ant-design/icons'; 11 | import 'style/basic.less'; 12 | import { INTERVIEW_MANAGE, INTERVIEW_ENTRANCE, TEST_MANAGE, SHOW_EXAM } from 'common/const'; 13 | 14 | export default class Navbar extends React.Component { 15 | 16 | state = { 17 | openKeys: ['writtenExamination'], 18 | } 19 | 20 | onOpenChange = (keys: any[]) => { 21 | let { openKeys } = this.state; 22 | const rootSubmenuKeys = ['writtenExamination', 'interviewExamination']; 23 | let latestOpenKey = keys.find(key => openKeys.indexOf(key) === -1); 24 | if (rootSubmenuKeys.indexOf(latestOpenKey) === -1) { 25 | this.setState({ openKeys: keys }); 26 | } else { 27 | latestOpenKey = latestOpenKey ? [latestOpenKey] : []; 28 | this.setState({ openKeys: latestOpenKey }); 29 | } 30 | } 31 | 32 | render() { 33 | // const { openKeys } = this.state; 34 | const selectedKeys = [`/${ window.location.pathname.split('/')[1] }`]; 35 | 36 | return( 37 | 45 |
46 | 47 | } title="笔试"> 48 | } > 49 | 试题管理 50 | 51 | } > 52 | 阅卷管理 53 | 54 | 55 | 56 | } title="面试"> 57 | } > 58 | 面试间管理 59 | 60 | } > 61 | 面试间入口 62 | 63 | 64 | 65 |
66 | ) 67 | } 68 | } -------------------------------------------------------------------------------- /src/style/candidate/drawer.css: -------------------------------------------------------------------------------- 1 | .ant-drawer-wrapper-body{ 2 | /* background: rgb(23, 33, 51); */ 3 | color: rgba(175,180,189,1); 4 | } 5 | .top{ 6 | border-bottom: 1px solid rgb(107, 119, 136); 7 | padding-bottom: 20px; 8 | display: flex; 9 | justify-content: space-between; 10 | } 11 | .top-left{ 12 | } 13 | .top-right{ 14 | display: flex; 15 | align-items: end; 16 | } 17 | .top-gap{ 18 | width: 10px; 19 | } 20 | .top-button{ 21 | width: 32px; 22 | height: 32px; 23 | flex: 1 1 32px; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | border-color: rgba(132,139,150,1); 28 | /* background: rgb(23, 33, 51); */ 29 | color: white; 30 | } 31 | .background-button{ 32 | background: rgb(23, 33, 51); 33 | } 34 | 35 | .filter-box{ 36 | width: 100%; 37 | display: flex; 38 | align-items: center; 39 | } 40 | .filter-button{ 41 | height: 24px; 42 | line-height: 24px; 43 | border: 1px solid white; 44 | border-radius: 12px; 45 | margin: 3px 5px 3px 0px; 46 | padding: 5px 10px; 47 | 48 | display: flex; 49 | align-items: center; 50 | color: rgba(255,255,255,1); 51 | } 52 | .filter-button-delete{ 53 | margin-left: 20px; 54 | height: 1em; 55 | } 56 | .exist{ 57 | border-bottom: 1px solid rgb(107, 119, 136); 58 | margin-top: 15px; 59 | padding-bottom: 10px; 60 | } 61 | .exist-content{ 62 | display: flex; 63 | flex-wrap: wrap; 64 | padding: 5px 0px; 65 | } 66 | .exist-icon{ 67 | display: flex; 68 | justify-content: center; 69 | cursor: pointer; 70 | } 71 | .exist-content-box{ 72 | display: flex; 73 | width: 50%; 74 | padding-bottom: 5px; 75 | } 76 | .exist-content-span{ 77 | width: 65px; 78 | height: 32px; 79 | line-height: 32px; 80 | } 81 | 82 | .content-box{ 83 | cursor: pointer; 84 | } 85 | .content-test-box{ 86 | display: flex; 87 | justify-content: space-between; 88 | font-size: 14px; 89 | padding: 0px 10px 0px 0px; 90 | height: 40px; 91 | line-height: 40px; 92 | } 93 | .content-test-left{ 94 | display: flex; 95 | } 96 | .content-test-box:hover{ 97 | background: rgba(247,248,250, 0.3); 98 | } 99 | .easy{ 100 | color: rgba(0,175,155,1); 101 | } 102 | .middle{ 103 | color: rgba(255,184,0,1); 104 | } 105 | .hard{ 106 | color: rgba(255,45,85,1); 107 | } 108 | .content-test-icon{ 109 | width: 15px; 110 | height: 36px; 111 | min-width: 10px; 112 | line-height: 36px; 113 | background: transparent; 114 | } 115 | .content-test-font{ 116 | color: rgba(175,180,189,1); 117 | } 118 | .content-test-right{ 119 | justify-self: end; 120 | } -------------------------------------------------------------------------------- /src/common/components/Socket.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Input, Form, message } from 'antd'; 3 | import { getCookie, nowTime } from 'common/utils'; 4 | import { searchEmail } from 'api/modules/user'; 5 | import ReconnectingWebsocket from 'reconnecting-websocket'; 6 | import { Client, TextOperation, } from 'ot'; 7 | // import sharedb from 'sharedb/lib/client'; 8 | 9 | interface Prop { 10 | socketUrl: string; // 连接 websocket 的地址 11 | identity: string; // 开启 websocket 的当前用户身份 12 | openMsg: any; // 连接成功时要发送的数据 13 | retMsg: any; // 接收数据的函数 14 | } 15 | 16 | interface State { 17 | 18 | } 19 | 20 | export default class Webrtc extends React.Component { 21 | 22 | socket: WebSocket = null; 23 | // identity: string = null; 24 | 25 | connection = async () => { 26 | // 查看当前用户的身份,是面试官还是候选人 27 | // await searchEmail({ cookie }).then(res => { 28 | // this.identity = res.data.identity; 29 | // }) 30 | 31 | // 检测当前浏览器是什么浏览器来决定用什么socket 32 | if ('WebSocket' in window) { 33 | // this.socket = new ReconnectingWebsocket(this.props.socketUrl); 34 | this.socket = new WebSocket(this.props.socketUrl); 35 | } else if ('MozWebSocket' in window) { 36 | // this.socket = new MozWebSocket('ws:localhost:7656'); 37 | } else { 38 | // this.socket = new SockJS('ws:localhost:7656'); 39 | } 40 | 41 | this.socket.onopen = this.onopen; 42 | this.socket.onmessage = this.onmessage; 43 | this.socket.onclose = this.onclose; 44 | this.socket.onerror = this.onerror; 45 | }; 46 | // 连接成功触发 47 | onopen = () => { 48 | if (this.socket.readyState === WebSocket.OPEN) { 49 | this.socket.send(JSON.stringify(this.props.openMsg)); 50 | } 51 | }; 52 | // 后端向前端推得数据 53 | onmessage = (event: { data: string; }) => { 54 | let { retMsg } = this.props; 55 | retMsg && retMsg(JSON.parse(event.data)); 56 | }; 57 | // 关闭连接触发 58 | onclose = (event: any) => { 59 | this.socket.close(); 60 | console.log("WebSocket onclose"); 61 | this.socket.onclose = (event) => { 62 | var code = event.code; 63 | var reason = event.reason; 64 | var wasClean = event.wasClean; 65 | console.log('code',code) 66 | console.log('reason',reason) 67 | console.log('wasClean',wasClean) 68 | if (this.socket.readyState === WebSocket.CLOSING) { 69 | this.socket.send('系统:正在断开服务器'); 70 | } else if (this.socket.readyState === WebSocket.CLOSED) { 71 | this.socket.send('系统:服务器已经断开'); 72 | } 73 | }; 74 | }; 75 | onerror = (e: any) => { 76 | return e; 77 | }; 78 | // 向后端发送数据 79 | sendMessage = (msg: any) => { 80 | this.socket.send(JSON.stringify(msg)); 81 | }; 82 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 项目地址 2 | GitHub 前端仓库地址:[https://github.com/HCYETY/Online-programming-platform](https://github.com/HCYETY/Online-programming-platform) 3 | 4 | GitHub 后端仓库地址:[https://github.com/HCYETY/Online-programming-platform_service](https://github.com/HCYETY/Online-programming-platform_service) 5 | 6 | 项目演示地址:[http://www.syandeg.com](http://www.syandeg.com) 7 | ## 项目介绍 8 | 该项目是一个前后端分离项目,按照用户角色分为两部分,分别是面试官和候选人,主要功能是候选人在线完成代码编写,面试官可查看编程结果,同时面试官和候选人还可进行实时文字交流。 9 | 10 | 项目实现: 11 | - 将项目前端和后端部署于服务器上,并实现了自动化部署。 12 | - 使用 WebSocket 实时更新聊天信息。面试官和候选人各自进入面试间后,客户端会显示房间信息及聊天情况,当两个人都进入面试间后,即可开始实时文字交流。 13 | - 使用 Cookie 存储用户登录时后端生成的随机码,使用 nodemailer 发送邮件信息(如注册验证码等)。 14 | - 相关数据诸如用户登录信息,试卷和试题信息等全部存储于后端 MySQL 数据库中,前端需要的数据渲染均通过请求后端接口获取。 15 | - 使用 react-monaco-editor 代码编辑器,自定义功能选项以编写代码;使用 wangEditor 富文本编辑器,完成试题的自定义编写。 16 | - 使用 Ant Design 完成 UI 设计,几乎使用了其中 80% 的组件。 17 | - 后端使用 Koa 框架,通过 koa-cors 实现前后端跨域。 18 | 19 | ## 项目技术栈 20 | 前端:React + TypeScript ,后端:Node.js + MySQL + Koa 。 21 | 22 | ## 项目任务拆解 23 | 1. 登录/注册模块 24 | - 支持邮箱登录和注册 25 | - 支持“退出登录”功能 26 | 2. 面试题模块 27 | 3. 在线编程模块 28 | 4. 在线留言模块 29 | 5. 在线编程模块支持自动刷新 30 | 6. 在线留言模块改成实时文字聊天 31 | 7. 在线语音聊天模块 32 | 8. 在线编程模块支持运行JS代码 33 | 9. 在线编程模块支持协同编辑 34 | ## 技术选型 35 | |技术|说明|理由| 36 | |:--:|:--:|:--:| 37 | |Ant Design|前端 UI 设计|成熟的 UI 组件库,GitHub 上有 `166 watch` & `32.5k fork` & `78.1k star`| 38 | ## 项目进度 39 | - [x] 2021-09-04:实现登录/注册的静态页面 40 | - [x] 2021-09-19:实现登录/注册逻辑(包括账号密码登录、登录拦截,session 身份验证) 41 | - [x] 2021-09-29:初步部署前后端项目到阿里云服务器 42 | - [x] 2021-10-04:购买域名并实现自动化部署项目 43 | - [x] 2021-10-05:支持使用邮箱 登录和注册 44 | - [x] 2021-10-09:面试官侧支持新建/删除试卷,在“新建试卷”里新建/修改/删除试题,发送邮件至候选人邮箱 45 | - [x] 2021-10-15:候选人侧展示试卷、开始编程、提交试卷 46 | - [x] 2021-10-16:支持“退出登录” 47 | - [ ] 2021-10-30: 48 | - [x] 2021-10-31:搭建个人博客 49 | - [x] 2021-11-06:沉淀项目所学,写博客中 50 | - [x] 2021-11-14:评论功能雏形(但因暂无该需求,先放一边) 51 | - [x] 2021-11-23:实现试题筛选功能,完成编程模块 52 | - [ ] 2021-12-01: 53 | - [x] 2021-12-03: 实现在线聊天功能 54 | - [ ] 2021-12-10:解决编辑冲突问题 55 | - [x] 2021-12-14:项目持续待机中,先学小程序开发去了 56 | ## 项目展示 57 | ### PC 端 58 | 1. 登录/注册 59 | ![登录界面](https://z3.ax1x.com/2021/11/02/IkyIBQ.png) 60 | ![注册界面](https://z3.ax1x.com/2021/11/02/Ik63gf.png) 61 | #### 面试官侧 62 | 2. 试题管理 63 | ![试卷展示](https://s4.ax1x.com/2022/02/18/H7OIxS.png) 64 | ![添加试题之试卷信息](https://s4.ax1x.com/2022/02/18/H7X6zT.png) 65 | ![添加试题之试题信息](https://s4.ax1x.com/2022/02/18/H7Xhw9.png) 66 | ![修改试卷1](https://s4.ax1x.com/2022/02/18/H7XLOe.png) 67 | ![修改试卷2](https://s4.ax1x.com/2022/02/18/H7jekn.png) 68 | 3. 阅卷管理 69 | ![试卷展示](https://s4.ax1x.com/2022/02/18/H7jMlT.png) 70 | ![试卷详细信息之1](https://s4.ax1x.com/2022/02/18/H7jYkR.png) 71 | ![试卷详细信息之2](https://s4.ax1x.com/2022/02/18/H7j0XD.png) 72 | ![试卷详细信息之3](https://s4.ax1x.com/2022/02/18/H7js7d.png) 73 | ![批阅试卷](https://s4.ax1x.com/2022/02/18/H7jR9P.png) 74 | 4. 面试间管理 75 | ![面试间信息展示](https://s4.ax1x.com/2022/02/18/H7jfc8.png) 76 | ![添加面试间](https://s4.ax1x.com/2022/02/18/H7jbhq.png) 77 | 5. 面试间入口 78 | ![进入面试间](https://s4.ax1x.com/2022/02/18/H7jO3V.png) 79 | #### 候选人侧 -------------------------------------------------------------------------------- /src/common/components/candidate/markdownEditor.tsx: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import Editor, { Plugins } from "react-markdown-editor-lite"; 3 | // import MarkdownIt from 'markdown-it'; 4 | // // import ReactMarkdown from "react-markdown"; 5 | // import "react-markdown-editor-lite/lib/index.css"; 6 | 7 | // import 'style/candidate/markdownEditor.css'; 8 | // // 1. 引入markdown-it库 9 | // import markdownIt from 'markdown-it' 10 | 11 | // const plugins = ['block-code-inline', 'block-code-block', 'link', 'list-unordered', 'list-ordered', 'full-screen']; 12 | // // 2. 生成实例对象 13 | // const mdParser = new MarkdownIt(); 14 | 15 | // export default class ProgramInform extends React.Component { 16 | 17 | // state = { 18 | // code: '', 19 | // } 20 | 21 | // parse = (e: any) => { 22 | // console.log(e) 23 | // const content = e.target.vlaue; 24 | // console.log(content) 25 | // mdParser.render(content); 26 | // this.setState({ code: content }); 27 | // } 28 | 29 | // render() { 30 | // const { code } = this.state; 31 | 32 | // return ( 33 | //
34 | // 80 | //
81 | //
82 | 83 | 84 | 85 | 86 | 87 | // mdParser.render(text)} 93 | // /> 94 | ) 95 | } 96 | } -------------------------------------------------------------------------------- /src/pages/interviewer/consult/lookOver.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Card, Form, Layout, message } from 'antd'; 3 | 4 | import 'style/interviewer/examReport.css'; 5 | import { search } from 'api/modules/candidate'; 6 | import { showTest } from 'api/modules/test'; 7 | import { submit } from 'api/modules/candidate'; 8 | import Navbar from 'common/components/navbar'; 9 | import ExamReport from 'common/components/interviewer/examReport'; 10 | import { connect } from 'react-redux'; 11 | import { lookOver } from 'src/api/modules/paper'; 12 | 13 | interface Prop { 14 | lookExam: string; 15 | lookEmail: string; 16 | } 17 | 18 | interface rateArrObj { 19 | testName: string; 20 | score: number; 21 | } 22 | 23 | class LookOver extends React.Component { 24 | 25 | state = { 26 | examInform: [] = [], 27 | exam: [] = [], 28 | test: [] = [], 29 | } 30 | 31 | token: string; 32 | componentDidMount() { 33 | const { lookExam, lookEmail } = this.props; 34 | search({ paper: lookExam, reqEmail: lookEmail }).then(item => { 35 | this.setState({ examInform: item.data.ret }); 36 | }) 37 | showTest({ paper: lookExam }).then(item => { 38 | this.setState({ test: item.data.show }); 39 | }) 40 | } 41 | 42 | rateArr: rateArrObj[] = []; 43 | getRate = (testName: string, score: number) => { 44 | const find = this.rateArr.find(item => item.testName === testName); 45 | find ? find['score'] = score : this.rateArr.push({ testName, score }); 46 | } 47 | 48 | submitRate = () => { 49 | const { lookExam, lookEmail } = this.props; 50 | console.log(lookExam, lookEmail) 51 | lookOver({ paper: lookExam, reqEmail: lookEmail, rate: this.rateArr }).then(res => { 52 | console.log('submitRate res', res) 53 | if (res.data.status === true) { 54 | message.success(res.msg); 55 | } else { 56 | message.error('试卷未完成批阅'); 57 | } 58 | }) 59 | } 60 | 61 | render() { 62 | const { examInform, exam, test } = this.state; 63 | 64 | return( 65 |
66 | 67 | 68 |
69 | {/*
*/} 70 | { examInform.map(item => { 71 | return( 72 | 77 | ) 78 | }) } 79 | {/* */} 80 | 81 | {/* */} 82 | {/* */} 83 |
84 |
85 | ) 86 | } 87 | } 88 | 89 | function mapStateToProps(state: any) { 90 | return{ 91 | lookExam: state.lookExam, 92 | lookEmail: state.lookEmail 93 | } 94 | } 95 | export default connect(mapStateToProps)(LookOver); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Online-programming-platform", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "homepage": ".", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "webpack-dev-server --config webpack/webpack.dev.js", 9 | "build": "webpack --config webpack/webpack.prod.js", 10 | "deploy": "npm run build && node ./deploy/index.js" 11 | }, 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@babel/core": "^7.11.4", 15 | "@babel/plugin-syntax-decorators": "^7.10.4", 16 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 17 | "@babel/plugin-transform-runtime": "^7.11.5", 18 | "@babel/preset-env": "^7.11.0", 19 | "@babel/preset-react": "^7.10.4", 20 | "@types/markdown-it": "^12.2.3", 21 | "@types/ot": "^0.0.5", 22 | "@types/pubsub-js": "^1.8.3", 23 | "@types/react": "^16.9.48", 24 | "@types/react-dom": "^16.9.8", 25 | "@types/react-router-dom": "^5.1.5", 26 | "@types/uuid": "^8.3.3", 27 | "autoprefixer": "^9.8.6", 28 | "babel-core": "^6.26.3", 29 | "babel-loader": "^8.1.0", 30 | "babel-plugin-import": "^1.13.0", 31 | "babel-polyfill": "^6.26.0", 32 | "babel-preset-env": "^1.7.0", 33 | "babel-preset-es2015": "^6.24.1", 34 | "babel-preset-react": "^6.24.1", 35 | "changesets": "^1.0.2", 36 | "clean-webpack-plugin": "^3.0.0", 37 | "css-loader": "^4.2.2", 38 | "cssnano": "^5.1.0", 39 | "file-loader": "^6.1.0", 40 | "html-webpack-plugin": "^4.3.0", 41 | "http-proxy-middleware": "^2.0.1", 42 | "less": "^3.12.2", 43 | "less-loader": "^7.0.0", 44 | "mini-css-extract-plugin": "^0.11.0", 45 | "moment": "^2.29.1", 46 | "monaco-editor": "^0.29.1", 47 | "monaco-editor-webpack-plugin": "^5.0.0", 48 | "nodemon": "^2.0.12", 49 | "optimize-css-assets-webpack-plugin": "^6.0.1", 50 | "ot": "^0.0.15", 51 | "otjs": "^1.2.1", 52 | "postcss-loader": "^3.0.0", 53 | "react-monaco-editor": "^0.45.0", 54 | "reconnecting-websocket": "^4.4.0", 55 | "scp2": "^0.5.0", 56 | "style-loader": "^1.2.1", 57 | "terser-webpack-plugin": "^5.3.1", 58 | "ts-loader": "^8.0.3", 59 | "typescript": "^4.0.2", 60 | "url-loader": "^4.1.0", 61 | "webpack": "^4.44.1", 62 | "webpack-bundle-analyzer": "^3.9.0", 63 | "webpack-cli": "^3.3.12", 64 | "webpack-deep-scope-plugin": "^1.6.2", 65 | "webpack-dev-server": "^3.11.0", 66 | "webpack-merge": "^5.1.3" 67 | }, 68 | "dependencies": { 69 | "antd": "^4.6.2", 70 | "axios": "^0.20.0", 71 | "copy-to-clipboard": "^3.3.1", 72 | "markdown-it": "^12.2.0", 73 | "monaco-editor": "^0.29.1", 74 | "react": "^16.13.1", 75 | "react-dom": "^16.13.1", 76 | "react-markdown": "^7.1.0", 77 | "react-markdown-editor-lite": "^1.3.1", 78 | "react-redux": "^7.2.6", 79 | "react-router": "^5.2.0", 80 | "react-router-dom": "^5.2.0", 81 | "redux": "^4.1.2", 82 | "sharedb": "^2.2.1", 83 | "socket.io-client": "^4.4.0", 84 | "uuid": "^8.3.2", 85 | "wangeditor": "^4.7.8" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/common/components/candidate/testAlone.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { message, Tag } from 'antd'; 4 | import { DoubleRightOutlined, CheckCircleOutlined } from '@ant-design/icons'; 5 | 6 | import 'style/interviewer/testAlone.less'; 7 | import { CANDIDATE_TEST, CANDIDATE_WATCH_TEST } from 'common/const'; 8 | import { getExamLevel } from 'common/utils'; 9 | 10 | export default class TestAlone extends React.Component { 11 | render() { 12 | const { watch, over, values } = this.props; 13 | const num = values.num, 14 | title = values.test_name, 15 | level = values.level, 16 | point = values.point, 17 | tags = values.tags, 18 | check = values.check, 19 | timeBegin = values.paper.time_begin, 20 | timeEnd = values.paper.time_end; 21 | const nowtime = new Date().getTime(); 22 | 23 | function doJump() { 24 | let url: string = 'javascript:;'; 25 | if (nowtime < timeBegin || timeEnd < nowtime) { 26 | message.error('不在答题时间范围之内无法进行答题'); 27 | } else if (timeBegin <= nowtime && nowtime <= timeEnd) { 28 | // window.location.href = `${ CANDIDATE_TEST }?test=${ title }`; 29 | url = `${ CANDIDATE_TEST }?test=${ title }`; 30 | } 31 | return url; 32 | } 33 | function nodoJump() { 34 | PubSub.publish('isProgram', { status: false }); 35 | // window.location.href = `${ CANDIDATE_WATCH_TEST }?test=${ title }`; 36 | return `${ CANDIDATE_WATCH_TEST }?test=${ title }`; 37 | } 38 | 39 | return( 40 |
41 |
42 |

{ num }. { title }

43 |
44 | { 45 | tags.map((tag: any) => { 46 | let color = tag.length > 2 ? 'geekblue' : 'green'; 47 | if (tag === 'loser') { 48 | color = 'volcano'; 49 | } 50 | return ( 51 | 52 | {tag} 53 | 54 | ); 55 | }) 56 | } 57 |
58 |
59 | 60 |

{ level }

61 | 62 |

分数:{ point }

63 | 64 |
65 | { 66 | (watch === true && over === true) ? 67 | 68 | 点击查看 69 | 70 | : 71 | (watch === false && over === true) ? 72 | 已完成 : 73 | 74 | 去做题 75 | 76 | 77 | } 78 |
79 | 80 |
81 | ) 82 | } 83 | } -------------------------------------------------------------------------------- /src/common/components/breadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, BrowserRouter as Router } from 'react-router-dom'; 3 | import { Breadcrumb, Col, Row } from 'antd'; 4 | import { LeftOutlined } from '@ant-design/icons'; 5 | 6 | import { routes } from 'common/const'; 7 | import { flattenRoutes } from 'common/utils'; 8 | 9 | export default class Breadcrumbs extends React.Component { 10 | 11 | shouldComponentUpdate() { 12 | const url = window.location.pathname; 13 | const retArr = flattenRoutes(routes); 14 | const breadcrumbs = this.getBreadcrumbs(retArr, url); 15 | const len = breadcrumbs.length; 16 | console.log('url', url) 17 | console.log('retArr', retArr) 18 | console.log('breadcrumbs', breadcrumbs) 19 | console.log('len', len) 20 | console.log('url', url) 21 | return url === breadcrumbs[len-1].path; 22 | } 23 | 24 | getBreadcrumb = (flattenRoutes: any[], pathSection: string) => { 25 | return flattenRoutes.find((ele: { breadcrumbName: any; path: any; }) => { 26 | const { breadcrumbName, path } = ele; 27 | if (!breadcrumbName || !path) { 28 | throw new Error('Router中的每一个route必须包含 `path` 以及 `breadcrumbName` 属性'); 29 | } 30 | return pathSection === path; 31 | }); 32 | } 33 | 34 | getBreadcrumbs = (arr: any, location: any) => { 35 | // 初始化匹配数组match 36 | let matches: any[] = []; 37 | location 38 | // 取得路径名,然后将路径分割成每一路由部分. 39 | .split('?')[0] 40 | .split('/') 41 | // 对每一部分执行一次调用`getBreadcrumb()`的reduce. 42 | .reduce((prev: any, curSection: any) => { 43 | // 将最后一个路由部分与当前部分合并,比如当路径为 `/x/xx/xxx` 时,pathSection分别检查 `/x` `/x/xx` `/x/xx/xxx` 的匹配,并分别生成面包屑 44 | const pathSection = `${prev}/${curSection}`; 45 | // 对于 拆分的路径,从 flattenRoutes 中查找对应的路由 46 | const breadcrumb = this.getBreadcrumb(arr, pathSection); 47 | 48 | // 将面包屑导入到matches数组中 49 | matches.push(breadcrumb); 50 | 51 | // 传递给下一次reduce的路径部分 52 | return pathSection; 53 | }); 54 | return matches; 55 | } 56 | 57 | render() { 58 | const url = window.location.pathname; 59 | const retArr = flattenRoutes(routes); 60 | const breadcrumbs = this.getBreadcrumbs(retArr, url); 61 | 62 | return( 63 |
64 | 65 | window.history.back() }/> 66 | 67 | { 68 | breadcrumbs.map(item => ( 69 | !item ? null : 70 | item.path === url ? 71 | 72 | { item.breadcrumbName } 73 | : 74 | 75 | { item.breadcrumbName } 76 | 77 | )) 78 | } 79 | 80 | 81 |
82 | ) 83 | } 84 | } -------------------------------------------------------------------------------- /src/style/candidate/program.less: -------------------------------------------------------------------------------- 1 | .whole{ 2 | // margin-top: 64px; 3 | // transform: translateY(63px); 4 | display: flex; 5 | flex-direction: row; 6 | flex: 1 1 auto; 7 | .left{ 8 | margin-right: 10px; 9 | flex: 1; 10 | // height: 100%; 11 | height: 608px; 12 | min-width: 480px; 13 | overflow: auto; 14 | background-color: white; 15 | .left-box{ 16 | display: flex; 17 | .ant-tabs{ 18 | flex-grow: 1; 19 | .ant-tabs-nav{ 20 | background: #ececec; 21 | .ant-tabs-nav-wrap{ 22 | margin-left: 20px; 23 | .ant-tabs-nav-list{ 24 | .ant-tabs-tab{ 25 | width: 110px; 26 | height: 50px; 27 | background: #ececec; 28 | padding-left: 10px; 29 | } 30 | } 31 | } 32 | } 33 | } 34 | .describe-top{ 35 | padding: 0px 20px 15px 20px; 36 | 37 | .describe-top-tag{ 38 | padding-bottom: 10px; 39 | border-bottom: 1px solid rgb(229,231,235); 40 | .easy{ 41 | color: rgba(0,175,155,1); 42 | } 43 | .middle{ 44 | color: rgba(255,184,0,1); 45 | } 46 | .hard{ 47 | color: rgba(255,45,85,1); 48 | } 49 | } 50 | } 51 | .describe-content{ 52 | padding: 0px 20px 15px 20px; 53 | // overflow: hidden; 54 | // overflow-x: hidden; 55 | // overflow-y: auto; 56 | } 57 | } 58 | .left-bottom{ 59 | position: fixed; 60 | bottom: 0; 61 | left: 0; 62 | width: 50%; 63 | height: 55px; 64 | background-color: #fff; 65 | .left-bottom-button{ 66 | min-width: 32px; 67 | } 68 | .left-bottom-list{ 69 | height: 35px; 70 | margin-left: 30px; 71 | } 72 | .left-bottom-previous{ 73 | float: right; 74 | height: 35px; 75 | margin-right: 100px; 76 | } 77 | .left-bottom-next{ 78 | float: right; 79 | height: 35px; 80 | } 81 | } 82 | } 83 | 84 | .right{ 85 | flex: 1; 86 | display: flex; 87 | flex-direction: column; 88 | z-index: 0; 89 | margin-left: 10px; 90 | // position: relative; 91 | // justify-items: center; 92 | // align-items: center; 93 | .right-top{ 94 | height: 50px; 95 | .right-top-button{ 96 | margin-right: 10px; 97 | } 98 | } 99 | .right-content{ 100 | overflow: hidden; 101 | } 102 | .right-bottom{ 103 | position: fixed; 104 | bottom: 0; 105 | right: 0; 106 | width: 50%; 107 | height: 55px; 108 | background-color: #fff; 109 | .right-bottom-button{ 110 | float: right; 111 | height: 35px; 112 | } 113 | .right-bottom-execute{ 114 | margin-right: 20px; 115 | } 116 | .right-bottom-submit{ 117 | width: 100px; 118 | } 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /src/common/components/header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, BrowserRouter as Router } from 'react-router-dom'; 3 | import { 4 | Avatar, 5 | Menu, 6 | Dropdown, 7 | Popconfirm, 8 | PageHeader, 9 | Switch 10 | } from 'antd'; 11 | import { 12 | UserOutlined, 13 | LogoutOutlined, 14 | StarOutlined, 15 | ContainerOutlined, 16 | FileOutlined, 17 | } from '@ant-design/icons'; 18 | 19 | import 'style/basic.less'; 20 | import logoImg from 'img/logo.png'; 21 | import { logout } from 'api/modules/user'; 22 | import { getCookie } from 'common/utils'; 23 | import { CANDIDATE, LOGIN, } from 'common/const'; 24 | import Breadcrumbs from 'common/components/breadcrumbs'; 25 | 26 | export default class Head extends React.PureComponent{ 27 | logOut = () => { 28 | const cookie = getCookie(); 29 | console.log(cookie) 30 | logout({ cookie: cookie }).then(res => { 31 | console.log(res) 32 | if (res.data.status === true) { 33 | const cookie = getCookie(); 34 | document.cookie = `session=${ cookie }; max-age=-1` // 删除cookie 35 | window.location.href = LOGIN; 36 | } 37 | }) 38 | } 39 | 40 | render() { 41 | const menu = ( 42 | 43 | 44 |  个人信息 45 | 46 | 47 |  我的题解 48 | 49 | 50 |  收藏夹 51 | 52 | 53 | 54 | 60 | {/* */} 67 | 68 |  退出登录 69 | 70 | 71 | 72 | 73 | ); 74 | 75 | return( 76 | <> 77 |
78 |
79 | 80 |
81 | 82 |

react-ts

83 |
84 |
85 | 86 | 87 | 88 | 89 | 90 | 91 |
92 | 93 | } 97 | shape="square" 98 | /> 99 | 100 |
101 | 102 |
103 | 104 | ) 105 | } 106 | } -------------------------------------------------------------------------------- /problem.md: -------------------------------------------------------------------------------- 1 | 1. 如何搭建 react+ts 的项目架子? 2 | > [使用create-react-app创建ts项目](https://www.cnblogs.com/feiyu159/p/14154963.html) 3 | 3. 按需引入antd,使组件生效? 4 | > 一开始按照这篇文章去弄:[按需加载](https://blog.csdn.net/weixin_46398902/article/details/104505491) 5 | > > 然而在“自定义”这一块却再次卡住,样式还是不能生效,于是谷歌 [解决方法](https://github.com/ant-design/ant-design-landing/issues/235) 6 | > > [官网的自定义主题 解决方法也或许有用](https://ant.design/docs/react/use-with-create-react-app-cn) 7 | 5. 启动并连接mysql数据库,同时打开MySQL workbench可视化工具方便查看数据? 8 | > 在 [mysql官网上](https://dev.mysql.com/downloads/installer/) 安装好mysql后,用命令行启动mysql服务。这时报错:服务名无效,于是在网上找到了:[cmd命令行启动MySQL提示服务名无效/服务无法启动](https://blog.csdn.net/weixin_43720619/article/details/89036335)。没想到一波未平一波又起,mysql还是启动不了,就连【WIN+R,输入services.msc】手动启动都出现警告框:启动后停止,无奈又去找:[Mysql启动后停止的解决方法](https://www.cnblogs.com/pandaly/p/11738789.html)+[MySQL 服务无法启动](https://blog.csdn.net/qq_32682301/article/details/118339414)。这下总算启动mysql服务了。 9 | > > 附:服务一直显示“正在启动”,则 [解决方法传送门](https://www.yisu.com/zixun/28154.html)。如果出现“错误: 无法终止 PID 为 7432 的进程。”则是你的权限问题,可以用管理员权限打开cmd,然后输入命令。 10 | > 接下来就是连接数据库了 11 | 8. ts报错:string 元素隐式具有 “any“ 类型,类型为 “string“ 的表达式不能用于索引类型 “{}“? 12 | > 解决方法:在tsconfig.json文件添加配置: 13 | `"suppressImplicitAnyIndexErrors": true` 14 | 10. 基于session 的身份验证? 15 | > 流程: 16 | > - 1.用户向服务器发送用户名和密码 17 | > - 2.服务器验证通过后,创建 session,该 session 是一个键值对,我存入【登录状态,登录时间】 18 | > - 3.同时还要设置cookie(这时会将session的相关信息自动存入cookie中;同时通过前后端分别配置,浏览器会自动将该cookie添加到请求头中,以后每次发送请求到服务器后便会自动发送这个cookie) 19 | > - 4.前端的登录验证,只需要获取session中的登录状态(由后端返回,前端是查找不到的),若通过则跳转至首页;后端的登录验证需要获取请求头中的cookie(之前服务端自定义的) 20 | > - 5.最后,前后端要设置登录拦截,防止用户在未登录的前提下访问到其他路径,这是不被允许的 21 | 22 | 14. 路由跳转后找不到文件路径,报错404? 23 | 当我将本地打包后的前端项目(dist文件夹)上传到服务器的 /usr/local/nginx/html/ 目录下后,在 nginx 正常启动、服务器防火墙以及安全组开放对应端口的前提下,我兴高采烈地打开我的 ip 地址,如我预料的成功打开了前端项目的页面。于是我开始登录注册,但老天爷似乎总喜欢跟人开玩笑,我登录跳转后居然报错:404 Not Found。根据我的另一篇文章 [解决报错的思路](),这时我应该查看 url 的情况,看它是否符合预期 24 | > [react部署完以后,刷新页面会报错找不到视图](https://www.jianshu.com/p/ffb7e3445414) 25 | 26 | 15. 引用nodejs模块报错:node_ssh is not a constructor? 27 | ```js 28 | const ssh = new node_ssh(); 29 | TypeError: node_ssh is not a constructor 30 | ``` 31 | 原因:根据网上的说法,是由于 node 官方尚未解决的一个 bug 导致的。 32 | 解决方法: 33 | ```js 34 | const node_ssh = require('node-ssh').NodeSSH; 35 | const ssh = new node_ssh(); 36 | ``` 37 | 38 | > [React中setState如何同步更新](https://www.cnblogs.com/younghxp/p/14803548.html) 39 | 40 | 23. 获取后端返回值后,使用antd4的表单组件并将返回值作为初始值赋给表单? 41 | 解决思路:首先查看官方文档,得知可以通过给
设置 initialValues ,参数为键值对。但这时候会发现表单值没显示出来,其实是因为 Form 约定 initialValues 只初始化一次。由于本项目使用的是类组件,所以这里通过添加 loading 判断,获取数据成功后设置 false ,再渲染表单 42 | ```js 43 | export default class Modify extends React.Component{ 44 | this.state = { loading: true }; 45 | 46 | // 调用后端接口,然后将 loading 设置为 false 47 | showPaper().then(res => { 48 | this.setState({ loading: false }); 49 | }) 50 | 51 | render() { 52 | const { loading } = this.state 53 | return( 54 | {!loading && ( 55 | 56 | 57 | 58 | 59 |
60 | )} 61 | ) 62 | } 63 | } 64 | ``` 65 | 66 | 67 | 待改进: 68 | 1. 面试题模块导航栏展开不同步? 69 | 4. 编辑代码固定头部? 70 | 5. 代码编程功能:中间高度不写死? 71 | 6. 倒计时与 antd 倒计时组件有秒数上的误差? 72 | 73 | 7. 面试官阅卷功能:设计?111 redux、代码展示 74 | 8. “题目列表”功能完善? 75 | 10. 富文本编辑器更换? 76 | 77 | 11. 代码编辑器删除小地图?111 78 | 12. 自定义答题时长传参失败? 79 | 13. webpack 打包无效?111 80 | 14. 候选人提交答案时之前的试题答案会被删除? 81 | 15. diff算法、双向数据绑定? 222 -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Route, Switch, Redirect, withRouter, RouteComponentProps } from "react-router-dom"; 3 | 4 | import Head from 'common/components/header'; 5 | import Login from 'pages/login'; 6 | 7 | // import Interviewer from 'pages/interviewer'; 8 | import Edit from 'pages/interviewer/edit/show'; 9 | import Add from 'pages/interviewer/edit/add'; 10 | import Modify from 'pages/interviewer/edit/modify'; 11 | 12 | import ShowExamContainer from 'src/pages/interviewer/consult/showExam'; 13 | import ExamInformContainer from 'pages/interviewer/consult/examInform'; 14 | import LookOverContainer from 'pages/interviewer/consult/lookOver'; 15 | 16 | import InterviewEntrance from 'pages/interviewer/interview/entrance'; 17 | import InterviewManage from 'pages/interviewer/interview/manage'; 18 | import InterviewRoom from 'pages/interviewer/interview/room'; 19 | 20 | import Candidate from 'pages/candidate'; 21 | import ShowTests from 'pages/candidate/showTests'; 22 | import WatchTest from 'pages/candidate/WatchTest'; 23 | import Program from 'pages/candidate/program'; 24 | 25 | import { testLogin } from 'api/modules/user'; 26 | import { getCookie } from 'common/utils'; 27 | import { 28 | TEST_ADD, 29 | TEST_MODIFY, 30 | CANDIDATE, 31 | LOGIN, 32 | CANDIDATE_SHOW_TESTS, 33 | CANDIDATE_TEST, 34 | CANDIDATE_WATCH_TEST, 35 | SHOW_EXAM, 36 | EXAM_INFORM, 37 | LOOK_OVER, 38 | TEST_MANAGE, 39 | INTERVIEW_MANAGE, 40 | INTERVIEW, 41 | INTERVIEW_ENTRANCE, 42 | } from 'common/const'; 43 | import Navbar from 'common/components/navbar'; 44 | 45 | 46 | class App extends React.Component { 47 | 48 | componentDidMount() { 49 | const cookie = getCookie(); 50 | const url = window.location.pathname; 51 | testLogin({ cookie: cookie }).then(res => { 52 | if (url !== LOGIN && (res.data.isLogin === false || cookie === undefined)) { 53 | window.location.href = LOGIN; 54 | } 55 | }) 56 | } 57 | 58 | render() { 59 | return ( 60 |
61 | { window.location.pathname !== LOGIN ? : null } 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
89 | ) 90 | } 91 | } 92 | 93 | export default App; -------------------------------------------------------------------------------- /src/common/components/interviewer/examReport.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, InputNumber, } from 'antd'; 3 | 4 | import { showTest } from 'api/modules/test'; 5 | import 'style/interviewer/examReport.css'; 6 | import { getDays, transTime } from 'common/utils'; 7 | 8 | interface Prop { 9 | examInform: [], 10 | getRate: any, 11 | } 12 | 13 | // interface programObj { 14 | // email: string; 15 | // paper: string; 16 | // program_answer: string; 17 | // watch: boolean; 18 | // test_name: string; 19 | // time_end: number; 20 | // test_level: string; 21 | // test_status: string; 22 | // score: number; 23 | // tags: string[]; 24 | // program_language: string; 25 | // submit_num: number; 26 | // answer_time: number; 27 | // answer_end: number; 28 | // } 29 | interface State { 30 | tableArr: string[]; 31 | testContent: string; 32 | testPoint: number; 33 | } 34 | 35 | export default class ExamReport extends React.Component { 36 | 37 | state = { 38 | tableArr: [], 39 | testContent: '', 40 | testPoint: 0, 41 | } 42 | 43 | componentDidMount() { 44 | showTest({ test: this.props.examInform['test_name'] }).then(res => { 45 | const ret = res.data.show; 46 | this.setState({ testContent: ret.test, testPoint: ret.point }); 47 | }) 48 | } 49 | 50 | returnNumber = (score: number) => { 51 | const { examInform, getRate } = this.props; 52 | getRate(examInform['test_name'], score); 53 | } 54 | 55 | render() { 56 | const { tableArr, testContent, testPoint } = this.state; 57 | const { examInform, getRate } = this.props; 58 | const use_time = transTime(examInform['answer_end'] - examInform['answer_begin']); 59 | const show = JSON.stringify(examInform['program_answer'], undefined, 2); 60 | 61 | return( 62 |
63 |
64 |

{ examInform['test_name'] }

65 | 66 |
67 | 68 | 69 |
70 | 71 |
72 | 用时:{ use_time } 73 | 语言:{ examInform['program_language'] } 74 | 提交次数:{ examInform['submit_num'] } 75 |
76 | 77 |
78 |

{ JSON.parse(show) }

79 | {/*

*/} 80 |
81 | 82 |
83 |
84 |
满分:{ testPoint }
85 |
86 | 评分: 87 | 93 |
94 | {/* 100 | 101 | */} 102 |
103 |
104 | 105 |
106 |
107 | ) 108 | } 109 | } -------------------------------------------------------------------------------- /src/pages/interviewer/consult/showExam.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, } from 'react-router-dom'; 3 | import { 4 | Layout, 5 | Table, 6 | Tag, 7 | } from 'antd'; 8 | import { 9 | } from '@ant-design/icons'; 10 | 11 | import { EXAM_INFORM, LOOK_OVER, PAPER_CONSULT, PAPER_STATUS } from 'common/const'; 12 | import Navbar from 'common/components/navbar'; 13 | import { showPaper } from 'api/modules/paper'; 14 | 15 | import { connect } from 'react-redux'; 16 | import { GET_EXAM } from 'useRedux/constant'; 17 | import { getCookie, transTime } from 'common/utils'; 18 | 19 | interface Prop { 20 | lookExam: string; 21 | changeEmail: any; 22 | dispatch: any 23 | } 24 | 25 | class ShowExam extends React.Component { 26 | 27 | state = { 28 | examArr: [] = [], 29 | } 30 | 31 | componentDidMount() { 32 | const cookie = getCookie(); 33 | showPaper({ cookie, interviewer: true }).then(result => { 34 | const res = result.data.show; 35 | const nowtime = new Date().getTime(); 36 | console.log('res', res) 37 | res.map((item: any) => { 38 | const timeBegin = Number(item.time_begin); 39 | const timeEnd = Number(item.time_end); 40 | item.time_begin = transTime(timeBegin); 41 | item.time_end = transTime(timeEnd); 42 | item.ought_num = item.candidate.length + '人'; 43 | item.status = item.time_end < nowtime ? PAPER_STATUS.END : item.time_begin > nowtime ? PAPER_STATUS.WILL : PAPER_STATUS.ING; 44 | item.look_over = item.look_over === false ? PAPER_CONSULT.NO : PAPER_CONSULT.YES; 45 | }) 46 | this.setState({ examArr: res }); 47 | }); 48 | } 49 | 50 | render() { 51 | const { changeEmail } = this.props; 52 | const { examArr } = this.state; 53 | const columns = [ 54 | { 55 | title: '状态', 56 | dataIndex: 'status', 57 | key: 'status', 58 | // render: (status: string) => { 59 | // 60 | // {(status: string) => { 61 | // let color = status === PAPER_STATUS.WILL ? 'yellow' : status === PAPER_STATUS.ING ? 'green' : 'red'; 62 | // return ( 63 | // 64 | // { status } 65 | // 66 | // ); 67 | // }} 68 | // 69 | // } 70 | }, 71 | { title: '试卷', dataIndex: 'paper', key: 'paper' }, 72 | { title: '时长', dataIndex: 'answer_time', key: 'answer_time' }, 73 | { title: '已参与', dataIndex: 'join_num', key: 'join_num' }, 74 | { title: '应参与', dataIndex: 'ought_num', key: 'ought_num' }, 75 | { title: '开放时间', dataIndex: 'time_begin', key: 'time_begin' }, 76 | { title: '截止时间', dataIndex: 'time_end', key: 'time_end' }, 77 | { title: '总题数', dataIndex: 'tests_num', key: 'tests_num' }, 78 | { title: '是否批阅', dataIndex: 'look_over', key: 'look_over' }, 79 | { 80 | title: '操作', 81 | dataIndex: 'action', 82 | render: (text: any, record: any) => { 83 | return( 84 | 85 | 查看试卷情况 86 | 87 | ) 88 | } 89 | } 90 | ]; 91 | 92 | return( 93 |
94 | 95 | 96 | 97 | { 102 | return { 103 | onClick: () => { changeEmail(record['paper']) } 104 | }; 105 | }} 106 | /> 107 | 108 | 109 | ) 110 | } 111 | } 112 | 113 | function mapStateToProps(state: any) { 114 | return{ 115 | lookExam: state.lookExam 116 | } 117 | } 118 | function mapDispatchToProps(dispatch: any, ownProps: any) { 119 | return{ 120 | changeEmail: (exam: string) => { 121 | dispatch({ 122 | type: GET_EXAM, 123 | lookExam: exam 124 | }); 125 | } 126 | } 127 | } 128 | const ShowExamContainer = connect(mapStateToProps, mapDispatchToProps)(ShowExam) 129 | export default ShowExamContainer; -------------------------------------------------------------------------------- /src/pages/interviewer/edit/add.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Form, 4 | Button, 5 | message, 6 | Drawer, 7 | Layout 8 | } from 'antd'; 9 | import { 10 | RightOutlined, 11 | ProfileOutlined, 12 | } from '@ant-design/icons'; 13 | import { FormInstance } from 'antd/es/form'; 14 | 15 | import { addTest } from 'api/modules/test'; 16 | import { addPaper } from 'api/modules/paper'; 17 | import Navbar from 'common/components/navbar'; 18 | import Foot from 'common/components/footer'; 19 | import Tabler from 'src/common/components/interviewer/tabler'; 20 | import Paper from 'src/common/components/interviewer/paper'; 21 | import { TEST_MANAGE } from 'common/const'; 22 | import { getCookie } from 'src/common/utils'; 23 | 24 | export default class Add extends React.Component { 25 | modalRef = React.createRef(); 26 | 27 | state={ 28 | visible: false, // 控制侧边抽屉 29 | button: true, // 控制【右上角“下一步”】按钮样式 30 | tableArr: [] = [], // 存储试题信息 31 | paper: '', // 存储试卷名 32 | candidateEmail: [] = [], 33 | watch: true, 34 | } 35 | 36 | // 抽屉提交试卷信息至数据库 37 | submitPaper = async (values: any) => { 38 | const cookie = getCookie(); 39 | const obj = { cookie, values }; 40 | const res = await addPaper(obj); 41 | if (res.data.status) { 42 | message.success(res.msg); 43 | this.setState({ 44 | button: false, 45 | visible: false, 46 | paper: values.paper, 47 | candidateEmail: values.candidate, 48 | watch: values.check 49 | }); 50 | } else { 51 | message.error(res.msg); 52 | } 53 | }; 54 | // 表格提交试题信息至数据库 55 | submitTest = async () => { 56 | const { tableArr, paper, watch, candidateEmail } = this.state; 57 | // const req: string[] = tableArr.length > 0 ? tableArr : []; 58 | const obj = { 59 | data: tableArr, 60 | paper, 61 | watch, 62 | candidateEmail 63 | } 64 | const res = await addTest(obj); 65 | if (res.data.status) { 66 | message.success(res.msg); 67 | window.location.href = TEST_MANAGE; 68 | } else { 69 | message.error(res.msg); 70 | } 71 | } 72 | 73 | // “完善试卷信息”的抽屉 74 | showDrawer = async () => { 75 | this.setState({ visible: true }); 76 | }; 77 | onClose = () => { 78 | this.setState({ visible: false }); 79 | }; 80 | // 获取 tabler 组件的表格数据 81 | getTest = (val: any) => { 82 | this.setState({ tableArr: val }); 83 | } 84 | 85 | 86 | render() { 87 | const { button, visible, tableArr, } = this.state; 88 | 89 | return( 90 |
91 | 92 | 93 | 94 | 95 | 105 | 106 | 116 | 117 | 124 |
128 | 129 | 130 | 131 | 132 | 135 | 136 | 137 |
138 | 139 | 140 |
141 |
142 | ) 143 | } 144 | } -------------------------------------------------------------------------------- /src/pages/interviewer/edit/modify.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Card, 4 | Form, 5 | Button, 6 | message, 7 | FormInstance, 8 | } from 'antd'; 9 | import moment from 'moment'; 10 | 11 | import { modifyPaper } from 'api/modules/paper'; 12 | import { showTest } from 'api/modules/test'; 13 | import { TEST_MANAGE, TAGS } from 'common/const'; 14 | import { getCookie, getUrlParam } from 'common/utils'; 15 | import Navbar from 'common/components/navbar'; 16 | import Foot from 'common/components/footer'; 17 | import Tabler from 'src/common/components/interviewer/tabler'; 18 | import Paper from 'src/common/components/interviewer/paper'; 19 | import 'style/interviewer/modify.less'; 20 | 21 | export default class Modify extends React.Component { 22 | state = { 23 | loading: true, 24 | value: 0, 25 | tableArr: [] = [], 26 | visible: false, 27 | inform: { 28 | paper: '', 29 | paper_description: '', 30 | time_begin: '', 31 | time_end: '', 32 | answer_time: '', 33 | candidate: [''], 34 | check: '', 35 | paper_point: 0, 36 | }, 37 | hour: 0, 38 | minute: 0, 39 | } 40 | 41 | componentDidMount() { 42 | const url = getUrlParam('paper'); 43 | const req = { paper: url, cookie: getCookie() }; 44 | showTest(req).then((testRes) => { 45 | const arr = []; 46 | for (let ch of testRes.data.show) { 47 | const obj = { 48 | key: ch.test_name, 49 | num: ch.num, 50 | testName: ch.test_name, 51 | description: ch.test, 52 | // ...ch 53 | tags: ch.tags, 54 | level: ch.level, 55 | point: ch.point, 56 | } 57 | arr.push(obj) 58 | } 59 | console.log('ddddd', testRes.data) 60 | this.setState({ 61 | tableArr: arr, 62 | loading: false, 63 | inform: testRes.data.show[0].paper 64 | }); 65 | }); 66 | } 67 | 68 | onChange = (e: any) => { 69 | this.setState({value: e.target.value}) 70 | } 71 | 72 | // 提交修改信息 73 | onFinish = async (values: any) => { 74 | const { tableArr, inform, hour, minute } = this.state; 75 | // values.answerTime = hour + '小时' + minute + '分钟'; 76 | values.modifyTests = tableArr; 77 | values.oldPaper = inform.paper; 78 | const res = await modifyPaper(values); 79 | if (res.data.status) { 80 | message.success(res.msg); 81 | window.location.href = TEST_MANAGE; 82 | } else { 83 | message.error(res.msg); 84 | } 85 | }; 86 | 87 | getTest = (val: any) => { 88 | this.setState({ tableArr: val }); 89 | } 90 | getHour = (val: number) => { 91 | this.setState({ hour: val }); 92 | } 93 | getMinute = (val: number) => { 94 | this.setState({ minute: val }); 95 | } 96 | 97 | render() { 98 | const { inform, loading, tableArr } = this.state; 99 | const timeBegin = new Date(Number(inform.time_begin)); 100 | const timeEnd = new Date(Number(inform.time_end)); 101 | 102 | return( 103 |
104 | 105 | 106 |
107 | 108 | {!loading && ( 109 |
122 |

试卷信息

123 | 124 | 125 |

试题信息

126 | 127 | 128 | 129 | 132 | 133 | 134 | )} 135 | 136 |
137 |
138 | 139 | 140 |
141 | ) 142 | } 143 | } -------------------------------------------------------------------------------- /src/pages/candidate/showTests.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Modal, Button, message, Input, Statistic } from 'antd'; 3 | 4 | import 'style/candidate/showTests.less'; 5 | import { getDays, getUrlParam, handleTime, getCookie, transTime } from 'common/utils'; 6 | import TestAlone from 'src/common/components/candidate/testAlone'; 7 | import CountDown from 'src/common/components/candidate/countdown'; 8 | import { showTest } from 'api/modules/test'; 9 | import { search, submit } from 'api/modules/candidate'; 10 | import { CANDIDATE, TEST_STATUS } from 'common/const'; 11 | 12 | export default class ShowTests extends React.Component { 13 | url = getUrlParam('paper'); 14 | cookie = getCookie(); 15 | obj = { paper: this.url, cookie: this.cookie }; 16 | 17 | state = { 18 | tableArr: [] = [], 19 | visible: false, 20 | isWatch: true, 21 | isOver: false, 22 | endTime: 0, 23 | time: '', 24 | inputInfo: '', 25 | count: 0, 26 | updateTime: '', 27 | } 28 | 29 | async componentDidMount() { 30 | const res = await showTest(this.obj); 31 | const ans = await search(this.obj); 32 | const ret = ans.data.ret[0]; 33 | ret.test_status = ret.test_status === TEST_STATUS.DONE ? true : false; 34 | this.setState({ 35 | tableArr: res.data.show, 36 | isWatch: ret.watch, 37 | isOver: ret.test_status, 38 | endTime: +ret.time_end, 39 | count: ret.time_end, 40 | }); 41 | this.countdown(); 42 | } 43 | componentWillUnmount() { 44 | clearTimeout(this.timer); 45 | } 46 | 47 | // 显示“提交试卷”抽屉,并执行倒计时函数 48 | showModal = () => { 49 | this.setState({ visible: true }); 50 | }; 51 | hideModal = () => { 52 | this.setState({ visible: false }); 53 | } 54 | 55 | // “提交试卷”抽屉中剩余时间倒计时 56 | timer: NodeJS.Timer = null; 57 | countdown = () => { 58 | const { count } = this.state; 59 | const nowtime = new Date().getTime(); 60 | const time = getDays(nowtime, count); 61 | this.setState({ count: count - 1000, updateTime: time }); 62 | this.timer = setTimeout(() => { 63 | this.countdown() 64 | }, 1000); 65 | } 66 | 67 | // 获取输入框的内容 68 | saveInput = (e: any) => { 69 | this.setState({ inputInfo: e.target.value}); 70 | } 71 | // 提交试卷事件 72 | submitPaper = () => { 73 | if (this.state.inputInfo === '确定提前交卷') { 74 | submit(this.obj).then(res => { 75 | if (res.data.status) { 76 | message.success(res.msg); 77 | window.location.href = CANDIDATE; 78 | } 79 | }) 80 | } else { 81 | message.error('请输入正确的信息,以确认提交!') 82 | } 83 | }; 84 | 85 | 86 | render() { 87 | const { tableArr, visible, isWatch, isOver, endTime, count, updateTime, } = this.state; 88 | const nowtime = new Date().getTime(); 89 | 90 | return( 91 |
92 |
93 | { 94 | tableArr && tableArr.map(item => { 95 | return( 96 | 101 | ) 102 | }) 103 | } 104 |
105 |
106 | {/* */} 111 |
112 | 118 |
119 | 120 | 128 | 136 | { 137 | '距离试卷截止时间 ' + (updateTime) + ' ,如果你确定要提前交卷,请务必填写如下内容:' 138 | } 139 | 140 | 141 |
142 |
143 | ) 144 | } 145 | } -------------------------------------------------------------------------------- /src/pages/interviewer/interview/entrance.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Button, Form, Input, Select, message, InputNumber, } from 'antd'; 4 | 5 | import 'style/interviewer/interviewEntrance.less'; 6 | 7 | import { findInterview } from 'api/modules/interview'; 8 | import { findEmail } from 'common/utils'; 9 | import Navbar from 'common/components/navbar'; 10 | import { RefSelectProps } from 'antd/lib/select'; 11 | 12 | interface Prop { 13 | 14 | } 15 | 16 | interface State { 17 | interviewArr: string[]; 18 | interviewRoomArr: string[]; 19 | interviewLinkArr: string[]; 20 | } 21 | export default class InterviewRoom extends React.Component { 22 | 23 | state = { 24 | interviewArr: [], 25 | interviewRoomArr: [], 26 | interviewLinkArr: [], 27 | } 28 | 29 | async componentDidMount() { 30 | const res = await findEmail(); 31 | const { allInterview } = res; 32 | this.setState({ interviewArr: allInterview }); 33 | } 34 | interviewRoomInform = []; 35 | changeSelect = async (value: any) => { 36 | const res = await findInterview({ interviewer: value }); 37 | this.interviewRoomInform = res.data.ret; 38 | let linkArr = []; 39 | this.interviewRoomInform.map(item => { 40 | linkArr.push(item.interviewer_link); 41 | }) 42 | this.setState({ interviewLinkArr: linkArr }); 43 | } 44 | choiceInterviewLink = async (value: any) => { 45 | const findInterview = this.interviewRoomInform.filter(item => item.interviewer_link === value); 46 | const newRoomArr = [findInterview[0].interview_room]; 47 | this.setState({ interviewRoomArr: newRoomArr }); 48 | } 49 | 50 | // 进入面试间的检验函数 51 | checkInterview = (value: any) => { 52 | findInterview({ findArr: value }).then(res => { 53 | if (res.data.status === true) { 54 | message.success(res.msg); 55 | window.location.href = value['interviewer_link']; 56 | } else { 57 | message.error(res.msg); 58 | } 59 | }) 60 | } 61 | 62 | render() { 63 | const { interviewArr, interviewRoomArr, interviewLinkArr } = this.state; 64 | 65 | return( 66 |
67 | 68 |
69 |
70 | 76 | 87 | 88 | 89 | 95 | 106 | {/* */} 107 | 108 | 109 | 115 | 125 | {/* */} 126 | 127 | 128 | 129 | 130 | 131 | 132 |
133 |
134 | ) 135 | } 136 | } -------------------------------------------------------------------------------- /src/common/const.ts: -------------------------------------------------------------------------------- 1 | import Add from "src/pages/interviewer/edit/add"; 2 | import Modify from "src/pages/interviewer/edit/modify"; 3 | import Edit from "src/pages/interviewer/edit/show"; 4 | import Login from "src/pages/login"; 5 | import ShowExam from 'pages/interviewer/consult/showExam'; 6 | import InterviewRoom from "src/pages/interviewer/interview/room"; 7 | import ExamInform from 'pages/interviewer/consult/examInform'; 8 | import LookOver from "src/pages/interviewer/consult/lookOver"; 9 | import { Route } from 'common/types'; 10 | 11 | export const REQUESTIP: string = 'http://www.syandeg.com:8080/api'; 12 | export const LOGIN: string = '/login'; 13 | 14 | export const TEST_MANAGE: string = '/test-manage'; 15 | // export const TEST_EDIT: string = '/test-manage/edit'; 16 | export const TEST_ADD: string = '/test-manage/add'; 17 | export const TEST_MODIFY: string = '/test-manage/modify'; 18 | 19 | export const SHOW_EXAM: string = '/show-exam'; 20 | export const EXAM_INFORM: string = '/show-exam/exam-inform'; 21 | export const LOOK_OVER: string = '/show-exam/look-over'; 22 | 23 | export const INTERVIEW_MANAGE: string = '/interview-manage'; 24 | export const INTERVIEW: string = '/interview'; 25 | export const INTERVIEW_ENTRANCE: string = '/interview-entrance'; 26 | 27 | export const CANDIDATE: string = '/candidate'; 28 | export const CANDIDATE_SHOW_TESTS: string = '/candidate/show-tests'; 29 | export const CANDIDATE_WATCH_TEST: string = '/candidate/watch-test'; 30 | export const CANDIDATE_TEST: string = '/candidate/test'; 31 | 32 | // 路由栈 33 | export const routes = [ 34 | { 35 | path: INTERVIEW_ENTRANCE, 36 | breadcrumbName: '面试间入口', 37 | // component: InterviewRoom, 38 | children: [ 39 | { 40 | path: INTERVIEW, 41 | breadcrumbName: '面试间', 42 | } 43 | ] 44 | }, 45 | { 46 | path: INTERVIEW_MANAGE, 47 | breadcrumbName: '面试间管理', 48 | }, 49 | { 50 | path: TEST_MANAGE, 51 | breadcrumbName: '试题管理', 52 | // component: Edit, 53 | children: [ 54 | { 55 | path: TEST_ADD, 56 | breadcrumbName: '新建试卷', 57 | // component: Add, 58 | }, 59 | { 60 | path: TEST_MODIFY, 61 | breadcrumbName: '修改试卷', 62 | // component: Modify, 63 | } 64 | ] 65 | }, 66 | { 67 | path: SHOW_EXAM, 68 | breadcrumbName: '阅卷管理', 69 | // component: ShowExam, 70 | children: [ 71 | { 72 | path: EXAM_INFORM, 73 | breadcrumbName: '展示阅卷信息', 74 | // component: ExamInform 75 | }, 76 | { 77 | path: LOOK_OVER, 78 | breadcrumbName: '开始阅卷', 79 | // component: LookOver 80 | } 81 | ] 82 | } 83 | ] 84 | 85 | // 试题难度 86 | export enum TEST_LEVEL { 87 | EASY = '简单', 88 | EASY_KEY = 'easy', 89 | MIDDLE = '中等', 90 | MIDDLE_KEY = 'middle', 91 | HARD = '困难', 92 | HARD_KEY = 'hard', 93 | }; 94 | // 答题情况 95 | export enum TEST_STATUS { 96 | NODO = '未做', 97 | NODO_KEY = 0, 98 | DONE = '已解答', 99 | DONE_KEY = -1, 100 | DOING = '尝试中', 101 | DOING_KEY = 1, 102 | WILL = '未开始', 103 | ING = '进行中', 104 | END = '已结束' 105 | } 106 | // 试卷状态 107 | export enum PAPER_STATUS { 108 | NODO = '试卷未开放', 109 | DONE = '试卷已提交', 110 | OVER = '试卷已过期', 111 | WILL = '未开始', 112 | ING = '进行中', 113 | END = '已结束' 114 | } 115 | // 试卷批阅情况 116 | export enum PAPER_CONSULT { 117 | YES = '已批阅', 118 | NO = '未批阅', 119 | } 120 | 121 | // 面试间的状态 122 | export enum INTERVIEW_STATUS { 123 | NO = '未开始', 124 | ING = '进行中', 125 | ON = '已结束' 126 | } 127 | 128 | // 动态生成试卷标签 129 | export const ARR = ['数组', '字符串', '排序', '矩阵', '模拟', '枚举', '字符串匹配', '桶排序', '计数排序', '基数排序', '双指针', '链表', '堆栈', '队列', '图']; 130 | export const TAGS = [ 131 | { key: 0, value: '数组'}, 132 | { key: 1, value: '字符串'}, 133 | { key: 2, value: '排序'}, 134 | { key: 3, value: '矩阵'}, 135 | { key: 4, value: '模拟'}, 136 | { key: 5, value: '枚举'}, 137 | { key: 6, value: '字符串匹配'}, 138 | { key: 7, value: '桶排序'}, 139 | { key: 8, value: '计数排序'}, 140 | { key: 9, value: '基数排序'}, 141 | { key: 10, value: '双指针'}, 142 | { key: 11, value: '链表'}, 143 | { key: 12, value: '堆栈'}, 144 | { key: 13, value: '队列'}, 145 | { key: 14, value: ''}, 146 | { key: 15, value: '图'}, 147 | ]; 148 | 149 | // 代码编辑器的语言 150 | export const PROGRAM_LANGUAGE = ['cpp', 'java', 'python', 'javascript', 'ruby', 'swift', 'go', 'rust', 'php', 'typescript']; 151 | // 代码编辑器的主题 152 | export const PROGRAM_THEME = ['vs', 'vs-dark', 'hc-black',]; 153 | 154 | // websocket 收到信息时的 type 155 | export enum WS_TYPE { 156 | CONNECT = 'connect', 157 | TALK = 'talk', 158 | CODE = 'code', 159 | REQ_VIDEO = 'req-video', 160 | RES_VIDEO = 'res-video', 161 | VIDEO_OFFER = 'video-offer', 162 | VIDEO_ANSWER = 'video-answer', 163 | NEW_ICE_CANDIDATE = 'new-ice-candidate', 164 | HANG_UP = 'hang-up', 165 | } -------------------------------------------------------------------------------- /webpack/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const { 4 | CleanWebpackPlugin 5 | } = require('clean-webpack-plugin'); 6 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 7 | const WebpackDeepScopeAnalysisPlugin = require('webpack-deep-scope-plugin').default; 8 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); 9 | // const TerserPlugin = require('terser-webpack-plugin'); 10 | // const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); 11 | 12 | const postCssLoaderConfig = { 13 | loader: 'postcss-loader', 14 | options: { 15 | plugins: [ 16 | require('autoprefixer')({ 17 | overrideBrowserslist: [ 18 | 'Chrome > 31', 19 | 'ff > 31', 20 | 'ie >= 10' 21 | ] 22 | }) 23 | ] 24 | } 25 | }; 26 | 27 | const commonConfig = { 28 | entry: [ 29 | 'babel-polyfill', 30 | './src/index.tsx' 31 | ], 32 | output: { 33 | path: path.resolve(__dirname, '../dist'), 34 | filename: 'static/js/[name].js', 35 | globalObject: 'self' 36 | }, 37 | plugins: [ 38 | new HtmlWebpackPlugin({ 39 | template: path.resolve(__dirname, '../src/index.html'), 40 | filename: 'index.html' 41 | }), 42 | new CleanWebpackPlugin(), 43 | new MiniCssExtractPlugin({ 44 | filename: "static/css/[name].[hash].css", 45 | }), 46 | new WebpackDeepScopeAnalysisPlugin(), 47 | // new MonacoWebpackPlugin(['cpp', 'java', 'python', 'javascript', 'ruby', 'swift', 'go', 'rust', 'php', 'typescript']), 48 | new MonacoWebpackPlugin(['apex', 'azcli', 'bat', 'clojure', 'coffee', 'cpp', 'csharp', 'csp', 'css', 'dockerfile', 'fsharp', 'go', 'handlebars', 'html', 'ini', 'java', 'javascript', 'json', 'less', 'lua', 'markdown', 'msdax', 'mysql', 'objective', 'perl', 'pgsql', 'php', 'postiats', 'powerquery', 'powershell', 'pug', 'python', 'r', 'razor', 'redis', 'redshift', 'ruby', 'rust', 'sb', 'scheme', 'scss', 'shell', 'solidity', 'sql', 'st', 'swift', 'typescript', 'vb', 'xml', 'yaml']) 49 | ], 50 | module: { 51 | rules: [{ 52 | test: /\.(jsx?|tsx?)$/, 53 | loader: 'babel-loader', 54 | exclude: /node_modules/, 55 | options: { 56 | plugins: [ 57 | ["import", { 58 | libraryName: "antd", 59 | style: "css" 60 | }] 61 | ] 62 | } 63 | }, { 64 | test: /\.tsx?$/, 65 | use: 'ts-loader', 66 | exclude: /node_modules/ 67 | }, { 68 | test: /\.css$/, 69 | use: [ 70 | MiniCssExtractPlugin.loader, 71 | 'css-loader', 72 | postCssLoaderConfig, 73 | ] 74 | }, { 75 | test: /\.ttf$/, 76 | use: ['file-loader'] 77 | }, { 78 | test: /\.less$/, 79 | use: [MiniCssExtractPlugin.loader, 'css-loader', postCssLoaderConfig, 'less-loader'] 80 | }, { 81 | test: /.*\.(gif|png|svg|jpe?g)$/i, 82 | use: [{ 83 | loader: 'url-loader', 84 | options: { 85 | limit: 5120, 86 | name: 'static/imgs/[name].[hash:8].[ext]', 87 | publicPath: '../../' 88 | } 89 | }] 90 | } 91 | ] 92 | }, 93 | optimization: { 94 | // minimize: true, 95 | // minimizer: [ 96 | // new TerserPlugin({ 97 | // parallel: 4, // 开启几个进程来处理压缩,默认是 os.cpus().length - 1 98 | // }), 99 | // new OptimizeCSSAssetsPlugin({ 100 | // cssProcessor: require("cssnano"), //引⼊cssnano配置压缩选项 101 | // cssProcessorOptions: { 102 | // discardComments: { removeAll: true } 103 | // } 104 | // }), 105 | // // new OptimizeCSSAssetsPlugin({ 106 | // // assetNameRegExp: /\.optimize\.css$/g, 107 | // // cssProcessor: require('cssnano'), 108 | // // cssProcessorPluginOptions: { 109 | // // preset: ['default', { discardComments: { removeAll: true } }], 110 | // // }, 111 | // // canPrint: true, 112 | // // }) 113 | // ], 114 | splitChunks: { 115 | cacheGroups: { 116 | vender: { 117 | chunks: 'all', 118 | name: 'vender', 119 | test: (module) => { 120 | return /[\\/]node_modules[\\/](lodash|moment|react|react-dom|react-router|react-router-dom|axios|antd)/.test(module.context); 121 | }, 122 | priority: 10, 123 | }, 124 | } 125 | } 126 | }, 127 | resolve: { 128 | extensions: ['.tsx', '.ts', '.js'], 129 | mainFiles: ['index.tsx', 'index.ts', 'index'], 130 | alias: { 131 | 'img': path.resolve(__dirname, '../img'), 132 | 'src': path.resolve(__dirname, '../src'), 133 | 'api': path.resolve(__dirname, '../src/api'), 134 | 'common': path.resolve(__dirname, '../src/common'), 135 | 'pages': path.resolve(__dirname, '../src/pages'), 136 | 'style': path.resolve(__dirname, '../src/style'), 137 | 'useRedux': path.resolve(__dirname, '../src/useRedux') 138 | }, 139 | }, 140 | }; 141 | 142 | module.exports = commonConfig; -------------------------------------------------------------------------------- /src/common/components/candidate/codeEditor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Select, } from 'antd'; 3 | import { PROGRAM_LANGUAGE, PROGRAM_THEME } from 'src/common/const'; 4 | import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'; 5 | import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution'; 6 | import 'monaco-editor/esm/vs/editor/contrib/find/findController.js'; 7 | import { Client, TextOperation, } from 'ot'; 8 | import ot from 'ot'; 9 | import { post } from 'api/index'; 10 | import { getCookie, nowTime } from 'src/common/utils'; 11 | 12 | interface Prop { 13 | sendCode?: any; // 发送 websocket 请求的函数 14 | getProgramCode?: any; // 获取代码 15 | codeObj?: any; 16 | language?: string; 17 | } 18 | 19 | interface State { 20 | 21 | } 22 | 23 | let monacoInstance: monaco.editor.IStandaloneCodeEditor = null; 24 | 25 | export default class CodeEditor extends React.Component { 26 | state = { 27 | language: 'javascript', 28 | theme: 'vs', 29 | } 30 | 31 | // 组件挂载后加载编辑器 32 | componentDidMount() { 33 | monacoInstance = monaco.editor.create(document.getElementById("container"), { 34 | value: `头部 35 | 作答时间 36 | 阅卷 37 | 删除面试间 38 | 39 | 视频 40 | 编辑冲突`, 41 | contextmenu: true, 42 | language:"javascript", 43 | theme: 'vs', 44 | minimap: { 45 | enabled: false 46 | } 47 | }); 48 | 49 | // 获取编辑器的内容 50 | monacoInstance.onDidChangeModelContent((e) => { 51 | let newValue = monacoInstance.getValue(); 52 | console.log(newValue) 53 | const { getProgramCode, sendCode, codeObj } = this.props; 54 | const cookie = getCookie(); 55 | getProgramCode && getProgramCode(newValue); 56 | if (sendCode) { 57 | const { changes } = e; 58 | let docLength = monacoInstance.getModel().getValueLength(); // 文档长度 59 | let operationDoc = new TextOperation().retain(docLength); // 初始化一个operation,并保留文档原始内容 60 | for (let i = changes.length - 1; i >= 0; i--) { 61 | const change = changes[i]; 62 | const restLength = docLength - change.rangeOffset - change.text.length; // 文档 63 | operationDoc = new TextOperation() 64 | .retain(change.rangeOffset) // 保留光标位置前的所有字符 65 | .delete(change.rangeLength) // 删除N个字符(如为0这个操作无效) 66 | .insert(change.text) // 插入字符 67 | .retain(restLength) // 保留剩余字符 68 | .compose(operationDoc); // 与初始operation组合为一个操作 69 | } 70 | 71 | const operation = operationDoc.toString(); 72 | const time = nowTime(); 73 | const operationObj = { operation, cookie, time }; 74 | //怎么发送? 75 | //怎么接收? 76 | //怎么更新? 77 | // sendCode(operationObj); 78 | } 79 | }) 80 | 81 | } 82 | 83 | // componentDidUpdate() { 84 | // monacoInstance.onDidChangeModelContent((e) => { 85 | // const { codeObj } = this.props; 86 | // console.log('要修改其他用户的代码了') 87 | // console.log(codeObj) 88 | // console.log(codeObj.code) 89 | // monacoInstance.setValue('nihao') 90 | // // monacoInstance.setValue(codeObj.code); 91 | // // monacoInstance.setValue(this.props.codeObj.code); 92 | // }) 93 | // } 94 | 95 | // 动态修改语言 96 | changeLanguage = (value: any) => { 97 | monacoInstance.onDidChangeModelLanguage(e => { 98 | monaco.editor.setModelLanguage(monacoInstance.getModel(), value); 99 | }) 100 | this.setState({ language: value }); 101 | } 102 | // 动态修改主题 103 | changeTheme = (value: any) => { 104 | monaco.editor.defineTheme('myTheme', { 105 | base: value,// 要继承的基础主题,即内置的三个:vs、vs-dark、hc-black 106 | inherit: false,// 是否继承 107 | rules: [// 高亮规则,即给代码里不同token类型的代码设置不同的显示样式 108 | { token: '', foreground: '000000', background: 'fffffe' } 109 | ], 110 | colors: {// 非代码部分的其他部分的颜色,比如背景、滚动条等 111 | // [editorBackground]: '#FFFFFE' 112 | } 113 | }); 114 | monaco.editor.setTheme('myTheme'); 115 | this.setState({ theme: value }); 116 | } 117 | 118 | // 组件卸载后销毁编辑器 119 | componentWillUnmount() { 120 | monacoInstance.dispose(); 121 | } 122 | 123 | render() { 124 | const { language, theme } = this.state; 125 | 126 | return( 127 | <> 128 |
129 | 142 | 143 | 156 |
157 | 158 |
159 | 160 | ) 161 | } 162 | } -------------------------------------------------------------------------------- /src/common/utils.ts: -------------------------------------------------------------------------------- 1 | import { TEST_LEVEL, PAPER_STATUS } from "common/const"; 2 | import { searchEmail } from "api/modules/user"; 3 | 4 | // 以递归的方式展平数组 5 | export function flattenRoutes(arr: any) { 6 | return arr.reduce(function(prev: any, item: any) { 7 | prev.push(item); 8 | return prev.concat( 9 | Array.isArray(item.children) ? flattenRoutes(item.children) : [] 10 | ); 11 | }, []); 12 | } 13 | 14 | // 获取地址栏的信息 15 | export function getUrlParam(key: string) { 16 | // 获取参数 17 | const url = window.location.search; 18 | // 正则筛选地址栏 19 | const reg = new RegExp("(^|&)" + key + "=([^&]*)(&|$)"); 20 | // 匹配目标参数 21 | const result = url.slice(1).match(reg); 22 | //返回参数值 23 | return result ? decodeURIComponent(result[2]) : null; 24 | } 25 | 26 | // 随机生成包含大小写字母和数字的6位数题号 27 | export function getTestNum() { 28 | let str = ''; 29 | for (let i = 0; i < 2; i++) { 30 | const num = Math.floor(Math.random() * 10); 31 | const az = String.fromCharCode(Math.random() * 26 + 65); 32 | const AZ = String.fromCharCode(Math.random() * 26 + 97); 33 | str += num + az + AZ; 34 | } 35 | return str; 36 | } 37 | 38 | // 获取 cookie 39 | export function getCookie() { 40 | const str = document.cookie; 41 | const cookie = str.split('='); 42 | return cookie[1]; 43 | } 44 | 45 | // 获取当前时间戳:毫秒格式 或者 hh:mm:ss格式 46 | // 不带参数是毫秒格式,否则为 hh:mm:ss 格式 47 | export function nowTime(data?: { click: boolean }): number | string { 48 | if (data && data.click === true) { 49 | const time = new Date(); 50 | const hour = time.getHours(); 51 | const minute = time.getMinutes(); 52 | const second = time.getSeconds(); 53 | const timer = hour + ':' + minute + ':' + second + ' '; 54 | return timer; 55 | } 56 | const time = new Date().getTime(); 57 | return time; 58 | } 59 | 60 | // 求出日期之间的天数 61 | export function getDays(start: number, end: number, diff?: number) { 62 | // const left = new Date(start); 63 | // const right = new Date(end); 64 | // const ms = Math.abs(right.getTime() - left.getTime()); 65 | const ms = Math.abs(end - start); 66 | const s = Math.floor(ms / 1000 % 60); 67 | const day = Math.floor(ms / 1000 / 60 / 60 / 24); 68 | const hour = Math.floor(ms/ 1000 / 60 / 60 - (24 * day)); 69 | const minute = Math.floor(ms / 1000 /60 - (24 * 60 * day) - (60 * hour)); 70 | const retTime = '剩余 ' + day + ' 天 ' + hour + ' 小时 ' + minute + ' 分钟 ' + s + ' 秒'; 71 | return diff === 4 ? s : diff === 3 ? minute : diff === 2 ? hour : diff === 1 ? day : retTime; 72 | } 73 | 74 | // 获取后端返回的试卷数据,对试卷时间数据进行处理 75 | export function handleTime(arr: any, status?: number) { 76 | let nodoArr: any[] = [], doingArr: any[] = [], doneArr: any[] = [], allArr: any[] = []; 77 | arr.map((item: any) => { 78 | // 毫秒数 79 | const timebegin = item.time_begin || item.paper.time_begin; 80 | const timend = item.time_end || item.paper.time_end; 81 | // yyyy-mm-dd hh:mm:ss 格式 82 | const timeBegin = transTime(timebegin); 83 | const timeEnd = transTime(timend); 84 | const nowtime = new Date().getTime(); 85 | item.time_begin = timeBegin; 86 | item.time_end = timeEnd; 87 | item.check = item.check === true ? '是' : '否'; 88 | item.key = item.paper.key || item.paper; 89 | allArr.push(item); 90 | 91 | if (item.remaining_time === true || item.paper.remaining_time === true){ 92 | // 求出日期之间的天数 93 | const remaining_time = getDays(nowtime, timend); 94 | item.remaining_time = remaining_time; 95 | doingArr.push(item); 96 | } else if (item.remaining_time === false && timend > nowtime) { 97 | item.remaining_time = PAPER_STATUS.DONE; 98 | doneArr.push(item); 99 | } else if (item.remaining_time === false && timend < nowtime) { 100 | item.remaining_time = PAPER_STATUS.OVER; 101 | doneArr.push(item); 102 | } else if (nowtime < timebegin) { 103 | item.remaining_time = PAPER_STATUS.NODO; 104 | nodoArr.push(item); 105 | } 106 | }) 107 | return status === 1 ? doingArr : status === 0 ? nodoArr : status === -1 ? doneArr : allArr; 108 | } 109 | 110 | // 转化日期控件时间值 111 | export function transTime(time: number) { 112 | const timeDate = new Date(+time); 113 | const getTime = new Date(+new Date(timeDate) + 8 * 3600 * 1000) 114 | .toISOString() 115 | .replace(/T/g,' ') 116 | .replace(/\.[\d]{3}Z/,''); 117 | return getTime; 118 | } 119 | 120 | // 获取试卷难度 121 | export function getExamLevel(difficulty: string) { 122 | if (difficulty === TEST_LEVEL.EASY) { 123 | return TEST_LEVEL.EASY_KEY; 124 | } else if (difficulty === TEST_LEVEL.MIDDLE) { 125 | return TEST_LEVEL.MIDDLE_KEY; 126 | } else if (difficulty === TEST_LEVEL.HARD) { 127 | return TEST_LEVEL.HARD_KEY; 128 | } 129 | } 130 | 131 | // 获取以“小时”为单位的数字 132 | export function getHour() { 133 | let ret = new Array(24); 134 | for (let i = 0; i < 24; i++) { 135 | ret[i] = i; 136 | } 137 | return ret; 138 | } 139 | // 获取以“分钟”为单位的数字 140 | export function getMinute() { 141 | let ret = new Array(12); 142 | for (let i = 0; i <= 55; i += 5) { 143 | ret[i] = i; 144 | } 145 | return ret; 146 | } 147 | 148 | // 获取该项目中所有面试官和候选人的邮箱 149 | export async function findEmail() { 150 | const result = await searchEmail(); 151 | const res = result.data.ret; 152 | const candArr: string[] = [], interArr: string[] = []; 153 | res.map((item: { interviewer: boolean; email: string; }) => { 154 | if (item.interviewer === true) { 155 | interArr.push(item.email); 156 | } else if (item.interviewer === false) { 157 | candArr.push(item.email); 158 | } 159 | }) 160 | const obj = { 161 | allInterview: interArr, 162 | allCandidate: candArr 163 | } 164 | return obj; 165 | } -------------------------------------------------------------------------------- /src/pages/interviewer/edit/show.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink, BrowserRouter as Router, Route } from 'react-router-dom'; 3 | import { 4 | Layout, 5 | Table, 6 | Button, 7 | Tag, 8 | Space, 9 | message, 10 | Popconfirm, 11 | Row, 12 | Col, 13 | } from 'antd'; 14 | import { 15 | PlusOutlined, 16 | DeleteOutlined, 17 | FormOutlined, 18 | } from '@ant-design/icons'; 19 | 20 | import Navbar from 'common/components/navbar'; 21 | import { TEST_ADD, TEST_MODIFY } from 'common/const'; 22 | import { showPaper, deletePaper } from 'api/modules/paper'; 23 | import { getCookie, handleTime } from 'common/utils'; 24 | import Head from 'common/components/header'; 25 | 26 | export default class Edit extends React.Component { 27 | state = { 28 | selectedRowKeys: [] = [], 29 | data: [] = [], 30 | }; 31 | 32 | // 在页面一渲染就立马从数据库中拿取所有试卷的数据 33 | componentDidMount() { 34 | const cookie = getCookie(); 35 | showPaper({ cookie: cookie, interviewer: true }).then((result: any) => { 36 | const res = result.data.show; 37 | const arr = handleTime(res, 2); 38 | this.setState({ data: arr }); 39 | }) 40 | } 41 | 42 | // 删除试卷的按钮事件 43 | delete = async () => { 44 | const arr = this.state.selectedRowKeys; 45 | if (arr.length !== 0) { 46 | const res = await deletePaper(arr); 47 | const ret = handleTime(res.data); 48 | this.setState({ data: ret }); 49 | message.success(res.msg); 50 | } 51 | }; 52 | // 新建试卷的按钮事件 53 | add = () => { 54 | window.location.href = TEST_ADD; 55 | // return( 56 | // 57 | // ) 58 | }; 59 | 60 | // 表格复选框的选择情况 61 | onSelectChange = (selectedRowKeys: any) => { 62 | setTimeout(() => { 63 | this.setState({ selectedRowKeys }); 64 | }, 0); 65 | }; 66 | 67 | render() { 68 | const { data, selectedRowKeys } = this.state; 69 | const rowSelection = { 70 | onChange: this.onSelectChange, 71 | selectedRowKeys, 72 | }; 73 | const columns = [ 74 | { title: '试卷', dataIndex: 'paper', key: 'paper' }, 75 | // { title: '试卷', dataIndex: 'paper', key: 'paper', fixed: 'left' }, 76 | { title: '试卷描述', dataIndex: 'paper_description', key: 'paper_description' }, 77 | { 78 | title: '试题数量', 79 | dataIndex: 'tests_num', 80 | key: 'tests_num', 81 | sorter: (a: { tests_num: number; }, b: { tests_num: number; }) => a.tests_num - b.tests_num, 82 | }, 83 | { 84 | title: '试卷总分', 85 | dataIndex: 'paper_point', 86 | key: 'paper_point', 87 | sorter: (a: { paper_point: number; }, b: { paper_point: number; }) => a.paper_point - b.paper_point, 88 | }, 89 | { 90 | title: '候选人', 91 | children: [ 92 | { 93 | title: '邮箱账号', 94 | dataIndex: 'candidate', 95 | key: 'candidate', 96 | width: 175, 97 | render: (candidate: any) => ( 98 | 99 | { 100 | candidate.map((item: string) => { 101 | let color = item.length > 16 ? 'green' : 'geekblue'; 102 | return ( 103 |
104 | 105 | { item } 106 | 107 | 108 | ); 109 | }) 110 | } 111 | 112 | ), 113 | }, 114 | { 115 | title: '试卷过期能否查看', 116 | dataIndex: 'check', 117 | key: 'check', 118 | }, 119 | ], 120 | }, 121 | { title: '开始时间', dataIndex: 'time_begin', key: 'time_begin' }, 122 | { title: '截止时间', dataIndex: 'time_end', key: 'time_end' }, 123 | { title: '剩余时间', dataIndex: 'remaining_time', key: 'remaining_time' }, 124 | { title: '作答时长', dataIndex: 'answer_time', key: 'answer_time' }, 125 | { 126 | title: '操作', 127 | dataIndex: 'action', 128 | key: 'action', 129 | // fixed: 'right', 130 | render: (text: any, record: any) => { 131 | return( 132 | 133 | 134 | 修改试卷 135 | 136 | 137 | ) 138 | } 139 | }, 140 | ] 141 | 142 | return( 143 |
144 | 145 | 146 | 147 | 153 | 160 | 161 | 162 | 170 | 171 |
179 | 180 | 181 | ) 182 | } 183 | } -------------------------------------------------------------------------------- /src/common/components/webrtcCopy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, message } from 'antd'; 3 | import { ProvidePlugin } from 'webpack'; 4 | 5 | interface Prop { 6 | 7 | } 8 | 9 | interface State { 10 | showVideoButton: boolean; 11 | } 12 | 13 | // 本地流和远端流 14 | let localVideo; 15 | let remoteVideo; 16 | let localStream; // 本地流 17 | let peerConnA; // 本地链接 18 | let peerConnB; // 远端链接 19 | 20 | const constraints = { 21 | audio: true, 22 | video: true 23 | // video: { 24 | // width: 100%, 25 | // height: 100 26 | // } 27 | } 28 | 29 | // 本地流和远端流 30 | export default class Webrtc extends React.Component { 31 | 32 | state = { 33 | showVideoButton: true 34 | } 35 | 36 | componentDidMount(): void { 37 | localVideo = document.getElementById("localVideo"); 38 | remoteVideo = document.getElementById("remoteVideo"); 39 | } 40 | 41 | openCamera = async (e) => { 42 | const stream = await navigator.mediaDevices.getUserMedia(constraints) 43 | .then(function(mediaStream) { 44 | localVideo['srcObject'] = mediaStream; 45 | localStream = stream; 46 | localVideo.onloadedmetadata = function(e) { 47 | localVideo.play(); 48 | }; 49 | }).catch(function(err) { 50 | this.handleError(err); 51 | }) 52 | } 53 | 54 | call = async () => { 55 | // 创建视频轨道 56 | // 创建音频轨道 57 | 58 | let configuration = { 59 | "iceServers": [{ 60 | "url": "stun:stun.l.google.com:19302" 61 | }] 62 | }; 63 | peerConnA = new RTCPeerConnection(configuration); 64 | peerConnA.addEventListener('icecandidate', this.onIceCandidateA); 65 | 66 | peerConnB = new RTCPeerConnection(configuration); 67 | peerConnB.addEventListener('icecandidate', this.onIceCandidateB); 68 | 69 | peerConnA.addEventListener('iceconnectionstatechange', this.onIceStateChangeA); 70 | peerConnB.addEventListener('iceconnectionstatechange', this.onIceStateChangeB); 71 | 72 | // 远程客户获取到远端流后的事件 73 | peerConnB.addEventListener('track', this.gotRemoteStream); 74 | localStream.getTracks().forEach(track => { 75 | peerConnA.addTrack(track, localStream); 76 | }); 77 | 78 | try{ 79 | const offer = await peerConnA.createOffer(); 80 | await this.onCreateOfferSuccess(offer); 81 | } catch(e) { 82 | console.log('创建会话描述SD失败:', e.toString()); 83 | } 84 | } 85 | 86 | onCreateOfferSuccess = async (desc) => { 87 | try{ 88 | await peerConnA.setLocalDescription(desc); 89 | this.onSetLocalSuccess(peerConnA); 90 | }catch(e) { 91 | 92 | } 93 | 94 | try{ 95 | await peerConnB.setRemoteDescription(desc); 96 | this.onSetRemoteSuccess(peerConnB); 97 | }catch(e) { 98 | 99 | } 100 | 101 | try{ 102 | const answer = await peerConnB.createAnswer(); 103 | this.onCreateAnswerSuccess(answer); 104 | }catch(e) { 105 | 106 | } 107 | } 108 | 109 | onCreateAnswerSuccess = async (desc) => { 110 | try{ 111 | await peerConnB.setLocalDescription(desc); 112 | this.onSetLocalSuccess(peerConnB); 113 | }catch(e) { 114 | 115 | } 116 | 117 | try{ 118 | await peerConnA.setRemoteDescription(desc); 119 | this.onSetRemoteSuccess(peerConnA); 120 | }catch(e) { 121 | 122 | } 123 | 124 | try{ 125 | const answer = await peerConnB.createAnswer(); 126 | this.onCreateAnswerSuccess(answer); 127 | }catch(e) { 128 | 129 | } 130 | } 131 | 132 | onIceStateChangeA = async (event) => { 133 | try{ 134 | if (event.candidate) { 135 | await peerConnB.addIceCandidate(event.candidate); 136 | this.onAddIceCandidateSuccess(peerConnB); 137 | } 138 | } catch (e) { 139 | 140 | } 141 | } 142 | onIceStateChangeB = async (event) => { 143 | try{ 144 | if (event.candidate) { 145 | await peerConnA.addIceCandidate(event.candidate); 146 | this.onAddIceCandidateSuccess(peerConnA); 147 | } 148 | } catch (e) { 149 | 150 | } 151 | } 152 | 153 | onSetLocalSuccess = (pc) => { 154 | 155 | } 156 | onSetRemoteSuccess = (pc) => { 157 | 158 | } 159 | 160 | onIceCandidateA = async (event) => { 161 | try{ 162 | if (event.candidate) { 163 | await peerConnB.addIceCandidate(event.candidate); 164 | this.onAddIceCandidateSuccess(peerConnB); 165 | } 166 | } catch(e) { 167 | 168 | } 169 | } 170 | onIceCandidateB = async (event) => { 171 | try{ 172 | if (event.candidate) { 173 | await peerConnA.addIceCandidate(event.candidate); 174 | this.onAddIceCandidateSuccess(peerConnA); 175 | } 176 | } catch(e) { 177 | 178 | } 179 | } 180 | onAddIceCandidateSuccess = (pc) => { 181 | 182 | } 183 | 184 | gotRemoteStream = (e) => { 185 | if (remoteVideo.srcObject !== e.stream[0]) { 186 | remoteVideo.srcObject = e.stream[0]; 187 | } 188 | } 189 | 190 | handleError = (err) => { 191 | if (err === 'ConstraintNotSatisfiedError') { 192 | const v = constraints.video; 193 | message.error(`宽:${ v.width.exact } 高:${ v.height.exact } 设备不支持`); 194 | } else if (err.name === 'PermissionDeniedError') { 195 | message.error('没有摄像头和麦克风的使用权限'); 196 | } 197 | message.error('getUserMedia错误', err); 198 | } 199 | 200 | hangup = () => { 201 | peerConnA.close(); 202 | peerConnB.close(); 203 | peerConnA = null; 204 | peerConnB = null; 205 | } 206 | 207 | render() { 208 | 209 | return( 210 |
211 | 212 | 213 | 216 | 217 | 218 |
219 | ) 220 | } 221 | } -------------------------------------------------------------------------------- /src/pages/candidate/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | import { 5 | Table, 6 | Tabs , 7 | Space, 8 | message, 9 | } from 'antd'; 10 | import { 11 | FileSearchOutlined, 12 | } from '@ant-design/icons'; 13 | import copy from 'copy-to-clipboard'; 14 | 15 | import 'style/candidate/candidateExam.css'; 16 | import { showPaper } from 'api/modules/paper'; 17 | import { getCookie, handleTime, nowTime, transTime, } from 'common/utils'; 18 | import { PAPER_STATUS, CANDIDATE_SHOW_TESTS, INTERVIEW_STATUS, } from 'common/const'; 19 | import { GET_PROGRAM_EXAM } from 'src/useRedux/constant'; 20 | import { findInterview } from 'src/api/modules/interview'; 21 | 22 | interface Prop { 23 | changeEmail; 24 | programExam; 25 | } 26 | 27 | interface State { 28 | 29 | } 30 | 31 | class Candidate extends React.Component { 32 | 33 | state = { 34 | allExam: [] = [], 35 | nodoExam: [] = [], 36 | doingExam: [] = [], 37 | doneExam: [] = [], 38 | interviewInform: [] = [], 39 | }; 40 | timer: NodeJS.Timer; 41 | 42 | componentDidMount() { 43 | this.findInterviewInform(); 44 | this.reqExam(); 45 | this.countdown(); 46 | } 47 | 48 | findInterviewInform = async () => { 49 | const cookie = getCookie(); 50 | const res = await findInterview({ cookie, isInterviewer: false }); 51 | res.data.ret.map(item => { 52 | item['interview_begin_time'] = transTime(+item['interview_begin_time']); 53 | item['interview_status'] = nowTime() >= item['interview_status'] ? INTERVIEW_STATUS.ING : INTERVIEW_STATUS.ON; 54 | }) 55 | this.setState({ interviewInform: res.data.ret }); 56 | } 57 | 58 | reqExam = () => { 59 | const cookie = getCookie(); 60 | showPaper({ cookie: cookie }).then(ret => { 61 | const allArr = handleTime(ret.data.show); 62 | let nodoArr: any[] = [], doingArr: any[] = [], doneArr: any[] = []; 63 | 64 | allArr.map(item => { 65 | if (item['remaining_time'] === PAPER_STATUS.NODO) { 66 | nodoArr.push(item); 67 | } else if (item['remaining_time'] === PAPER_STATUS.DONE || item['remaining_time'] === PAPER_STATUS.OVER) { 68 | doneArr.push(item); 69 | } else { 70 | doingArr.push(item); 71 | } 72 | }) 73 | 74 | this.setState({ 75 | allExam: allArr, 76 | nodoExam: nodoArr, 77 | doingExam: doingArr, 78 | doneExam: doneArr 79 | }); 80 | }); 81 | this.timer = setTimeout(() => { this.countdown() }, 1000); 82 | } 83 | 84 | countdown = () => { 85 | 86 | } 87 | 88 | componentWillUnmount() { 89 | clearTimeout(this.timer); 90 | } 91 | 92 | 93 | render() { 94 | const { allExam, nodoExam, doingExam, doneExam, interviewInform } = this.state; 95 | const { changeEmail } = this.props; 96 | const columns = [ 97 | { title: '试卷', dataIndex: 'paper', key: 'paper' }, 98 | { title: '试卷描述', dataIndex: 'paper_description', key: 'paper_description' }, 99 | { title: '开始时间', dataIndex: 'time_begin', key: 'time_begin' }, 100 | { title: '截止时间', dataIndex: 'time_end', key: 'time_end' }, 101 | { title: '剩余时间', dataIndex: 'remaining_time', key: 'remaining_time' }, 102 | { title: '作答时长', dataIndex: 'answer_time', key: 'answer_time' }, 103 | { 104 | title: '试题数量', 105 | dataIndex: 'tests_num', 106 | key: 'tests_num', 107 | sorter: (a: any, b: any) => a.tests_num - b.tests_num, 108 | }, 109 | { 110 | title: '试卷总分数', 111 | dataIndex: 'paper_point', 112 | key: 'paper_point', 113 | sorter: (a: any, b: any) => a.paper_point - b.paper_point, 114 | }, 115 | { 116 | title: '操作', 117 | dataIndex: 'action', 118 | key: 'action', 119 | render: (text: any, record: { paper: string; }) => { 120 | return( 121 | 122 | changeEmail(record.paper) } to={ `${ CANDIDATE_SHOW_TESTS }?paper=${ record.paper }` }> 查看试卷 123 | 124 | ) 125 | } 126 | } 127 | ] 128 | const interviewColumns = [ 129 | { title: '面试官', dataIndex: 'interviewer', key: 'interviewer' }, 130 | { title: '面试房间号', dataIndex: 'interview_room', key: 'interview_room' }, 131 | { title: '面试开始时间', dataIndex: 'interview_begin_time', key: 'interview_begin_time' }, 132 | { 133 | title: '面试链接', 134 | dataIndex: 'candidate_link', 135 | key: 'candidate_link', 136 | render: (candidate_link: any) => { 137 | return( 138 | 146 | ) 147 | } 148 | }, 149 | { title: '状态', dataIndex: 'interview_status', key: 'interview_status' }, 150 | { 151 | title: '操作', 152 | dataIndex: 'action', 153 | key: 'action', 154 | render: (text: any, record: { paper: string; }) => { 155 | return( 156 | 157 | 去面试 158 | 159 | ) 160 | } 161 | } 162 | ] 163 | 164 | return( 165 |
166 | 167 | 168 |
173 | 174 | 175 |
180 | 181 | 182 |
187 | 188 | 189 |
194 | 195 | 196 |
201 | 202 | 203 | 204 | ) 205 | } 206 | } 207 | 208 | function mapStateToProps(state: any) { 209 | return{ 210 | programExam: state.programExam 211 | } 212 | } 213 | function mapDispatchToProps(dispatch: any, ownProps: any) { 214 | return{ 215 | changeEmail: (exam: string) => { 216 | dispatch({ 217 | type: GET_PROGRAM_EXAM, 218 | programExam: exam 219 | }); 220 | } 221 | } 222 | } 223 | const CandidateContainer = connect(mapStateToProps, mapDispatchToProps)(Candidate) 224 | export default CandidateContainer; -------------------------------------------------------------------------------- /src/common/components/interviewer/paper.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Form, 4 | Input, 5 | DatePicker, 6 | Radio, 7 | Select, 8 | Dropdown, 9 | Menu, 10 | Button, 11 | Space, 12 | } from 'antd'; 13 | import moment from 'moment'; 14 | import 'moment/locale/zh-cn'; 15 | import locale from 'antd/es/date-picker/locale/zh_CN'; 16 | 17 | import { search } from 'api/modules/candidate'; 18 | import { searchEmail } from 'api/modules/user'; 19 | import { getHour, getMinute } from 'common/utils'; 20 | 21 | interface get{ 22 | getHour?: any, 23 | getMinute?: any, 24 | } 25 | 26 | export default class Paper extends React.Component { 27 | 28 | state = { 29 | candidateEmail: [] = [], 30 | setTime: false, 31 | hour: 0, 32 | minute: 0, 33 | } 34 | 35 | // 点击“邀请候选人”之后发送请求的函数 36 | onFocus = async () => { 37 | const res = await searchEmail({ interviewer: false }); 38 | const getInform = res.data.ret; 39 | const arr: any[] = []; 40 | getInform.map((item: { email: string; }) => { 41 | // 数组去重 42 | if (arr.indexOf(item.email) === -1) { 43 | arr.push(item.email); 44 | } 45 | }) 46 | this.setState({ candidateEmail: arr }); 47 | } 48 | 49 | // 点击“作答时长”后的“选择时间”按钮调用的函数 50 | handleTime = () => { 51 | this.state.setTime === false ? this.setState({ setTime: true }) : this.setState({ setTime: false }); 52 | } 53 | 54 | // 选中下拉菜单的值后调用函数,并传递对应值给父组件 55 | getHours = (e: any) => { 56 | this.setState({ hour: e.key }); 57 | this.props.getHour(e.key); 58 | } 59 | getMinutes = (e: any) => { 60 | this.setState({ minute: e.key }); 61 | this.props.getMinute(e.key); 62 | } 63 | 64 | 65 | // 不可选日期,限制“试卷截止日期”的选择范围 66 | range = (start: any, end: any) => { 67 | const result = []; 68 | for (let i = start; i < end; i += 1) { 69 | result.push(i); 70 | } 71 | return result; 72 | }; 73 | disabledDate = (current: any) => { 74 | return current && current < moment().endOf('day'); 75 | } 76 | disabledDateTime = () => { 77 | return { 78 | disabledHours: () => this.range(0, 24).splice(4, 20), 79 | disabledMinutes: () => this.range(30, 60), 80 | disabledSeconds: () => [55, 56], 81 | }; 82 | } 83 | 84 | render() { 85 | const { candidateEmail, setTime, } = this.state; 86 | const hours = getHour(), minutes = getMinute(); 87 | const hour = ( 88 | 89 | {hours.map(item => { 90 | return( 91 | { item } 92 | ) 93 | })} 94 | 95 | ) 96 | const minute = ( 97 | 98 | {minutes.map(item => { 99 | return( 100 | { item } 101 | ) 102 | })} 103 | 104 | ) 105 | 106 | return( 107 | <> 108 | 117 | 118 | 119 | 120 | 126 | 127 | 128 | 129 | 135 | 141 | 142 | 143 | 149 | 157 | 158 | 159 | 165 | 166 | 30分钟 167 | 45分钟 168 | 1小时 169 | 1小时30分钟 170 | 2小时 171 | 2小时30分钟 172 | {/* 手动设置 */} 173 | 174 | {/* { 175 | setTime === false ? 176 | 177 | 30分钟 178 | 45分钟 179 | 1小时 180 | 1小时30分钟 181 | 2小时 182 | 2小时30分钟 183 | 手动设置 184 | : 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | } */} 195 | 196 | 197 | 198 | 203 | 221 | 222 | 223 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | ) 235 | } 236 | } -------------------------------------------------------------------------------- /src/common/components/candidate/programInform.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { List, Tabs, Tooltip, Comment, Avatar } from 'antd'; 3 | import { 4 | ClockCircleOutlined, 5 | ProfileOutlined, 6 | CommentOutlined, 7 | ExperimentOutlined, 8 | LikeOutlined, 9 | DislikeOutlined, 10 | DislikeFilled, 11 | LikeFilled, 12 | VerticalAlignTopOutlined, 13 | } from '@ant-design/icons'; 14 | import moment from 'moment'; 15 | 16 | // import 'style/code.css'; 17 | // import 'style/program.less' 18 | import { getCookie, getExamLevel, getUrlParam } from 'common/utils'; 19 | import { showTest } from 'api/modules/test'; 20 | import { comment } from 'api/modules/candidate'; 21 | import { TEST_LEVEL } from 'common/const'; 22 | import Wangeditor from 'common/components/interviewer/wangeditor'; 23 | import MarkdownEditor from 'common/components/candidate/markdownEditor'; 24 | 25 | const cookie: string = getCookie(); 26 | 27 | export default class ProgramInform extends React.Component { 28 | authorRef = React.createRef(); 29 | 30 | state = { 31 | testInform: {}, 32 | likes: 0, // 赞的数量 33 | dislikes: 0, // 踩的数量 34 | replys: 0, // 单条评论的回复数量 35 | checkLike: '', // 标识用户点击的是“赞”还是“踩” 36 | checkReply: false // 标识用户是否点击了“查看评论” 37 | } 38 | callback: (activeKey: string) => void; 39 | 40 | componentDidMount() { 41 | const url = getUrlParam('test'); 42 | showTest({ test: url }).then(res => { 43 | this.setState({ testInform: res.data.show }) 44 | }) 45 | comment().then(res => { 46 | 47 | }) 48 | } 49 | 50 | // 点击“评论”操作列表中的“赞”时的函数 51 | like = () => { 52 | // console.log(this.authorRef.current.focus()) 53 | 54 | // const { likes } = this.state; 55 | // comment({ like_num: likes + 1, cookie, status: true }).then(res => { 56 | // this.setState({ likes: res.like_num, checkLike: 'like' }); 57 | // }) 58 | } 59 | // 点击“评论”操作列表中的“踩”时的函数 60 | dislike = () => { 61 | const { dislikes } = this.state; 62 | comment({ dislike_num: dislikes + 1, cookie, status: true }).then(res => { 63 | this.setState({ dislikes: res.dislike_num, checkLike: 'dislike'}); 64 | }) 65 | } 66 | // 点击“评论”操作列表中的“查看评论”时的函数 67 | checkReply = () => { 68 | const { checkReply } = this.state; 69 | checkReply === false ? this.setState({ checkReply: true }) : this.setState({ checkReply: false }); 70 | } 71 | // 点击“评论”操作列表中的“回复”时的函数 72 | reply = () => { 73 | // comment({ comments, cookie, status: true }).then(res => { 74 | 75 | // }) 76 | } 77 | // 创建评论 78 | createReply = () => { 79 | 80 | } 81 | 82 | render() { 83 | const { testInform, likes, dislikes, checkLike, replys, checkReply, } = this.state; 84 | const data = [ 85 | { 86 | actions: [ 87 | 88 | 89 | { checkLike === 'like' ? : } 90 | { likes === 0 ? '赞' : likes } 91 | 92 | , 93 | 94 | 95 | { checkLike === 'dislike' ? : } 96 | { dislikes === 0 ? '踩' : dislikes } 97 | 98 | , 99 | 100 | 101 | 102 | 103 | { checkReply === false ? `查看 ${ replys } 条回复` : "收起回复" } 104 | 105 | 106 | , 107 | 108 | 109 | 110 | 回复 111 | 112 | , 113 | ], 114 | author: ( 115 | 1164939253@qq.com 116 | ), 117 | avatar: 'https://joeschmoe.io/api/v1/random', 118 | content: ( 119 |

120 | We supply a series of design principles, practical patterns and high quality design 121 | resources (Sketch and Axure), to help people create their product prototypes beautifully and 122 | efficiently. 123 |

124 | ), 125 | datetime: ( 126 | 127 | {moment().subtract(2, 'days').fromNow()} 128 | 129 | ), 130 | } 131 | ]; 132 | 133 | return( 134 | <> 135 | 136 | 题目描述 } 138 | key='test' 139 | > 140 |
141 |

{ testInform['num'] }. { testInform['test_name'] }

142 |
143 | 难度:{ testInform['level'] } 144 |
145 |
146 |
147 | 148 |
149 |
150 | 151 | 评论 } 153 | key='comments' 154 | > 155 | {/* */} 156 | ( 162 |
  • 163 | 170 | { 171 | checkReply === false ? null : 172 | Reply to]} 174 | author={Han Solo} 175 | avatar={} 176 | content={ 177 |

    178 | We supply a series of design principles, practical patterns and high quality design 179 | resources (Sketch and Axure). 180 |

    181 | } 182 | > 183 |
    184 | } 185 |
    186 |
  • 187 | )} 188 | /> 189 |
    190 | 191 | 题解 } 193 | key='solution' 194 | > 195 | 196 | 197 | 198 | 提交记录 } 200 | key='submissions' 201 | > 202 | 203 | 204 |
    205 | 206 | ) 207 | } 208 | } -------------------------------------------------------------------------------- /src/pages/interviewer/consult/examInform.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { connect } from 'react-redux' 4 | import { 5 | Button, 6 | Descriptions, 7 | Layout, 8 | Space, 9 | Table, 10 | Tabs, 11 | Tag, 12 | Divider, 13 | } from 'antd'; 14 | import { 15 | CheckCircleOutlined, 16 | ClockCircleOutlined, 17 | EditOutlined, 18 | DeleteOutlined, 19 | } from '@ant-design/icons'; 20 | 21 | import 'style/interviewer/examInform.css'; 22 | import Navbar from 'common/components/navbar'; 23 | import TestAlone from 'src/common/components/candidate/testAlone'; 24 | import { LOOK_OVER, TEST_STATUS } from 'common/const'; 25 | import { getCookie, getUrlParam, transTime } from 'common/utils'; 26 | import { search } from 'api/modules/candidate'; 27 | import { showTest } from 'api/modules/test'; 28 | import { lookOver, showPaper } from 'api/modules/paper'; 29 | import { GET_EMAIL } from 'src/useRedux/constant'; 30 | 31 | interface Prop { 32 | lookExam: string; 33 | lookEmail: string; 34 | changeEmail: any; 35 | dispatch: any; 36 | } 37 | 38 | class ExamInform extends React.Component { 39 | 40 | state = { 41 | tableArr: [] = [], 42 | examContent: [] = [], 43 | examInform: { 44 | candidate: [] = [], 45 | answer_time: '', 46 | check: false, 47 | interviewer: '', 48 | time_begin: 0, 49 | time_end: 0, 50 | }, 51 | } 52 | 53 | componentDidMount() { 54 | const cookie = getCookie(), paper = this.props.lookExam; 55 | lookOver({ cookie, paper }).then(item => { 56 | this.setState({ tableArr: item.data.ret }); 57 | }) 58 | showTest({ paper }).then(item => { 59 | this.setState({ examContent: item.data.show }); 60 | }) 61 | showPaper({ paper }).then(item => { 62 | this.setState({ examInform: item.data }); 63 | }) 64 | } 65 | 66 | render() { 67 | const { tableArr, examContent, examInform, } = this.state; 68 | const candidateColumns = [ 69 | { title: '作答情况', dataIndex: 'test_status', key: 'test_status' }, 70 | { title: '候选人', dataIndex: 'email', key: 'email' }, 71 | { 72 | title: '总分', 73 | dataIndex: 'total_score', 74 | key: 'total_score', 75 | sorter: (a: { total_score: number; }, b: { total_score: number; }) => a.total_score - b.total_score, 76 | }, 77 | { 78 | title: '排名', 79 | dataIndex: 'rank', 80 | key: 'rank', 81 | sorter: (a: { rank: number; }, b: { rank: number; }) => a.rank - b.rank, 82 | }, 83 | { 84 | title: '用时', 85 | dataIndex: 'use_time', 86 | key: 'use_time', 87 | sorter: (a: { use_time: number; }, b: { use_time: number; }) => a.use_time - b.use_time, 88 | }, 89 | { 90 | title: '操作', 91 | dataIndex: 'action', 92 | key: 'action', 93 | render: (text: any, record: any) => { 94 | return( 95 | 前往批阅 96 | ) 97 | } 98 | } 99 | ] 100 | const testColumns = [ 101 | { title: '试题号', dataIndex: 'num', key: 'num' }, 102 | { title: '试题名', dataIndex: 'test_name', key: 'test_name' }, 103 | { 104 | title: '标签', 105 | dataIndex: 'tags', 106 | key: 'tags', 107 | render: (tags: [string]) => ( 108 | 109 | {tags.map(tag => { 110 | let color = tag.length > 2 ? 'geekblue' : 'green'; 111 | if (tag === 'loser') { 112 | color = 'volcano'; 113 | } 114 | return ( 115 | 116 | {tag} 117 | 118 | ); 119 | })} 120 | 121 | ) 122 | }, 123 | { title: '难易度', dataIndex: 'level', key: 'level' }, 124 | { title: '分数', dataIndex: 'point', key: 'point' }, 125 | ]; 126 | const timeBegin = transTime(+examInform.time_begin); 127 | const timeEnd = transTime(+examInform.time_end); 128 | 129 | return( 130 |
    131 | 132 | 133 | 134 |
    135 |
    136 |

    { this.props.lookExam }

    137 |
    138 | 结束时间:{ timeEnd } 139 |
    140 |
    141 |
    142 | 143 | 144 |  未参加  145 | 146 | 147 | 148 | 149 |  进行中  150 | 151 | 152 | 153 | 154 |  已结束  155 | 156 | 157 |
    158 |
    159 | 160 | 161 | 162 | 试卷每道题的作答情况,比如正确率--用折线图、柱形图表示 163 | 164 | 165 | 166 |
    { 171 | return { 172 | onClick: () => { this.props.changeEmail(record['email']); }, 173 | }; 174 | }} 175 | /> 176 | 177 | 178 | 179 |

    , 185 | rowExpandable: () => true, 186 | }} 187 | /> 188 | 189 | 190 | 191 | 192 | { timeBegin } 193 | { timeEnd } 194 | { examInform.answer_time } 195 | { examInform.candidate.length } 196 | { examInform.interviewer } 197 | { examInform.check === true ? '可' : '否' } 198 | 199 | 200 | 201 | 202 | 203 | 204 | ) 205 | } 206 | } 207 | 208 | function mapStateToProps(state: any) { 209 | return{ 210 | lookExam: state.lookExam, 211 | lookEmail: state.lookEmail 212 | } 213 | } 214 | function mapDispatchToProps(dispatch: any, ownProps: any) { 215 | return{ 216 | changeEmail: (email: string) => { 217 | dispatch({ 218 | type: GET_EMAIL, 219 | lookEmail: email 220 | }); 221 | } 222 | } 223 | } 224 | export default connect(mapStateToProps, mapDispatchToProps)(ExamInform); -------------------------------------------------------------------------------- /src/pages/interviewer/interview/manage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { 4 | Col, 5 | Table, 6 | Tag, 7 | Button, 8 | Form, 9 | Select, 10 | Modal, 11 | FormInstance, 12 | DatePicker, 13 | message, 14 | Popconfirm, 15 | } from 'antd'; 16 | import { 17 | FileSearchOutlined, 18 | LinkOutlined, 19 | DeleteOutlined, 20 | PlusOutlined 21 | } from '@ant-design/icons'; 22 | import moment from 'moment'; 23 | import 'moment/locale/zh-cn'; 24 | import locale from 'antd/es/date-picker/locale/zh_CN'; 25 | import copy from 'copy-to-clipboard'; 26 | 27 | import 'style/interviewer/interviewManage.less'; 28 | import Navbar from 'common/components/navbar'; 29 | import { createInterview, deleteInterview, findInterview } from 'api/modules/interview'; 30 | import { searchEmail } from 'api/modules/user'; 31 | import { findEmail, getCookie, transTime } from 'src/common/utils'; 32 | 33 | interface Prop { 34 | interviewer: string[]; 35 | candidate: string; 36 | interview_begin: string; 37 | interviewer_room: number; 38 | interviewer_link: string; 39 | candidate_link: string; 40 | } 41 | 42 | interface informObj { 43 | 44 | } 45 | interface State { 46 | visible: boolean; 47 | value: boolean; 48 | interviewerArr: string[]; 49 | candidateArr: string[]; 50 | informArr: informObj[]; 51 | selectedRowKeys: string[]; 52 | } 53 | 54 | export default class interviewManage extends React.Component { 55 | 56 | formRef = React.createRef(); 57 | 58 | state = { 59 | visible: false, 60 | value: true, 61 | interviewerArr: [], 62 | candidateArr: [], 63 | informArr: [], 64 | selectedRowKeys: [] = [], 65 | } 66 | 67 | componentDidMount() { 68 | this.renderInform(); 69 | } 70 | 71 | // 渲染表格信息 72 | renderInform = () => { 73 | const cookie = getCookie(); 74 | findInterview({ cookie, isInterviewer: true }).then(result => { 75 | const arr = result.data.ret.slice(); 76 | arr.map((item: { interview_begin_time: string; key: any; interview_room: any; }) => { 77 | const time = transTime(+item.interview_begin_time); 78 | item.key = item.interview_room; 79 | item.interview_begin_time = time; 80 | }) 81 | this.setState({ informArr: arr }); 82 | }) 83 | } 84 | 85 | // 创建面试间,即打开对话框时请求所有面试官和候选人的邮箱 86 | showModal = async () => { 87 | const res = await findEmail(); 88 | const { allCandidate, allInterview } = res; 89 | this.setState({ visible: true, candidateArr: allCandidate, interviewerArr: allInterview }); 90 | } 91 | // 点击“创建”时的回调函数 92 | handelModal = () => { 93 | const value = this.formRef.current.getFieldsValue(); 94 | value.linkPath = window.location.origin; 95 | createInterview({ inform: value }).then(res => { 96 | // if (res.data.status === true) { 97 | message.success(res.msg); 98 | this.setState({ visible: false }); 99 | this.renderInform(); 100 | // } 101 | }) 102 | } 103 | // 关闭对话框 104 | cancelModal = () => { 105 | this.setState({ visible: false }); 106 | } 107 | 108 | // 禁止日期选择器选择当时之前的时间作为面试开始时间 109 | disabledDate = (current: any) => { 110 | return current && current < moment().endOf('day'); 111 | } 112 | 113 | // 删除面试间的按钮事件 114 | deleteRoom = async () => { 115 | const { selectedRowKeys } = this.state; 116 | if (selectedRowKeys.length > 0) { 117 | const res = await deleteInterview({ inform: selectedRowKeys }); 118 | if (res.data.status === true) { 119 | message.success(res.msg); 120 | this.setState({ informArr: res.data.findInterview }); 121 | } 122 | } 123 | }; 124 | // 表格复选框的选择情况 125 | onSelectChange = (selectedRowKeys: any) => { 126 | this.setState({ selectedRowKeys }); 127 | }; 128 | 129 | render() { 130 | const { visible, value, candidateArr, interviewerArr, informArr, selectedRowKeys } = this.state; 131 | 132 | const rowSelection = { 133 | onChange: this.onSelectChange, 134 | selectedRowKeys, 135 | }; 136 | const interviewArr = [ 137 | { title: '候选人', dataIndex: 'candidate', key: 'candidate' }, 138 | { 139 | title: '面试官', 140 | dataIndex: 'interviewer', 141 | key: 'interviewer', 142 | render: (arr: any) => ( 143 | 144 | { arr.map((item: string) => { 145 | return ( 146 |

    147 | { item } 148 | 149 | ); 150 | }) } 151 | 152 | ), 153 | }, 154 | { title: '面试时间', dataIndex: 'interview_begin_time', key: 'interview_begin_time' }, 155 | { title: '面试房间号', dataIndex: 'interview_room', key: 'interview_room' }, 156 | { 157 | title: '面试官面试房间链接', 158 | dataIdex: 'interviewer_link', 159 | key: 'interviewer_link', 160 | render: (arr: any) => ( 161 | 169 | ) 170 | }, 171 | { 172 | title: '候选人面试房间链接', 173 | dataIdex: 'candidate_link', 174 | key: 'candidate_link', 175 | render: (arr: any) => ( 176 | 184 | ) 185 | }, 186 | { title: '状态', dataIndex: 'interview_status', key: 'interview_status' }, 187 | ] 188 | 189 | return( 190 |
    191 | 192 | 193 | 199 | 206 | 207 | 208 | 216 | 217 |
    223 | 224 | 232 |
    233 | 241 | 254 | 255 | 256 | 264 | 273 | 274 | 275 | 283 | 291 | 292 | 293 |
    294 | 295 | ) 296 | } 297 | } -------------------------------------------------------------------------------- /src/common/components/interviewer/tabler.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Table, Space, Button, Tag, InputNumber, Select, Form, Input, Drawer, message, } from 'antd'; 3 | import { 4 | PlusOutlined, 5 | DeleteOutlined, 6 | EditOutlined, 7 | } from '@ant-design/icons'; 8 | import { FormInstance } from 'antd/es/form'; 9 | import PubSub from 'pubsub-js'; 10 | 11 | import { TAGS } from 'common/const'; 12 | import { getTestNum } from 'common/utils'; 13 | import Wangeditor from 'common/components/interviewer/wangeditor'; 14 | import Wangeditors from 'common/components/interviewer/wangeditor2'; 15 | 16 | interface get{ 17 | getTest?: any, 18 | pushTests?: any, 19 | } 20 | 21 | export default class Tabler extends React.Component { 22 | formRef = React.createRef(); 23 | 24 | state={ 25 | value: 0, 26 | button: true, 27 | visible: false, // 控制底下抽屉 28 | selectedRowKeys: [] = [], // 获取选中哪一个试题 29 | testInform: '', // 存储富文本内容 30 | testAnswer: '', // 存储富文本内容 31 | tableArr: [] = [], // 存储试题信息 32 | } 33 | 34 | // 订阅消息,获取 wangeditor 组件中富文本的内容 35 | token: string | PubSubJS.SubscriptionListener = null; 36 | token2: string | PubSubJS.SubscriptionListener = null; 37 | componentDidMount() { 38 | this.token = PubSub.subscribe('testInform', (_, data) => { 39 | this.setState({ testInform: data.test }); 40 | }); 41 | this.token2 = PubSub.subscribe('testAnswer', (_, data) => { 42 | this.setState({ testAnswer: data.test }); 43 | }); 44 | // 在“修改试卷”中修改试题,获取从父组件传过来的已有试题 45 | if (this.props.pushTests) { 46 | this.setState({ tableArr: this.props.pushTests }); 47 | } 48 | } 49 | componentWillUnmount() { 50 | PubSub.unsubscribe(this.token); 51 | PubSub.unsubscribe(this.token2); 52 | } 53 | 54 | // 表格复选框的选择情况 55 | onSelectChange = (selectedRowKeys: any) => { 56 | this.setState({ selectedRowKeys }); 57 | }; 58 | // 获取单选框的值,更新状态 59 | onChange = (e: any) => { 60 | this.setState({ value: e.target.value }) 61 | } 62 | // “添加试卷”的抽屉 63 | showDrawer = () => { 64 | this.setState({ visible: true }); 65 | // 添加新试卷时要将表单数据清空 66 | setTimeout(() => { 67 | this.formRef.current.resetFields(); 68 | }, 0); 69 | }; 70 | onClose = () => { 71 | this.setState({ visible: false }); 72 | }; 73 | 74 | // 从 testArr 数组中删除试题 75 | deleteTest = (values: any) => { 76 | const arr = this.state.selectedRowKeys; 77 | const ret = this.state.tableArr; 78 | if (arr.length !== 0) { 79 | for (let number of arr) { 80 | ret.forEach((item, index) => { 81 | if (item['testName'] === number) { 82 | ret.splice(index, 1); 83 | } 84 | }) 85 | } 86 | } else if (values) { 87 | ret.forEach((item, index) => { 88 | if (item['testName'] === values) { 89 | ret.splice(index, 1); 90 | } 91 | }) 92 | } 93 | this.setState({ tableArr: ret }); 94 | this.props.getTest(this.state.tableArr); 95 | } 96 | // 将添加的试题加载到 testArr 数组中,在调用接口的时候作为参数传递 97 | addTest = async (values: any) => { 98 | const { tableArr, testInform, testAnswer } = this.state; 99 | // let sign: any[] = []; 100 | let sign = null; 101 | console.log('1', this.state.tableArr) 102 | tableArr.map(item => { 103 | if (item['testName'] === values.testName) { 104 | sign = item['testName']; 105 | return; 106 | } 107 | }) 108 | if (!sign) { 109 | // if (sign.indexOf(values.testName) === -1) { 110 | // sign.push(values.testName) 111 | const obj = { 112 | key: values.testName, 113 | num: getTestNum(), 114 | testName: values.testName, 115 | description: testInform, 116 | answer: testAnswer, 117 | level: values.level, 118 | tags: values.tags, 119 | point: values.point, 120 | } 121 | this.setState({ 122 | tableArr: [...tableArr, obj], 123 | visible: false, 124 | }); 125 | console.log('2', this.state.tableArr) 126 | this.props.getTest(this.state.tableArr); 127 | } else { 128 | message.error('添加的试题名不能重复'); 129 | } 130 | } 131 | // 修改试题 132 | handleModal = (record: any) => { 133 | this.setState({ visible: true, button: false }); 134 | setTimeout(() => { 135 | // 表单重置 136 | this.formRef.current.resetFields(); 137 | // 获取 Form 的 ref 138 | const form = this.formRef.current; 139 | // 要渲染的数据 140 | this.state.tableArr.forEach(item => { 141 | if (item && item['testName'] === record) { 142 | PubSub.publish('modifyTest', { test: item['description'] }) 143 | PubSub.publish('modifyAnswer', { answer: item['answer'] }) 144 | form.setFieldsValue(item); 145 | return; 146 | } 147 | }) 148 | }) 149 | }; 150 | modifyTest = async (values: any) => { 151 | const obj = { 152 | key: values.testName, 153 | num: getTestNum(), 154 | testName: values.testName, 155 | description: this.state.testInform, 156 | answer: this.state.testAnswer, 157 | tags: values.tags, 158 | level: values.level, 159 | point: values.point, 160 | } 161 | const tableArr = [...this.state.tableArr]; 162 | this.setState({ 163 | visible: false, 164 | button: true, 165 | tableArr: tableArr.map((item) => item['testName'] === values.testName ? obj : item), 166 | }) 167 | this.props.getTest(this.state.tableArr); 168 | } 169 | 170 | 171 | render() { 172 | const { selectedRowKeys, visible, button, tableArr, } = this.state; 173 | const rowSelection = { 174 | onChange: this.onSelectChange, 175 | selectedRowKeys, 176 | }; 177 | const columns = [ 178 | { title: '试题号', dataIndex: 'num', key: 'num' }, 179 | { title: '试题名', dataIndex: 'testName', key: 'testName' }, 180 | { 181 | title: '标签', 182 | dataIndex: 'tags', 183 | key: 'tags', 184 | render: (tags: [string]) => ( 185 | 186 | {tags.map(tag => { 187 | let color = tag.length > 2 ? 'geekblue' : 'green'; 188 | if (tag === 'loser') { 189 | color = 'volcano'; 190 | } 191 | return ( 192 | 193 | {tag} 194 | 195 | ); 196 | })} 197 | 198 | ) 199 | }, 200 | { title: '难易度', dataIndex: 'level', key: 'level' }, 201 | { title: '分数', dataIndex: 'point', key: 'point' }, 202 | { 203 | title: '操作', 204 | key: 'action', 205 | render: (_: any, record: { [x: string]: string; }) => ( 206 | 207 | 213 | 219 | 220 | ), 221 | }, 222 | ]; 223 | // console.log(tableArr) 224 | 225 | return( 226 |
    227 | 236 | 237 | 246 | 247 | 257 | // {/* icon={ < />} */} 258 | // 259 | // 260 | // 263 | // 264 | // } 265 | > 266 |
    271 | 272 | 280 | 281 | 282 | 283 | 288 | 289 | 290 | 291 | 296 | 297 | 298 | 299 | 307 | 312 | 313 | 314 | 322 | 336 | 337 | 338 | 346 | 347 | 348 | 349 | 350 | 356 | 357 | 358 |
    359 | 360 |

    , 367 | rowExpandable: () => true, 368 | }} 369 | /> 370 | 371 | ) 372 | } 373 | } -------------------------------------------------------------------------------- /src/common/components/webrtc.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, message, Modal } from 'antd'; 3 | import { getCookie } from '../utils'; 4 | import { WS_TYPE } from '../const'; 5 | 6 | // 本地流和远端流 7 | let localStream: MediaStream; 8 | let remoteStream: MediaStream; 9 | 10 | // 本地和远端连接对象 11 | let localPeerConnection: RTCPeerConnection; 12 | let remotePeerConnection: RTCPeerConnection; 13 | 14 | // 本地视频和远端视频 15 | let localVideo; 16 | let remoteVideo; 17 | 18 | // 设置约束 19 | const mediaStreamConstraints = { 20 | audio: true, 21 | video: { 22 | width: 150, 23 | height: 150 24 | } 25 | } 26 | 27 | // 设置仅交换视频 28 | const offerOptions = { 29 | offerToReceiveVideo: true, 30 | offerToReceiveAudio: true 31 | } 32 | 33 | interface Prop { 34 | sendVideo; 35 | reqVideo; 36 | resVideo; 37 | resOff; 38 | resAns; 39 | resIce; 40 | } 41 | 42 | interface State { 43 | beginVideo: boolean; 44 | showVideo: boolean; 45 | } 46 | 47 | let myPeerConnection = null; 48 | const cookie = getCookie(); 49 | 50 | export default class Webrtc extends React.Component { 51 | 52 | candidates = []; 53 | 54 | state = { 55 | beginVideo: false, 56 | showVideo: false 57 | } 58 | 59 | sendToWs = (data: any) => { 60 | const { sendVideo } = this.props; 61 | sendVideo(data); 62 | } 63 | 64 | componentDidMount(): void { 65 | localVideo = document.getElementById("localVideo"); 66 | remoteVideo = document.getElementById("remoteVideo"); 67 | } 68 | 69 | // 关闭摄像头 70 | closeCamera = async () => { 71 | localStream.getTracks().forEach(function (track) { 72 | track.stop(); 73 | }); 74 | } 75 | 76 | // 1.获取本地音视频流(打开摄像头) 77 | openCamera = async () => { 78 | // .catch(handleGetUserMediaError); 79 | 80 | await navigator.mediaDevices.getUserMedia(mediaStreamConstraints) 81 | .then(function(mediaStream) { 82 | console.log('本地视频:', mediaStream) 83 | localVideo.srcObject = mediaStream; 84 | localStream = mediaStream; 85 | // myPeerConnection = mediaStream; 86 | localVideo.onloadedmetadata = function(e) { 87 | localVideo.play(); 88 | }; 89 | }) 90 | .catch((err) => { 91 | console.log('getUserMedia 错误', err); 92 | }); 93 | } 94 | 95 | // 发送视频请求,等待对方通过 96 | call = async (data: { type: any }) => { 97 | if (myPeerConnection) { 98 | alert("你正处于视频通话中,无法再打开一个视频通话!"); 99 | } else { 100 | const type = data.type === 'click' ? WS_TYPE.REQ_VIDEO : WS_TYPE.RES_VIDEO; 101 | await this.openCamera(); 102 | this.sendToWs({ showVideo: true, type }); 103 | } 104 | } 105 | 106 | // 视频通话呼叫 107 | localCall = async (data?: { type: string }) => { 108 | // const videoTracks = localStream.getVideoTracks(); 109 | // const audioTracks = localStream.getAudioTracks(); 110 | if (!localPeerConnection) { 111 | let configuration = { 112 | "iceServers": [{ 113 | // "urls": "stun:stun.stunprotocol.org" 114 | // "urls": "turn:turnserver.com" 115 | "urls": "stun:stun.l.google.com:19302" 116 | }] 117 | }; 118 | 119 | // 创建 RTCPeerConnection 对象 120 | localPeerConnection = new RTCPeerConnection(configuration); 121 | localPeerConnection.onicecandidate = event => this.handleConnection(localPeerConnection, event); 122 | localPeerConnection.oniceconnectionstatechange = event => this.handleConnectionChange(localPeerConnection, event); 123 | 124 | // 遍历本地流的所有轨道 125 | localStream.getTracks().forEach((track: any) => { 126 | localPeerConnection.addTrack(track, localStream); 127 | }); 128 | 129 | localPeerConnection.ontrack = this.gotRemoteStream; 130 | 131 | const { resVideo } = this.props; 132 | resVideo.sign = false; 133 | 134 | if (data && data.type === 'local') { 135 | this.createOffer(); 136 | } 137 | } 138 | } 139 | 140 | createOffer = async () => { 141 | // 2.交换媒体描述信息 142 | const offer = await localPeerConnection.createOffer(offerOptions) 143 | console.log('A 创建offfer成功'); 144 | // this.onCreateOfferSuccess(offer); 145 | try{ 146 | await localPeerConnection.setLocalDescription(offer); 147 | console.log('A 保存offfer成功'); 148 | this.sendToWs({ offer: localPeerConnection.localDescription, type: WS_TYPE.VIDEO_OFFER, sign: true }); 149 | console.log('A 发送offfer成功'); 150 | this.setState({ beginVideo: true }); 151 | } catch(err) { 152 | console.log('A 保存offer错误', err); 153 | } 154 | } 155 | onCreateOfferSuccess = (desc: any) => { 156 | localPeerConnection 157 | .setLocalDescription(desc) 158 | .then( 159 | () => console.log("A 保存offfer成功"), 160 | error => console.log("A 保存offer错误", error.toString()) 161 | ); 162 | remotePeerConnection 163 | .setRemoteDescription(desc) 164 | .then( 165 | async () => { 166 | console.log("B 保存offer成功"); 167 | const answer = await remotePeerConnection.createAnswer(); 168 | try{ 169 | this.onCreateAnswerSuccess(answer); 170 | } catch{(err: any) => { 171 | console.log("B 创建answer错误", err.toString()) 172 | }} 173 | }, 174 | error => console.log("B 保存offer错误", error.toString()) 175 | ); 176 | } 177 | onCreateAnswerSuccess = (desc: any) => { 178 | localPeerConnection 179 | .setRemoteDescription(desc) 180 | .then( 181 | () => console.log("A 保存answer成功" ), 182 | error => console.log("A 保存answer错误", error.toString()) 183 | ); 184 | remotePeerConnection 185 | .setLocalDescription(desc) 186 | .then( 187 | () => console.log( "B 保存answer成功" ), 188 | error => console.log("B 保存answer错误", error.toString()) 189 | ); 190 | console.log('开始视频通话呼叫了', localPeerConnection, remotePeerConnection) 191 | } 192 | handleOffer = async (msg) => { 193 | const { resOff } = this.props; 194 | resOff.sign = false; 195 | 196 | const remoteDescription = new RTCSessionDescription(msg.offer); 197 | localPeerConnection.setRemoteDescription(remoteDescription) 198 | // localPeerConnection.setRemoteDescription(msg.offer) 199 | .then(async () => { 200 | console.log('B 保存offer成功'); 201 | const answer = await localPeerConnection.createAnswer(); 202 | console.log('B 创建answer成功'); 203 | try{ 204 | await localPeerConnection.setLocalDescription(answer); 205 | // console.log('B 保存answer成功'); 206 | // this.sendToWs({ sdp: answer, type: WS_TYPE.VIDEO_ANSWER, sign: true }) 207 | this.sendToWs({ sdp: localPeerConnection.localDescription, type: WS_TYPE.VIDEO_ANSWER, sign: true }) 208 | console.log('B 发送answer成功'); 209 | // this.createdAnswer(answer); 210 | } catch(err) { 211 | console.log('B 创建answer错误', err); 212 | } 213 | // this.sendToWs({ sdp: description, type: WS_TYPE.VIDEO_OFFER }); 214 | }).catch((err: any) => { 215 | console.log('B 保存offer错误', err) 216 | }); 217 | } 218 | handleAnswer = async (msg) => { 219 | console.log('处理接收到的answer......', msg) 220 | // const { resAns } = this.props; 221 | // resAns.sign = false; 222 | msg.sign = false; 223 | const remoteDescription = new RTCSessionDescription(msg.sdp); 224 | localPeerConnection.setRemoteDescription(remoteDescription) 225 | // localPeerConnection.setRemoteDescription(msg.sdp) 226 | .then(() => { 227 | console.log('A 保存answer成功'); 228 | }).catch((err: any) => { 229 | console.log('A 保存answer错误', err); 230 | }); 231 | } 232 | 233 | // 3.端与端建立连接 234 | handleConnection = (PeerConnection: any, event: { candidate: any; }) => { 235 | if (event.candidate) { 236 | this.sendToWs({ 237 | type: WS_TYPE.NEW_ICE_CANDIDATE, 238 | candidate: event.candidate, 239 | sign: true 240 | }); 241 | } 242 | } 243 | handleIceCandidate = async (msg: any) => { 244 | // msg && this.candidates.push(msg); 245 | // if (this.setedRemoteDesc) { 246 | // this.candidates.forEach(candidate => { 247 | // localPeerConnection.addIceCandidate(new RTCIceCandidate(msg)); 248 | // }); 249 | // } 250 | 251 | const candidate = new RTCIceCandidate(msg.candidate); 252 | await localPeerConnection.addIceCandidate(candidate); 253 | console.log('可以接收addIceCandidate') 254 | const { resIce } = this.props; 255 | resIce.sign = false; 256 | } 257 | // addCandidate = (candidate) => { 258 | // // console.log('addCandidate', candidate); 259 | // candidate && this.candidates.push(candidate); 260 | // if (this.setedRemoteDesc) { 261 | // this.candidates.forEach(candidate => { 262 | // localPeerConnection.addIceCandidate(new RTCIceCandidate(candidate)); 263 | // }); 264 | // } 265 | // } 266 | gotRemoteStream = event => { 267 | console.log('设置远端视频'); 268 | if (remoteVideo.srcObject !== event.streams[0]) { 269 | console.log('远端视频放置完毕', event.streams[0]) 270 | remoteVideo['srcObject'] = event.streams[0]; 271 | } 272 | }; 273 | handleConnectionChange = (pc, event) => { 274 | console.log("ICE state:", pc.iceConnectionState); 275 | if (pc.iceConnectionState === 'disconnected') { 276 | console.log('对方已关闭连接'); 277 | localStream.getVideoTracks()[0].stop(); 278 | remoteVideo['srcObject'] = null; 279 | } 280 | }; 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | // 挂断电话 293 | hangup = () => { 294 | if (localPeerConnection) { 295 | localPeerConnection.ontrack = null; 296 | localPeerConnection.onicecandidate = null; 297 | localPeerConnection.oniceconnectionstatechange = null; 298 | 299 | if (remoteVideo['srcObject']) { 300 | remoteVideo['srcObject'].getTracks().forEach(track => track.stop()); 301 | } 302 | 303 | if (localVideo['srcObject']) { 304 | localVideo['srcObject'].getTracks().forEach(track => track.stop()); 305 | } 306 | 307 | localPeerConnection.close(); 308 | localPeerConnection = null; 309 | } 310 | 311 | localVideo.removeAttribute("srcObject"); 312 | remoteVideo.removeAttribute("srcObject"); 313 | 314 | this.setState({ beginVideo: false }); 315 | } 316 | 317 | // 接受通话,告知对端建立连接 318 | videok = async () => { 319 | let { reqVideo } = this.props; 320 | reqVideo.showVideo = false; 321 | await this.openCamera(); 322 | this.sendToWs({ canVideo: true, type: WS_TYPE.RES_VIDEO, sign: true }); 323 | await this.localCall(); 324 | } 325 | // 拒绝通话,告知对端断开连接 326 | handleCancel = () => { 327 | let { reqVideo } = this.props; 328 | reqVideo.showVideo = false; 329 | this.sendToWs({ canVideo: false, type: WS_TYPE.RES_VIDEO }); 330 | this.hangup(); 331 | message.success("你已拒绝视频通话"); 332 | } 333 | judgeShowVideo = () => { 334 | const { reqVideo } = this.props; 335 | const cookie = getCookie(); 336 | const showVideo = reqVideo.showVideo === true && reqVideo.id !== cookie ? true : false; 337 | return showVideo; 338 | } 339 | 340 | handleRefuse = () => { 341 | message.error("对方拒绝了你的视频通话"); 342 | this.hangup(); 343 | } 344 | 345 | render() { 346 | const { beginVideo, } = this.state; 347 | const { reqVideo, resVideo, resOff, resAns, resIce } = this.props; 348 | // console.log('五个属性:', reqVideo, resVideo, resOff, resAns, resIce); 349 | // console.log('两个全局变量:', localPeerConnection); 350 | !resVideo.type ? null : resVideo.canVideo === true && resVideo.id !== cookie && resVideo.sign === true ? this.localCall({ type: 'local' }) : resVideo.canVideo === false && resVideo.id !== cookie ? this.handleRefuse() : null; 351 | !resOff.type ? null : resOff.id !== cookie && resOff.sign === true ? this.handleOffer(resOff) : null; 352 | !resAns.type ? null : resAns.id !== cookie && resAns.sign === true ? this.handleAnswer(resAns) : null; 353 | !resIce.type ? null : resIce.id !== cookie && resIce.sign === true ? this.handleIceCandidate(resIce) : null; 354 | 355 | return( 356 |

    357 | 358 | 359 | { 360 | beginVideo === false ? 361 |
    362 | 363 |
    : 364 |
    365 | 366 | 367 |
    368 | } 369 | 377 |

    你收到一个来自{ reqVideo.identity }视频通话,是否接通?

    378 |
    379 |
    380 | ) 381 | } 382 | } 383 | 384 | // 综合训练 385 | // 85、 109、 157、 181 -------------------------------------------------------------------------------- /src/pages/interviewer/interview/room.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { Button, Tabs, Space, notification, Radio, Form, Input, Alert, message, Modal, } from 'antd'; 4 | 5 | import 'style/interviewer/interviewRoom.css'; 6 | import CodeEditor from 'common/components/candidate/codeEditor'; 7 | import ShowTest from 'pages/interviewer/interview/showTest'; 8 | import { testObj } from 'common/types'; 9 | import { showTest } from 'api/modules/test'; 10 | import { submitInterview } from 'api/modules/interview'; 11 | import { getCookie, nowTime } from 'common/utils'; 12 | import { searchEmail } from 'api/modules/user'; 13 | import Socket from 'common/components/Socket'; 14 | import Webrtc from 'common/components/Webrtc'; 15 | import { WS_TYPE } from 'src/common/const'; 16 | 17 | interface Prop { 18 | 19 | } 20 | 21 | interface websocketTalkMsg { 22 | time: string; 23 | identity: string; 24 | msg: string; 25 | name: string; 26 | } 27 | interface websocketCodeMsg { 28 | code: string; 29 | cookie: string; 30 | } 31 | interface showTestObj { 32 | test_name: string; 33 | language: string; 34 | test: string; 35 | } 36 | // interface showTestObj { 37 | // num: string; 38 | // test_name: string; 39 | // test: string; 40 | // level: string; 41 | // point: number; 42 | // tags: Array; 43 | // } 44 | interface videobj { 45 | candidate: string; 46 | id: string; 47 | type: WS_TYPE; 48 | } 49 | interface reqVideobj { 50 | showVideo: boolean; 51 | id: string; 52 | identity: string; 53 | type: WS_TYPE; 54 | sign: boolean; 55 | } 56 | interface resVideobj { 57 | canVideo: boolean; 58 | id: string; 59 | identity: string; 60 | type: WS_TYPE; 61 | sign: boolean; 62 | } 63 | interface resOffObj { 64 | offer: any; 65 | id: string; 66 | identity: string; 67 | type: WS_TYPE; 68 | sign: boolean; 69 | } 70 | interface resAnsObj { 71 | sdp: any; 72 | id: string; 73 | identity: string; 74 | type: WS_TYPE; 75 | sign: boolean; 76 | } 77 | interface resIceObj { 78 | candidate: any; 79 | id: string; 80 | identity: string; 81 | type: WS_TYPE; 82 | sign: boolean; 83 | } 84 | interface State { 85 | talk: websocketTalkMsg[]; 86 | codeObj: websocketCodeMsg; 87 | showInterview: boolean; 88 | showTestSwitch: boolean; 89 | choiceTestSwitch: boolean; 90 | reqVideo: reqVideobj; 91 | resVideo: resVideobj; 92 | resOff: resOffObj; 93 | resAns: resAnsObj; 94 | resIce: resIceObj; 95 | // getVideo: videobj; 96 | showTest: showTestObj; 97 | allTest: testObj[]; 98 | } 99 | 100 | const cookie = getCookie(); 101 | 102 | export default class InterviewRoom extends React.Component { 103 | 104 | socket: Socket = null; 105 | identity: string = null; 106 | 107 | state = { 108 | talk: [], 109 | codeObj: { code: '', cookie: '' }, 110 | showInterview: false, 111 | showTestSwitch: false, 112 | choiceTestSwitch: false, 113 | reqVideo: { showVideo: false, id: '', type: null, identity: '', sign: false }, 114 | resVideo: { canVideo: false, id: '', type: null, identity: '', sign: false }, 115 | resOff: { offer: null, id: '', type: null, identity: '', sign: false }, 116 | resAns: { sdp: null, id: '', type: null, identity: '', sign: false }, 117 | resIce: { candidate: null, id: '', type: null, identity: '', sign: false }, 118 | showTest: { test_name: '', test: '', language: '', sign: false}, 119 | allTest: [], 120 | } 121 | 122 | async componentDidMount() { 123 | // 查看当前用户的身份,是面试官还是候选人 124 | await searchEmail({ cookie }).then(res => { 125 | this.identity = res.data.identity; 126 | }) 127 | 128 | let talkArr = []; 129 | 130 | this.openNotificationWithIcon('success'); 131 | this.socket = new Socket({ 132 | socketUrl: 'ws://120.79.193.126:9090', 133 | identity: this.identity, 134 | openMsg: { cookie, identity: this.identity, type: WS_TYPE.CONNECT }, 135 | retMsg: (receive: any) => { 136 | const type = receive instanceof Array ? receive[0].type : receive.type; 137 | switch (type) { 138 | case WS_TYPE.CONNECT: 139 | case WS_TYPE.TALK: 140 | // const { talk } = this.state; 141 | // const arr = [...talk, receive]; 142 | // this.setState({ talk: arr }); 143 | talkArr.push(receive); 144 | this.setState({ talk: talkArr }); 145 | break; 146 | case WS_TYPE.CODE: 147 | this.setState({ codeObj: receive[0] }); 148 | break; 149 | case WS_TYPE.REQ_VIDEO: 150 | this.setState({ reqVideo: receive }); 151 | break; 152 | case WS_TYPE.RES_VIDEO: 153 | this.setState({ resVideo: receive }); 154 | break; 155 | case WS_TYPE.VIDEO_OFFER: 156 | this.setState({ resOff: receive }); 157 | case WS_TYPE.VIDEO_ANSWER: 158 | this.setState({ resAns: receive }); 159 | break; 160 | case WS_TYPE.NEW_ICE_CANDIDATE: 161 | this.setState({ resIce: receive }); 162 | break; 163 | case WS_TYPE.HANG_UP: 164 | break; 165 | default: 166 | return; 167 | } 168 | // if (receive instanceof Array && Object.keys(receive[0]).filter(item => item==='code').length !== 0) { 169 | // this.setState({ codeObj: receive[0] }); 170 | // } else if (receive.showVideo) { 171 | // this.setState({ reqVideo: receive }); 172 | // } else if (receive.canVideo) { 173 | // this.setState({ resVideo: receive }); 174 | // } else { 175 | // this.setState({ talk: receive }); 176 | // } 177 | } 178 | }); 179 | 180 | try { 181 | this.socket.connection(); 182 | } catch(e) { 183 | message.error(e); 184 | } 185 | } 186 | 187 | // 发送 websocket 聊天消息 188 | sendChat = (msg: any) => { 189 | msg.id = cookie; 190 | msg.identity = this.identity; 191 | msg.type = WS_TYPE.TALK; 192 | this.socket.sendMessage(msg); 193 | } 194 | // 编辑代码时发送 websocket 请求 195 | sendCode = (msg: any) => { 196 | msg.type = WS_TYPE.CODE; 197 | this.socket.sendMessage(msg); 198 | } 199 | // 发送视频请求 200 | sendVideo = (msg: any) => { 201 | msg.id = cookie; 202 | msg.identity = this.identity; 203 | this.socket.sendMessage(msg); 204 | } 205 | 206 | // 弹出 antd 提醒框 207 | openNotificationWithIcon = (type: string) => { 208 | notification[type]({ 209 | message: ' 号房间', 210 | description: 211 | '您已进入面试间,即将开始面试!' 212 | }); 213 | }; 214 | 215 | // 面试官自己编写试题 216 | addTest = () => { 217 | 218 | } 219 | // 面试官从已有题库中挑选试题 220 | choiceTest = () => { 221 | const { choiceTestSwitch } = this.state; 222 | showTest().then(res => { 223 | this.setState({ choiceTestSwitch: !choiceTestSwitch, allTest: res.data.show }); 224 | }) 225 | } 226 | // 面试官选择好试题之后,更改控制页面显示的按钮的状态 227 | getTest = (val: any) => { 228 | this.setState({ showTestSwitch: true, choiceTestSwitch: false, showTest: val }); 229 | } 230 | 231 | // 面试官提交面试评价的回调函数 232 | submitEvaluation = (value: any) => { 233 | const interviewer_link = window.location.pathname + window.location.search; 234 | value.interviewer_link = interviewer_link; 235 | submitInterview({ submitArr: value }).then(res => { 236 | if (res.data.status === true) { 237 | message.success(res.msg); 238 | } 239 | }) 240 | } 241 | 242 | handleEnter = (e) => { 243 | console.log('看看输入框按下回车后的响应:', e, e.target, e.target.value) 244 | e.target.value = ''; 245 | } 246 | 247 | render() { 248 | const { talk, codeObj, showTestSwitch, choiceTestSwitch, reqVideo, resVideo, resOff, resAns, showTest, allTest, resIce } = this.state; 249 | 250 | return( 251 |
    252 |
    253 | 254 | 255 |
    256 |
    257 | { 258 | showTestSwitch === false ? 259 |
    260 | 261 | 262 | 265 | 266 |
    : 267 |
    268 |
    269 | 任务 270 | 273 |
    274 |
    { showTest.test_name }
    275 |
    { showTest.language }
    276 |
    277 | 278 |
    279 |
    280 | } 281 |
    282 |
    283 | { 284 | choiceTestSwitch === false ? 285 | : 286 | allTest.map(item => { 287 | return( 288 |
    289 | 290 |
    291 | ) 292 | }) 293 | } 294 |
    295 |
    296 |
    297 | 298 |
    299 |
    300 |
    301 | 302 |
    303 |
    304 |
    305 | 306 | 面试评价 307 | 候选人无法查看您的评价 308 |
    309 | 314 | 315 | 5(卓越) 316 | 4(优秀) 317 | 3(标准) 318 | 2(搁置) 319 | 1(淘汰) 320 | 321 | 322 | 323 | 324 | 评语(优势/劣势/需下轮面试官关注点): 325 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 |
    337 |
    338 |
    339 | 340 |
    341 | {/* 视频通话部分 */} 342 | 350 | 351 | {/* 文字聊天部分 */} 352 |
    353 | { 354 | talk.length > 0 && talk.map(item => { 355 | return ( 356 |
    357 | { item.time }  358 | { item.identity === '系统' || item.id !== cookie ? item.identity : '我' }  359 | :{ !item.name ? null : item.id === cookie ? '你' : item.name }{ item.msg } 360 |
    361 | ) 362 | }) 363 | } 364 |
    365 | 366 |
    367 | 368 | 369 | 370 | 回车键发送 371 | 372 |
    373 |
    374 | ) 375 | } 376 | } --------------------------------------------------------------------------------