page.js 56 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634
  1. import React, { Component } from 'react';
  2. import { Link } from 'react-router-dom';
  3. import './index.less';
  4. import { Icon, Dropdown } from 'antd';
  5. import FileUpload from '@src/components/FileUpload';
  6. import Page from '@src/containers/Page';
  7. import Assets from '@src/components/Assets';
  8. import { asyncSMessage } from '@src/services/AsyncTools';
  9. import { formatDate, formatSeconds, formatPercent } from '@src/services/Tools';
  10. import UserLayout from '../../../layouts/User';
  11. import Button from '../../../components/Button';
  12. import ProgressText from '../../../components/ProgressText';
  13. import IconButton from '../../../components/IconButton';
  14. import { Icon as GIcon } from '../../../components/Icon';
  15. import menu from '../index';
  16. import Tabs from '../../../components/Tabs';
  17. import UserTable from '../../../components/UserTable';
  18. import More from '../../../components/More';
  19. import Modal from '../../../components/Modal';
  20. import DatePlane from '../../../components/Date';
  21. import Note from '../../../components/Note';
  22. import { My } from '../../../stores/my';
  23. import { User } from '../../../stores/user';
  24. import { Question } from '../../../stores/question';
  25. import { Order } from '../../../stores/order';
  26. import { Common } from '../../../stores/common';
  27. export default class extends Page {
  28. initState() {
  29. return {
  30. tab: 'online',
  31. status: 'all',
  32. };
  33. }
  34. formatRecord(row) {
  35. if (row.useEndTime) {
  36. if (row.isSuspend && !row.restoreTime) {
  37. row.status = 'suspend';
  38. } else if (new Date(row.useEndTime).getTime() < new Date().getTime()) {
  39. row.status = 'end';
  40. } else {
  41. row.status = 'ing';
  42. }
  43. } else {
  44. row.status = 'not';
  45. }
  46. row.paperMap = {};
  47. row.appointmentPaperMap = {};
  48. if (row.papers) {
  49. row.papers.forEach(paper => {
  50. if (paper.courseNo) row.paperMap[paper.courseNo] = paper;
  51. if (paper.appointment) row.appointmentPaperMap[paper.appointment] = paper;
  52. });
  53. }
  54. row.progressMap = {};
  55. if (row.progress) {
  56. row.progress.forEach(progress => {
  57. row.progressMap[progress.courseNoId] = progress;
  58. });
  59. }
  60. row.courseNoMap = {};
  61. row.courseTime = 0;
  62. if (row.courseNos) {
  63. row.courseNos.forEach(no => {
  64. row.courseNoMap[no.id] = no;
  65. row.courseTime += no.time;
  66. no.paper = row.paperMap[no.id];
  67. no.progress = row.progressMap[no.id];
  68. });
  69. }
  70. if (row.currentNo) {
  71. row.currentCourseNo = row.courseNoMap[row.currentNo];
  72. } else {
  73. row.currentNo = 0;
  74. }
  75. // 如果已经最新预约结束,则添加一个空记录作为最新预约
  76. if (row.appointments) {
  77. row.appointments.forEach(r => {
  78. r.paper = row.appointmentPaperMap[r.id];
  79. r.noteList = (row.comments || []).filter(c => c.type === 'note' && c.appointmentId === r.id);
  80. r.supplyList = (row.comments || []).filter(c => c.type === 'supply' && c.appointmentId === r.id);
  81. });
  82. // 是否是最后一课时,是否过预约时间
  83. const last = row.appointments.length - 1;
  84. const appointment = row.appointments[last];
  85. if (new Date(appointment.endTime).getTime() < new Date().getTime()) {
  86. // row.status = 'end';
  87. if (row.number !== row.appointments.length) {
  88. row.appointments.push([{}]);
  89. }
  90. }
  91. }
  92. row.days = new Date(row.userEndTime);
  93. return row;
  94. }
  95. initData() {
  96. const data = Object.assign(this.state, this.state.search);
  97. data.filterMap = this.state.search;
  98. if (data.order) {
  99. data.sortMap = { [data.order]: data.direction };
  100. }
  101. this.setState(data);
  102. let isUsed = null;
  103. let isEnd = null;
  104. switch (data.status) {
  105. case 'learning':
  106. isUsed = true;
  107. isEnd = false;
  108. break;
  109. case 'end':
  110. isUsed = true;
  111. isEnd = true;
  112. break;
  113. case 'nouse':
  114. isUsed = false;
  115. isEnd = false;
  116. break;
  117. case 'all':
  118. default:
  119. }
  120. My.listCourse(Object.assign({ courseModule: data.tab }, this.state.search, { isUsed, isEnd })).then(result => {
  121. result.list = result.list.map(row => {
  122. return this.formatRecord(row);
  123. });
  124. this.setState({ list: result.list, total: result.total, page: data.page });
  125. });
  126. }
  127. onAction() { }
  128. onTabChange(tab) {
  129. const data = { tab };
  130. this.refreshQuery(data);
  131. }
  132. onStatusChange(status) {
  133. this.search({ status });
  134. }
  135. openTime(record) {
  136. if (record.courseTimeMap) {
  137. this.setState({ data: record, showTime: true });
  138. return;
  139. }
  140. My.timeCourse(record.id).then(result => {
  141. const courseMap = {};
  142. const previewMap = {};
  143. const stopMap = {};
  144. result.forEach(row => {
  145. const date = formatDate(row.day, 'YYYY-MM-DD');
  146. if (row.type === 'stop') {
  147. stopMap[date] = true;
  148. } else if (row.type === 'preview') {
  149. previewMap[date] = true;
  150. } else if (row.type === 'course') {
  151. courseMap[date] = true;
  152. }
  153. });
  154. record.courseTimeMap = courseMap;
  155. record.previewTimeMap = previewMap;
  156. record.stopTimeMap = stopMap;
  157. this.setState({ data: record, showTime: true });
  158. });
  159. }
  160. refreshDetail(recordId) {
  161. My.detailCourse(recordId).then(result => {
  162. let { list } = this.state;
  163. list = list.map(row => {
  164. if (row.id === recordId) return this.formatRecord(result);
  165. return row;
  166. });
  167. this.setState({ list });
  168. });
  169. }
  170. suspend() {
  171. const { suspend } = this.state;
  172. My.suspendCourse(suspend.id)
  173. .then(() => {
  174. asyncSMessage('停课成功');
  175. this.setState({ showSuspend: false });
  176. this.refreshDetail(suspend.id);
  177. })
  178. .catch(err => {
  179. asyncSMessage(err.message, 'error');
  180. });
  181. }
  182. restore() {
  183. const { restore } = this.state;
  184. My.restoreCourse(restore.id)
  185. .then(() => {
  186. asyncSMessage('恢复成功');
  187. this.setState({ showRestore: false });
  188. this.refreshDetail(restore.id);
  189. })
  190. .catch(err => {
  191. asyncSMessage(err.message, 'error');
  192. });
  193. }
  194. submitAppointmentComment(data, type) {
  195. data.type = type;
  196. if (data.id) {
  197. My.editAppointmentComment(data).then(() => {
  198. this.refreshDetail(data.recordId);
  199. });
  200. } else {
  201. My.addAppointmentComment(data).then(() => {
  202. this.refreshDetail(data.recordId);
  203. });
  204. }
  205. }
  206. deleteAppointmentComment(row) {
  207. My.delAppointmentComment(row.id).then(() => {
  208. this.refreshDetail(row.recordId);
  209. });
  210. }
  211. submitQuestionFile(data) {
  212. My.uploadAppointmentQuestion(data).then(() => {
  213. this.refreshDetail(data.recordId);
  214. });
  215. }
  216. setCCTalkName(recordId, cctalkname) {
  217. My.setCCTalkName(recordId, cctalkname).then(() => {
  218. this.refreshDetail(recordId);
  219. });
  220. }
  221. submitComment() {
  222. const { comment } = this.state;
  223. My.addComment(comment.channel, comment.position, comment.content).then(() => {
  224. this.setState({ showComment: false, showFinish: true, comment: {} });
  225. });
  226. }
  227. open(recordId) {
  228. Order.useRecord(recordId).then(() => {
  229. asyncSMessage('开通成功');
  230. this.refreshDetail(recordId);
  231. });
  232. }
  233. closeCommentTips(recordId) {
  234. My.courseCommentTips(recordId);
  235. }
  236. uploadNote(file) {
  237. const { note = {} } = this.state;
  238. Common.uploadImage(file).then(result => {
  239. note.file = result.url;
  240. note.name = file.name;
  241. this.setState({ note });
  242. });
  243. }
  244. uploadSupply(file) {
  245. const { supply = {} } = this.state;
  246. Common.uploadImage(file).then(result => {
  247. supply.file = result.url;
  248. supply.name = file.name;
  249. this.setState({ supply });
  250. });
  251. }
  252. renderView() {
  253. const { config } = this.props;
  254. return <UserLayout active={config.key} menu={menu} center={this.renderDetail()} />;
  255. }
  256. renderDetail() {
  257. const {
  258. tab,
  259. status,
  260. showTips,
  261. showTime,
  262. showSuspend,
  263. showRestore,
  264. showUploadNote,
  265. showUploadSupply,
  266. showSupply,
  267. showNote,
  268. showComment,
  269. showFinish,
  270. comment = {},
  271. data = {},
  272. note = {},
  273. supply = {},
  274. appointment = {},
  275. } = this.state;
  276. return (
  277. <div className="detail-layout">
  278. <div hidden={!showTips} className="tip">
  279. <div className="text">理清备考思路,避开常见误区,让学习的每一分钟发光发热!</div>
  280. <a href="" target="_blank">
  281. 去填写 >
  282. </a>
  283. <Icon type="close-circle" theme="filled" onClick={() => this.setState({ showTips: false })} />
  284. </div>
  285. <Tabs
  286. type="division"
  287. theme="theme"
  288. size="small"
  289. space={2.5}
  290. width={100}
  291. active={tab}
  292. tabs={[{ key: 'online', title: '在线课程' }, { key: 'vs', title: '1V1私教' }]}
  293. onChange={key => this.onTabChange(key)}
  294. />
  295. <Tabs
  296. type="tag"
  297. theme="white"
  298. size="small"
  299. space={5}
  300. width={54}
  301. active={status}
  302. tabs={[
  303. { key: 'all', title: '全部' },
  304. { key: 'nouse', title: '未开通' },
  305. { key: 'learning', title: '学习中' },
  306. { key: 'end', title: '已结束' },
  307. ]}
  308. onChange={key => this.onStatusChange(key)}
  309. />
  310. {this[`renderTab${tab}`]()}
  311. <Modal show={showTime} className="clock-modal" title="打卡表" width={460} onClose={() => this.setState({ showTime: false })}>
  312. <div>
  313. <div className="d-i-b w-3">
  314. <div className="t-2 t-s-14">听课频率</div>
  315. <div className="t-4 t-s-18">{data.currentNo ? data.totalDays / data.currentNo : 0}天/课时</div>
  316. </div>
  317. <div className="d-i-b w-3">
  318. <div className="t-2 t-s-14">作业完成度</div>
  319. <div className="t-4 t-s-18">{data.previewProgress || 0}%</div>
  320. </div>
  321. </div>
  322. <div className="b-b m-t-1 m-b-1" />
  323. <div className="m-b-1">
  324. <div className="tag d-i-b t-s-16">
  325. <i style={{ background: '#6865FD' }} />
  326. 听课
  327. </div>
  328. <div className="tag d-i-b t-s-16">
  329. <i style={{ background: '#4299FF' }} />
  330. 预习
  331. </div>
  332. <div className="tag d-i-b t-s-16">
  333. <i style={{ background: '#E7E7E7' }} />
  334. 停课
  335. </div>
  336. </div>
  337. <DatePlane
  338. hideInput
  339. show={showTime}
  340. onChange={() => { }}
  341. disabledDate={(current) => {
  342. const date = current.format('YYYY-MM-DD');
  343. return data.stopTimeMap[date];
  344. }}
  345. dateRender={current => {
  346. const date = current.format('YYYY-MM-DD');
  347. return (
  348. <div className="ant-calendar-date">
  349. {current.get('date')}
  350. {data.courseTimeMap[date] && <i className="s1" style={{ background: '#6865FD' }} />}
  351. {data.previewTimeMap[date] && <i className="s2" style={{ background: '#4299FF' }} />}
  352. </div>
  353. );
  354. }}
  355. checkRefresh={(date, refresh) => {
  356. setTimeout(() => {
  357. refresh();
  358. }, 2000);
  359. return true;
  360. }}
  361. />
  362. <div className="t-2 t-s-12">*听课频率≤2天/课时,作业完成度≥90%,课程有效期可延长7-10天。</div>
  363. </Modal>
  364. <Modal
  365. show={showComment}
  366. title="评价"
  367. onConfirm={() => comment.content && this.submitComment()}
  368. onCancel={() => this.setState({ showComment: false, comment: {} })}
  369. >
  370. <textarea
  371. value={comment.content}
  372. className="b-c-1 w-10 p-10"
  373. rows={6}
  374. placeholder="您的看法对我们来说很重要!"
  375. onChange={e => {
  376. comment.content = e.target.value;
  377. this.setState({ comment });
  378. }}
  379. />
  380. <div className="b-b m-t-2" />
  381. </Modal>
  382. <Modal
  383. show={showFinish}
  384. title="提交成功"
  385. confirmText="好的,知道了"
  386. btnAlign="center"
  387. onConfirm={() => this.setState({ showFinish: false })}
  388. >
  389. <div className="t-2 t-s-18">
  390. <Icon type="check" className="t-5 m-r-5" />
  391. 您的每一次反馈都是千行进步的动力。
  392. </div>
  393. </Modal>
  394. <Modal
  395. show={showSuspend}
  396. title="申请停课"
  397. width={630}
  398. confirmText="立即停课"
  399. onConfirm={() => this.suspend()}
  400. onCancel={() => this.setState({ showSuspend: false })}
  401. >
  402. <div className="t-2 t-s-18">最长停课周期为1个月,到期后系统自动恢复至“学习中”状态。</div>
  403. <div className="t-2 t-s-18">每门课程只有一次申请机会,是否需要停课?</div>
  404. <div style={{ bottom: 20, left: 0 }} className="p-a t-3 t-s-14">
  405. *停课不影响听课频率的计算;
  406. </div>
  407. <div style={{ bottom: 0, left: 0 }} className="p-a t-3 t-s-14">
  408. {' '}
  409. 停课期间可随时恢复学习。
  410. </div>
  411. </Modal>
  412. <Modal
  413. show={showRestore}
  414. title="恢复学习"
  415. width={630}
  416. confirmText="立即恢复"
  417. onConfirm={() => this.restore()}
  418. onCancel={() => this.setState({ showRestore: false })}
  419. >
  420. <div className="t-2 t-s-18">恢复学习后,本课程的停课功能将无法使用。是否恢复学习?</div>
  421. </Modal>
  422. <Modal
  423. show={showUploadNote}
  424. title="上传笔记"
  425. width={630}
  426. confirmText="提交"
  427. onConfirm={() => this.submitAppointmentComment(note, 'note')}
  428. onCancel={() => this.setState({ showUploadNote: false, note: {} })}
  429. >
  430. <textarea
  431. className="b-c-1 w-10 p-10 m-b-1"
  432. rows={6}
  433. value={note.content}
  434. placeholder="请输入留言内容。"
  435. onChange={e => {
  436. note.content = e.target.value;
  437. this.setState({ note });
  438. }}
  439. />
  440. <div className={`t-c drag-upload ${this.state.draging ? 'draging' : ''}`}>
  441. <Button theme="file">上传文件</Button>
  442. <span className="m-l-1 t-3 t-s-14">{note.file ? `上传文件:${note.name} 成功` : '支持 docx, xls, PDF, rar, zip, PNG, JPG 等类型的文件'}</span>
  443. <div className="fixed">
  444. <span className="f-w-b t-s-18">放开文件立刻上传</span>
  445. </div>
  446. <FileUpload
  447. type="none"
  448. onDragEnter={() => this.setState({ draging: true })}
  449. onDragLeave={() => this.setState({ draging: false })}
  450. onUpload={({ file }) => Common.upload(file).then(result => {
  451. note.file = result.url;
  452. note.name = file.name;
  453. this.setState({ note });
  454. })}
  455. />
  456. </div>
  457. <div className="b-b m-t-2" />
  458. </Modal>
  459. <Modal
  460. show={showNote}
  461. title="课后笔记"
  462. width={500}
  463. height={500}
  464. onClose={() => this.setState({ showNote: false })}
  465. >
  466. {appointment.noteList &&
  467. appointment.noteList.map(row => {
  468. return <Note user={this.props.user} teacher={data.teacher} data={row} />;
  469. })}
  470. </Modal>
  471. <Modal
  472. show={showSupply}
  473. title="课后留言"
  474. width={500}
  475. height={500}
  476. onClose={() => this.setState({ showSupply: false })}
  477. >
  478. {appointment.supplyList &&
  479. appointment.supplyList.map(row => {
  480. return <Note user={this.props.user} teacher={data.teacher} data={row} />;
  481. })}
  482. </Modal>
  483. <Modal
  484. show={showUploadSupply}
  485. title="上传留言"
  486. width={630}
  487. confirmText="提交"
  488. onConfirm={() => this.submitAppointmentComment(supply, 'supply')}
  489. onCancel={() => this.setState({ showUploadSupply: false, supply: {} })}
  490. >
  491. <textarea
  492. className="b-c-1 w-10 p-10 m-b-1"
  493. rows={6}
  494. value={supply.content}
  495. placeholder="请输入留言内容。"
  496. onChange={e => {
  497. supply.content = e.target.value;
  498. this.setState({ supply });
  499. }}
  500. />
  501. <div className={`t-c drag-upload ${this.state.draging ? 'draging' : ''}`}>
  502. <Button theme="file">上传文件</Button>
  503. <span className="m-l-1 t-3 t-s-14">{supply.file ? `上传文件:${supply.name} 成功` : '支持 docx, xls, PDF, rar, zip, PNG, JPG 等类型的文件'}</span>
  504. <div className="fixed">
  505. <span className="f-w-b t-s-18">放开文件立刻上传</span>
  506. </div>
  507. <FileUpload
  508. type="none"
  509. onDragEnter={() => this.setState({ draging: true })}
  510. onDragLeave={() => this.setState({ draging: false })}
  511. onUpload={({ file }) => Common.upload(file).then(result => {
  512. supply.file = result.url;
  513. supply.name = file.name;
  514. this.setState({ supply });
  515. })}
  516. />
  517. </div>
  518. <div className="b-b m-t-2" />
  519. </Modal>
  520. </div>
  521. );
  522. }
  523. renderTabonline() {
  524. const { list = [] } = this.state;
  525. return list.map(item => {
  526. return (
  527. <CourseOnline
  528. data={item}
  529. user={this.props.user}
  530. refreshDetail={recordId => {
  531. this.refreshDetail(recordId);
  532. }}
  533. onRestore={() => {
  534. this.setState({ showRestore: true, restore: item });
  535. }}
  536. onSuspend={() => {
  537. this.setState({ showSuspend: true, suspend: item });
  538. }}
  539. onTime={() => {
  540. this.openTime(item);
  541. }}
  542. onComment={() => {
  543. this.setState({ showComment: true, comment: { channel: 'course-video', position: item.course.id } });
  544. }}
  545. closeCommentTips={() => this.closeCommentTips(item.id)}
  546. />
  547. );
  548. });
  549. }
  550. renderTabvs() {
  551. const { list = [] } = this.state;
  552. return list.map(item => {
  553. return <CourseVs data={item} user={this.props.user} refreshDetail={(recordId) => {
  554. this.refreshDetail(recordId);
  555. }} onRestore={() => {
  556. this.setState({ showRestore: true, restore: item });
  557. }} onSuspend={() => {
  558. this.setState({ showSuspend: true, suspend: item });
  559. }} onComment={() => {
  560. this.setState({ showComment: true, comment: { channel: 'course-vs', position: item.course.id } });
  561. }} closeCommentTips={() => this.closeCommentTips(item.id)}
  562. onUploadNote={(appointment, row) => this.setState({ showUploadNote: true, appointment, data: item, note: row || {} })}
  563. onUploadSupply={(appointment, row) => this.setState({ showUploadSupply: true, appointment, data: item, supply: row || {} })}
  564. onDeleteNote={(appointment, row) => this.deleteAppointmentComment(row)}
  565. onDeleteSupply={(appointment, row) => this.deleteAppointmentComment(row)}
  566. onNote={(appointment) => this.setState({ showNote: true, appointment, data: item })}
  567. onSupply={(appointment) => this.setState({ showSupply: true, appointment, data: item })}
  568. onUploadQuestion={(appointment, file, name) => this.submitQuestionFile({ id: appointment.id, recordId: appointment.recordId, questionFile: file, questionFileName: name })}
  569. setCCTalkName={(appointment, cctalkName) => this.setCCTalkName(appointment.recordId, cctalkName)} />;
  570. });
  571. }
  572. }
  573. class CourseOnline extends Component {
  574. constructor(props) {
  575. super(props);
  576. this.columns = [
  577. {
  578. title: '学习内容',
  579. key: 'title',
  580. render: (text, record) => {
  581. return `课时 ${record.no}: ${text}`;
  582. },
  583. },
  584. {
  585. title: '预习作业',
  586. key: 'paper',
  587. render: (text) => {
  588. const progress = text.report ? formatPercent(text.report.userNumber, text.report.questionNumber) : 0;
  589. const times = text.paper ? text.paper.times : 0;
  590. return <div>
  591. <div className="v-a-m d-i-b">
  592. <ProgressText width={50} size="small" times={times} progress={progress} unit="次" />
  593. </div>
  594. {!text.report && <IconButton className="m-l-2" type="start" tip="Start" onClick={() => {
  595. User.needLogin()
  596. .then(() => {
  597. Question.startLink('preview', text);
  598. });
  599. }} />}
  600. {text.report && !text.report.isFinish && <IconButton className="m-l-2" type="continue" tip="Continue" onClick={() => {
  601. User.needLogin()
  602. .then(() => {
  603. Question.continueLink('preview', text);
  604. });
  605. }} />}
  606. {text.report && !!text.report.isFinish && <IconButton className="m-l-2" type="restart" tip="Restart" onClick={() => {
  607. User.needLogin()
  608. .then(() => {
  609. Question.restart('preview', text);
  610. });
  611. }} />}
  612. {text.report && !!text.report.isFinish && <IconButton className="m-l-5" type="report" tip="Report" onClick={() => {
  613. User.needLogin()
  614. .then(() => {
  615. Question.reportLink('preview', text);
  616. });
  617. }} />}
  618. </div>;
  619. },
  620. },
  621. {
  622. title: '进度',
  623. key: 'progress',
  624. render: (text, record) => {
  625. const { paper = {} } = record;
  626. return `${paper.paper && paper.paper.times > 0 ? `${paper.paper.times}次+` : ''}${
  627. paper.report ? formatPercent(paper.report.userNumber, paper.report.questionNumber, false) : '0%'}`;
  628. },
  629. },
  630. {
  631. title: '最近学习',
  632. key: 'lastTime',
  633. render: (text, record) => {
  634. const { paper = {} } = record;
  635. return paper.report && formatDate(paper.report.updateTime, 'YYYY-MM-DD HH:mm:ss');
  636. },
  637. },
  638. {
  639. title: '笔记',
  640. key: 'note',
  641. render: () => {
  642. return <GIcon name="note" active />;
  643. },
  644. },
  645. {
  646. title: '问答',
  647. key: 'ask',
  648. render: (text, record) => {
  649. return (
  650. <Link to={`/course/ask/${record.courseId}?no=${record.id}`}>{`${record.answerNumber ||
  651. 0}/${record.askNumber || 0}`}</Link>
  652. );
  653. },
  654. },
  655. ];
  656. this.state = { open: props.data.status === 'ing' };
  657. }
  658. render() {
  659. const { data = {} } = this.props;
  660. switch (data.status) {
  661. case 'ing':
  662. return this.renderIng();
  663. case 'not':
  664. return this.renderNot();
  665. case 'end':
  666. return this.renderEnd();
  667. case 'suspend':
  668. return this.renderSuspend();
  669. default:
  670. return <div />;
  671. }
  672. }
  673. renderIng() {
  674. const { data, onTime, onComment, onSuspend } = this.props;
  675. const { open } = this.state;
  676. return (
  677. <div className="course-item ing">
  678. <div className="title">
  679. <div className="tag">学习中</div>
  680. <div className="text">{data.course.title}</div>
  681. <div className="right">
  682. <More
  683. menu={[{ label: '评价', key: 'comment' }, { label: '停课', key: 'suspend' }]}
  684. onClick={value => {
  685. const { key } = value;
  686. if (key === 'comment') {
  687. onComment();
  688. } else if (key === 'suspend') {
  689. onSuspend();
  690. }
  691. }}
  692. />
  693. </div>
  694. </div>
  695. {data.currentCourseNo && (
  696. <div
  697. className="continue"
  698. onClick={() => {
  699. linkTo(`/course/detail/${data.course.id}`);
  700. }}
  701. >
  702. <Assets name="notice" />
  703. 继续学习:课时 {data.currentNo}:{data.currentCourseNo.title}
  704. </div>
  705. )}
  706. <div className="detail">
  707. <div className="left">
  708. <Assets name="sun_blue" src={data.course.cover} />
  709. <div className="info">
  710. <div className="t1">授课老师</div>
  711. <div className="t2">{data.course.teacher}</div>
  712. <div className="t1">有效期</div>
  713. <div className="t2">{data.useExpireDays}Days</div>
  714. </div>
  715. </div>
  716. <div className="right">
  717. <div className="item">
  718. <GIcon name="speed-block" active noHover />
  719. <div className="text">
  720. <span>{data.currentNo}</span>/{data.courseNos.length}
  721. </div>
  722. </div>
  723. <div className="item">
  724. <GIcon name="question-block" active noHover />
  725. <div className="text">
  726. <span>{data.answerNumber}</span>/{data.askNumber}
  727. </div>
  728. </div>
  729. <div className="item" onClick={() => onTime()}>
  730. <GIcon name="clockin-block" active noHover />
  731. <div className="text">
  732. <span>{formatSeconds(data.totalTime)}</span> {data.previewProgress || 0}%
  733. </div>
  734. </div>
  735. <div className="item">
  736. <GIcon name="note-block" active noHover />
  737. <div className="text">{data.noteNumber}</div>
  738. </div>
  739. </div>
  740. <div className="open">
  741. <GIcon name={open ? 'up' : 'down'} onClick={() => this.setState({ open: !open })} />
  742. </div>
  743. </div>
  744. {open && this.renderTable()}
  745. </div>
  746. );
  747. }
  748. renderNot() {
  749. const { data } = this.props;
  750. return (
  751. <div className="course-item not">
  752. <div className="title">
  753. <div className="tag">未开通</div>
  754. <div className="text">{data.course.title}</div>
  755. </div>
  756. <div className="detail">
  757. <div className="left">
  758. <Assets name="sun_blue" src={data.course.cover} />
  759. <div className="info">
  760. <div className="t1">授课老师</div>
  761. <div className="t2">{data.course.teacher}</div>
  762. <div>
  763. <div className="d-i-b m-r-2">
  764. <div className="t-2">课时</div>
  765. <div className="t-1 t-s-16">{data.courseNos.length}</div>
  766. </div>
  767. <div className="d-i-b">
  768. <div className="t-2">总时长</div>
  769. <div className="t-1 t-s-16">{formatSeconds(data.courseTime)}</div>
  770. </div>
  771. </div>
  772. </div>
  773. </div>
  774. <div className="right t-c">
  775. <div className="text">请于 {formatDate(data.endTime, 'YYYY-MM-DD')} 前开通</div>
  776. <Button
  777. size="lager"
  778. radius
  779. onClick={() => {
  780. this.open(data.id);
  781. }}
  782. >
  783. 立即开通
  784. </Button>
  785. </div>
  786. </div>
  787. </div>
  788. );
  789. }
  790. renderEnd() {
  791. const { data, onTime, onComment } = this.props;
  792. const { open } = this.state;
  793. return (
  794. <div className="course-item end">
  795. <div className="title">
  796. <div className="tag">已结束</div>
  797. <div className="text">{data.course.title}</div>
  798. <div className="right">
  799. <More
  800. menu={[{ label: '评价', key: 'comment' }]}
  801. onClick={value => {
  802. const { key } = value;
  803. if (key === 'comment') {
  804. onComment();
  805. }
  806. }}
  807. />
  808. </div>
  809. </div>
  810. <div className="detail">
  811. <div className="left">
  812. <Assets name="sun_blue" src={data.course.cover} />
  813. <div className="info">
  814. <div className="t1">授课老师</div>
  815. <div className="t2">{data.course.teacher}</div>
  816. <div className="t1">有效期</div>
  817. <div className="t-s-12">{formatDate(data.useStartTime, 'YYYY-MM-DD')}<br />至{formatDate(data.useEndTime, 'YYYY-MM-DD')}</div>
  818. </div>
  819. </div>
  820. <div className="right">
  821. <div className="item">
  822. <GIcon name="question-block" active noHover />
  823. <div className="text">
  824. <span>{data.answerNumber}</span>/{data.askNumber}
  825. </div>
  826. </div>
  827. <div className="item" onClick={() => onTime()}>
  828. <GIcon name="clockin-block" active noHover />
  829. <div className="text">
  830. <span>{formatSeconds(data.totalTime)}</span> {data.previewProgress || 0}%
  831. </div>
  832. </div>
  833. <div className="item">
  834. <GIcon name="note-block" active noHover />
  835. <div className="text">{data.noteNumber}</div>
  836. </div>
  837. {data.courseAward > 0 && (
  838. <div className="item">
  839. <GIcon name="gift-block" active />
  840. <div className="text">赠送{data.courseAward}天</div>
  841. </div>
  842. )}
  843. </div>
  844. <div className="open">
  845. <GIcon name={open ? 'up' : 'down'} onClick={() => this.setState({ open: !open })} />
  846. </div>
  847. </div>
  848. {open && this.renderTable()}
  849. </div>
  850. );
  851. }
  852. renderSuspend() {
  853. const { data, onTime, onRestore, onComment } = this.props;
  854. const { open } = this.state;
  855. return (
  856. <div className="course-item end">
  857. <div className="title">
  858. <div className="tag">已停课</div>
  859. <div className="text">
  860. {data.course.title}
  861. <Button size="small" radius onClick={() => onRestore()}>
  862. 恢复上课
  863. </Button>
  864. </div>
  865. <div className="t-s-12">
  866. <span className="t-3">申请时间:</span>
  867. <span className="t-2 m-r-1">{formatDate(data.suspendTime, 'YYYY-MM-DD HH:mm:ss')}</span>
  868. <span className="t-3">已停课:</span>
  869. <span>{parseInt((new Date().getTime() - new Date(data.suspendTime).getTime()) / 86400000, 10)}Days/30</span>
  870. </div>
  871. <div className="right">
  872. <More
  873. menu={[{ label: '评价', key: 'comment' }]}
  874. onClick={value => {
  875. const { key } = value;
  876. if (key === 'comment') {
  877. onComment();
  878. }
  879. }}
  880. />
  881. </div>
  882. </div>
  883. <div className="detail">
  884. <div className="left">
  885. <Assets name="sun_blue" src={data.course.cover} />
  886. <div className="info">
  887. <div className="t1">授课老师</div>
  888. <div className="t2">{data.course.teacher}</div>
  889. <div className="t1">有效期</div>
  890. <div className="t2">{data.useExpireDays}Days</div>
  891. </div>
  892. </div>
  893. <div className="right">
  894. <div className="item">
  895. <GIcon name="speed-block" noHover />
  896. <div className="text">
  897. <span>{data.currentNo}</span>/{data.courseNos.length}
  898. </div>
  899. </div>
  900. <div className="item">
  901. <GIcon name="question-block" active noHover />
  902. <div className="text">
  903. <span>{data.answerNumber}</span>/{data.askNumber}
  904. </div>
  905. </div>
  906. <div className="item" onClick={() => onTime()}>
  907. <GIcon name="clockin-block" active noHover />
  908. <div className="text">
  909. <span>{formatSeconds(data.totalTime)}</span> {data.previewProgress || 0}%
  910. </div>
  911. </div>
  912. <div className="item">
  913. <GIcon name="note-block" active noHover />
  914. <div className="text">{data.noteNumber}</div>
  915. </div>
  916. </div>
  917. <div className="open">
  918. <GIcon name={open ? 'up' : 'down'} onClick={() => this.setState({ open: !open })} />
  919. </div>
  920. </div>
  921. {open && this.renderTable()}
  922. </div>
  923. );
  924. }
  925. renderTable() {
  926. const { data = {} } = this.props;
  927. const { courseNos = [] } = data;
  928. return <UserTable size="small" columns={this.columns} data={courseNos} />;
  929. }
  930. }
  931. const titleMap = {
  932. 1: '预约时间',
  933. 2: '答疑文档',
  934. 3: '上课',
  935. 4: '课后笔记',
  936. 5: '课后补充',
  937. 6: '备考信息',
  938. 7: '完成作业',
  939. };
  940. const iconMap = {
  941. 1: 'time-icon',
  942. 2: 'QA-icon',
  943. 3: 'class-icon',
  944. 4: 'note-icon',
  945. 5: 'supplement-icon',
  946. 6: 'information-icon',
  947. 7: 'homework-icon',
  948. };
  949. const statusMap = {
  950. 1: appointment => {
  951. if (!appointment.id) return 'end';
  952. return '';
  953. },
  954. 2: appointment => {
  955. if (!appointment.questionFile) return 'not';
  956. return '';
  957. },
  958. 3: (appointment, data) => {
  959. if (!data.cctalkName) return 'not';
  960. // if (new Date(appointment.endTime).getTime() > new Date()) return 'not';
  961. return '';
  962. },
  963. 4: (appointment) => {
  964. if (!appointment.noteList || appointment.noteList.length === 0) return 'not';
  965. return '';
  966. },
  967. 5: (appointment) => {
  968. if (!appointment.supplyList || appointment.supplyList.length === 0) return 'not';
  969. return '';
  970. },
  971. 6: () => {
  972. return '';
  973. },
  974. 7: (appointment) => {
  975. const { paper = {} } = appointment;
  976. if (!paper.report || formatPercent(paper.report.userNumber, paper.report.questionNumber) < 100) return 'not';
  977. return '';
  978. },
  979. };
  980. class CourseVs extends Component {
  981. constructor(props) {
  982. super(props);
  983. this.columns = {
  984. system: [
  985. { title: '学习内容', key: 'title' },
  986. {
  987. title: '预习作业',
  988. key: 'paper',
  989. render: (text) => {
  990. const progress = text.report ? formatPercent(text.report.userNumber, text.report.questionNumber) : 0;
  991. const times = text.paper ? text.paper.times : 0;
  992. return <div>
  993. <div className="v-a-m d-i-b">
  994. <ProgressText width={50} size="small" times={times} progress={progress} unit="次" />
  995. </div>
  996. {!text.report && <IconButton className="m-l-2" type="start" tip="Start" onClick={() => {
  997. User.needLogin()
  998. .then(() => {
  999. Question.startLink('preview', text);
  1000. });
  1001. }} />}
  1002. {text.report && !text.report.isFinish && <IconButton className="m-l-2" type="continue" tip="Continue" onClick={() => {
  1003. User.needLogin()
  1004. .then(() => {
  1005. Question.continueLink('preview', text);
  1006. });
  1007. }} />}
  1008. {text.report && !!text.report.isFinish && <IconButton className="m-l-2" type="restart" tip="Restart" onClick={() => {
  1009. User.needLogin()
  1010. .then(() => {
  1011. Question.restart('preview', text);
  1012. });
  1013. }} />}
  1014. {text.report && !!text.report.isFinish && <IconButton className="m-l-5" type="report" tip="Report" onClick={() => {
  1015. User.needLogin()
  1016. .then(() => {
  1017. Question.reportLink('preview', text);
  1018. });
  1019. }} />}
  1020. </div>;
  1021. },
  1022. },
  1023. {
  1024. title: '授课时间',
  1025. key: 'time',
  1026. render: (text, record) => {
  1027. return `${formatDate(record.startTime, 'YYYY-MM-DD HH:mm:ss')} ~ ${formatDate(record.endTime, 'HH:mm:ss')}`;
  1028. },
  1029. },
  1030. {
  1031. title: '课后笔记',
  1032. key: 'note',
  1033. render: (text, record) => {
  1034. return record.noteList ? <a onClick={() => this.props.onNote(record)}>查看</a> : <span>查看</span>;
  1035. },
  1036. },
  1037. {
  1038. title: '课后补充',
  1039. key: 'supply',
  1040. render: (text, record) => {
  1041. return record.supplyList ? <a onClick={() => this.props.onSupply(record)}>查看</a> : <span>查看</span>;
  1042. },
  1043. },
  1044. ],
  1045. answer: [
  1046. { title: '学习内容', key: 'title' },
  1047. {
  1048. title: '答疑文档',
  1049. key: 'questionFile',
  1050. render: (text, record) => {
  1051. return (
  1052. <a href={text} target="_blank">
  1053. {record.questionFileName}
  1054. </a>
  1055. );
  1056. },
  1057. },
  1058. {
  1059. title: '授课时间',
  1060. key: 'time',
  1061. render: (text, record) => {
  1062. return `${formatDate(record.startTime, 'YYYY-MM-DD HH:mm:ss')} ~ ${formatDate(record.endTime, 'HH:mm:ss')}`;
  1063. },
  1064. },
  1065. {
  1066. title: '课后补充',
  1067. key: 'supply',
  1068. render: (text, record) => {
  1069. return record.supplyList ? <a onClick={() => this.props.onSupply(record)}>查看</a> : <span>查看</span>;
  1070. },
  1071. },
  1072. ],
  1073. };
  1074. this.listMap = {
  1075. novice: [1, 3],
  1076. coach: [1, 6, 3],
  1077. system: [1, 7, 3, 4],
  1078. answer: [1, 2, 3],
  1079. };
  1080. const index = props.data.appointments.length - 1;
  1081. this.state = {
  1082. open: props.data.status === 'ing',
  1083. tab: 'ing',
  1084. index,
  1085. list: this.listMap[this.props.data.course.vsType],
  1086. showTips:
  1087. props.data.commentTips === 0 &&
  1088. (props.data.appointments.length === Math.ceil(props.data.number / 2) ||
  1089. props.data.appointments.length === props.data.number),
  1090. };
  1091. }
  1092. closeCommentTips() {
  1093. My.courseCommentTips(this.props.data.id).then(() => {
  1094. this.setState({ showTips: false });
  1095. });
  1096. }
  1097. render() {
  1098. const { data = {} } = this.props;
  1099. switch (data.status) {
  1100. case 'ing':
  1101. return this.renderIng();
  1102. case 'not':
  1103. return this.renderNot();
  1104. case 'end':
  1105. return this.renderEnd();
  1106. case 'suspend':
  1107. return this.renderSuspend();
  1108. default:
  1109. return <div />;
  1110. }
  1111. }
  1112. renderIng() {
  1113. const { data, onComment, closeCommentTips } = this.props;
  1114. const { tab, showTips, open } = this.state;
  1115. return (
  1116. <div className="education-item ing">
  1117. <div className="title">
  1118. <div className="tag">学习中</div>
  1119. <div className="text">
  1120. {data.course.title}
  1121. {data.vsNo > 0 ? `V${data.vsNo}` : ''}
  1122. {data.number > 0 ? `(${data.number}课时)` : ''}
  1123. </div>
  1124. <div className="right">
  1125. <More
  1126. menu={[{ label: '评价', key: 'comment' }, { label: '停课', key: 'suspend' }]}
  1127. onClick={value => {
  1128. const { key } = value;
  1129. if (key === 'comment') {
  1130. onComment();
  1131. }
  1132. }}
  1133. />
  1134. </div>
  1135. </div>
  1136. {showTips && <div className="continue">
  1137. <Icon className='close m-r-5 t-3' type="close-circle" theme="filled" onClick={() => closeCommentTips()} />
  1138. 课程已过半,可以来写写评价啦<a onClick={() => onComment()}>去写评价 ></a>
  1139. </div>}
  1140. <div className="detail">
  1141. <div className="left">
  1142. <Assets name="sun_blue" src={data.course.cover} />
  1143. <div className="info">
  1144. <div className="t1">授课老师</div>
  1145. <div className="t2">{data.teacher.realname}</div>
  1146. <div className="t1">有效期</div>
  1147. <div className="t2">{data.useExpireDays}Days</div>
  1148. </div>
  1149. </div>
  1150. <div className="right">
  1151. <div className="item">
  1152. <GIcon name="speed-block" active noHover />
  1153. <div className="text">
  1154. <span>{data.appointments.length}</span>/{data.number}
  1155. </div>
  1156. </div>
  1157. <div className="item">
  1158. <GIcon name="time-block" active noHover />
  1159. <div className="text">
  1160. <span>{data.appointments.length > 0 ? data.totalDays / data.appointments.length : 0}</span>/课时
  1161. </div>
  1162. </div>
  1163. </div>
  1164. <div className="open">
  1165. <GIcon name={open ? 'up' : 'down'} onClick={() => this.setState({ open: !open })} />
  1166. </div>
  1167. </div>
  1168. {open && (data.course.vsType === 'system' || data.course.vsType === 'answer') && <Tabs
  1169. className="t-l"
  1170. type="line"
  1171. theme="theme"
  1172. size="small"
  1173. width={80}
  1174. active={tab}
  1175. tabs={[{ key: 'ing', title: '授课中' }, { key: 'end', title: '已结课' }]}
  1176. onChange={key => this.setState({ tab: key })}
  1177. />}
  1178. {open && (tab === 'ing' ? this.renderTimeLine() : this.renderTable())}
  1179. </div>
  1180. );
  1181. }
  1182. renderNot() {
  1183. const { data } = this.props;
  1184. return (
  1185. <div className="education-item not">
  1186. <div className="title">
  1187. <div className="tag">未开通</div>
  1188. <div className="text">
  1189. {data.course.title}
  1190. {data.vsNo > 0 ? `V${data.vsNo}` : ''}
  1191. {data.number > 0 ? `(${data.number}课时)` : ''}
  1192. </div>
  1193. </div>
  1194. <div className="detail">
  1195. <div className="left">
  1196. <Assets name="sun_blue" src={data.course.cover} />
  1197. <div className="info">
  1198. <div className="t1">授课老师</div>
  1199. <div className="t2">{data.teacher.realname}</div>
  1200. <div>
  1201. <div className="d-i-b m-r-2">
  1202. <div className="t-2">课时</div>
  1203. <div className="t-1 t-s-16">{data.number}</div>
  1204. </div>
  1205. <div className="d-i-b">
  1206. <div className="t-2">总时长</div>
  1207. <div className="t-1 t-s-16">{data.number}Hours</div>
  1208. </div>
  1209. </div>
  1210. </div>
  1211. </div>
  1212. <div className="right">
  1213. <div className="qr-code">
  1214. <Assets name="qrcode" src={data.teacher.qr} />
  1215. <div className="i">
  1216. <div className="t1">请尽快与老师预约上课时间</div>
  1217. <div className="t2">请于 {formatDate(data.endTime, 'YYYY-MM-DD')} 前开通课程</div>
  1218. </div>
  1219. </div>
  1220. </div>
  1221. </div>
  1222. {/* {this.renderTimeLine()} */}
  1223. </div>
  1224. );
  1225. }
  1226. renderEnd() {
  1227. const { data, onComment, closeCommentTips } = this.props;
  1228. const { open, tab, showTips } = this.state;
  1229. return (
  1230. <div className="education-item end">
  1231. <div className="title">
  1232. <div className="tag">已结课</div>
  1233. <div className="text">
  1234. {data.course.title}
  1235. {data.vsNo > 0 ? `V${data.vsNo}` : ''}
  1236. {data.number > 0 ? `(${data.number}课时)` : ''}
  1237. </div>
  1238. <div className="right">
  1239. <More
  1240. menu={[{ label: '评价', key: 'comment' }]}
  1241. onClick={value => {
  1242. const { key } = value;
  1243. if (key === 'comment') {
  1244. onComment();
  1245. }
  1246. }}
  1247. />
  1248. </div>
  1249. </div>
  1250. {showTips && <div className="continue">
  1251. <Icon className='close m-r-5 t-3' type="close-circle" theme="filled" onClick={() => closeCommentTips()} />
  1252. 课程已结束,可以来写写评价啦<a onClick={() => onComment()}>去写评价 ></a>
  1253. </div>}
  1254. <div className="detail">
  1255. <div className="left">
  1256. <Assets name="sun_blue" src={data.course.cover} />
  1257. <div className="info">
  1258. <div className="t1">授课老师</div>
  1259. <div className="t2">{data.teacher.realname}</div>
  1260. <div className="t1">有效期</div>
  1261. <div className="t2">{data.useExpireDays}Days</div>
  1262. </div>
  1263. </div>
  1264. <div className="right">
  1265. <div className="item">
  1266. <GIcon name="speed-block" active noHover />
  1267. <div className="text">
  1268. <span>{data.appointments.length}</span>/{data.number}
  1269. </div>
  1270. </div>
  1271. <div className="item">
  1272. <GIcon name="time-block" active noHover />
  1273. <div className="text">
  1274. <span>{data.appointments.length > 0 ? data.totalDays / data.appointments.length : 0}</span>/课时
  1275. </div>
  1276. </div>
  1277. </div>
  1278. <div className="open">
  1279. <GIcon name={open ? 'up' : 'down'} onClick={() => this.setState({ open: !open })} />
  1280. </div>
  1281. </div>
  1282. {open && (data.course.vsType === 'system' || data.course.vsType === 'answer') && <Tabs
  1283. className="t-l"
  1284. type="line"
  1285. theme="theme"
  1286. size="small"
  1287. width={80}
  1288. active={tab}
  1289. tabs={[{ key: 'ing', title: '授课中' }, { key: 'end', title: '已结课' }]}
  1290. onChange={key => this.setState({ tab: key })}
  1291. />}
  1292. {open && (tab === 'ing' ? this.renderTimeLine() : this.renderTable())}
  1293. </div>
  1294. );
  1295. }
  1296. renderSuspend() {
  1297. const { data, onRestore, onComment } = this.props;
  1298. return (
  1299. <div className="education-item end">
  1300. <div className="title">
  1301. <div className="tag">已停课</div>
  1302. <div className="text">
  1303. {data.course.title}
  1304. {data.vsNo > 0 ? `V${data.vsNo}` : ''}
  1305. {data.number > 0 ? `(${data.number}课时)` : ''}
  1306. <Button size="small" onClick={() => onRestore()}>
  1307. 恢复上课
  1308. </Button>
  1309. </div>
  1310. <div className="t-s-12">
  1311. <span className="t-3">申请时间:</span>
  1312. <span className="t-2 m-r-1">{formatDate(data.suspendTime, 'YYYY-MM-DD HH:mm:ss')}</span>
  1313. <span className="t-3">已停课:</span>
  1314. <span>{parseInt((new Date().getTime() - new Date(data.suspendTime).getTime()) / 86400000, 10)}Days/30</span>
  1315. </div>
  1316. <div className="right">
  1317. <More
  1318. menu={[{ label: '评价', key: 'comment' }]}
  1319. onClick={value => {
  1320. const { key } = value;
  1321. if (key === 'comment') {
  1322. onComment();
  1323. }
  1324. }}
  1325. />
  1326. </div>
  1327. </div>
  1328. <div className="detail">
  1329. <div className="left">
  1330. <Assets name="sun_blue" src={data.course.cover} />
  1331. <div className="info">
  1332. <div className="t1">授课老师</div>
  1333. <div className="t2">{data.teacher.realname}</div>
  1334. <div className="t1">有效期</div>
  1335. <div className="t2">{data.useExpireDays}Days</div>
  1336. </div>
  1337. </div>
  1338. <div className="right">
  1339. <div className="item">
  1340. <GIcon name="speed-block" active noHover />
  1341. <div className="text">
  1342. <span>{data.appointments.length}</span>/{data.number}
  1343. </div>
  1344. </div>
  1345. <div className="item">
  1346. <GIcon name="time-block" active noHover />
  1347. <div className="text">
  1348. <span>{data.appointments.length > 0 ? data.totalDays / data.appointments.length : 0}</span>/课时
  1349. </div>
  1350. </div>
  1351. </div>
  1352. </div>
  1353. </div>
  1354. );
  1355. }
  1356. renderTimeLine() {
  1357. const { data = {}, onUploadNote, onUploadSupply, onUploadQuestion, setCCTalkName } = this.props;
  1358. const { list = [], index } = this.state;
  1359. const appointment = data.appointments[index] || {};
  1360. let status = '';
  1361. return [
  1362. <div className="class-hour">
  1363. {data.number > 1 && <div className="text">课时 {appointment.no}:{appointment.title}</div>}
  1364. {data.number > 1 && <div className="right">
  1365. <GIcon name="prev" onClick={() => this.setState({ index: index === 0 ? index : index - 1 })} />
  1366. <span>上一课时</span>
  1367. <span>下一课时</span>
  1368. <GIcon name="next" onClick={() => this.setState({ index: index >= data.appointments.length - 1 ? index : index + 1 })} />
  1369. </div>}
  1370. </div>,
  1371. <div className="time-line">
  1372. {list.map(item => {
  1373. if (status === '') {
  1374. // 上一阶段完成
  1375. status = statusMap[item](appointment, data);
  1376. } else {
  1377. // 上一阶段未完成
  1378. status = 'end';
  1379. }
  1380. return <TimeLineItem type={`${item}`} user={this.props.user} appointment={appointment} data={data} status={status} onUploadNote={onUploadNote} onUploadSupply={onUploadSupply} onUploadQuestion={onUploadQuestion} setCCTalkName={setCCTalkName} />;
  1381. })}
  1382. </div>,
  1383. ];
  1384. }
  1385. renderTable() {
  1386. const { data = {} } = this.props;
  1387. const { appointments = [] } = data;
  1388. return <UserTable size="small" columns={this.columns[data.course.vsType]} data={appointments.filter(row => row.id)} />;
  1389. }
  1390. }
  1391. class TimeLineItem extends Component {
  1392. constructor(props) {
  1393. super(props);
  1394. this.state = {};
  1395. }
  1396. onClick(key) {
  1397. const { onClick } = this.props;
  1398. const { status } = this.props;
  1399. if (status === 'not') return;
  1400. if (onClick) onClick(key);
  1401. }
  1402. render() {
  1403. const { type } = this.props;
  1404. const { status } = this.props;
  1405. return (
  1406. <div className={`time-line-item ${status}`}>
  1407. <div className="icon-title">
  1408. <GIcon name={iconMap[type]} active={!status} noHover />
  1409. <div className="title">{titleMap[type]}</div>
  1410. </div>
  1411. <div className="time-line-detail">{this.renderDetail()}</div>
  1412. </div>
  1413. );
  1414. }
  1415. renderDetail() {
  1416. const {
  1417. data = {},
  1418. appointment = {},
  1419. type,
  1420. onUploadNote,
  1421. onUploadSupply,
  1422. onDeleteNote,
  1423. onDeleteSupply,
  1424. onUploadQuestion,
  1425. setCCTalkName,
  1426. } = this.props;
  1427. const { status } = this.props;
  1428. const { paper } = appointment;
  1429. switch (type) {
  1430. case '1':
  1431. switch (status) {
  1432. case 'end':
  1433. case 'not':
  1434. return (
  1435. <span>
  1436. 请尽快与老师预约上课时间,老师微信:{data.teacher.wechat}扫码加微信{' '}
  1437. <Dropdown overlay={<Assets name="qrcode" src={data.teacher.qr} />}>
  1438. <span>
  1439. <Assets className="m-l-1" name="erweima" />
  1440. </span>
  1441. </Dropdown>
  1442. </span>
  1443. );
  1444. default:
  1445. return (
  1446. <span>
  1447. {formatDate(appointment.startTime, 'YYYY-MM-DD HH:mm:ss')} ~{' '}
  1448. {formatDate(appointment.endTime, 'HH:mm:ss')}
  1449. </span>
  1450. );
  1451. }
  1452. case '2':
  1453. switch (status) {
  1454. case 'end':
  1455. return <span className="link">点此上传</span>;
  1456. case 'not':
  1457. return <FileUpload onUpload={(file) => {
  1458. return Common.upload({ file }).then((result => onUploadQuestion(appointment, result.url, file.name)));
  1459. }}><span className="link">点此上传</span></FileUpload>;
  1460. default:
  1461. return (
  1462. <a href={appointment.questionFile} target="_blank">
  1463. {appointment.questionFileName || '文件'}
  1464. </a>
  1465. );
  1466. }
  1467. case '3':
  1468. switch (status) {
  1469. case 'end':
  1470. return data.cctalkName ? <span>
  1471. CCtalk 频道号 :{appointment.cctalkChannel} <a className="link" href="" target="_black">CC talk使用手册</a>
  1472. </span> : <div><input style={{ width: 200 }} className='b-c-1 p-l-1 p-r-1 t-s-12 m-r-1' placeholder="请输入CCtalk用户名查看授课频道" onChange={(e) => {
  1473. this.setState({ cctalkName: e.target.value });
  1474. }} /><Button size="small" radius disabled>提交</Button></div>;
  1475. case 'not':
  1476. return data.cctalkName ? <span>
  1477. CCtalk 频道号 :{appointment.cctalkChannel} <a className="link" href="" target="_black">CC talk使用手册</a>
  1478. </span> : <div><input style={{ width: 200 }} className='b-c-1 p-l-1 p-r-1 t-s-12 m-r-1' placeholder="请输入CCtalk用户名查看授课频道" onChange={(e) => {
  1479. this.setState({ cctalkName: e.target.value });
  1480. }} /><Button size="small" radius onClick={() => {
  1481. if (this.state.cctalkName) setCCTalkName(appointment, this.state.cctalkName);
  1482. }} >提交</Button></div>;
  1483. default:
  1484. return (
  1485. <span>
  1486. CCtalk 频道号 :{appointment.cctalkChannel}{' '}
  1487. <a className="link" href="" target="_black">
  1488. CC talk使用手册
  1489. </a>
  1490. </span>
  1491. );
  1492. }
  1493. case '4':
  1494. switch (status) {
  1495. case 'end':
  1496. return <span className="link">点此上传</span>;
  1497. case 'not':
  1498. return <span className="link" onClick={() => onUploadNote(appointment, { appointmentId: appointment.id, recordId: appointment.recordId })}>点此上传</span>;
  1499. default:
  1500. return (
  1501. <div>
  1502. <div>
  1503. <span className="link" onClick={() => onUploadNote(appointment, { appointmentId: appointment.id, recordId: appointment.recordId })}>点此上传</span>
  1504. </div>
  1505. <div className="note-list">
  1506. {appointment.noteList.map(row => {
  1507. console.log(row);
  1508. return <Note user={this.props.user} teacher={data.teacher} data={row} reply={!row.userId} actionList={row.userId ? [{ key: 'edit', label: '编辑' }, { key: 'delete', label: '删除' }] : null} onAction={(key) => {
  1509. switch (key) {
  1510. case 'edit':
  1511. onUploadNote(appointment, row);
  1512. break;
  1513. case 'delete':
  1514. onDeleteNote(appointment, row);
  1515. break;
  1516. case 'reply':
  1517. onUploadNote(appointment, { parentId: row.id, appointmentId: appointment.id, recordId: appointment.recordId });
  1518. break;
  1519. default:
  1520. }
  1521. }} />;
  1522. })}
  1523. </div>
  1524. </div>
  1525. );
  1526. }
  1527. case '5':
  1528. switch (status) {
  1529. case 'end':
  1530. return <span className="link">写留言</span>;
  1531. case 'not':
  1532. return <span className="link" onClick={() => onUploadSupply(appointment, { appointmentId: appointment.id, recordId: appointment.recordId })}>写留言</span>;
  1533. default:
  1534. return (
  1535. <div>
  1536. <div>
  1537. <span className="link" onClick={() => onUploadSupply(appointment, { appointmentId: appointment.id, recordId: appointment.recordId })}>写留言</span>
  1538. </div>
  1539. <div className="note-list">
  1540. {appointment.supplyList.map(row => {
  1541. return <Note user={this.props.user} teacher={data.teacher} data={row} reply={!row.userId} actionList={row.userId ? [{ key: 'edit', label: '编辑' }, { key: 'delete', label: '删除' }] : null} onAction={(key) => {
  1542. switch (key) {
  1543. case 'edit':
  1544. onUploadSupply(appointment, row);
  1545. break;
  1546. case 'delete':
  1547. onDeleteSupply(appointment, row);
  1548. break;
  1549. case 'reply':
  1550. onUploadSupply(appointment, { parentId: row.id, appointmentId: appointment.id, recordId: appointment.recordId });
  1551. break;
  1552. default:
  1553. }
  1554. }} />;
  1555. })}
  1556. </div>
  1557. </div>
  1558. );
  1559. }
  1560. case '6':
  1561. return [
  1562. <div>
  1563. <span>基本情况</span>
  1564. <a href={status !== 'end' ? '' : ''} className="link">
  1565. 填写
  1566. </a>
  1567. </div>,
  1568. <div>
  1569. <span>备考细节</span>
  1570. <a href={status !== 'end' ? '' : ''} className="link">
  1571. 填写
  1572. </a>
  1573. </div>,
  1574. ];
  1575. case '7':
  1576. return (
  1577. <ProgressText
  1578. progress={paper ? formatPercent(paper.report.userNumber, paper.report.questionNumber) : 0}
  1579. size="small"
  1580. />
  1581. );
  1582. default:
  1583. return <div />;
  1584. }
  1585. }
  1586. }