page.js 62 KB

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