page.js 62 KB

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