page.js 61 KB

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