page.js 66 KB

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