page.js 60 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861
  1. import React, { Component } from 'react';
  2. import { Link } from 'react-router-dom';
  3. import './index.less';
  4. import { Icon, Popover } 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
  312. show={showTime}
  313. className="clock-modal"
  314. title="打卡表"
  315. width={460}
  316. onClose={() => this.setState({ showTime: false })}
  317. >
  318. <div>
  319. <div className="d-i-b w-3">
  320. <div className="t-2 t-s-14">听课频率</div>
  321. <div className="t-4 t-s-18">{data.currentNo ? data.totalDays / data.currentNo : 0}天/课时</div>
  322. </div>
  323. <div className="d-i-b w-3">
  324. <div className="t-2 t-s-14">作业完成度</div>
  325. <div className="t-4 t-s-18">{data.previewProgress || 0}%</div>
  326. </div>
  327. </div>
  328. <div className="b-b m-t-1 m-b-1" />
  329. <div className="m-b-1">
  330. <div className="tag d-i-b t-s-16">
  331. <i style={{ background: '#6865FD' }} />
  332. 听课
  333. </div>
  334. <div className="tag d-i-b t-s-16">
  335. <i style={{ background: '#4299FF' }} />
  336. 预习
  337. </div>
  338. <div className="tag d-i-b t-s-16">
  339. <i style={{ background: '#E7E7E7' }} />
  340. 停课
  341. </div>
  342. </div>
  343. <DatePlane
  344. hideInput
  345. show={showTime}
  346. onChange={() => {}}
  347. disabledDate={current => {
  348. const date = current.format('YYYY-MM-DD');
  349. return data.stopTimeMap[date];
  350. }}
  351. dateRender={current => {
  352. const date = current.format('YYYY-MM-DD');
  353. return (
  354. <div className="ant-calendar-date">
  355. {current.get('date')}
  356. {data.courseTimeMap[date] && <i className="s1" style={{ background: '#6865FD' }} />}
  357. {data.previewTimeMap[date] && <i className="s2" style={{ background: '#4299FF' }} />}
  358. </div>
  359. );
  360. }}
  361. checkRefresh={(date, refresh) => {
  362. setTimeout(() => {
  363. refresh();
  364. }, 2000);
  365. return true;
  366. }}
  367. />
  368. <div className="t-2 t-s-12">*听课频率≤2天/课时,作业完成度≥90%,课程有效期可延长7-10天。</div>
  369. </Modal>
  370. <Modal
  371. show={showComment}
  372. title="评价"
  373. onConfirm={() => comment.content && this.submitComment()}
  374. onCancel={() => this.setState({ showComment: false, comment: {} })}
  375. >
  376. <textarea
  377. value={comment.content}
  378. className="b-c-1 w-10 p-10"
  379. rows={6}
  380. placeholder="您的看法对我们来说很重要!"
  381. onChange={e => {
  382. comment.content = e.target.value;
  383. this.setState({ comment });
  384. }}
  385. />
  386. <div className="b-b m-t-2" />
  387. </Modal>
  388. <Modal
  389. show={showFinish}
  390. title="提交成功"
  391. confirmText="好的,知道了"
  392. btnAlign="center"
  393. onConfirm={() => this.setState({ showFinish: false })}
  394. >
  395. <div className="t-2 t-s-18">
  396. <Icon type="check" className="t-5 m-r-5" />
  397. 您的每一次反馈都是千行进步的动力。
  398. </div>
  399. </Modal>
  400. <Modal
  401. show={showSuspend}
  402. title="申请停课"
  403. width={630}
  404. confirmText="立即停课"
  405. onConfirm={() => this.suspend()}
  406. onCancel={() => this.setState({ showSuspend: false })}
  407. >
  408. <div className="t-2 t-s-18">最长停课周期为1个月,到期后系统自动恢复至“学习中”状态。</div>
  409. <div className="t-2 t-s-18">每门课程只有一次申请机会,是否需要停课?</div>
  410. <div style={{ bottom: 20, left: 0 }} className="p-a t-3 t-s-14">
  411. *停课不影响听课频率的计算;
  412. </div>
  413. <div style={{ bottom: 0, left: 0 }} className="p-a t-3 t-s-14">
  414. {' '}
  415. 停课期间可随时恢复学习。
  416. </div>
  417. </Modal>
  418. <Modal
  419. show={showRestore}
  420. title="恢复学习"
  421. width={630}
  422. confirmText="立即恢复"
  423. onConfirm={() => this.restore()}
  424. onCancel={() => this.setState({ showRestore: false })}
  425. >
  426. <div className="t-2 t-s-18">恢复学习后,本课程的停课功能将无法使用。是否恢复学习?</div>
  427. </Modal>
  428. <Modal
  429. show={showUploadNote}
  430. title="上传笔记"
  431. width={630}
  432. confirmText="提交"
  433. onConfirm={() => this.submitAppointmentComment(note, 'note')}
  434. onCancel={() => this.setState({ showUploadNote: false, note: {} })}
  435. >
  436. <textarea
  437. className="b-c-1 w-10 p-10 m-b-1"
  438. rows={6}
  439. value={note.content}
  440. placeholder="请输入留言内容。"
  441. onChange={e => {
  442. note.content = e.target.value;
  443. this.setState({ note });
  444. }}
  445. />
  446. <div className={`t-c drag-upload ${this.state.draging ? 'draging' : ''}`}>
  447. <Button theme="file">上传文件</Button>
  448. <span className="m-l-1 t-3 t-s-14">
  449. {note.file ? `上传文件:${note.name} 成功` : '支持 docx, xls, PDF, rar, zip, PNG, JPG 等类型的文件'}
  450. </span>
  451. <div className="fixed">
  452. <span className="f-w-b t-s-18">放开文件立刻上传</span>
  453. </div>
  454. <FileUpload
  455. type="none"
  456. onDragEnter={() => this.setState({ draging: true })}
  457. onDragLeave={() => this.setState({ draging: false })}
  458. onUpload={({ file }) => {
  459. Common.upload(file).then(result => {
  460. note.file = result.url;
  461. note.name = file.name;
  462. this.setState({ note });
  463. });
  464. }}
  465. />
  466. </div>
  467. <div className="b-b m-t-2" />
  468. </Modal>
  469. <Modal
  470. show={showNote}
  471. title="课后笔记"
  472. width={500}
  473. height={500}
  474. onClose={() => this.setState({ showNote: false })}
  475. >
  476. {appointment.noteList &&
  477. appointment.noteList.map(row => {
  478. return <Note user={this.props.user} teacher={data.teacher} data={row} />;
  479. })}
  480. </Modal>
  481. <Modal
  482. show={showSupply}
  483. title="课后留言"
  484. width={500}
  485. height={500}
  486. onClose={() => this.setState({ showSupply: false })}
  487. >
  488. {appointment.supplyList &&
  489. appointment.supplyList.map(row => {
  490. return <Note user={this.props.user} teacher={data.teacher} data={row} />;
  491. })}
  492. </Modal>
  493. <Modal
  494. show={showUploadSupply}
  495. title="上传留言"
  496. width={630}
  497. confirmText="提交"
  498. onConfirm={() => this.submitAppointmentComment(supply, 'supply')}
  499. onCancel={() => this.setState({ showUploadSupply: false, supply: {} })}
  500. >
  501. <textarea
  502. className="b-c-1 w-10 p-10 m-b-1"
  503. rows={6}
  504. value={supply.content}
  505. placeholder="请输入留言内容。"
  506. onChange={e => {
  507. supply.content = e.target.value;
  508. this.setState({ supply });
  509. }}
  510. />
  511. <div className={`t-c drag-upload ${this.state.draging ? 'draging' : ''}`}>
  512. <Button theme="file">上传文件</Button>
  513. <span className="m-l-1 t-3 t-s-14">
  514. {supply.file ? `上传文件:${supply.name} 成功` : '支持 docx, xls, PDF, rar, zip, PNG, JPG 等类型的文件'}
  515. </span>
  516. <div className="fixed">
  517. <span className="f-w-b t-s-18">放开文件立刻上传</span>
  518. </div>
  519. <FileUpload
  520. type="none"
  521. onDragEnter={() => this.setState({ draging: true })}
  522. onDragLeave={() => this.setState({ draging: false })}
  523. onUpload={({ file }) => {
  524. Common.upload(file).then(result => {
  525. supply.file = result.url;
  526. supply.name = file.name;
  527. this.setState({ supply });
  528. });
  529. }}
  530. />
  531. </div>
  532. <div className="b-b m-t-2" />
  533. </Modal>
  534. </div>
  535. );
  536. }
  537. renderTabonline() {
  538. const { list = [] } = this.state;
  539. return list.map(item => {
  540. return (
  541. <CourseOnline
  542. data={item}
  543. user={this.props.user}
  544. refreshDetail={recordId => {
  545. this.refreshDetail(recordId);
  546. }}
  547. onRestore={() => {
  548. this.setState({ showRestore: true, restore: item });
  549. }}
  550. onSuspend={() => {
  551. this.setState({ showSuspend: true, suspend: item });
  552. }}
  553. onTime={() => {
  554. this.openTime(item);
  555. }}
  556. onComment={() => {
  557. this.setState({ showComment: true, comment: { channel: 'course-video', position: item.course.id } });
  558. }}
  559. closeCommentTips={() => this.closeCommentTips(item.id)}
  560. />
  561. );
  562. });
  563. }
  564. renderTabvs() {
  565. const { list = [] } = this.state;
  566. return list.map(item => {
  567. return (
  568. <CourseVs
  569. data={item}
  570. user={this.props.user}
  571. refreshDetail={recordId => {
  572. this.refreshDetail(recordId);
  573. }}
  574. onRestore={() => {
  575. this.setState({ showRestore: true, restore: item });
  576. }}
  577. onSuspend={() => {
  578. this.setState({ showSuspend: true, suspend: item });
  579. }}
  580. onComment={() => {
  581. this.setState({ showComment: true, comment: { channel: 'course-vs', position: item.course.id } });
  582. }}
  583. closeCommentTips={() => this.closeCommentTips(item.id)}
  584. onUploadNote={(appointment, row) => {
  585. this.setState({ showUploadNote: true, appointment, data: item, note: row || {} });
  586. }}
  587. onUploadSupply={(appointment, row) => {
  588. this.setState({ showUploadSupply: true, appointment, data: item, supply: row || {} });
  589. }}
  590. onDeleteNote={(appointment, row) => this.deleteAppointmentComment(row)}
  591. onDeleteSupply={(appointment, row) => this.deleteAppointmentComment(row)}
  592. onNote={appointment => this.setState({ showNote: true, appointment, data: item })}
  593. onSupply={appointment => this.setState({ showSupply: true, appointment, data: item })}
  594. onUploadQuestion={(appointment, file, name) => {
  595. this.submitQuestionFile({
  596. id: appointment.id,
  597. recordId: appointment.recordId,
  598. questionFile: file,
  599. questionFileName: name,
  600. });
  601. }}
  602. setCCTalkName={(appointment, cctalkName) => this.setCCTalkName(appointment.recordId, cctalkName)}
  603. />
  604. );
  605. });
  606. }
  607. }
  608. class CourseOnline extends Component {
  609. constructor(props) {
  610. super(props);
  611. this.columns = [
  612. {
  613. title: '学习内容',
  614. key: 'title',
  615. render: (text, record) => {
  616. return `课时 ${record.no}: ${text}`;
  617. },
  618. },
  619. {
  620. title: '预习作业',
  621. key: 'paper',
  622. render: text => {
  623. const progress = text.report ? formatPercent(text.report.userNumber, text.report.questionNumber) : 0;
  624. const times = text.paper ? text.paper.times : 0;
  625. return (
  626. <div>
  627. <div className="v-a-m d-i-b">
  628. <ProgressText width={50} size="small" times={times} progress={progress} unit="次" />
  629. </div>
  630. {!text.report && (
  631. <IconButton
  632. className="m-l-2"
  633. type="start"
  634. tip="Start"
  635. onClick={() => {
  636. User.needLogin().then(() => {
  637. Question.startLink('preview', text);
  638. });
  639. }}
  640. />
  641. )}
  642. {text.report && !text.report.isFinish && (
  643. <IconButton
  644. className="m-l-2"
  645. type="continue"
  646. tip="Continue"
  647. onClick={() => {
  648. User.needLogin().then(() => {
  649. Question.continueLink('preview', text);
  650. });
  651. }}
  652. />
  653. )}
  654. {text.report && !!text.report.isFinish && (
  655. <IconButton
  656. className="m-l-2"
  657. type="restart"
  658. tip="Restart"
  659. onClick={() => {
  660. User.needLogin().then(() => {
  661. Question.restart('preview', text);
  662. });
  663. }}
  664. />
  665. )}
  666. {text.report && !!text.report.isFinish && (
  667. <IconButton
  668. className="m-l-5"
  669. type="report"
  670. tip="Report"
  671. onClick={() => {
  672. User.needLogin().then(() => {
  673. Question.reportLink('preview', text);
  674. });
  675. }}
  676. />
  677. )}
  678. </div>
  679. );
  680. },
  681. },
  682. {
  683. title: '进度',
  684. key: 'progress',
  685. render: (text, record) => {
  686. const { paper = {} } = record;
  687. return `${paper.paper && paper.paper.times > 0 ? `${paper.paper.times}次+` : ''}${
  688. paper.report ? formatPercent(paper.report.userNumber, paper.report.questionNumber, false) : '0%'
  689. }`;
  690. },
  691. },
  692. {
  693. title: '最近学习',
  694. key: 'lastTime',
  695. render: (text, record) => {
  696. const { paper = {} } = record;
  697. return paper.report && formatDate(paper.report.updateTime, 'YYYY-MM-DD HH:mm:ss');
  698. },
  699. },
  700. {
  701. title: '笔记',
  702. key: 'note',
  703. render: () => {
  704. return <GIcon name="note" active />;
  705. },
  706. },
  707. {
  708. title: '问答',
  709. key: 'ask',
  710. render: (text, record) => {
  711. return (
  712. <Link to={`/course/ask/${record.courseId}?no=${record.id}`}>{`${record.answerNumber ||
  713. 0}/${record.askNumber || 0}`}</Link>
  714. );
  715. },
  716. },
  717. ];
  718. this.state = { open: props.data.status === 'ing' };
  719. }
  720. render() {
  721. const { data = {} } = this.props;
  722. switch (data.status) {
  723. case 'ing':
  724. return this.renderIng();
  725. case 'not':
  726. return this.renderNot();
  727. case 'end':
  728. return this.renderEnd();
  729. case 'suspend':
  730. return this.renderSuspend();
  731. default:
  732. return <div />;
  733. }
  734. }
  735. renderIng() {
  736. const { data, onTime, onComment, onSuspend } = this.props;
  737. const { open } = this.state;
  738. return (
  739. <div className="course-item ing">
  740. <div className="title">
  741. <div className="tag">学习中</div>
  742. <div className="text">{data.course.title}</div>
  743. <div className="right">
  744. <More
  745. menu={[{ label: '评价', key: 'comment' }, { label: '停课', key: 'suspend' }]}
  746. onClick={value => {
  747. const { key } = value;
  748. if (key === 'comment') {
  749. onComment();
  750. } else if (key === 'suspend') {
  751. onSuspend();
  752. }
  753. }}
  754. />
  755. </div>
  756. </div>
  757. {data.currentCourseNo && (
  758. <div
  759. className="continue"
  760. onClick={() => {
  761. linkTo(`/course/detail/${data.course.id}`);
  762. }}
  763. >
  764. <Assets name="notice" />
  765. 继续学习:课时 {data.currentNo}:{data.currentCourseNo.title}
  766. </div>
  767. )}
  768. <div className="detail">
  769. <div className="left">
  770. <Assets name="sun_blue" src={data.course.cover} />
  771. <div className="info">
  772. <div className="t1">授课老师</div>
  773. <div className="t2">{data.course.teacher}</div>
  774. <div className="t1">有效期</div>
  775. <div className="t2">{data.useExpireDays}Days</div>
  776. </div>
  777. </div>
  778. <div className="right">
  779. <div className="item">
  780. <GIcon name="speed-block" active noHover />
  781. <div className="text">
  782. <span>{data.currentNo}</span>/{data.courseNos.length}
  783. </div>
  784. </div>
  785. <div className="item">
  786. <GIcon name="question-block" active noHover />
  787. <div className="text">
  788. <span>{data.answerNumber}</span>/{data.askNumber}
  789. </div>
  790. </div>
  791. <div className="item" onClick={() => onTime()}>
  792. <GIcon name="clockin-block" active noHover />
  793. <div className="text">
  794. <span>{formatSeconds(data.totalTime)}</span> {data.previewProgress || 0}%
  795. </div>
  796. </div>
  797. <div className="item">
  798. <GIcon name="note-block" active noHover />
  799. <div className="text">{data.noteNumber}</div>
  800. </div>
  801. </div>
  802. <div className="open">
  803. <GIcon name={open ? 'up' : 'down'} onClick={() => this.setState({ open: !open })} />
  804. </div>
  805. </div>
  806. {open && this.renderTable()}
  807. </div>
  808. );
  809. }
  810. renderNot() {
  811. const { data } = this.props;
  812. return (
  813. <div className="course-item not">
  814. <div className="title">
  815. <div className="tag">未开通</div>
  816. <div className="text">{data.course.title}</div>
  817. </div>
  818. <div className="detail">
  819. <div className="left">
  820. <Assets name="sun_blue" src={data.course.cover} />
  821. <div className="info">
  822. <div className="t1">授课老师</div>
  823. <div className="t2">{data.course.teacher}</div>
  824. <div>
  825. <div className="d-i-b m-r-2">
  826. <div className="t-2">课时</div>
  827. <div className="t-1 t-s-16">{data.courseNos.length}</div>
  828. </div>
  829. <div className="d-i-b">
  830. <div className="t-2">总时长</div>
  831. <div className="t-1 t-s-16">{formatSeconds(data.courseTime)}</div>
  832. </div>
  833. </div>
  834. </div>
  835. </div>
  836. <div className="right t-c">
  837. <div className="text">请于 {formatDate(data.endTime, 'YYYY-MM-DD')} 前开通</div>
  838. <Button
  839. size="lager"
  840. radius
  841. onClick={() => {
  842. this.open(data.id);
  843. }}
  844. >
  845. 立即开通
  846. </Button>
  847. </div>
  848. </div>
  849. </div>
  850. );
  851. }
  852. renderEnd() {
  853. const { data, onTime, 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">{data.course.title}</div>
  860. <div className="right">
  861. <More
  862. menu={[{ label: '评价', key: 'comment' }]}
  863. onClick={value => {
  864. const { key } = value;
  865. if (key === 'comment') {
  866. onComment();
  867. }
  868. }}
  869. />
  870. </div>
  871. </div>
  872. <div className="detail">
  873. <div className="left">
  874. <Assets name="sun_blue" src={data.course.cover} />
  875. <div className="info">
  876. <div className="t1">授课老师</div>
  877. <div className="t2">{data.course.teacher}</div>
  878. <div className="t1">有效期</div>
  879. <div className="t-s-12">
  880. {formatDate(data.useStartTime, 'YYYY-MM-DD')}
  881. <br />至{formatDate(data.useEndTime, 'YYYY-MM-DD')}
  882. </div>
  883. </div>
  884. </div>
  885. <div className="right">
  886. <div className="item">
  887. <GIcon name="question-block" active noHover />
  888. <div className="text">
  889. <span>{data.answerNumber}</span>/{data.askNumber}
  890. </div>
  891. </div>
  892. <div className="item" onClick={() => onTime()}>
  893. <GIcon name="clockin-block" active noHover />
  894. <div className="text">
  895. <span>{formatSeconds(data.totalTime)}</span> {data.previewProgress || 0}%
  896. </div>
  897. </div>
  898. <div className="item">
  899. <GIcon name="note-block" active noHover />
  900. <div className="text">{data.noteNumber}</div>
  901. </div>
  902. {data.courseAward > 0 && (
  903. <div className="item">
  904. <GIcon name="gift-block" active />
  905. <div className="text">赠送{data.courseAward}天</div>
  906. </div>
  907. )}
  908. </div>
  909. <div className="open">
  910. <GIcon name={open ? 'up' : 'down'} onClick={() => this.setState({ open: !open })} />
  911. </div>
  912. </div>
  913. {open && this.renderTable()}
  914. </div>
  915. );
  916. }
  917. renderSuspend() {
  918. const { data, onTime, onRestore, onComment } = this.props;
  919. const { open } = this.state;
  920. return (
  921. <div className="course-item end">
  922. <div className="title">
  923. <div className="tag">已停课</div>
  924. <div className="text">
  925. {data.course.title}
  926. <Button size="small" radius onClick={() => onRestore()}>
  927. 恢复上课
  928. </Button>
  929. </div>
  930. <div className="t-s-12">
  931. <span className="t-3">申请时间:</span>
  932. <span className="t-2 m-r-1">{formatDate(data.suspendTime, 'YYYY-MM-DD HH:mm:ss')}</span>
  933. <span className="t-3">已停课:</span>
  934. <span>{parseInt((new Date().getTime() - new Date(data.suspendTime).getTime()) / 86400000, 10)}Days/30</span>
  935. </div>
  936. <div className="right">
  937. <More
  938. menu={[{ label: '评价', key: 'comment' }]}
  939. onClick={value => {
  940. const { key } = value;
  941. if (key === 'comment') {
  942. onComment();
  943. }
  944. }}
  945. />
  946. </div>
  947. </div>
  948. <div className="detail">
  949. <div className="left">
  950. <Assets name="sun_blue" src={data.course.cover} />
  951. <div className="info">
  952. <div className="t1">授课老师</div>
  953. <div className="t2">{data.course.teacher}</div>
  954. <div className="t1">有效期</div>
  955. <div className="t2">{data.useExpireDays}Days</div>
  956. </div>
  957. </div>
  958. <div className="right">
  959. <div className="item">
  960. <GIcon name="speed-block" noHover />
  961. <div className="text">
  962. <span>{data.currentNo}</span>/{data.courseNos.length}
  963. </div>
  964. </div>
  965. <div className="item">
  966. <GIcon name="question-block" active noHover />
  967. <div className="text">
  968. <span>{data.answerNumber}</span>/{data.askNumber}
  969. </div>
  970. </div>
  971. <div className="item" onClick={() => onTime()}>
  972. <GIcon name="clockin-block" active noHover />
  973. <div className="text">
  974. <span>{formatSeconds(data.totalTime)}</span> {data.previewProgress || 0}%
  975. </div>
  976. </div>
  977. <div className="item">
  978. <GIcon name="note-block" active noHover />
  979. <div className="text">{data.noteNumber}</div>
  980. </div>
  981. </div>
  982. <div className="open">
  983. <GIcon name={open ? 'up' : 'down'} onClick={() => this.setState({ open: !open })} />
  984. </div>
  985. </div>
  986. {open && this.renderTable()}
  987. </div>
  988. );
  989. }
  990. renderTable() {
  991. const { data = {} } = this.props;
  992. const { courseNos = [] } = data;
  993. return <UserTable size="small" columns={this.columns} data={courseNos} />;
  994. }
  995. }
  996. const titleMap = {
  997. 1: '预约时间',
  998. 2: '答疑文档',
  999. 3: '上课',
  1000. 4: '课后笔记',
  1001. 5: '课后补充',
  1002. 6: '备考信息',
  1003. 7: '完成作业',
  1004. };
  1005. const iconMap = {
  1006. 1: 'time-icon',
  1007. 2: 'QA-icon',
  1008. 3: 'class-icon',
  1009. 4: 'note-icon',
  1010. 5: 'supplement-icon',
  1011. 6: 'information-icon',
  1012. 7: 'homework-icon',
  1013. };
  1014. const statusMap = {
  1015. 1: appointment => {
  1016. if (!appointment.id) return 'end';
  1017. return '';
  1018. },
  1019. 2: appointment => {
  1020. if (!appointment.questionFile) return 'not';
  1021. return '';
  1022. },
  1023. 3: (appointment, data) => {
  1024. if (!data.cctalkName) return 'not';
  1025. // if (new Date(appointment.endTime).getTime() > new Date()) return 'not';
  1026. return '';
  1027. },
  1028. 4: appointment => {
  1029. if (!appointment.noteList || appointment.noteList.length === 0) return 'not';
  1030. return '';
  1031. },
  1032. 5: appointment => {
  1033. if (!appointment.supplyList || appointment.supplyList.length === 0) return 'not';
  1034. return '';
  1035. },
  1036. 6: () => {
  1037. return '';
  1038. },
  1039. 7: appointment => {
  1040. const { paper = {} } = appointment;
  1041. if (!paper.report || formatPercent(paper.report.userNumber, paper.report.questionNumber) < 100) return 'not';
  1042. return '';
  1043. },
  1044. };
  1045. class CourseVs extends Component {
  1046. constructor(props) {
  1047. super(props);
  1048. this.columns = {
  1049. system: [
  1050. { title: '学习内容', key: 'title' },
  1051. {
  1052. title: '预习作业',
  1053. key: 'paper',
  1054. render: text => {
  1055. const progress = text.report ? formatPercent(text.report.userNumber, text.report.questionNumber) : 0;
  1056. const times = text.paper ? text.paper.times : 0;
  1057. return (
  1058. <div>
  1059. <div className="v-a-m d-i-b">
  1060. <ProgressText width={50} size="small" times={times} progress={progress} unit="次" />
  1061. </div>
  1062. {!text.report && (
  1063. <IconButton
  1064. className="m-l-2"
  1065. type="start"
  1066. tip="Start"
  1067. onClick={() => {
  1068. User.needLogin().then(() => {
  1069. Question.startLink('preview', text);
  1070. });
  1071. }}
  1072. />
  1073. )}
  1074. {text.report && !text.report.isFinish && (
  1075. <IconButton
  1076. className="m-l-2"
  1077. type="continue"
  1078. tip="Continue"
  1079. onClick={() => {
  1080. User.needLogin().then(() => {
  1081. Question.continueLink('preview', text);
  1082. });
  1083. }}
  1084. />
  1085. )}
  1086. {text.report && !!text.report.isFinish && (
  1087. <IconButton
  1088. className="m-l-2"
  1089. type="restart"
  1090. tip="Restart"
  1091. onClick={() => {
  1092. User.needLogin().then(() => {
  1093. Question.restart('preview', text);
  1094. });
  1095. }}
  1096. />
  1097. )}
  1098. {text.report && !!text.report.isFinish && (
  1099. <IconButton
  1100. className="m-l-5"
  1101. type="report"
  1102. tip="Report"
  1103. onClick={() => {
  1104. User.needLogin().then(() => {
  1105. Question.reportLink('preview', text);
  1106. });
  1107. }}
  1108. />
  1109. )}
  1110. </div>
  1111. );
  1112. },
  1113. },
  1114. {
  1115. title: '授课时间',
  1116. key: 'time',
  1117. render: (text, record) => {
  1118. return `${formatDate(record.startTime, 'YYYY-MM-DD HH:mm:ss')} ~ ${formatDate(record.endTime, 'HH:mm:ss')}`;
  1119. },
  1120. },
  1121. {
  1122. title: '课后笔记',
  1123. key: 'note',
  1124. render: (text, record) => {
  1125. return record.noteList ? <a onClick={() => this.props.onNote(record)}>查看</a> : <span>查看</span>;
  1126. },
  1127. },
  1128. {
  1129. title: '课后补充',
  1130. key: 'supply',
  1131. render: (text, record) => {
  1132. return record.supplyList ? <a onClick={() => this.props.onSupply(record)}>查看</a> : <span>查看</span>;
  1133. },
  1134. },
  1135. ],
  1136. answer: [
  1137. { title: '学习内容', key: 'title' },
  1138. {
  1139. title: '答疑文档',
  1140. key: 'questionFile',
  1141. render: (text, record) => {
  1142. return (
  1143. <a href={text} target="_blank">
  1144. {record.questionFileName}
  1145. </a>
  1146. );
  1147. },
  1148. },
  1149. {
  1150. title: '授课时间',
  1151. key: 'time',
  1152. render: (text, record) => {
  1153. return `${formatDate(record.startTime, 'YYYY-MM-DD HH:mm:ss')} ~ ${formatDate(record.endTime, 'HH:mm:ss')}`;
  1154. },
  1155. },
  1156. {
  1157. title: '课后补充',
  1158. key: 'supply',
  1159. render: (text, record) => {
  1160. return record.supplyList ? <a onClick={() => this.props.onSupply(record)}>查看</a> : <span>查看</span>;
  1161. },
  1162. },
  1163. ],
  1164. };
  1165. this.listMap = {
  1166. novice: [1, 3],
  1167. coach: [1, 6, 3],
  1168. system: [1, 7, 3, 4],
  1169. answer: [1, 2, 3],
  1170. };
  1171. const index = props.data.appointments.length - 1;
  1172. this.state = {
  1173. open: props.data.status === 'ing',
  1174. tab: 'ing',
  1175. index,
  1176. list: this.listMap[this.props.data.course.vsType],
  1177. showTips:
  1178. props.data.commentTips === 0 &&
  1179. (props.data.appointments.length === Math.ceil(props.data.number / 2) ||
  1180. props.data.appointments.length === props.data.number),
  1181. };
  1182. }
  1183. closeCommentTips() {
  1184. My.courseCommentTips(this.props.data.id).then(() => {
  1185. this.setState({ showTips: false });
  1186. });
  1187. }
  1188. render() {
  1189. const { data = {} } = this.props;
  1190. switch (data.status) {
  1191. case 'ing':
  1192. return this.renderIng();
  1193. case 'not':
  1194. return this.renderNot();
  1195. case 'end':
  1196. return this.renderEnd();
  1197. case 'suspend':
  1198. return this.renderSuspend();
  1199. default:
  1200. return <div />;
  1201. }
  1202. }
  1203. renderIng() {
  1204. const { data, onComment, closeCommentTips } = this.props;
  1205. const { tab, showTips, open } = this.state;
  1206. return (
  1207. <div className="education-item ing">
  1208. <div className="title">
  1209. <div className="tag">学习中</div>
  1210. <div className="text">
  1211. {data.course.title}
  1212. {data.vsNo > 0 ? `V${data.vsNo}` : ''}
  1213. {data.number > 0 ? `(${data.number}课时)` : ''}
  1214. </div>
  1215. <div className="right">
  1216. <More
  1217. menu={[{ label: '评价', key: 'comment' }, { label: '停课', key: 'suspend' }]}
  1218. onClick={value => {
  1219. const { key } = value;
  1220. if (key === 'comment') {
  1221. onComment();
  1222. }
  1223. }}
  1224. />
  1225. </div>
  1226. </div>
  1227. {showTips && (
  1228. <div className="continue">
  1229. <Icon className="close m-r-5 t-3" type="close-circle" theme="filled" onClick={() => closeCommentTips()} />
  1230. 课程已过半,可以来写写评价啦<a onClick={() => onComment()}>去写评价 ></a>
  1231. </div>
  1232. )}
  1233. <div className="detail">
  1234. <div className="left">
  1235. <Assets name="sun_blue" src={data.course.cover} />
  1236. <div className="info">
  1237. <div className="t1">授课老师</div>
  1238. <div className="t2">{data.teacher.realname}</div>
  1239. <div className="t1">有效期</div>
  1240. <div className="t2">{data.useExpireDays}Days</div>
  1241. </div>
  1242. </div>
  1243. <div className="right">
  1244. <div className="item">
  1245. <GIcon name="speed-block" active noHover />
  1246. <div className="text">
  1247. <span>{data.appointments.length}</span>/{data.number}
  1248. </div>
  1249. </div>
  1250. <div className="item">
  1251. <GIcon name="time-block" active noHover />
  1252. <div className="text">
  1253. <span>{data.appointments.length > 0 ? data.totalDays / data.appointments.length : 0}</span>/课时
  1254. </div>
  1255. </div>
  1256. </div>
  1257. <div className="open">
  1258. <GIcon name={open ? 'up' : 'down'} onClick={() => this.setState({ open: !open })} />
  1259. </div>
  1260. </div>
  1261. {open && (data.course.vsType === 'system' || data.course.vsType === 'answer') && (
  1262. <Tabs
  1263. className="t-l"
  1264. type="line"
  1265. theme="theme"
  1266. size="small"
  1267. width={80}
  1268. active={tab}
  1269. tabs={[{ key: 'ing', title: '授课中' }, { key: 'end', title: '已结课' }]}
  1270. onChange={key => this.setState({ tab: key })}
  1271. />
  1272. )}
  1273. {open && (tab === 'ing' ? this.renderTimeLine() : this.renderTable())}
  1274. </div>
  1275. );
  1276. }
  1277. renderNot() {
  1278. const { data } = this.props;
  1279. return (
  1280. <div className="education-item not">
  1281. <div className="title">
  1282. <div className="tag">未开通</div>
  1283. <div className="text">
  1284. {data.course.title}
  1285. {data.vsNo > 0 ? `V${data.vsNo}` : ''}
  1286. {data.number > 0 ? `(${data.number}课时)` : ''}
  1287. </div>
  1288. </div>
  1289. <div className="detail">
  1290. <div className="left">
  1291. <Assets name="sun_blue" src={data.course.cover} />
  1292. <div className="info">
  1293. <div className="t1">授课老师</div>
  1294. <div className="t2">{data.teacher.realname}</div>
  1295. <div>
  1296. <div className="d-i-b m-r-2">
  1297. <div className="t-2">课时</div>
  1298. <div className="t-1 t-s-16">{data.number}</div>
  1299. </div>
  1300. <div className="d-i-b">
  1301. <div className="t-2">总时长</div>
  1302. <div className="t-1 t-s-16">{data.number}Hours</div>
  1303. </div>
  1304. </div>
  1305. </div>
  1306. </div>
  1307. <div className="right">
  1308. <div className="qr-code">
  1309. <Assets name="qrcode" src={data.teacher.qr} />
  1310. <div className="i">
  1311. <div className="t1">请尽快与老师预约上课时间</div>
  1312. <div className="t2">请于 {formatDate(data.endTime, 'YYYY-MM-DD')} 前开通课程</div>
  1313. </div>
  1314. </div>
  1315. </div>
  1316. </div>
  1317. {/* {this.renderTimeLine()} */}
  1318. </div>
  1319. );
  1320. }
  1321. renderEnd() {
  1322. const { data, onComment, closeCommentTips } = this.props;
  1323. const { open, tab, showTips } = this.state;
  1324. return (
  1325. <div className="education-item end">
  1326. <div className="title">
  1327. <div className="tag">已结课</div>
  1328. <div className="text">
  1329. {data.course.title}
  1330. {data.vsNo > 0 ? `V${data.vsNo}` : ''}
  1331. {data.number > 0 ? `(${data.number}课时)` : ''}
  1332. </div>
  1333. <div className="right">
  1334. <More
  1335. menu={[{ label: '评价', key: 'comment' }]}
  1336. onClick={value => {
  1337. const { key } = value;
  1338. if (key === 'comment') {
  1339. onComment();
  1340. }
  1341. }}
  1342. />
  1343. </div>
  1344. </div>
  1345. {showTips && (
  1346. <div className="continue">
  1347. <Icon className="close m-r-5 t-3" type="close-circle" theme="filled" onClick={() => closeCommentTips()} />
  1348. 课程已结束,可以来写写评价啦<a onClick={() => onComment()}>去写评价 ></a>
  1349. </div>
  1350. )}
  1351. <div className="detail">
  1352. <div className="left">
  1353. <Assets name="sun_blue" src={data.course.cover} />
  1354. <div className="info">
  1355. <div className="t1">授课老师</div>
  1356. <div className="t2">{data.teacher.realname}</div>
  1357. <div className="t1">有效期</div>
  1358. <div className="t2">{data.useExpireDays}Days</div>
  1359. </div>
  1360. </div>
  1361. <div className="right">
  1362. <div className="item">
  1363. <GIcon name="speed-block" active noHover />
  1364. <div className="text">
  1365. <span>{data.appointments.length}</span>/{data.number}
  1366. </div>
  1367. </div>
  1368. <div className="item">
  1369. <GIcon name="time-block" active noHover />
  1370. <div className="text">
  1371. <span>{data.appointments.length > 0 ? data.totalDays / data.appointments.length : 0}</span>/课时
  1372. </div>
  1373. </div>
  1374. </div>
  1375. <div className="open">
  1376. <GIcon name={open ? 'up' : 'down'} onClick={() => this.setState({ open: !open })} />
  1377. </div>
  1378. </div>
  1379. {open && (data.course.vsType === 'system' || data.course.vsType === 'answer') && (
  1380. <Tabs
  1381. className="t-l"
  1382. type="line"
  1383. theme="theme"
  1384. size="small"
  1385. width={80}
  1386. active={tab}
  1387. tabs={[{ key: 'ing', title: '授课中' }, { key: 'end', title: '已结课' }]}
  1388. onChange={key => this.setState({ tab: key })}
  1389. />
  1390. )}
  1391. {open && (tab === 'ing' ? this.renderTimeLine() : this.renderTable())}
  1392. </div>
  1393. );
  1394. }
  1395. renderSuspend() {
  1396. const { data, onRestore, onComment } = this.props;
  1397. return (
  1398. <div className="education-item end">
  1399. <div className="title">
  1400. <div className="tag">已停课</div>
  1401. <div className="text">
  1402. {data.course.title}
  1403. {data.vsNo > 0 ? `V${data.vsNo}` : ''}
  1404. {data.number > 0 ? `(${data.number}课时)` : ''}
  1405. <Button size="small" onClick={() => onRestore()}>
  1406. 恢复上课
  1407. </Button>
  1408. </div>
  1409. <div className="t-s-12">
  1410. <span className="t-3">申请时间:</span>
  1411. <span className="t-2 m-r-1">{formatDate(data.suspendTime, 'YYYY-MM-DD HH:mm:ss')}</span>
  1412. <span className="t-3">已停课:</span>
  1413. <span>{parseInt((new Date().getTime() - new Date(data.suspendTime).getTime()) / 86400000, 10)}Days/30</span>
  1414. </div>
  1415. <div className="right">
  1416. <More
  1417. menu={[{ label: '评价', key: 'comment' }]}
  1418. onClick={value => {
  1419. const { key } = value;
  1420. if (key === 'comment') {
  1421. onComment();
  1422. }
  1423. }}
  1424. />
  1425. </div>
  1426. </div>
  1427. <div className="detail">
  1428. <div className="left">
  1429. <Assets name="sun_blue" src={data.course.cover} />
  1430. <div className="info">
  1431. <div className="t1">授课老师</div>
  1432. <div className="t2">{data.teacher.realname}</div>
  1433. <div className="t1">有效期</div>
  1434. <div className="t2">{data.useExpireDays}Days</div>
  1435. </div>
  1436. </div>
  1437. <div className="right">
  1438. <div className="item">
  1439. <GIcon name="speed-block" active noHover />
  1440. <div className="text">
  1441. <span>{data.appointments.length}</span>/{data.number}
  1442. </div>
  1443. </div>
  1444. <div className="item">
  1445. <GIcon name="time-block" active noHover />
  1446. <div className="text">
  1447. <span>{data.appointments.length > 0 ? data.totalDays / data.appointments.length : 0}</span>/课时
  1448. </div>
  1449. </div>
  1450. </div>
  1451. </div>
  1452. </div>
  1453. );
  1454. }
  1455. renderTimeLine() {
  1456. const { data = {}, onUploadNote, onUploadSupply, onUploadQuestion, setCCTalkName } = this.props;
  1457. const { list = [], index } = this.state;
  1458. const appointment = data.appointments[index] || {};
  1459. let status = '';
  1460. return [
  1461. <div className="class-hour">
  1462. {data.number > 1 && (
  1463. <div className="text">
  1464. 课时 {appointment.no}:{appointment.title}
  1465. </div>
  1466. )}
  1467. {data.number > 1 && (
  1468. <div className="right">
  1469. <GIcon name="prev" onClick={() => this.setState({ index: index === 0 ? index : index - 1 })} />
  1470. <span>上一课时</span>
  1471. <span>下一课时</span>
  1472. <GIcon
  1473. name="next"
  1474. onClick={() => this.setState({ index: index >= data.appointments.length - 1 ? index : index + 1 })}
  1475. />
  1476. </div>
  1477. )}
  1478. </div>,
  1479. <div className="time-line">
  1480. {list.map(item => {
  1481. if (status === '') {
  1482. // 上一阶段完成
  1483. status = statusMap[item](appointment, data);
  1484. } else {
  1485. // 上一阶段未完成
  1486. status = 'end';
  1487. }
  1488. return (
  1489. <TimeLineItem
  1490. type={`${item}`}
  1491. user={this.props.user}
  1492. appointment={appointment}
  1493. data={data}
  1494. status={status}
  1495. onUploadNote={onUploadNote}
  1496. onUploadSupply={onUploadSupply}
  1497. onUploadQuestion={onUploadQuestion}
  1498. setCCTalkName={setCCTalkName}
  1499. />
  1500. );
  1501. })}
  1502. </div>,
  1503. ];
  1504. }
  1505. renderTable() {
  1506. const { data = {} } = this.props;
  1507. const { appointments = [] } = data;
  1508. return (
  1509. <UserTable size="small" columns={this.columns[data.course.vsType]} data={appointments.filter(row => row.id)} />
  1510. );
  1511. }
  1512. }
  1513. class TimeLineItem extends Component {
  1514. constructor(props) {
  1515. super(props);
  1516. this.state = {};
  1517. }
  1518. onClick(key) {
  1519. const { onClick } = this.props;
  1520. const { status } = this.props;
  1521. if (status === 'not') return;
  1522. if (onClick) onClick(key);
  1523. }
  1524. render() {
  1525. const { type } = this.props;
  1526. const { status } = this.props;
  1527. return (
  1528. <div className={`time-line-item ${status}`}>
  1529. <div className="icon-title">
  1530. <GIcon name={iconMap[type]} active={!status} noHover />
  1531. <div className="title">{titleMap[type]}</div>
  1532. </div>
  1533. <div className="time-line-detail">{this.renderDetail()}</div>
  1534. </div>
  1535. );
  1536. }
  1537. renderDetail() {
  1538. const {
  1539. data = {},
  1540. appointment = {},
  1541. type,
  1542. onUploadNote,
  1543. onUploadSupply,
  1544. onDeleteNote,
  1545. onDeleteSupply,
  1546. onUploadQuestion,
  1547. setCCTalkName,
  1548. } = this.props;
  1549. const { status } = this.props;
  1550. const { paper } = appointment;
  1551. switch (type) {
  1552. case '1':
  1553. switch (status) {
  1554. case 'end':
  1555. case 'not':
  1556. return (
  1557. <span>
  1558. 请尽快与老师预约上课时间,老师微信:{data.teacher.wechat}扫码加微信{' '}
  1559. <Popover content={<Assets name="qrcode" src={data.teacher.qr} />}>
  1560. <span>
  1561. <Assets className="m-l-1" name="erweima" />
  1562. </span>
  1563. </Popover>
  1564. </span>
  1565. );
  1566. default:
  1567. return (
  1568. <span>
  1569. {formatDate(appointment.startTime, 'YYYY-MM-DD HH:mm:ss')} ~{' '}
  1570. {formatDate(appointment.endTime, 'HH:mm:ss')}
  1571. </span>
  1572. );
  1573. }
  1574. case '2':
  1575. switch (status) {
  1576. case 'end':
  1577. return <span className="link">点此上传</span>;
  1578. case 'not':
  1579. return (
  1580. <FileUpload
  1581. onUpload={file => {
  1582. return Common.upload({ file }).then(result => onUploadQuestion(appointment, result.url, file.name));
  1583. }}
  1584. >
  1585. <span className="link">点此上传</span>
  1586. </FileUpload>
  1587. );
  1588. default:
  1589. return (
  1590. <a href={appointment.questionFile} target="_blank">
  1591. {appointment.questionFileName || '文件'}
  1592. </a>
  1593. );
  1594. }
  1595. case '3':
  1596. switch (status) {
  1597. case 'end':
  1598. return data.cctalkName ? (
  1599. <span>
  1600. CCtalk 频道号 :{appointment.cctalkChannel}{' '}
  1601. <a className="link" href="" target="_black">
  1602. CC talk使用手册
  1603. </a>
  1604. </span>
  1605. ) : (
  1606. <div>
  1607. <input
  1608. style={{ width: 200 }}
  1609. className="b-c-1 p-l-1 p-r-1 t-s-12 m-r-1"
  1610. placeholder="请输入CCtalk用户名查看授课频道"
  1611. onChange={e => {
  1612. this.setState({ cctalkName: e.target.value });
  1613. }}
  1614. />
  1615. <Button size="small" radius disabled>
  1616. 提交
  1617. </Button>
  1618. </div>
  1619. );
  1620. case 'not':
  1621. return data.cctalkName ? (
  1622. <span>
  1623. CCtalk 频道号 :{appointment.cctalkChannel}{' '}
  1624. <a className="link" href="" target="_black">
  1625. CC talk使用手册
  1626. </a>
  1627. </span>
  1628. ) : (
  1629. <div>
  1630. <input
  1631. style={{ width: 200 }}
  1632. className="b-c-1 p-l-1 p-r-1 t-s-12 m-r-1"
  1633. placeholder="请输入CCtalk用户名查看授课频道"
  1634. onChange={e => {
  1635. this.setState({ cctalkName: e.target.value });
  1636. }}
  1637. />
  1638. <Button
  1639. size="small"
  1640. radius
  1641. onClick={() => {
  1642. if (this.state.cctalkName) setCCTalkName(appointment, this.state.cctalkName);
  1643. }}
  1644. >
  1645. 提交
  1646. </Button>
  1647. </div>
  1648. );
  1649. default:
  1650. return (
  1651. <span>
  1652. CCtalk 频道号 :{appointment.cctalkChannel}{' '}
  1653. <a className="link" href="" target="_black">
  1654. CC talk使用手册
  1655. </a>
  1656. </span>
  1657. );
  1658. }
  1659. case '4':
  1660. switch (status) {
  1661. case 'end':
  1662. return <span className="link">点此上传</span>;
  1663. case 'not':
  1664. return (
  1665. <span
  1666. className="link"
  1667. onClick={() => {
  1668. onUploadNote(appointment, { appointmentId: appointment.id, recordId: appointment.recordId });
  1669. }}
  1670. >
  1671. 点此上传
  1672. </span>
  1673. );
  1674. default:
  1675. return (
  1676. <div>
  1677. <div>
  1678. <span
  1679. className="link"
  1680. onClick={() => {
  1681. onUploadNote(appointment, { appointmentId: appointment.id, recordId: appointment.recordId });
  1682. }}
  1683. >
  1684. 点此上传
  1685. </span>
  1686. </div>
  1687. <div className="note-list">
  1688. {appointment.noteList.map(row => {
  1689. return (
  1690. <Note
  1691. user={this.props.user}
  1692. teacher={data.teacher}
  1693. data={row}
  1694. reply={!row.userId}
  1695. actionList={
  1696. row.userId ? [{ key: 'edit', label: '编辑' }, { key: 'delete', label: '删除' }] : null
  1697. }
  1698. onAction={key => {
  1699. switch (key) {
  1700. case 'edit':
  1701. onUploadNote(appointment, row);
  1702. break;
  1703. case 'delete':
  1704. onDeleteNote(appointment, row);
  1705. break;
  1706. case 'reply':
  1707. onUploadNote(appointment, {
  1708. parentId: row.id,
  1709. appointmentId: appointment.id,
  1710. recordId: appointment.recordId,
  1711. });
  1712. break;
  1713. default:
  1714. }
  1715. }}
  1716. />
  1717. );
  1718. })}
  1719. </div>
  1720. </div>
  1721. );
  1722. }
  1723. case '5':
  1724. switch (status) {
  1725. case 'end':
  1726. return <span className="link">写留言</span>;
  1727. case 'not':
  1728. return (
  1729. <span
  1730. className="link"
  1731. onClick={() => {
  1732. onUploadSupply(appointment, { appointmentId: appointment.id, recordId: appointment.recordId });
  1733. }}
  1734. >
  1735. 写留言
  1736. </span>
  1737. );
  1738. default:
  1739. return (
  1740. <div>
  1741. <div>
  1742. <span
  1743. className="link"
  1744. onClick={() => {
  1745. onUploadSupply(appointment, { appointmentId: appointment.id, recordId: appointment.recordId });
  1746. }}
  1747. >
  1748. 写留言
  1749. </span>
  1750. </div>
  1751. <div className="note-list">
  1752. {appointment.supplyList.map(row => {
  1753. return (
  1754. <Note
  1755. user={this.props.user}
  1756. teacher={data.teacher}
  1757. data={row}
  1758. reply={!row.userId}
  1759. actionList={
  1760. row.userId ? [{ key: 'edit', label: '编辑' }, { key: 'delete', label: '删除' }] : null
  1761. }
  1762. onAction={key => {
  1763. switch (key) {
  1764. case 'edit':
  1765. onUploadSupply(appointment, row);
  1766. break;
  1767. case 'delete':
  1768. onDeleteSupply(appointment, row);
  1769. break;
  1770. case 'reply':
  1771. onUploadSupply(appointment, {
  1772. parentId: row.id,
  1773. appointmentId: appointment.id,
  1774. recordId: appointment.recordId,
  1775. });
  1776. break;
  1777. default:
  1778. }
  1779. }}
  1780. />
  1781. );
  1782. })}
  1783. </div>
  1784. </div>
  1785. );
  1786. }
  1787. case '6':
  1788. return [
  1789. <div>
  1790. <span>基本情况</span>
  1791. <a href={status !== 'end' ? '' : ''} className="link">
  1792. 填写
  1793. </a>
  1794. </div>,
  1795. <div>
  1796. <span>备考细节</span>
  1797. <a href={status !== 'end' ? '' : ''} className="link">
  1798. 填写
  1799. </a>
  1800. </div>,
  1801. ];
  1802. case '7':
  1803. return (
  1804. <ProgressText
  1805. progress={paper ? formatPercent(paper.report.userNumber, paper.report.questionNumber) : 0}
  1806. size="small"
  1807. />
  1808. );
  1809. default:
  1810. return <div />;
  1811. }
  1812. }
  1813. }