import React from 'react'; import { Link } from 'react-router-dom'; import './index.less'; import Page from '@src/containers/Page'; import Assets from '@src/components/Assets'; import { getMap, formatPercent, formatDate } from '@src/services/Tools'; import Footer from '../../../components/Footer'; import { FaqModal, CommentModal, FinishModal, CourseNoteModal, AskCourseModal } from '../../../components/OtherModal'; import { Contact, AnswerCarousel, Comment } from '../../../components/Other'; import Tabs from '../../../components/Tabs'; import { Icon as GIcon } from '../../../components/Icon'; import Button from '../../../components/Button'; import IconButton from '../../../components/IconButton'; import UserTable from '../../../components/UserTable'; import ProgressText from '../../../components/ProgressText'; import { OpenText } from '../../../components/Open'; import Video from '../../../components/Video'; import { Main } from '../../../stores/main'; import { Course } from '../../../stores/course'; import { User } from '../../../stores/user'; import { Order } from '../../../stores/order'; import { Question } from '../../../stores/question'; import { My } from '../../../stores/my'; export default class extends Page { initState() { this.columns = [ { title: '学习内容', key: 'title', render: (text, record) => { const { no } = this.state; if (no === record.no) { // 当前正在 } return `课时 ${record.no}: ${text}`; }, }, { title: '预习作业', key: 'paper', render: text => { text = text || {}; const progress = text.report ? formatPercent(text.report.userNumber, text.report.questionNumber) : 0; const times = text.paper ? text.paper.times : 0; return ( <div> <div className="v-a-m d-i-b"> <ProgressText width={50} size="small" times={times} progress={progress} unit="次" /> </div> {!text.report && ( <IconButton className="m-l-2" type="start" tip="Start" onClick={() => { User.needLogin().then(() => { Question.startLink('preview', text); }); }} /> )} {text.report && !text.report.isFinish && ( <IconButton className="m-l-2" type="continue" tip="Continue" onClick={() => { User.needLogin().then(() => { Question.continueLink('preview', text); }); }} /> )} {text.report && !!text.report.isFinish && ( <IconButton className="m-l-2" type="restart" tip="Restart" onClick={() => { User.needLogin().then(() => { Question.restart('preview', text); }); }} /> )} {text.report && !!text.report.isFinish && ( <IconButton className="m-l-5" type="report" tip="Report" onClick={() => { User.needLogin().then(() => { Question.reportLink('preview', text); }); }} /> )} </div> ); }, }, { title: '进度', key: 'progress', render: (text, record) => { const { paper = {} } = record; return `${paper.paper && paper.paper.times > 0 ? `${paper.paper.times}次+` : ''}${ paper.report ? formatPercent(paper.report.userNumber, paper.report.questionNumber, false) : '0%'}`; }, }, { title: '最近学习', key: 'lastTime', render: (text, record) => { const { paper = {} } = record; return paper.report && formatDate(paper.report.updateTime, 'YYYY-MM-DD HH:mm:ss'); }, }, { title: '笔记', key: 'note', render: (text, record) => { return <GIcon name="note" active={record.note} />; }, }, { title: '问答', key: 'ask', render: (text, record) => { return ( <Link to={`/course/answer/${record.courseId}?tab=my&courseNoId=${record.id}`}>{`${record.answerNumber || 0}/${record.askNumber || 0}`}</Link> ); }, }, ]; return { tab: '1', rightTab: '1', key: '1', add: false, list: [{ key: '1' }, { key: '2' }, { key: '3' }], data: {}, position: 0, }; } init() { Main.dataStruct().then(result => { const dataStructSelect = result.map(row => { return { title: `${row.titleZh}${row.titleEn}`, key: row.id, }; }); const dataStructMap = getMap(dataStructSelect, 'key'); this.setState({ dataStructSelect, dataStructMap }); }); Main.getBase().then(result => { this.setState({ base: result }); }); } formatRecord(row) { row.paperMap = {}; if (row.papers) { row.papers.forEach(paper => { if (paper.courseNo) row.paperMap[paper.courseNo] = paper; }); } row.progressMap = {}; if (row.progress) { row.progress.forEach(progress => { row.progressMap[progress.courseNoId] = progress; }); } row.courseNoMap = {}; row.courseTime = 0; if (row.courseNos) { row.courseNos.forEach(no => { row.courseNoMap[no.id] = no; row.courseTime += no.time; no.paper = row.paperMap[no.id]; no.progress = row.progressMap[no.id]; }); } if (row.currentNo) { row.currentCourseNo = row.courseNoMap[row.currentNo]; } else { row.currentNo = 0; } return row; } initData() { const { id } = this.params; Course.detail(id).then(result => { result = this.formatRecord(result); // result.have = true; // result.waters = ['高*', '152****4895']; this.setState({ data: result }); // 选择课时 if (this.state.search.no) { this.onChangeItem(this.state.search.no); } else { this.onChangeItem(1); } this.refreshNote(); }); Main.listComment({ page: 1, size: 100, channel: 'course-video', position: id }).then(result => { this.comments = result.list; this.setState({ comments: result.list }); }); } refreshAsk(position) { const { id } = this.params; const { item = {} } = this.state; Course.listAsk(Object.assign({ page: 1, size: 1000, courseId: id, courseNoId: item.id, position })).then(result => { this.setState({ asks: result.list }); }); } refreshNote() { const { id } = this.params; const { data } = this.state; if (!data.have) return; My.listCourseNote({ courseId: id, page: 1, size: data.courseNos.length }) .then((result) => { this.noteMap = getMap(result.list, 'courseNoId'); data.courseNos.forEach((row) => { const note = this.noteMap[row.id]; if (note) row.note = true; }); this.setState({ data }); }); } onChangeRightTab(rightTab) { this.setState({ rightTab }); } onChangeTab(tab) { this.setState({ tab }); } onChangeItem(key) { key = Number(key); this.changeQuery({ no: key }); this.setState({ no: key }); const { data, item } = this.state; const index = key - 1; const timelineSelect = []; const current = data.courseNos[index]; if (current) { const max = current.time; let start = 0; let end = start + 5; while (start < max) { timelineSelect.push({ title: `${start} - ${end}min`, key: `${start}`, }); start += 5; end = Math.min(start + 5, max); } } // 切换播放,记录进度 if (item) { this.updateProgress(item.id, this.lastSecond, item.time, true); } this.setState({ item: current, timelineSelect }); } onTimeUpdate(second) { const { position, item = {}, data } = this.state; if (!data.have) { // 如果是试用,则按秒数增加 second += (item.startTrail || 0) * 60; } const minute = parseInt(second / 60, 10); const nowPosition = parseInt(minute / 5, 10) * 5; if (nowPosition !== position) { this.refreshAsk(position); this.setState({ position: nowPosition }); // 定时更新进度 this.updateProgress(item.id, second, item.time); } this.lastSecond = second; this.showWater(second); } playVideo(second) { // 开始计时 this.lastTime = new Date(); this.showWater(second); } showWater(second, extend) { const { data, water = {} } = this.state; const { waters = [] } = data; if (!this.lastTime && water.show) { water.show = false; second -= 1; water.transition = ''; } else if (!water.show) { water.show = true; water.transition = 'transform 1s linear 0s'; } if (water.show) { water.opacity = 1; } else { water.opacity = 0; } if (extend) { Object.assign(water, extend); } const time = 15; const stop = 0; const times = (second / (time + stop)); const index = parseInt(times % waters.length, 10); const current = (second % (time + stop)); water.style = [2, 0, 2, 1][parseInt((times % 4), 10)] + 1; water.text = waters[[0, 1][index]]; // console.log(water.show, current, current / 4, (current / 4) % 2); if (water.show) { water.opacity = (current / 4) % 2 > 1 ? 0 : 1; } // if (water.style % 2) { // water.transform = `translateX(${current * 100 / time}%)`; // } else { water.transform = `translateX(${100 - ((current - stop / 2) * 100 / time)}%)`; // } this.setState({ water }); } onChangeProgress(second) { this.showWater(second - 1, this.lastTime ? { transition: '', opacity: 0 } : { transition: '', opacity: 0 }); setTimeout(() => { this.showWater(second, this.lastTime ? { transition: 'transform 1s linear 0s', opacity: 1 } : { transition: '', opacity: 0 }); }, 1); } pauseVideo(second) { // 停止计时 const now = new Date(); if (this.lastTime != null) { this.time += (now.getTime() - this.lastTime.getTime()) / 1000; } this.lastTime = null; this.showWater(second); } next() { const { data, item } = this.state; if (data.courseNos.length === item.no) { return; } this.onChangeItem(item.no + 1); } onVideoAction(key) { const { rightTab, showTab, showAsk, showNote, item = {} } = this.state; switch (key) { case 'ask': return this.setState({ showAsk: !showAsk }); case 'note': return this.setState({ showNote: !showNote, note: this.noteMap ? this.noteMap[item.id] || {} : {} }); case 'answer': return this.setState({ showTab: rightTab === '1' ? !showTab : true, rightTab: '1' }); case 'list': return this.setState({ showTab: rightTab === '2' ? !showTab : true, rightTab: '2' }); default: return ''; } } updateProgress(courseNoId, currentTime, totalTime, record) { if (!this.lastTime) return; const { id } = this.params; const now = new Date(); this.time += (now.getTime() - this.lastTime.getTime()) / 1000; this.lastTime = now; const progress = formatPercent(currentTime, totalTime); if (record || this.time > 600) { // 最长5分钟记录一次 Course.noProgress(id, courseNoId, progress, this.time, courseNoId); this.time = 0; } else { Course.noProgress(id, courseNoId, progress, null, null); } } buy() { const { data } = this.props; User.needLogin().then(() => { Order.speedPay({ productType: 'course', productId: data.id }).then(result => { User.needPay(result).then(() => { this.refresh(); }); }); }); } add() { const { data } = this.props; User.needLogin().then(() => { Order.addCheckout({ productType: 'course', productId: data.id }).then(() => { this.setState({ add: true }); }); }); } viewAsk(id) { Course.askView(id); } setVideo(video) { this.video = video; } renderView() { const { base = {}, data = {}, item = {}, add, rightTab, showTab, showAsk, showNote, dataStructMap = {}, showComment, comment = {}, showFaq, faq = {}, showFinish, note = {}, ask = {}, timelineSelect = [], water = {} } = this.state; const { courseNos = [] } = data; return ( <div> <div className="top content t-8"> 千行课堂 > 全部课程 > {data.parentStructId > 0 ? `${(dataStructMap[data.parentStructId] || {}).title} >` : ''}{' '} {(dataStructMap[data.structId] || {}).title} > {data.title} > <span className="t-1">课程详情</span> </div> <div className="center content"> <div className="t-1 t-s-20"> {data.title} <div className="action f-r"> {!data.have && <Button className="m-r-1" radius size="lager" onClick={() => this.buy()}> 立即购买 </Button>} {!data.have && <Button theme="default" radius size="lager" disabled={data.add || add} onClick={() => this.add()}> <Assets name="add" /> </Button>} {data.have && <Button className="m-r-1" radius size="lager" onClick={() => linkTo('/my/course')}> 我的课程 </Button>} </div> </div> <div className="t-2 m-b-1">授课老师:{data.teacher}</div> <div className={'detail'}> <div className="left"> {data.have && <div hidden={(item.paper && item.paper.times > 0)} className="left-top"> <span className="d-i-b m-r-1">预习作业</span> <span className="d-i-b m-r-2"> <ProgressText width={480} size="small" progress={item.report ? formatPercent(item.report.userNumber, item.report.questionNumber) : 0} /> </span> <Button className="f-r" radius onClick={() => (item.report ? Question.continueLink('preview', item) : Question.startLink('preview', item))}> 做题 </Button> </div>} <div className="video-layout"> {item && <Video key={item.id} src={item.resource || '/1.mp4'} width={750} height={467} ref={ref => this.setVideo(ref)} water={<div key={water.style} className={`video-water style-${water.style}`} style={{ ...water }}>{water.text}</div>} btnList={[ { title: '提问', key: 'ask', show: data.have, active: showAsk, pause: true }, { key: 'answer', render(active) { return <Assets name={active ? 'question_on' : 'question_off'} />; }, full: true, show: true, active: showTab && rightTab === '1', }, { title: '笔记', key: 'note', show: data.have, active: showNote, pause: true }, { title: '课表', key: 'list', show: true, full: true, active: showTab && rightTab === '2' }, ]} onPlay={(second) => this.playVideo(second)} onPause={(second) => this.pauseVideo(second)} onChangeProgress={(second) => this.onChangeProgress(second)} onNext={() => this.next()} onAction={key => this.onVideoAction(key)} onTimeUpdate={time => this.onTimeUpdate(time)} onFullChange={() => this.setState({ showTab: true, rightTab: '1' })} > <div hidden={!showTab} className="video-fixed tab-warpper"> <Tabs type="division" theme="gray" space={2.5} active={rightTab} tabs={[{ title: '精选问答', key: '1' }, { title: '课时列表', key: '2' }]} onChange={key => this.onChangeRightTab(key)} /> <div className="tab-body">{this[`renderRightTab${rightTab}`]()}</div> </div> </Video>} </div> </div> <div className={`detail-right ${data.have && !(item.paper && item.paper.times > 0) ? 'have' : ''} tab-warpper`}> <Tabs type="division" theme="gray" space={2.5} active={rightTab} tabs={[{ title: '精选问答', key: '1' }, { title: '课时列表', key: '2' }]} onChange={key => this.onChangeRightTab(key)} /> <div className="tab-body">{this[`renderRightTab${rightTab}`]()}</div> </div> </div> {data.have && <UserTable columns={this.columns} list={courseNos} />} </div> <div hidden={data.have} className="bottom"> <div className="content">{this.renderTab()}</div> </div> <Contact data={base.contact} /> <Footer /> <AskCourseModal getContainer={() => (showTab ? document.getElementById(this.video.state.id) : document.body)} show={showAsk} defaultData={ask} course={data} courseNo={item} selectList={timelineSelect} onConfirm={() => this.setState({ showAsk: false })} onCancel={() => this.setState({ showAsk: false })} /> <CourseNoteModal getContainer={() => (showTab ? document.getElementById(this.video.state.id) : document.body)} show={showNote} defaultData={note} course={data} courseNos={courseNos} noteMap={this.noteMap} onConfirm={() => { this.setState({ showNote: false }); this.refreshNote(); }} onCancel={() => this.setState({ showNote: false })} /> <CommentModal show={showComment} defaultData={comment} onConfirm={() => this.setState({ showComment: false, commnet: {}, showFinish: true })} onCancel={() => this.setState({ showComment: false, commnet: {} })} onClose={() => this.setState({ showComment: false, commnet: {} })} /> <FaqModal show={showFaq} defaultData={faq} onCancel={() => this.setState({ showFaq: false, faq: {} })} onConfirm={() => this.setState({ showFaq: false, faq: {}, showFinish: true })} /> <FinishModal getContainer={() => document.getElementById(this.video.state.id)} show={showFinish} onConfirm={() => this.setState({ showFinish: false })} /> </div > ); } renderRightTab1() { const { asks = [], data = {}, position } = this.state; return [ <div className="all-answer"> <span className="d-i-b b m-r-5" /> <span className="d-i-b t-6">{position}:00~{position + 5}:00</span> <a className="f-r d-i-b t-4 c-p" href={`/course/answer/${data.id}`} target="_blank">全部问答 ></a> </div>, <div className="answer-layout"> {asks.map(item => { return ( <div className="answer-item"> <div> <div className="small-tag">提问</div> </div> <div className="desc"> <OpenText>{item.content}</OpenText> </div> {item.answerStatus > 0 && ( <div> <div className="small-tag">回答</div> </div> )} {item.answerStatus > 0 && ( <div className="desc"> <OpenText onOpen={() => this.viewAsk(item.id)}>{item.answer}</OpenText> </div> )} </div> ); })} </div>, ]; } renderRightTab2() { const { data = {}, no } = this.state; const { courseNos = [] } = data; return ( <div className="item-layout"> {courseNos.map(item => { return ( <div className={`item ${item.no === no ? 'active' : ''}`} onClick={() => this.onChangeItem(item.no)}> <span className="t-1">课时{item.no}</span> <span className="t-2">{item.title}</span> </div> ); })} </div> ); } renderTab() { const { tab } = this.state; return [ <div className="bottom"> <div className="content"> <Tabs type="full" border active={tab} tabs={[ { title: '课程介绍', key: '1' }, { title: '授课大纲', key: '2' }, { title: '小助手', key: '3' }, { title: 'FAQs', key: '4' }, { title: '优惠信息', key: '5' }, { title: '学员评价', key: '6' }, ]} onChange={key => this.onChangeTab(key)} /> {this[`renderTab${tab}`]()} </div> </div>, ]; } renderTab1() { const { data = {} } = this.state; return ( <div className="tab-layout"> <div className="tab-title">老师资历</div> <div className="tab-desc" dangerouslySetInnerHTML={{ __html: data.teacherContent }} /> <div className="tab-title">基本参数</div> <div className="tab-desc" dangerouslySetInnerHTML={{ __html: data.baseContent }} /> <div className="tab-title">授课重点</div> <div className="tab-desc" dangerouslySetInnerHTML={{ __html: data.pointContent }} /> <div className="tab-title">适合人群</div> <div className="tab-desc" dangerouslySetInnerHTML={{ __html: data.crowdContent }} /> </div> ); } renderTab2() { const { data } = this.state; return ( <div className="tab-layout"> <div className="tab-desc" dangerouslySetInnerHTML={{ __html: data.syllabusContent }} /> </div> ); } renderTab3() { return ( <div className="tab-layout"> <div className="qr-layout"> <Assets name="qrcode" className="m-r-2 v-a-t" /> <div className="p-l-1 d-i-b t-l"> <div className="t-1">千行小助手:</div> <div className="t-1 m-b-2">1232104-310431</div> <div className="t-2 t-s-12">微信扫码添加千行小助手为好友,</div> <div className="t-2 t-s-12">咨询课程,了解更多信息</div> </div> </div> </div> ); } renderTab4() { const { faqs, data = {} } = this.state; return ( <div className="tab-layout"> <AnswerCarousel hideBtn list={faqs} onFaq={() => User.needLogin().then(() => this.setState({ showFaq: true, faq: { channel: 'course-video', position: data.id } }))} /> </div> ); } renderTab5() { const { data = {} } = this.state; return ( <div className="tab-layout"> <div className="tab-desc" dangerouslySetInnerHTML={{ __html: data.promoteContent }} /> </div> ); } renderTab6() { const { data = {}, comments = [] } = this.state; return ( <div className="tab-layout"> {data.have && <div className="m-b-1 t-r"> <Button width={100} radius onClick={() => User.needLogin().then(() => this.setState({ showComment: true, comment: { channel: 'course-video', position: data.id } }))}> 写评论 </Button> </div>} {(comments || []).map(item => { return <Comment data={item} />; })} </div> ); } }