Commit fedf53dc by zhanghaozhe

Merge branch 'mini-program-editor-page' into dev

parents c95dc3d3 79e360c7
......@@ -1847,9 +1847,9 @@
}
},
"ace-builds": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.7.tgz",
"integrity": "sha512-gwQGVFewBopRLho08BfahyvRa9FlB43JUig5ItAKTYc9kJJsbA9QNz75p28QtQomoPQ9rJx82ymL21x4ZSZmdg=="
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.11.tgz",
"integrity": "sha512-keACH1d7MvAh72fE/us36WQzOFQPJbHphNpj33pXwVZOM84pTWcdFzIAvngxOGIGLTm7gtUP2eJ4Ku6VaPo8bw=="
},
"acorn": {
"version": "5.7.3",
......@@ -15799,6 +15799,16 @@
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
},
"xterm": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/xterm/-/xterm-4.5.0.tgz",
"integrity": "sha512-4t12tsvtYnv13FBJwewddxdI/j4kSonmbQQv50j34R/rPIFbUNGtptbprmuUlTDAKvHLMDZ/Np2XcpNimga/HQ=="
},
"xterm-addon-fit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.4.0.tgz",
"integrity": "sha512-p4BESuV/g2L6pZzFHpeNLLnep9mp/DkF3qrPglMiucSFtD8iJxtMufEoEJbN8LZwB4i+8PFpFvVuFrGOSpW05w=="
},
"y18n": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
......
......@@ -8,6 +8,7 @@
"@babel/runtime": "^7.7.7",
"@loadable/component": "^5.10.1",
"@svgr/webpack": "4.1.0",
"ace-builds": "^1.4.11",
"antd-mobile": "^2.3.1",
"autoprefixer": "^9.6.0",
"axios": "^0.19.0",
......@@ -90,7 +91,9 @@
"webpack": "4.28.3",
"webpack-dev-server": "3.1.14",
"webpack-manifest-plugin": "2.0.4",
"workbox-webpack-plugin": "3.6.3"
"workbox-webpack-plugin": "3.6.3",
"xterm": "^4.5.0",
"xterm-addon-fit": "^0.4.0"
},
"scripts": {
"start": "node scripts/start.js",
......
var API = {
'www': 'http://www-test.julyedu.com',
'home': 'http://fast-test.julyedu.com',
'search-api': 'https://search.julyedu.com',
'passport-api': 'http://passport-test.julyedu.com',
'base-api': 'http://api-test.julyedu.com',
'record': 'record.julyedu.com:8001',
'process-api': 'ws:process-test.julyedu.com:9502',
'm': 'http://m-test.julyedu.com',
'www': 'http://www-test.julyedu.com',
'home': 'http://fast-test.julyedu.com',
'search-api': 'https://search.julyedu.com',
'passport-api': 'http://passport-test.julyedu.com',
'base-api': 'http://api-test.julyedu.com',
'record': 'record.julyedu.com:8001',
'process-api': 'ws:process-test.julyedu.com:9502',
'm': 'http://m-test.julyedu.com',
//terminal
'ws': 'ws://47.93.119.175:8888/ws',
}
......@@ -367,6 +367,9 @@ class App extends Component {
alt=""/>
</div>
}
<div style={{backgroundColor: '#fff', color: '#000', position: 'fixed', bottom: '50px', left: '0'}}>
<Link to={'/interactive-study'}>编辑器</Link>
</div>
</>
}
}
......
import React, { Component } from 'react';
import './index.scss'
class Catalog extends Component {
render() {
return (
<div className={'catalog'}>
<div className="icon">
<img src="https://julyedu-cdn.oss-cn-beijing.aliyuncs.com/interactive_tutorial/study/level1-title-icon.png"
alt=""/>
</div>
<div className="title">{this.props.title}</div>
</div>
);
}
}
export default Catalog;
\ No newline at end of file
.catalog {
display: flex;
padding-left: 12px;
margin-bottom: 24px;
font-size: 20px;
color: #FFD667;
align-items: center;
.icon {
padding-right: 10px;
line-height: 22px;
}
img {
width: 20px;
height: 22px;
vertical-align: middle;
}
.title {
padding-left: 19px;
}
}
\ No newline at end of file
export const questionType = {
text: 1,
image: 2,
singleAnswer: 3,
codeBlock: 4,
program: 5,
}
export const status = {
practicingSingle: '练习中...',
practicingProgram: '练习中...(无法运行时退出重进即可)',
resumePractice: '继续',
nextSection: '下一关',
}
\ No newline at end of file
import React, { Component } from 'react';
import './index.scss'
class Container extends Component {
render() {
const {user, content, children} = this.props
return (
<div className={'container'}>
<div className="user">
{
user && <img className={'avatar'} src={user} alt=""/>
}
</div>
<div className="divide"></div>
<div className="content">{content ? content : children}</div>
</div>
);
}
}
export default Container;
\ No newline at end of file
.container {
width: 100%;
display: flex;
padding-right: 10px;
margin-bottom: 21.5px;
.user {
width: 39px;
flex: 0 0 auto;
padding-left: 9px;
.avatar {
width: 22px;
height: 22px;
border-radius: 50%;
}
}
.divide {
flex: 0 0 auto;
border-right: 1px solid #FFD667;
//background-color: #FFD667;
opacity: .3;
}
.content {
width: 100%;
padding-left: 10.5px;
color: rgba(207, 219, 229, .8);
font-size: 15px;
}
.subtitle{
font-size: 14px;
color: #FFD667;
}
}
\ No newline at end of file
import React, { Component } from 'react';
import './index.scss'
import Container from './container'
import SingleAnswerQuestion from "@components/interactive-tutorial/single-answer-question"
import Program from "@components/interactive-tutorial/program"
import Project from './project'
import { getParam, http, getWXObject } from "@/utils"
import { Toast } from 'antd-mobile'
import { questionType, status } from './consts'
import Catalog from './catalog'
class InteractiveStudy extends Component {
container = null
state = {
page: 1,
progress: 0,
schedule: {},
pageData: {},
cachedList: [],
processContent: [],
processStatus: status.resumePractice,
isComplete: false,
avatar: '',
isFirst: true,
isProgramShowed: 0,
videoId: '',
}
componentDidMount() {
this.getSchedule().then(res => {
const {code, msg, data} = res.data
if (code === 200) {
const pageInfo = data.page_info
const page = pageInfo.current_page
this.setState({
schedule: data,
avatar: data.teacher_avatar,
page,
videoId: data.current_video_id,
})
document.title = data.current_video_name
this.getPageContent(data.current_video_id, () => {
this.updatePosition()
this.getPageContent(data.current_video_id)
})
} else {
this.showToast(msg)
}
})
}
getSchedule = () => {
return http.post(`${API.home}/m/it/study/schedule`, {
course_id: getParam('id'),
})
}
updatePosition = () => {
setTimeout(() => {
window.scrollTo(0, this.container.scrollHeight)
})
}
resetProcessStatus = () => {
this.setState({
processStatus: status.resumePractice,
})
}
showToast = (msg, type = 'info') => {
Toast[type](msg, 2, null, false)
}
getPageContent = (videoId, cb) => {
const {page, schedule} = this.state
if (page > schedule.page_info.total_pages) {
return
}
http.post(`${API.home}/m/it/study/syllabus?page=${this.state.isFirst ? 1 : this.state.page}`, {
course_id: getParam('id'),
video_id: videoId,
type: this.state.isFirst ? 1 : 2,
}).then(res => {
const {code, msg, data} = res.data
if (code === 200) {
this.setState(state => {
const list = data.syllabus_list
const progress = state.processContent.length
? list.findIndex(item => item.syllabus_id === state.processContent[state.processContent.length - 1].syllabus_id) + 1
: 0
if (state.isFirst) {
return {
processContent: state.processContent.concat(list),
isFirst: false,
}
}
return {
cachedList: state.cachedList.reverse().concat(list.slice(progress)).reverse(),
page: state.page + 1,
pageData: data,
progress: progress,
}
}, () => {
cb && cb()
})
} else {
this.showToast(msg)
}
})
}
saveSchedule = (syllabusId, unitInfoId) => {
let data = {syllabus_id: syllabusId}
if (unitInfoId) {
data.unit_info_id = unitInfoId
}
http.post(`/m/it/user/saveSchedule`, data).then(res => {
const {code, msg, data} = res.data
if (code === 200) {
} else {
this.showToast(msg, 'fail')
}
})
}
savePractice = ({syllabusId, type, answer, result, lines}) => {
let data = {
syllabus_id: syllabusId,
type,
answer,
result,
}
if (type === 2 && lines) {
data.lines = lines
}
http.post(`${API.home}/m/it/user/savePractice`, data).then(res => {
const {code, msg, data} = res.data
if (code === 200) {
} else {
Toast.fail(msg, 2)
}
})
}
process = () => {
const {processStatus} = this.state
if (processStatus === status.practicingProgram || processStatus === status.practicingSingle) {
this.showToast('有其他正在进行的练习')
return
}
if (processStatus === status.nextSection) {
console.log('下一关')
return
}
this.setState(state => {
let nextQuestion, cachedList = state.cachedList, pageData = state.pageData
const length = cachedList.length
if (length) {
nextQuestion = [cachedList.pop()]
if (length < 6) {
this.getPageContent(state.videoId)
}
}
if (!cachedList.length) {
return {
processStatus: status.nextSection,
}
}
let processStatus = nextQuestion[0].type === questionType.singleAnswer
? status.practicingSingle
: nextQuestion[0].type === questionType.program
? status.practicingProgram
: status.resumePractice
return {
processContent: state.processContent.concat(nextQuestion),
processStatus,
isProgramShowed: processStatus === status.practicingProgram && (state.isProgramShowed + 1),
cachedList,
pageData,
}
}, () => {
this.updatePosition()
})
}
render() {
const {processStatus, processContent, avatar, isProgramShowed} = this.state
return (
<div id={'interactive-study'} ref={el => this.container = el}>
{
!!processContent.length && processContent.map(item => {
if (item.catalogue === 1) {
switch (item.type) {
case questionType.text:
return <Container user={avatar} content={item.content} key={item.syllabus_id}/>
case questionType.image:
return <Container user={avatar} key={item.syllabus_id}>
<img src={item.content} alt=""/>
</Container>
case questionType.singleAnswer:
return <SingleAnswerQuestion user={avatar} topic={item.content} question={item}
key={item.syllabus_id} updatePosition={this.updatePosition}
resetStatus={this.resetProcessStatus}/>
case questionType.codeBlock:
case questionType.program:
return <Program question={item} key={item.syllabus_id} isProgramShowed={isProgramShowed}/>
}
} else if (item.catalogue === 2) {
return <Catalog title={item.content} key={item.syllabus_id}/>
} else {
return <Container key={item.syllabus_id}>
<span className={'subtitle'}>{item.content}</span>
</Container>
}
})
}
{
processStatus === status.nextSection && <Project user={avatar}/>
}
<div className="status-bar" onClick={this.process}>
{processStatus && <span className={'status'}>{processStatus}</span>}
{/*<span className={'complete'}>已学完全部课时</span>*/}
{/*<div className="free-trial-end">*/}
{/* <span>试学体验结束,389.1元学习全部课时</span>*/}
{/* <button className={'purchase'}>立即购买</button>*/}
{/*</div>*/}
</div>
</div>
);
}
}
export default InteractiveStudy;
\ No newline at end of file
body{
background-color: #252529;
}
#interactive-study {
min-height: 100%;
position: relative;
background-color: #252529;
padding-bottom: 49px;
padding-top: 20px;
& + .year19-index {
display: none;
}
.container:last-child {
margin-bottom: 0;
}
.status-bar {
$height: 49px;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: $height;
background: #3E3F47;
line-height: $height;
text-align: center;
color: #CFDBE5;
.complete {
color: rgba(207, 219, 229, .2);
font-size: 18px;
}
.status {
color: #CFDBE5;
font-size: 16px;
}
.free-trial-end {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 15px;
button {
width: 80px;
height: 30px;
-webkit-appearance: none;
border: none;
outline: 0;
background: #FA5C19;
font-size: 14px;
color: #fff;
}
}
}
}
\ No newline at end of file
import React, { Component } from 'react';
import './index.scss'
import Container from '../container'
import Terminal from '../terminal'
import AceEditor from 'react-ace'
import 'ace-builds/src-min-noconflict/theme-dracula'
import 'ace-builds/src-min-noconflict/mode-python'
import StatusBar from './status-bar'
import { http } from "@/utils"
import { Toast } from "antd-mobile";
import { questionType } from '../consts'
const {First, Normal, Pass, Error, InputTip, Skipped, Finished} = StatusBar
class Program extends Component {
userEditor = null
icon = 'https://julyedu-cdn.oss-cn-beijing.aliyuncs.com/interactive_tutorial/study/program-icon.png'
ws = null
state = {
editorWidth: '',
showTerminal: false,
code: '',
answer: '',
showAnswer: false,
result: '',
filename: '',
executing: false,
}
componentDidMount() {
const contentElem = document.querySelector('.container .content')
const contentStyles = window.getComputedStyle(contentElem)
const contentWidth = contentElem.clientWidth
const contentPaddingLeft = parseFloat(contentStyles.getPropertyValue('padding-left'))
const {type, code_ques, content, user_answer} = this.props.question
let code = content
let answer = '', result = '', showTerminal = false
if (type === questionType.program) {
code = code_ques.code
answer = code_ques.answer
if (user_answer && user_answer.is_finish && user_answer.result) {
result = user_answer.result
showTerminal = true
}
}
this.setState({
editorWidth: `${contentWidth - contentPaddingLeft}px`,
code,
answer,
result,
showTerminal,
})
}
onLoad = editor => {
this.userEditor = editor
}
execute = () => {
// this.getAuthenticationData()
this.uploadCode().then(filename => {
this.setState({
filename,
executing: true,
showTerminal: true,
})
})
}
uploadCode = () => {
return http.post(`${API.home}/web/python/practice/file`, {
code: this.userEditor.getValue(),
}).then(res => {
const {code, msg, data} = res.data
if (code === 200) {
return data.filename
} else {
Toast.fail(msg, 2)
}
})
}
render() {
const {editorWidth, showTerminal, code, answer, showAnswer, result, executing, filename} = this.state
const {user, question: {type, user_answer, content}, isProgramShowed} = this.props
return (
<>
{
content && type !== questionType.codeBlock && <Container content={content}></Container>
}
<Container
user={this.icon}
content={
<div className="program">
<AceEditor
name={'ace-editor'}
mode={'python'}
theme={'dracula'}
readOnly={true}
value={code}
width={editorWidth}
height={'141px'}
onLoad={this.onLoad}
/>
{
showAnswer && <AceEditor
name={'ace-editor'}
mode={'python'}
theme={'dracula'}
readOnly={true}
value={answer}
width={editorWidth}
height={'141px'}
/>
}
{
type !== questionType.codeBlock &&
<ToolBar isProgramShowed={isProgramShowed} userAnswer={user_answer} execute={this.execute}/>
}
{
showTerminal && <Terminal result={result} executing={executing} filename={filename}/>
}
</div>
}
/>
</>
);
}
}
function ToolBar({isProgramShowed, userAnswer, isSuccessful, execute}) {
if (userAnswer && userAnswer.is_finish) {
if (userAnswer.is_jump) {
return <Skipped/>
}
return <Finished/>
}
return isProgramShowed > 1 ? <Normal/> : <First execute={execute}/>
}
export default Program;
\ No newline at end of file
.program {
width: 100%;
background-color: #1f1f24;
padding-top: 12px;
.ace-dracula {
background: rgba(31, 31, 36, 1);
.ace_keyword {
color: rgba(0, 153, 255, 1);
}
.ace_gutter {
background: transparent;
}
.ace_identifier {
color: rgba(0, 207, 230, 1);
}
.ace_string {
color: rgba(219, 88, 131, 1);
}
.ace_marker-layer .ace_active-line {
background: rgba(207, 219, 229, .1);
}
}
.ace_scrollbar {
&.ace_scrollbar-v::-webkit-scrollbar {
width: 6px;
background: transparent;
}
&-v::-webkit-scrollbar-thumb {
height: 50px;
background: rgba(207, 219, 229, .1);
opacity: 0.1;
border-radius: 3px;
}
&.ace_scrollbar-h::-webkit-scrollbar {
height: 6px;
background: transparent;
}
&-h::-webkit-scrollbar-thumb {
width: 8px;
background: rgba(207, 219, 229, .1);
border-radius: 3px;
}
}
.ace_dark.ace_editor.ace_autocomplete {
background: rgba(35, 35, 41, 1);
border-radius: 4px;
}
.ace_dark.ace_editor.ace_autocomplete .ace_marker-layer .ace_active-line {
background: rgba(0, 153, 255, .1);
}
.ace_dark.ace_editor.ace_autocomplete .ace_marker-layer .ace_line-hover {
background: rgba(0, 153, 255, .1);
border: none;
}
.ace_dark.ace_editor.ace_autocomplete .ace_completion-highlight {
color: #09f;
}
.ace-dracula {
.ace_gutter {
background: transparent;
}
.ace_gutter-active-line {
background: rgba(207, 219, 229, .1);
}
}
}
\ No newline at end of file
import React from 'react';
import './index.scss'
import classnames from "classnames";
const Base = ({children, className}) => {
return (
<div className={classnames(['base-status-bar', className])}>
{children}
</div>
);
};
const First = ({execute}) => {
return <Base className={'first'}>
<div className="tip">当前环境暂不支持编写程序,可前往PC端练习或点击按钮</div>
<button className={'btn'} onClick={execute}>运行并查看结果</button>
</Base>
}
const Normal = ({execute, checkAnswer, showAnswerButton = true}) => {
return <Base className={'normal'}>
{
showAnswerButton && <button className={'check-answer'} onClick={checkAnswer}>查看参考答案</button>
}
<button className={'btn'} onClick={execute}>运行</button>
</Base>
}
const Pass = () => {
return <Base className={'pass'}>
<i className={'iconfont icondanseshixintubiao-5'}></i>
<span>运行通过</span>
</Base>
}
const Error = ({showSkip = true}) => {
return <Base className={'error'}>
<div className="left">
<i className="iconfont icondanseshixintubiao-3"></i>
<span>7行出现错误</span>
{showSkip && <a href="javascript:void(0);">跳过本题</a>}
</div>
<button>查看答案</button>
</Base>
}
const InputTip = () => {
return <Base className={'input-tip'}>
<div className="tip">输入完毕后请点击确认</div>
<button>确认</button>
</Base>
}
const Skipped = () => {
return <Base className={'skipped complete'}>
<img src="https://julyedu-cdn.oss-cn-beijing.aliyuncs.com/interactive_tutorial/study/exclamation-mark.png" alt=""/>
<div className="tip">已跳过课堂练习!</div>
<a href="javascript:void(0);">重新运行</a>
</Base>
}
const Finished = () => {
return <Base className={'finished complete'}>
<i className={'iconfont icondanseshixintubiao-5'}></i>
<div className="tip">已跳过课堂练习!</div>
<a href="javascript:void(0);">重新运行</a>
</Base>
}
export default {
First,
Normal,
Pass,
Error,
InputTip,
Skipped,
Finished
};
\ No newline at end of file
$primaryColor: rgba(0, 153, 255, .8);
.base-status-bar {
display: flex;
justify-content: space-between;
align-items: center;
height: 44px;
padding: 8px 10px;
background: #2B2B33;
color: #CFDBE5;
font-size: 12px;
button {
font-size: 12px;
color: #fff;
background-color: $primaryColor;
border: 0;
border-radius: 4px;
-webkit-appearance: none;
outline: 0;
padding: 8px 12px;
}
}
.first {
.tip {
width: 169px;
}
.am-button {
font-size: 12px;
}
}
.normal {
justify-content: flex-end;
.check-answer {
margin-right: 18px;
border: 1px solid $primaryColor;
border-radius: 4px;
font-size: 12px;
color: $primaryColor;
background-color: transparent;
}
}
.pass {
justify-content: flex-start;
align-items: center;
color: #74c27c;
background: rgba(116, 193, 124, .3);
.iconfont {
font-size: 18px;
margin-right: 10px;
}
}
.only-execute {
justify-content: flex-end;
}
.error {
align-items: center;
.left {
color: #C24E55;
a {
color: #fff;
text-decoration: underline;
margin-left: 15px;
}
}
.iconfont {
font-size: 18px;
margin-right: 10px;
}
span {
font-size: 15px;
}
}
.input-tip {
.tip {
font-size: 12px;
color: #CFDBE5;
}
button {
border: 1px solid #09f;
background: transparent;
color: #09f;
}
}
.complete {
display: flex;
align-items: center;
justify-content: flex-start;
.iconfont {
font-size: 18px;
}
a {
font-size: 12px;
color: #CFDBE5;
text-decoration: underline;
}
img {
width: 18px;
height: 18px;
}
.tip {
margin-left: 11px;
font-size: 15px;
}
&.finish {
color: #74C17C;
}
&.skipped {
color: #CFDBE6;
}
}
\ No newline at end of file
import React, { Component } from 'react';
import Container from '../container'
import './index.scss'
class Project extends Component {
render() {
return (
<Container user={this.props.user}>
<div id={'project'}>
<table border={1}>
<thead>
<tr>
<th>项目资料</th>
<th>项目进度</th>
<th>项目成绩</th>
</tr>
</thead>
<tbody>
<tr>
<td className={'pc-tip'}>请前往PC端下载</td>
<td>
<span className={'pc-tip'}>请前往PC端提交</span><br/>
<span>2020-02-28截止</span>
</td>
<td>2020-XX-XX公布</td>
</tr>
</tbody>
</table>
</div>
</Container>
);
}
}
export default Project;
\ No newline at end of file
#project {
table {
width: 315px;
border: 1px solid #252529;
font-size: 11px;
color: #CFDBE5;
background: #2E2E33;
.pc-tip {
color: #09f;
}
td, th {
width: 99px;
height: 44px;
vertical-align: middle;
text-align: center;
}
}
thead {
th {
font-size: 15px;
color: #CFDBE6;
border-bottom: 1px solid #252529;
&:nth-child(2) {
border-left: 1px solid #252529;
border-right: 1px solid #252529;
}
}
}
tbody {
td {
&:nth-child(2) {
border-left: 1px solid #252529;
border-right: 1px solid #252529;
}
}
}
}
\ No newline at end of file
import React, { Component } from 'react';
import './index.scss'
import Container from '../container'
import classnames from 'classnames'
import { html } from "@/utils"
class SingleAnswerQuestion extends Component {
icon = 'https://julyedu-cdn.oss-cn-beijing.aliyuncs.com/interactive_tutorial/study/single-answer-icon.png'
state = {
correct: '',
wrong: '',
replies: [],
selectable: false,
}
handleSelect = (info, index) => {
if (!this.state.selectable) {
return
}
const {question: {unit_questions}, updatePosition, resetStatus} = this.props
const userSelect = unit_questions[index]
this.setState({
wrong: userSelect.is_ans ? '' : index,
correct: userSelect.is_ans ? index : '',
replies: info,
selectable: false,
}, () => {
updatePosition()
resetStatus()
})
}
componentDidMount() {
const {user_answer: userAnswer, unit_questions: options} = this.props.question
if (userAnswer && userAnswer.answer) {
const userSelectId = userAnswer.answer
const userSelectIndex = options.findIndex(item => item.unit_id == userSelectId)
const isUserCorrect = options[userSelectIndex].is_ans
this.setState({
correct: isUserCorrect ? userSelectIndex : '',
wrong: isUserCorrect ? '' : userSelectIndex,
replies: options[userSelectIndex].unit_info,
selectable: false,
})
} else {
this.setState({
selectable: true,
})
}
}
render() {
const {correct, wrong, replies} = this.state
const {user, topic, img, question: {unit_questions: options}} = this.props
return (
<>
<Container
user={this.icon}
content={
<div className={'single-answer-question'}>
<div className="topic" dangerouslySetInnerHTML={html(topic)}></div>
{img && <img src={img} alt=""/>}
<ul>
{
options && !!options.length && options.map((item, index) => {
return <li
className={classnames(['option', {'correct': index === correct, 'wrong': index === wrong}])}
key={index}
onClick={this.handleSelect.bind(this, item.unit_info, index)}>{item.des}</li>
})
}
</ul>
</div>
}
/>
{
!!replies.length && replies.map(item => <Container key={item.info_id} content={item.content}/>)
}
</>
);
}
}
export default SingleAnswerQuestion;
\ No newline at end of file
.single-answer-question {
img {
width: 100%;
margin: 10px 0 5px;
}
.option {
padding: 10px 0 10px 15px;
margin-top: 10px;
background: rgba(46, 46, 51, 1);
border-radius: 4px;
border: 1px solid transparent;
&.correct {
border: 1px solid #74C17C;
}
&.wrong {
border: 1px solid #C24E55;
}
}
}
\ No newline at end of file
import React, { Component } from 'react';
import './index.scss'
import { Terminal } from "xterm"
import 'xterm/css/xterm.css'
import { FitAddon } from "xterm-addon-fit";
import { http } from "@/utils"
import { Toast } from "antd-mobile";
import axios from 'axios'
class TerminalInterface extends Component {
terminal = null
termElem = null
quit = '\u0003'
socket = null
state = {
terminalWidth: '',
}
componentDidMount() {
const content = document.querySelector('.container .content')
const contentStyles = window.getComputedStyle(content)
const contentWidth = content.clientWidth
const contentPaddingLeft = parseFloat(contentStyles.getPropertyValue('padding-left'))
this.setState({
terminalWidth: `${contentWidth - contentPaddingLeft}px`,
})
this.terminal = new Terminal()
const fitAddon = new FitAddon()
this.terminal.loadAddon(fitAddon)
this.terminal.open(this.termElem)
const {result, executing} = this.props
setTimeout(() => {
fitAddon.fit()
if (result) {
this.terminal && this.terminal.write(this.props.result)
} else if (executing) {
this.getAuthenticationData()
}
})
}
getAuthenticationData = () => {
axios.post(`http://47.93.119.175:8888/auth`, {
username: '3guh394h3fhj0f4',
password: 'okqdw029j038hrv3890cv',
}, {
withCredentials: false,
}).then(res => {
const {code, msg, data} = res.data
if (code === 0) {
this.getLinkId(data)
} else {
Toast.fail(msg, 2)
}
})
}
getLinkId = token => {
axios.post(`http://47.93.119.175:8888`, {}, {
headers: {
Token: token,
},
withCredentials: false,
}).then(res => {
const {id} = res.data
this.connectServer(id)
})
}
connectServer = (id) => {
this.socket = new WebSocket(`${API.ws}?id=${id}`)
this.socket.addEventListener('open', () => {
this.socket.send(JSON.stringify({
data: this.props.filename,
}))
})
this.socket.onmessage = event => {
const reader = new FileReader()
reader.onload = () => {
this.terminal.write(this.ab2str(reader.result))
}
reader.readAsArrayBuffer(event.data)
}
this.socket.onerror = (event) => {
console.log(event)
// this.connectServer(id)
}
this.socket.onclose = (event) => {
console.log('closed')
}
}
ab2str = buf => {
return String.fromCharCode.apply(null, new Uint8Array(buf));
}
render() {
const {terminalWidth} = this.state
return (
<div className={'terminal-container'}>
<div className={'terminal'} ref={el => this.termElem = el} style={{width: `${terminalWidth}`}}></div>
</div>
);
}
}
export default TerminalInterface;
\ No newline at end of file
.terminal {
height: 141px;
padding: 5px;
background: #000;
}
\ No newline at end of file
......@@ -12,19 +12,19 @@ import App from './App'
const reduxDevToolOptions = process.env.REACT_APP_BUILD_ENV === 'production' ? {} : {trace: true, traceLimit: 25}
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__(reduxDevToolOptions) || compose;
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__(reduxDevToolOptions) || compose;
const store = createStore(
rootReducers,
composeEnhancers(
applyMiddleware(thunk, logger)
)
rootReducers,
composeEnhancers(
applyMiddleware(thunk, logger)
)
)
ReactDOM.render(
<Provider store={store}>
<Router>
<App/>
</Router>
</Provider>,
document.getElementById('root'));
\ No newline at end of file
<Provider store={store}>
<Router>
<App/>
</Router>
</Provider>,
document.getElementById('root'));
\ No newline at end of file
......@@ -330,4 +330,9 @@ export default [
path: '/mlShare',
component: loadable(() => import('@/components/mlShare'))
},
{
path: '/interactive-tutorial',
component: loadable(() => import(/* webpackChunkName: 'interactive-tutorial' */'@/components/interactive-tutorial'))
}
]
......@@ -3,7 +3,7 @@ import {
differenceInDays,
differenceInHours,
differenceInMinutes,
differenceInSeconds
differenceInSeconds,
} from 'date-fns'
......@@ -15,7 +15,7 @@ export const getParam = (key, str) => {
}
const html = content => ({
__html: htmlDecode(content)
__html: htmlDecode(content),
})
const htmlDecode = content => {
......@@ -102,7 +102,7 @@ const browser = (function () {
isIOS: /\(i[^;]+;( U;)? CPU.+Mac OS X/i.test(ua),
isIPad: /iPad/i.test(ua),
isAndroidApp: /Android/i.test(ua) && getParam('version'),
isIOSApp: /iPhone/i.test(ua) && getParam('version')
isIOSApp: /iPhone/i.test(ua) && getParam('version'),
}
})()
......@@ -119,19 +119,20 @@ const dateCountDown = (later, earlier) => {
d,
h,
m,
s
s,
}
}
export {
default as http
default as http,
}
from './http'
export {
default as wxShare
default as wxShare,
}
from './wechat/share'
export { getWXObject } from './wechat/base'
export {
html,
initCaptcha,
......@@ -143,6 +144,6 @@ export {
dateCountDown,
}
export {
default as SendMessageToApp
default as SendMessageToApp,
}
from './app'
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment