page.js 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639
  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. text = text || {};
  589. const progress = text.report ? formatPercent(text.report.userNumber, text.report.questionNumber) : 0;
  590. const times = text.paper ? text.paper.times : 0;
  591. return <div>
  592. <div className="v-a-m d-i-b">
  593. <ProgressText width={50} size="small" times={times} progress={progress} unit="次" />
  594. </div>
  595. {!text.report && <IconButton className="m-l-2" type="start" tip="Start" onClick={() => {
  596. User.needLogin()
  597. .then(() => {
  598. Question.startLink('preview', text);
  599. });
  600. }} />}
  601. {text.report && !text.report.isFinish && <IconButton className="m-l-2" type="continue" tip="Continue" onClick={() => {
  602. User.needLogin()
  603. .then(() => {
  604. Question.continueLink('preview', text);
  605. });
  606. }} />}
  607. {text.report && !!text.report.isFinish && <IconButton className="m-l-2" type="restart" tip="Restart" onClick={() => {
  608. User.needLogin()
  609. .then(() => {
  610. Question.restart('preview', text);
  611. });
  612. }} />}
  613. {text.report && !!text.report.isFinish && <IconButton className="m-l-5" type="report" tip="Report" onClick={() => {
  614. User.needLogin()
  615. .then(() => {
  616. Question.reportLink('preview', text);
  617. });
  618. }} />}
  619. </div>;
  620. },
  621. },
  622. {
  623. title: '进度',
  624. key: 'progress',
  625. render: (text, record) => {
  626. const { paper = {} } = record;
  627. return `${paper.paper && paper.paper.times > 0 ? `${paper.paper.times}次+` : ''}${
  628. paper.report ? formatPercent(paper.report.userNumber, paper.report.questionNumber, false) : '0%'}`;
  629. },
  630. },
  631. {
  632. title: '最近学习',
  633. key: 'lastTime',
  634. render: (text, record) => {
  635. const { paper = {} } = record;
  636. return paper.report && formatDate(paper.report.updateTime, 'YYYY-MM-DD HH:mm:ss');
  637. },
  638. },
  639. {
  640. title: '笔记',
  641. key: 'note',
  642. render: (text, record) => {
  643. return <GIcon name="note" active={record.note} />;
  644. },
  645. },
  646. {
  647. title: '问答',
  648. key: 'ask',
  649. render: (text, record) => {
  650. return (
  651. <Link to={`/course/ask/${record.courseId}?no=${record.id}`}>{`${record.answerNumber ||
  652. 0}/${record.askNumber || 0}`}</Link>
  653. );
  654. },
  655. },
  656. ];
  657. this.state = { open: props.data.status === 'ing' };
  658. }
  659. render() {
  660. const { data = {} } = this.props;
  661. switch (data.status) {
  662. case 'ing':
  663. return this.renderIng();
  664. case 'not':
  665. return this.renderNot();
  666. case 'end':
  667. return this.renderEnd();
  668. case 'suspend':
  669. return this.renderSuspend();
  670. default:
  671. return <div />;
  672. }
  673. }
  674. renderIng() {
  675. const { data, onTime, onComment, onSuspend } = this.props;
  676. const { open } = this.state;
  677. return (
  678. <div className="course-item ing">
  679. <div className="title">
  680. <div className="tag">学习中</div>
  681. <div className="text">{data.course.title}</div>
  682. <div className="right">
  683. <More
  684. menu={[{ label: '评价', key: 'comment' }, { label: '停课', key: 'suspend' }]}
  685. onClick={value => {
  686. const { key } = value;
  687. if (key === 'comment') {
  688. onComment();
  689. } else if (key === 'suspend') {
  690. onSuspend();
  691. }
  692. }}
  693. />
  694. </div>
  695. </div>
  696. {data.currentCourseNo && (
  697. <div
  698. className="continue"
  699. onClick={() => {
  700. linkTo(`/course/detail/${data.course.id}`);
  701. }}
  702. >
  703. <Assets name="notice" />
  704. 继续学习:课时 {data.currentNo}:{data.currentCourseNo.title}
  705. </div>
  706. )}
  707. <div className="detail">
  708. <div className="left">
  709. <Assets name="sun_blue" src={data.course.cover} />
  710. <div className="info">
  711. <div className="t1">授课老师</div>
  712. <div className="t2">{data.course.teacher}</div>
  713. <div className="t1">有效期</div>
  714. <div className="t2">{data.useExpireDays}Days</div>
  715. </div>
  716. </div>
  717. <div className="right">
  718. <div className="item">
  719. <GIcon name="speed-block" active noHover />
  720. <div className="text">
  721. <span>{data.currentNo}</span>/{data.courseNos.length}
  722. </div>
  723. </div>
  724. <div className="item">
  725. <GIcon name="question-block" active noHover />
  726. <div className="text">
  727. <span>{data.answerNumber}</span>/{data.askNumber}
  728. </div>
  729. </div>
  730. <div className="item" onClick={() => onTime()}>
  731. <GIcon name="clockin-block" active noHover />
  732. <div className="text">
  733. <span>{formatSeconds(data.totalTime)}</span> {data.previewProgress || 0}%
  734. </div>
  735. </div>
  736. <div className="item">
  737. <GIcon name="note-block" active noHover />
  738. <div className="text">{data.noteNumber}</div>
  739. </div>
  740. </div>
  741. <div className="open">
  742. <GIcon name={open ? 'up' : 'down'} onClick={() => this.setState({ open: !open })} />
  743. </div>
  744. </div>
  745. {open && this.renderTable()}
  746. </div>
  747. );
  748. }
  749. renderNot() {
  750. const { data } = this.props;
  751. return (
  752. <div className="course-item not">
  753. <div className="title">
  754. <div className="tag">未开通</div>
  755. <div className="text">{data.course.title}</div>
  756. </div>
  757. <div className="detail">
  758. <div className="left">
  759. <Assets name="sun_blue" src={data.course.cover} />
  760. <div className="info">
  761. <div className="t1">授课老师</div>
  762. <div className="t2">{data.course.teacher}</div>
  763. <div>
  764. <div className="d-i-b m-r-2">
  765. <div className="t-2">课时</div>
  766. <div className="t-1 t-s-16">{data.courseNos.length}</div>
  767. </div>
  768. <div className="d-i-b">
  769. <div className="t-2">总时长</div>
  770. <div className="t-1 t-s-16">{formatSeconds(data.courseTime)}</div>
  771. </div>
  772. </div>
  773. </div>
  774. </div>
  775. <div className="right t-c">
  776. <div className="text">请于 {formatDate(data.endTime, 'YYYY-MM-DD')} 前开通</div>
  777. <Button
  778. size="lager"
  779. radius
  780. onClick={() => {
  781. this.open(data.id);
  782. }}
  783. >
  784. 立即开通
  785. </Button>
  786. </div>
  787. </div>
  788. </div>
  789. );
  790. }
  791. renderEnd() {
  792. const { data, onTime, onComment } = this.props;
  793. const { open } = this.state;
  794. return (
  795. <div className="course-item end">
  796. <div className="title">
  797. <div className="tag">已结束</div>
  798. <div className="text">{data.course.title}</div>
  799. <div className="right">
  800. <More
  801. menu={[{ label: '评价', key: 'comment' }]}
  802. onClick={value => {
  803. const { key } = value;
  804. if (key === 'comment') {
  805. onComment();
  806. }
  807. }}
  808. />
  809. </div>
  810. </div>
  811. <div className="detail">
  812. <div className="left">
  813. <Assets name="sun_blue" src={data.course.cover} />
  814. <div className="info">
  815. <div className="t1">授课老师</div>
  816. <div className="t2">{data.course.teacher}</div>
  817. <div className="t1">有效期</div>
  818. <div className="t-s-12">{formatDate(data.useStartTime, 'YYYY-MM-DD')}<br />至{formatDate(data.useEndTime, 'YYYY-MM-DD')}</div>
  819. </div>
  820. </div>
  821. <div className="right">
  822. <div className="item">
  823. <GIcon name="question-block" active noHover />
  824. <div className="text">
  825. <span>{data.answerNumber}</span>/{data.askNumber}
  826. </div>
  827. </div>
  828. <div className="item" onClick={() => onTime()}>
  829. <GIcon name="clockin-block" active noHover />
  830. <div className="text">
  831. <span>{formatSeconds(data.totalTime)}</span> {data.previewProgress || 0}%
  832. </div>
  833. </div>
  834. <div className="item">
  835. <GIcon name="note-block" active noHover />
  836. <div className="text">{data.noteNumber}</div>
  837. </div>
  838. {data.courseAward > 0 && (
  839. <div className="item">
  840. <GIcon name="gift-block" active />
  841. <div className="text">赠送{data.courseAward}天</div>
  842. </div>
  843. )}
  844. </div>
  845. <div className="open">
  846. <GIcon name={open ? 'up' : 'down'} onClick={() => this.setState({ open: !open })} />
  847. </div>
  848. </div>
  849. {open && this.renderTable()}
  850. </div>
  851. );
  852. }
  853. renderSuspend() {
  854. const { data, onTime, onRestore, onComment } = this.props;
  855. const { open } = this.state;
  856. return (
  857. <div className="course-item end">
  858. <div className="title">
  859. <div className="tag">已停课</div>
  860. <div className="text">
  861. {data.course.title}
  862. <Button size="small" radius onClick={() => onRestore()}>
  863. 恢复上课
  864. </Button>
  865. </div>
  866. <div className="t-s-12">
  867. <span className="t-3">申请时间:</span>
  868. <span className="t-2 m-r-1">{formatDate(data.suspendTime, 'YYYY-MM-DD HH:mm:ss')}</span>
  869. <span className="t-3">已停课:</span>
  870. <span>{parseInt((new Date().getTime() - new Date(data.suspendTime).getTime()) / 86400000, 10)}Days/30</span>
  871. </div>
  872. <div className="right">
  873. <More
  874. menu={[{ label: '评价', key: 'comment' }]}
  875. onClick={value => {
  876. const { key } = value;
  877. if (key === 'comment') {
  878. onComment();
  879. }
  880. }}
  881. />
  882. </div>
  883. </div>
  884. <div className="detail">
  885. <div className="left">
  886. <Assets name="sun_blue" src={data.course.cover} />
  887. <div className="info">
  888. <div className="t1">授课老师</div>
  889. <div className="t2">{data.course.teacher}</div>
  890. <div className="t1">有效期</div>
  891. <div className="t2">{data.useExpireDays}Days</div>
  892. </div>
  893. </div>
  894. <div className="right">
  895. <div className="item">
  896. <GIcon name="speed-block" noHover />
  897. <div className="text">
  898. <span>{data.currentNo}</span>/{data.courseNos.length}
  899. </div>
  900. </div>
  901. <div className="item">
  902. <GIcon name="question-block" active noHover />
  903. <div className="text">
  904. <span>{data.answerNumber}</span>/{data.askNumber}
  905. </div>
  906. </div>
  907. <div className="item" onClick={() => onTime()}>
  908. <GIcon name="clockin-block" active noHover />
  909. <div className="text">
  910. <span>{formatSeconds(data.totalTime)}</span> {data.previewProgress || 0}%
  911. </div>
  912. </div>
  913. <div className="item">
  914. <GIcon name="note-block" active noHover />
  915. <div className="text">{data.noteNumber}</div>
  916. </div>
  917. </div>
  918. <div className="open">
  919. <GIcon name={open ? 'up' : 'down'} onClick={() => this.setState({ open: !open })} />
  920. </div>
  921. </div>
  922. {open && this.renderTable()}
  923. </div>
  924. );
  925. }
  926. renderTable() {
  927. const { data = {} } = this.props;
  928. const { courseNos = [] } = data;
  929. return <UserTable size="small" columns={this.columns} data={courseNos} />;
  930. }
  931. }
  932. const titleMap = {
  933. 1: '预约时间',
  934. 2: '答疑文档',
  935. 3: '上课',
  936. 4: '课后笔记',
  937. 5: '课后补充',
  938. 6: '备考信息',
  939. 7: '完成作业',
  940. };
  941. const iconMap = {
  942. 1: 'time-icon',
  943. 2: 'QA-icon',
  944. 3: 'class-icon',
  945. 4: 'note-icon',
  946. 5: 'supplement-icon',
  947. 6: 'information-icon',
  948. 7: 'homework-icon',
  949. };
  950. const statusMap = {
  951. 1: appointment => {
  952. if (!appointment.id) return 'end';
  953. return '';
  954. },
  955. 2: appointment => {
  956. if (!appointment.questionFile) return 'not';
  957. return '';
  958. },
  959. 3: (appointment, data) => {
  960. if (!data.cctalkName) return 'not';
  961. // if (new Date(appointment.endTime).getTime() > new Date()) return 'not';
  962. return '';
  963. },
  964. 4: (appointment) => {
  965. if (!appointment.noteList || appointment.noteList.length === 0) return 'not';
  966. return '';
  967. },
  968. 5: (appointment) => {
  969. if (!appointment.supplyList || appointment.supplyList.length === 0) return 'not';
  970. return '';
  971. },
  972. 6: () => {
  973. return '';
  974. },
  975. 7: (appointment) => {
  976. const { paper = {} } = appointment;
  977. if (!paper.report || formatPercent(paper.report.userNumber, paper.report.questionNumber) < 100) return 'not';
  978. return '';
  979. },
  980. };
  981. class CourseVs extends Component {
  982. constructor(props) {
  983. super(props);
  984. this.columns = {
  985. system: [
  986. { title: '学习内容', key: 'title' },
  987. {
  988. title: '预习作业',
  989. key: 'paper',
  990. render: (text) => {
  991. text = text || {};
  992. const progress = text.report ? formatPercent(text.report.userNumber, text.report.questionNumber) : 0;
  993. const times = text.paper ? text.paper.times : 0;
  994. return <div>
  995. <div className="v-a-m d-i-b">
  996. <ProgressText width={50} size="small" times={times} progress={progress} unit="次" />
  997. </div>
  998. {!text.report && <IconButton className="m-l-2" type="start" tip="Start" onClick={() => {
  999. User.needLogin()
  1000. .then(() => {
  1001. Question.startLink('preview', text);
  1002. });
  1003. }} />}
  1004. {text.report && !text.report.isFinish && <IconButton className="m-l-2" type="continue" tip="Continue" onClick={() => {
  1005. User.needLogin()
  1006. .then(() => {
  1007. Question.continueLink('preview', text);
  1008. });
  1009. }} />}
  1010. {text.report && !!text.report.isFinish && <IconButton className="m-l-2" type="restart" tip="Restart" onClick={() => {
  1011. User.needLogin()
  1012. .then(() => {
  1013. Question.restart('preview', text);
  1014. });
  1015. }} />}
  1016. {text.report && !!text.report.isFinish && <IconButton className="m-l-5" type="report" tip="Report" onClick={() => {
  1017. User.needLogin()
  1018. .then(() => {
  1019. Question.reportLink('preview', text);
  1020. });
  1021. }} />}
  1022. </div>;
  1023. },
  1024. },
  1025. {
  1026. title: '授课时间',
  1027. key: 'time',
  1028. render: (text, record) => {
  1029. return <div className="sub">
  1030. <div className="t-2 t-s-12">{formatDate(record.startTime, 'YYYY-MM-DD')}</div>
  1031. <div className="t-6 t-s-12">{formatDate(record.startTime, 'HH:mm:ss')} ~ {formatDate(record.endTime, 'HH:mm:ss')}</div>
  1032. </div>;
  1033. },
  1034. },
  1035. {
  1036. title: '课后笔记',
  1037. key: 'note',
  1038. render: (text, record) => {
  1039. return record.noteList && record.noteList.length > 0 ? <a onClick={() => this.props.onNote(record)}>查看</a> : <span>查看</span>;
  1040. },
  1041. },
  1042. {
  1043. title: '课后补充',
  1044. key: 'supply',
  1045. render: (text, record) => {
  1046. return record.supplyList && record.supplyList.length > 0 ? <a onClick={() => this.props.onSupply(record)}>查看</a> : <span>查看</span>;
  1047. },
  1048. },
  1049. ],
  1050. answer: [
  1051. { title: '学习内容', key: 'title' },
  1052. {
  1053. title: '答疑文档',
  1054. key: 'questionFile',
  1055. render: (text, record) => {
  1056. return (
  1057. <a href={text} target="_blank">
  1058. {record.questionFileName}
  1059. </a>
  1060. );
  1061. },
  1062. },
  1063. {
  1064. title: '授课时间',
  1065. key: 'time',
  1066. render: (text, record) => {
  1067. return `${formatDate(record.startTime, 'YYYY-MM-DD HH:mm:ss')} ~ ${formatDate(record.endTime, 'HH:mm:ss')}`;
  1068. },
  1069. },
  1070. {
  1071. title: '课后补充',
  1072. key: 'supply',
  1073. render: (text, record) => {
  1074. return record.supplyList ? <a onClick={() => this.props.onSupply(record)}>查看</a> : <span>查看</span>;
  1075. },
  1076. },
  1077. ],
  1078. };
  1079. this.listMap = {
  1080. novice: [1, 3],
  1081. coach: [1, 6, 3],
  1082. system: [1, 7, 3, 4],
  1083. answer: [1, 2, 3],
  1084. };
  1085. const index = props.data.appointments.length - 1;
  1086. this.state = {
  1087. open: props.data.status === 'ing',
  1088. tab: 'ing',
  1089. index,
  1090. list: this.listMap[this.props.data.course.vsType],
  1091. showTips:
  1092. props.data.commentTips === 0 &&
  1093. (props.data.appointments.length === Math.ceil(props.data.number / 2) ||
  1094. props.data.appointments.length === props.data.number),
  1095. };
  1096. }
  1097. closeCommentTips() {
  1098. My.courseCommentTips(this.props.data.id).then(() => {
  1099. this.setState({ showTips: false });
  1100. });
  1101. }
  1102. render() {
  1103. const { data = {} } = this.props;
  1104. switch (data.status) {
  1105. case 'ing':
  1106. return this.renderIng();
  1107. case 'not':
  1108. return this.renderNot();
  1109. case 'end':
  1110. return this.renderEnd();
  1111. case 'suspend':
  1112. return this.renderSuspend();
  1113. default:
  1114. return <div />;
  1115. }
  1116. }
  1117. renderIng() {
  1118. const { data, onComment, closeCommentTips } = this.props;
  1119. const { tab, showTips, open } = this.state;
  1120. return (
  1121. <div className="education-item ing">
  1122. <div className="title">
  1123. <div className="tag">学习中</div>
  1124. <div className="text">
  1125. {data.course.title}
  1126. {data.vsNo > 0 ? `V${data.vsNo}` : ''}
  1127. {data.number > 0 ? `(${data.number}课时)` : ''}
  1128. </div>
  1129. <div className="right">
  1130. <More
  1131. menu={[{ label: '评价', key: 'comment' }, { label: '停课', key: 'suspend' }]}
  1132. onClick={value => {
  1133. const { key } = value;
  1134. if (key === 'comment') {
  1135. onComment();
  1136. }
  1137. }}
  1138. />
  1139. </div>
  1140. </div>
  1141. {showTips && <div className="continue">
  1142. <Icon className='close m-r-5 t-3' type="close-circle" theme="filled" onClick={() => closeCommentTips()} />
  1143. 课程已过半,可以来写写评价啦<a onClick={() => onComment()}>去写评价 ></a>
  1144. </div>}
  1145. <div className="detail">
  1146. <div className="left">
  1147. <Assets name="sun_blue" src={data.course.cover} />
  1148. <div className="info">
  1149. <div className="t1">授课老师</div>
  1150. <div className="t2">{data.teacher.realname}</div>
  1151. <div className="t1">有效期</div>
  1152. <div className="t2">{data.useExpireDays}Days</div>
  1153. </div>
  1154. </div>
  1155. <div className="right">
  1156. <div className="item">
  1157. <GIcon name="speed-block" active noHover />
  1158. <div className="text">
  1159. <span>{data.appointments.length}</span>/{data.number}
  1160. </div>
  1161. </div>
  1162. <div className="item">
  1163. <GIcon name="time-block" active noHover />
  1164. <div className="text">
  1165. <span>{data.appointments.length > 0 ? data.totalDays / data.appointments.length : 0}</span>/课时
  1166. </div>
  1167. </div>
  1168. </div>
  1169. <div className="open">
  1170. <GIcon name={open ? 'up' : 'down'} onClick={() => this.setState({ open: !open })} />
  1171. </div>
  1172. </div>
  1173. {open && (data.course.vsType === 'system' || data.course.vsType === 'answer') && <Tabs
  1174. className="t-l"
  1175. type="line"
  1176. theme="theme"
  1177. size="small"
  1178. width={80}
  1179. active={tab}
  1180. tabs={[{ key: 'ing', title: '授课中' }, { key: 'end', title: '已结课' }]}
  1181. onChange={key => this.setState({ tab: key })}
  1182. />}
  1183. {open && (tab === 'ing' ? this.renderTimeLine() : this.renderTable())}
  1184. </div>
  1185. );
  1186. }
  1187. renderNot() {
  1188. const { data } = this.props;
  1189. return (
  1190. <div className="education-item not">
  1191. <div className="title">
  1192. <div className="tag">未开通</div>
  1193. <div className="text">
  1194. {data.course.title}
  1195. {data.vsNo > 0 ? `V${data.vsNo}` : ''}
  1196. {data.number > 0 ? `(${data.number}课时)` : ''}
  1197. </div>
  1198. </div>
  1199. <div className="detail">
  1200. <div className="left">
  1201. <Assets name="sun_blue" src={data.course.cover} />
  1202. <div className="info">
  1203. <div className="t1">授课老师</div>
  1204. <div className="t2">{data.teacher.realname}</div>
  1205. <div>
  1206. <div className="d-i-b m-r-2">
  1207. <div className="t-2">课时</div>
  1208. <div className="t-1 t-s-16">{data.number}</div>
  1209. </div>
  1210. <div className="d-i-b">
  1211. <div className="t-2">总时长</div>
  1212. <div className="t-1 t-s-16">{data.number}Hours</div>
  1213. </div>
  1214. </div>
  1215. </div>
  1216. </div>
  1217. <div className="right">
  1218. <div className="qr-code">
  1219. <Assets name="qrcode" src={data.teacher.qr} />
  1220. <div className="i">
  1221. <div className="t1">请尽快与老师预约上课时间</div>
  1222. <div className="t2">请于 {formatDate(data.endTime, 'YYYY-MM-DD')} 前开通课程</div>
  1223. </div>
  1224. </div>
  1225. </div>
  1226. </div>
  1227. {/* {this.renderTimeLine()} */}
  1228. </div>
  1229. );
  1230. }
  1231. renderEnd() {
  1232. const { data, onComment, closeCommentTips } = this.props;
  1233. const { open, tab, showTips } = this.state;
  1234. return (
  1235. <div className="education-item end">
  1236. <div className="title">
  1237. <div className="tag">已结课</div>
  1238. <div className="text">
  1239. {data.course.title}
  1240. {data.vsNo > 0 ? `V${data.vsNo}` : ''}
  1241. {data.number > 0 ? `(${data.number}课时)` : ''}
  1242. </div>
  1243. <div className="right">
  1244. <More
  1245. menu={[{ label: '评价', key: 'comment' }]}
  1246. onClick={value => {
  1247. const { key } = value;
  1248. if (key === 'comment') {
  1249. onComment();
  1250. }
  1251. }}
  1252. />
  1253. </div>
  1254. </div>
  1255. {showTips && <div className="continue">
  1256. <Icon className='close m-r-5 t-3' type="close-circle" theme="filled" onClick={() => closeCommentTips()} />
  1257. 课程已结束,可以来写写评价啦<a onClick={() => onComment()}>去写评价 ></a>
  1258. </div>}
  1259. <div className="detail">
  1260. <div className="left">
  1261. <Assets name="sun_blue" src={data.course.cover} />
  1262. <div className="info">
  1263. <div className="t1">授课老师</div>
  1264. <div className="t2">{data.teacher.realname}</div>
  1265. <div className="t1">有效期</div>
  1266. <div className="t2">{data.useExpireDays}Days</div>
  1267. </div>
  1268. </div>
  1269. <div className="right">
  1270. <div className="item">
  1271. <GIcon name="speed-block" active noHover />
  1272. <div className="text">
  1273. <span>{data.appointments.length}</span>/{data.number}
  1274. </div>
  1275. </div>
  1276. <div className="item">
  1277. <GIcon name="time-block" active noHover />
  1278. <div className="text">
  1279. <span>{data.appointments.length > 0 ? data.totalDays / data.appointments.length : 0}</span>/课时
  1280. </div>
  1281. </div>
  1282. </div>
  1283. <div className="open">
  1284. <GIcon name={open ? 'up' : 'down'} onClick={() => this.setState({ open: !open })} />
  1285. </div>
  1286. </div>
  1287. {open && (data.course.vsType === 'system' || data.course.vsType === 'answer') && <Tabs
  1288. className="t-l"
  1289. type="line"
  1290. theme="theme"
  1291. size="small"
  1292. width={80}
  1293. active={tab}
  1294. tabs={[{ key: 'ing', title: '授课中' }, { key: 'end', title: '已结课' }]}
  1295. onChange={key => this.setState({ tab: key })}
  1296. />}
  1297. {open && (tab === 'ing' ? this.renderTimeLine() : this.renderTable())}
  1298. </div>
  1299. );
  1300. }
  1301. renderSuspend() {
  1302. const { data, onRestore, onComment } = this.props;
  1303. return (
  1304. <div className="education-item end">
  1305. <div className="title">
  1306. <div className="tag">已停课</div>
  1307. <div className="text">
  1308. {data.course.title}
  1309. {data.vsNo > 0 ? `V${data.vsNo}` : ''}
  1310. {data.number > 0 ? `(${data.number}课时)` : ''}
  1311. <Button size="small" onClick={() => onRestore()}>
  1312. 恢复上课
  1313. </Button>
  1314. </div>
  1315. <div className="t-s-12">
  1316. <span className="t-3">申请时间:</span>
  1317. <span className="t-2 m-r-1">{formatDate(data.suspendTime, 'YYYY-MM-DD HH:mm:ss')}</span>
  1318. <span className="t-3">已停课:</span>
  1319. <span>{parseInt((new Date().getTime() - new Date(data.suspendTime).getTime()) / 86400000, 10)}Days/30</span>
  1320. </div>
  1321. <div className="right">
  1322. <More
  1323. menu={[{ label: '评价', key: 'comment' }]}
  1324. onClick={value => {
  1325. const { key } = value;
  1326. if (key === 'comment') {
  1327. onComment();
  1328. }
  1329. }}
  1330. />
  1331. </div>
  1332. </div>
  1333. <div className="detail">
  1334. <div className="left">
  1335. <Assets name="sun_blue" src={data.course.cover} />
  1336. <div className="info">
  1337. <div className="t1">授课老师</div>
  1338. <div className="t2">{data.teacher.realname}</div>
  1339. <div className="t1">有效期</div>
  1340. <div className="t2">{data.useExpireDays}Days</div>
  1341. </div>
  1342. </div>
  1343. <div className="right">
  1344. <div className="item">
  1345. <GIcon name="speed-block" active noHover />
  1346. <div className="text">
  1347. <span>{data.appointments.length}</span>/{data.number}
  1348. </div>
  1349. </div>
  1350. <div className="item">
  1351. <GIcon name="time-block" active noHover />
  1352. <div className="text">
  1353. <span>{data.appointments.length > 0 ? data.totalDays / data.appointments.length : 0}</span>/课时
  1354. </div>
  1355. </div>
  1356. </div>
  1357. </div>
  1358. </div>
  1359. );
  1360. }
  1361. renderTimeLine() {
  1362. const { data = {}, onUploadNote, onUploadSupply, onUploadQuestion, setCCTalkName } = this.props;
  1363. const { list = [], index } = this.state;
  1364. const appointment = data.appointments[index] || {};
  1365. let status = '';
  1366. return [
  1367. <div className="class-hour">
  1368. {data.number > 1 && <div className="text">课时 {appointment.no}:{appointment.title}</div>}
  1369. {data.number > 1 && <div className="right">
  1370. <GIcon name="prev" onClick={() => this.setState({ index: index === 0 ? index : index - 1 })} />
  1371. <span>上一课时</span>
  1372. <span>下一课时</span>
  1373. <GIcon name="next" onClick={() => this.setState({ index: index >= data.appointments.length - 1 ? index : index + 1 })} />
  1374. </div>}
  1375. </div>,
  1376. <div className="time-line">
  1377. {list.map(item => {
  1378. if (status === '') {
  1379. // 上一阶段完成
  1380. status = statusMap[item](appointment, data);
  1381. } else {
  1382. // 上一阶段未完成
  1383. status = 'end';
  1384. }
  1385. return <TimeLineItem type={`${item}`} user={this.props.user} appointment={appointment} data={data} status={status} onUploadNote={onUploadNote} onUploadSupply={onUploadSupply} onUploadQuestion={onUploadQuestion} setCCTalkName={setCCTalkName} />;
  1386. })}
  1387. </div>,
  1388. ];
  1389. }
  1390. renderTable() {
  1391. const { data = {} } = this.props;
  1392. const { appointments = [] } = data;
  1393. return <UserTable size="small" columns={this.columns[data.course.vsType]} data={appointments.filter(row => row.id)} />;
  1394. }
  1395. }
  1396. class TimeLineItem extends Component {
  1397. constructor(props) {
  1398. super(props);
  1399. this.state = {};
  1400. }
  1401. onClick(key) {
  1402. const { onClick } = this.props;
  1403. const { status } = this.props;
  1404. if (status === 'not') return;
  1405. if (onClick) onClick(key);
  1406. }
  1407. render() {
  1408. const { type } = this.props;
  1409. const { status } = this.props;
  1410. return (
  1411. <div className={`time-line-item ${status}`}>
  1412. <div className="icon-title">
  1413. <GIcon name={iconMap[type]} active={!status} noHover />
  1414. <div className="title">{titleMap[type]}</div>
  1415. </div>
  1416. <div className="time-line-detail">{this.renderDetail()}</div>
  1417. </div>
  1418. );
  1419. }
  1420. renderDetail() {
  1421. const {
  1422. data = {},
  1423. appointment = {},
  1424. type,
  1425. onUploadNote,
  1426. onUploadSupply,
  1427. onDeleteNote,
  1428. onDeleteSupply,
  1429. onUploadQuestion,
  1430. setCCTalkName,
  1431. } = this.props;
  1432. const { status } = this.props;
  1433. const { paper } = appointment;
  1434. switch (type) {
  1435. case '1':
  1436. switch (status) {
  1437. case 'end':
  1438. case 'not':
  1439. return (
  1440. <span>
  1441. 请尽快与老师预约上课时间,老师微信:{data.teacher.wechat}扫码加微信{' '}
  1442. <Dropdown overlay={<Assets name="qrcode" src={data.teacher.qr} />}>
  1443. <span>
  1444. <Assets className="m-l-1" name="erweima" />
  1445. </span>
  1446. </Dropdown>
  1447. </span>
  1448. );
  1449. default:
  1450. return (
  1451. <span>
  1452. {formatDate(appointment.startTime, 'YYYY-MM-DD HH:mm:ss')} ~{' '}
  1453. {formatDate(appointment.endTime, 'HH:mm:ss')}
  1454. </span>
  1455. );
  1456. }
  1457. case '2':
  1458. switch (status) {
  1459. case 'end':
  1460. return <span className="link">点此上传</span>;
  1461. case 'not':
  1462. return <FileUpload onUpload={(file) => {
  1463. return Common.upload({ file }).then((result => onUploadQuestion(appointment, result.url, file.name)));
  1464. }}><span className="link">点此上传</span></FileUpload>;
  1465. default:
  1466. return (
  1467. <a href={appointment.questionFile} target="_blank">
  1468. {appointment.questionFileName || '文件'}
  1469. </a>
  1470. );
  1471. }
  1472. case '3':
  1473. switch (status) {
  1474. case 'end':
  1475. return data.cctalkName ? <span>
  1476. CCtalk 频道号 :{appointment.cctalkChannel} <a className="link" href="" target="_black">CC talk使用手册</a>
  1477. </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) => {
  1478. this.setState({ cctalkName: e.target.value });
  1479. }} /><Button size="small" radius disabled>提交</Button></div>;
  1480. case 'not':
  1481. return data.cctalkName ? <span>
  1482. CCtalk 频道号 :{appointment.cctalkChannel} <a className="link" href="" target="_black">CC talk使用手册</a>
  1483. </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) => {
  1484. this.setState({ cctalkName: e.target.value });
  1485. }} /><Button size="small" radius onClick={() => {
  1486. if (this.state.cctalkName) setCCTalkName(appointment, this.state.cctalkName);
  1487. }} >提交</Button></div>;
  1488. default:
  1489. return (
  1490. <span>
  1491. CCtalk 频道号 :{appointment.cctalkChannel}{' '}
  1492. <a className="link" href="" target="_black">
  1493. CC talk使用手册
  1494. </a>
  1495. </span>
  1496. );
  1497. }
  1498. case '4':
  1499. switch (status) {
  1500. case 'end':
  1501. return <span className="link">点此上传</span>;
  1502. case 'not':
  1503. return <span className="link" onClick={() => onUploadNote(appointment, { appointmentId: appointment.id, recordId: appointment.recordId })}>点此上传</span>;
  1504. default:
  1505. return (
  1506. <div>
  1507. <div>
  1508. <span className="link" onClick={() => onUploadNote(appointment, { appointmentId: appointment.id, recordId: appointment.recordId })}>点此上传</span>
  1509. </div>
  1510. <div className="note-list">
  1511. {appointment.noteList.map(row => {
  1512. console.log(row);
  1513. 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) => {
  1514. switch (key) {
  1515. case 'edit':
  1516. onUploadNote(appointment, row);
  1517. break;
  1518. case 'delete':
  1519. onDeleteNote(appointment, row);
  1520. break;
  1521. case 'reply':
  1522. onUploadNote(appointment, { parentId: row.id, appointmentId: appointment.id, recordId: appointment.recordId });
  1523. break;
  1524. default:
  1525. }
  1526. }} />;
  1527. })}
  1528. </div>
  1529. </div>
  1530. );
  1531. }
  1532. case '5':
  1533. switch (status) {
  1534. case 'end':
  1535. return <span className="link">写留言</span>;
  1536. case 'not':
  1537. return <span className="link" onClick={() => onUploadSupply(appointment, { appointmentId: appointment.id, recordId: appointment.recordId })}>写留言</span>;
  1538. default:
  1539. return (
  1540. <div>
  1541. <div>
  1542. <span className="link" onClick={() => onUploadSupply(appointment, { appointmentId: appointment.id, recordId: appointment.recordId })}>写留言</span>
  1543. </div>
  1544. <div className="note-list">
  1545. {appointment.supplyList.map(row => {
  1546. 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) => {
  1547. switch (key) {
  1548. case 'edit':
  1549. onUploadSupply(appointment, row);
  1550. break;
  1551. case 'delete':
  1552. onDeleteSupply(appointment, row);
  1553. break;
  1554. case 'reply':
  1555. onUploadSupply(appointment, { parentId: row.id, appointmentId: appointment.id, recordId: appointment.recordId });
  1556. break;
  1557. default:
  1558. }
  1559. }} />;
  1560. })}
  1561. </div>
  1562. </div>
  1563. );
  1564. }
  1565. case '6':
  1566. return [
  1567. <div>
  1568. <span>基本情况</span>
  1569. <a href={status !== 'end' ? '' : ''} className="link">
  1570. 填写
  1571. </a>
  1572. </div>,
  1573. <div>
  1574. <span>备考细节</span>
  1575. <a href={status !== 'end' ? '' : ''} className="link">
  1576. 填写
  1577. </a>
  1578. </div>,
  1579. ];
  1580. case '7':
  1581. return (
  1582. <ProgressText
  1583. progress={paper ? formatPercent(paper.report.userNumber, paper.report.questionNumber) : 0}
  1584. size="small"
  1585. />
  1586. );
  1587. default:
  1588. return <div />;
  1589. }
  1590. }
  1591. }