Browse Source

feat(front): 课程详情

Go 5 years ago
parent
commit
90c3e605a9
24 changed files with 738 additions and 205 deletions
  1. 3 0
      front/project/Constant.js
  2. 5 6
      front/project/www/components/Other/index.js
  3. 9 3
      front/project/www/components/OtherModal/index.js
  4. 39 15
      front/project/www/components/Video/index.js
  5. 19 0
      front/project/www/components/Video/index.less
  6. 1 1
      front/project/www/routes/course/answer/page.js
  7. 30 33
      front/project/www/routes/course/dataDetail/page.js
  8. 2 2
      front/project/www/routes/course/detail/index.less
  9. 380 93
      front/project/www/routes/course/detail/page.js
  10. 13 4
      front/project/www/routes/course/main/page.js
  11. 4 4
      front/project/www/routes/course/online/index.less
  12. 7 3
      front/project/www/routes/course/online/page.js
  13. 38 28
      front/project/www/routes/course/vs/page.js
  14. 1 1
      front/project/www/routes/my/course/page.js
  15. 2 0
      server/gateway-api/src/main/java/com/qxgmat/controller/admin/UserController.java
  16. 75 3
      server/gateway-api/src/main/java/com/qxgmat/controller/api/CourseController.java
  17. 1 1
      server/gateway-api/src/main/java/com/qxgmat/controller/api/MyController.java
  18. 72 4
      server/gateway-api/src/main/java/com/qxgmat/dto/response/CourseDetailDto.java
  19. 13 1
      server/gateway-api/src/main/java/com/qxgmat/service/UserNoteCourseService.java
  20. 0 2
      server/gateway-api/src/main/java/com/qxgmat/service/extend/MessageExtendService.java
  21. 4 0
      server/gateway-api/src/main/java/com/qxgmat/service/extend/PreviewService.java
  22. 9 0
      server/gateway-api/src/main/java/com/qxgmat/service/inline/UserAskCourseService.java
  23. 10 0
      server/gateway-api/src/main/java/com/qxgmat/service/inline/UserCourseProgressService.java
  24. 1 1
      server/gateway-api/src/main/resources/application.yml

+ 3 - 0
front/project/Constant.js

@@ -155,6 +155,9 @@ export const FaqChannel = [
   { label: '非CAT', value: 'base', parent: 'examination' },
   { label: '数学机经', value: 'textbook' },
   { label: '换库', value: 'library' },
+  { label: '换库知识', value: 'baselibrary', parent: 'library' },
+  { label: '机经知识', value: 'basetextbook', parent: 'library' },
+  { label: '千行机经', value: 'qxtextbook', parent: 'library' },
   { label: '课堂-课程', value: 'course' },
   { label: '首页', value: 'index', parent: 'course' },
   { label: '视频首页', value: 'video_index', parent: 'course' },

+ 5 - 6
front/project/www/components/Other/index.js

@@ -171,16 +171,15 @@ export class Contact extends Component {
 
 export class Comment extends Component {
   render() {
+    const { data } = this.props;
     return (
       <div className="comment-item">
-        <Assets className="m-r-1" src="" />
+        <Assets className="m-r-1" src={data.user ? data.user.avatar : data.avatar} />
         <div className="d-i-b">
-          <div className="t-1 t-s-18">王大锤</div>
-          <div className="t-3">2018-10-20</div>
-        </div>
-        <div className="t-1 t-s-18 m-t-1">
-          掌握了学习方法以后,再加上刻苦勤奋的练习,第一次考试就过关了。掌握了学习方法以后,再加上刻苦勤奋的练习,第一次考试就过关了掌握了学习方法以后,再加上刻苦勤奋的练习,第一次考试就过关了。
+          <div className="t-1 t-s-18">{data.user ? data.user.nickname : data.nickname}</div>
+          <div className="t-3">{formatDate(data.createTime, 'YYYY-MM-DD')}</div>
         </div>
+        <div className="t-1 t-s-18 m-t-1">{data.content}</div>
       </div>
     );
   }

+ 9 - 3
front/project/www/components/OtherModal/index.js

@@ -818,7 +818,10 @@ export class AskCourseModal extends Component {
         onCancel={() => this.onCancel()}
       >
         <div className="t-2 m-b-1 t-s-16">
-          针对<span className="t-4">课时{courseNo.no}</span>的 <Select value={data.position} theme="white" list={selectList} />
+          针对<span className="t-4">课时{courseNo.no}</span>的 <Select value={data.position} theme="white" list={selectList} onChange={(item) => {
+          data.position = item.key;
+          this.setState({ data });
+        }} />
           进行提问.
         </div>
         <div className="t-2 t-s-16">老师讲解的内容是:</div>
@@ -882,7 +885,7 @@ export class CourseNoteModal extends Component {
   }
 
   render() {
-    const { show, course = {}, courseNos = [] } = this.props;
+    const { show, course = {}, courseNos = [], noteMap = {} } = this.props;
     const { data } = this.state;
     return (
       <Modal
@@ -901,7 +904,10 @@ export class CourseNoteModal extends Component {
               key: row.id,
             };
           })} onChange={(item) => {
-            data.courseNoId = item.id;
+            if (data.courseNoId !== item.key) {
+              data.courseNoId = item.key;
+              data.content = noteMap[item.key] ? noteMap[item.key].content : '';
+            }
             this.setState({ data });
           }} />
         </div>

+ 39 - 15
front/project/www/components/Video/index.js

@@ -81,16 +81,38 @@ export default class Video extends Component {
     }
   }
 
+  refreshTimeUpdate() {
+    if (this.timeInterval) {
+      clearInterval(this.timeInterval);
+      this.timeInterval = null;
+    }
+    this.timeInterval = setInterval(() => {
+      const { onTimeUpdate } = this.props;
+      if (onTimeUpdate) onTimeUpdate(this.player.currentTime());
+    }, this.props.duration ? this.props.duration * 1000 : 1000);
+  }
+
   onPlay() {
     if (!this.ready) return;
+    const { onPlay } = this.props;
     this.player.play();
     this.setState({ playing: true });
+    if (onPlay) onPlay();
   }
 
-  onPuase() {
+  onPause() {
     if (!this.ready) return;
+    const { onPause } = this.props;
     this.player.pause();
     this.setState({ playing: false });
+    if (onPause) onPause();
+  }
+
+  onNext() {
+    const { onNext } = this.props;
+    this.player.pause();
+    this.setState({ playing: false });
+    if (onNext) onNext();
   }
 
   onSpeed(speed) {
@@ -112,7 +134,7 @@ export default class Video extends Component {
   }
 
   render() {
-    const { action = true, btnList = [], children, onAction } = this.props;
+    const { action = true, btnList = [], children, onAction, hideAction } = this.props;
     const { playing, fulling, id } = this.state;
     return (
       <div id={id} className={`video-item ${action ? 'action' : ''} ${fulling ? 'full' : ''}`}>
@@ -125,34 +147,36 @@ export default class Video extends Component {
           {!playing && <Assets className="play" name="play" onClick={() => this.onPlay()} />}
           {playing && <Assets className="stop" name="stop" onClick={() => this.onPuase()} />}
         </div>
-        <div className="video-bottom">
+        {!hideAction && <div className="video-bottom">
           <div className="progress" />
           {action && (
             <div className="action-bar">
               <div className="d-i-b m-r-1">
                 {!playing && <Assets name="play2" onClick={() => this.onPlay()} />}
-                {playing && <Assets name="stop2" onClick={() => this.onPuase()} />}
+                {playing && <Assets name="stop2" onClick={() => this.onPause()} />}
               </div>
               <div className="d-i-b m-r-1">
-                <Assets name="next2" />
+                <Assets name="next2" onClick={() => this.onNext()} />
               </div>
               <div className="flex-block" />
               {btnList.map(btn => {
                 if (btn.full && !fulling) return '';
+                if (!btn.show) return '';
                 return (
                   <div className="d-i-b m-r-1">
                     {btn.render ? (
-                      <div className="fix-btn d-i-b" onClick={() => onAction && onAction(btn.key)}>
+                      <div className="fix-btn d-i-b" onClick={() => {
+                        if (btn.pause) this.onPause();
+                        if (onAction) onAction(btn.key);
+                      }}>
                         {btn.render(btn.active)}
                       </div>
-                    ) : (
-                      <div
-                        className={`btn ${btn.active ? 'active' : ''}`}
-                        onClick={() => onAction && onAction(btn.key)}
-                      >
-                        {btn.title}
-                      </div>
-                    )}
+                    ) : (<div
+                      className={`btn ${btn.active ? 'active' : ''}`}
+                      onClick={() => onAction && onAction(btn.key)}
+                    >
+                      {btn.title}
+                    </div>)}
                   </div>
                 );
               })}
@@ -165,7 +189,7 @@ export default class Video extends Component {
               </div>
             </div>
           )}
-        </div>
+        </div>}
         {children}
       </div>
     );

+ 19 - 0
front/project/www/components/Video/index.less

@@ -6,6 +6,25 @@
   overflow: hidden;
   background: #3A3A3AFF;
 
+  .video-wrapper {
+
+    .vjs-loading-spinner {
+      display: none;
+    }
+
+    .vjs-big-play-button {
+      display: none;
+    }
+
+    .vjs-control-bar {
+      display: none;
+    }
+
+    .vjs-modal-dialog {
+      display: none;
+    }
+  }
+
   .video-bottom {
     position: absolute;
     bottom: 0;

+ 1 - 1
front/project/www/routes/course/answer/page.js

@@ -216,7 +216,7 @@ export default class extends Page {
                 )}
                 <div>
                   <div className="small-tag">提问</div>
-                  <div className="f-r t-2 t-s-12">{formatDate(item.createTime, 'YYYY-MM-DD HH:mm:ss')}</div>
+                  <div className="f-r t-2 t-s-12">{formatDate(item.createTime, 'YYYY-MM-DD HH:mm:ss')} <span hidden={tab === 'my'}>阅读 {item.viewNumber}</span></div>
                 </div>
                 <div className="desc">
                   <OpenText>{item.content}</OpenText>

+ 30 - 33
front/project/www/routes/course/dataDetail/page.js

@@ -5,6 +5,7 @@ import Assets from '@src/components/Assets';
 import { getMap, formatDate } from '@src/services/Tools';
 import Footer from '../../../components/Footer';
 import { Contact, AnswerCarousel, Comment } from '../../../components/Other';
+import { FaqModal, CommentModal, FinishModal } from '../../../components/OtherModal';
 import Tabs from '../../../components/Tabs';
 import Button from '../../../components/Button';
 import { User } from '../../../stores/user';
@@ -80,7 +81,7 @@ export default class extends Page {
   }
 
   renderView() {
-    const { base = {}, data = {}, dataStructMap = {} } = this.state;
+    const { base = {}, data = {}, dataStructMap = {}, showComment, showFinish, comment = {}, showFaq, faq = {} } = this.state;
     return (
       <div>
         <div className="top content t-8">
@@ -90,6 +91,18 @@ export default class extends Page {
         {this.renderDetail()}
         <Contact data={base.contact} />
         <Footer />
+        <CommentModal
+          show={showComment}
+          defaultData={comment}
+          onConfirm={() => this.setState({ showComment: false, showFinish: true })}
+          onCancel={() => this.setState({ showComment: false })}
+          onClose={() => this.setState({ showComment: false })}
+        />
+        <FaqModal show={showFaq} defaultData={faq} onCancel={() => this.setState({ showFaq: false })} onConfirm={() => this.setState({ showFaq: false, showFinish: true })} />
+        <FinishModal
+          show={showFinish}
+          onConfirm={() => this.setState({ showFinish: false })}
+        />
       </div>
     );
   }
@@ -195,73 +208,57 @@ export default class extends Page {
   }
 
   renderTab1() {
+    const { data } = this.state;
     return (
       <div className="tab-layout">
-        <div className="tab-desc">
-          已经参加过GMAT基础班,对GMAT考试内容已经有全面认识,已经掌握了GMAT考试所需要的所有语言能力,希望学习具体的做题方法和应试技巧,提高实战做题能力的学员;
-          住宿管理, 让学生高效利用时间在最短的时间内攻破GMAT考试。
-          已经参加过GMAT基础班,对GMAT考试内容已经有全面认识,已经掌握了GMAT考试所需要的所有语言能力,希望学习具体的做题方法和应试技巧,提高实战做题能力的学员;
-          住宿管理, 让学生高效利用时间在最短的时间内攻破GMAT考试。
-        </div>
+        <div className="tab-desc" dangerouslySetInnerHTML={{ __html: data.content }} />
       </div>
     );
   }
 
   renderTab2() {
+    const { data } = this.state;
     return (
       <div className="tab-layout">
-        <div className="tab-desc">
-          已经参加过GMAT基础班,对GMAT考试内容已经有全面认识,已经掌握了GMAT考试所需要的所有语言能力,希望学习具体的做题方法和应试技巧,提高实战做题能力的学员;
-          住宿管理, 让学生高效利用时间在最短的时间内攻破GMAT考试。
-          已经参加过GMAT基础班,对GMAT考试内容已经有全面认识,已经掌握了GMAT考试所需要的所有语言能力,希望学习具体的做题方法和应试技巧,提高实战做题能力的学员;
-          住宿管理, 让学生高效利用时间在最短的时间内攻破GMAT考试。
-        </div>
+        <div className="tab-desc" dangerouslySetInnerHTML={{ __html: data.authorContent }} />
       </div>
     );
   }
 
   renderTab3() {
+    const { data } = this.state;
     return (
       <div className="tab-layout">
-        <div className="tab-desc">
-          已经参加过GMAT基础班,对GMAT考试内容已经有全面认识,已经掌握了GMAT考试所需要的所有语言能力,希望学习具体的做题方法和应试技巧,提高实战做题能力的学员;
-          住宿管理, 让学生高效利用时间在最短的时间内攻破GMAT考试。
-          已经参加过GMAT基础班,对GMAT考试内容已经有全面认识,已经掌握了GMAT考试所需要的所有语言能力,希望学习具体的做题方法和应试技巧,提高实战做题能力的学员;
-          住宿管理, 让学生高效利用时间在最短的时间内攻破GMAT考试。
-        </div>
+        <div className="tab-desc" dangerouslySetInnerHTML={{ __html: data.methodContent }} />
       </div>
     );
   }
 
   renderTab4() {
+    const { faqs, data = {} } = this.state;
     return (
       <div className="tab-layout">
         <AnswerCarousel
           hideBtn
-          list={[
-            { answer: '123123', content: '12312312' },
-            { answer: '123123', content: '12312312' },
-            { answer: '123123', content: '12312312' },
-            { answer: '123123', content: '12312312' },
-          ]}
+          list={faqs}
+          onFaq={() => this.setState({ showFaq: true, faq: { channel: 'course_data', position: data.id } })}
         />
       </div>
     );
   }
 
   renderTab5() {
+    const { data, comments } = this.state;
     return (
       <div className="tab-layout">
-        <div className="m-b-1 t-r">
-          <Button width={100} radius>
+        {data.have && <div className="m-b-1 t-r">
+          <Button width={100} radius onClick={() => User.needLogin().then(() => this.setState({ showComment: true, comment: { channel: 'course_data', position: data.id } }))}>
             写评论
           </Button>
-        </div>
-        <Comment />
-        <Comment />
-        <Comment />
-        <Comment />
-        <Comment />
+        </div>}
+        {(comments || []).map(item => {
+          return <Comment data={item} />;
+        })}
       </div>
     );
   }

+ 2 - 2
front/project/www/routes/course/detail/index.less

@@ -149,11 +149,11 @@
         padding-left: 790px;
 
         .answer-layout {
-          height: 430px;
+          height: 480px;
         }
 
         .item-layout {
-          height: 480px;
+          height: 530px;
         }
       }
 

+ 380 - 93
front/project/www/routes/course/detail/page.js

@@ -1,42 +1,138 @@
 import React from 'react';
+import { Link } from 'react-router-dom';
 import './index.less';
 import Page from '@src/containers/Page';
 import Assets from '@src/components/Assets';
+import { getMap, formatPercent, formatDate } from '@src/services/Tools';
 import Footer from '../../../components/Footer';
+import { FaqModal, CommentModal, FinishModal, CourseNoteModal, AskCourseModal } from '../../../components/OtherModal';
 import { Contact, AnswerCarousel, Comment } from '../../../components/Other';
 import Tabs from '../../../components/Tabs';
+import { Icon as GIcon } from '../../../components/Icon';
 import Button from '../../../components/Button';
+import IconButton from '../../../components/IconButton';
 import UserTable from '../../../components/UserTable';
 import ProgressText from '../../../components/ProgressText';
 import { OpenText } from '../../../components/Open';
 import Video from '../../../components/Video';
+import { Main } from '../../../stores/main';
+import { Course } from '../../../stores/course';
+import { User } from '../../../stores/user';
+import { Order } from '../../../stores/order';
+import { Question } from '../../../stores/question';
+import { My } from '../../../stores/my';
 
 export default class extends Page {
   initState() {
     this.columns = [
       {
-        key: '1',
         title: '学习内容',
+        key: 'title',
+        render: (text, record) => {
+          const { no } = this.state;
+          if (no === record.no) {
+            // 当前正在
+          }
+          return `课时 ${record.no}: ${text}`;
+        },
       },
       {
-        key: '2',
         title: '预习作业',
+        key: 'paper',
+        render: text => {
+          text = text || {};
+          const progress = text.report ? formatPercent(text.report.userNumber, text.report.questionNumber) : 0;
+          const times = text.paper ? text.paper.times : 0;
+          return (
+            <div>
+              <div className="v-a-m d-i-b">
+                <ProgressText width={50} size="small" times={times} progress={progress} unit="次" />
+              </div>
+              {!text.report && (
+                <IconButton
+                  className="m-l-2"
+                  type="start"
+                  tip="Start"
+                  onClick={() => {
+                    User.needLogin().then(() => {
+                      Question.startLink('preview', text);
+                    });
+                  }}
+                />
+              )}
+              {text.report && !text.report.isFinish && (
+                <IconButton
+                  className="m-l-2"
+                  type="continue"
+                  tip="Continue"
+                  onClick={() => {
+                    User.needLogin().then(() => {
+                      Question.continueLink('preview', text);
+                    });
+                  }}
+                />
+              )}
+              {text.report && !!text.report.isFinish && (
+                <IconButton
+                  className="m-l-2"
+                  type="restart"
+                  tip="Restart"
+                  onClick={() => {
+                    User.needLogin().then(() => {
+                      Question.restart('preview', text);
+                    });
+                  }}
+                />
+              )}
+              {text.report && !!text.report.isFinish && (
+                <IconButton
+                  className="m-l-5"
+                  type="report"
+                  tip="Report"
+                  onClick={() => {
+                    User.needLogin().then(() => {
+                      Question.reportLink('preview', text);
+                    });
+                  }}
+                />
+              )}
+            </div>
+          );
+        },
       },
       {
-        key: '3',
-        title: '进展',
+        title: '进度',
+        key: 'progress',
+        render: (text, record) => {
+          const { paper = {} } = record;
+          return `${paper.paper && paper.paper.times > 0 ? `${paper.paper.times}次+` : ''}${
+            paper.report ? formatPercent(paper.report.userNumber, paper.report.questionNumber, false) : '0%'}`;
+        },
       },
       {
-        key: '4',
         title: '最近学习',
+        key: 'lastTime',
+        render: (text, record) => {
+          const { paper = {} } = record;
+          return paper.report && formatDate(paper.report.updateTime, 'YYYY-MM-DD HH:mm:ss');
+        },
       },
       {
-        key: '5',
         title: '笔记',
+        key: 'note',
+        render: (text, record) => {
+          return <GIcon name="note" active={record.note} />;
+        },
       },
       {
-        key: '6',
         title: '问答',
+        key: 'ask',
+        render: (text, record) => {
+          return (
+            <Link to={`/course/answer/${record.courseId}?tab=my&courseNoId=${record.id}`}>{`${record.answerNumber ||
+              0}/${record.askNumber || 0}`}</Link>
+          );
+        },
       },
     ];
     return {
@@ -45,11 +141,97 @@ export default class extends Page {
       key: '1',
       add: false,
       list: [{ key: '1' }, { key: '2' }, { key: '3' }],
-      progress: 0,
-      data: { title: '语法SC系统授课—课时10:逻辑语义解题专题讲解(1)' },
+      data: {},
+      position: 0,
     };
   }
 
+  init() {
+    Main.dataStruct().then(result => {
+      const dataStructSelect = result.map(row => {
+        return {
+          title: `${row.titleZh}${row.titleEn}`,
+          key: row.id,
+        };
+      });
+      const dataStructMap = getMap(dataStructSelect, 'key');
+      this.setState({ dataStructSelect, dataStructMap });
+    });
+    Main.getBase().then(result => {
+      this.setState({ base: result });
+    });
+  }
+
+  formatRecord(row) {
+    row.paperMap = {};
+    if (row.papers) {
+      row.papers.forEach(paper => {
+        if (paper.courseNo) row.paperMap[paper.courseNo] = paper;
+      });
+    }
+    row.progressMap = {};
+    if (row.progress) {
+      row.progress.forEach(progress => {
+        row.progressMap[progress.courseNoId] = progress;
+      });
+    }
+
+    row.courseNoMap = {};
+    row.courseTime = 0;
+    if (row.courseNos) {
+      row.courseNos.forEach(no => {
+        row.courseNoMap[no.id] = no;
+        row.courseTime += no.time;
+        no.paper = row.paperMap[no.id];
+        no.progress = row.progressMap[no.id];
+      });
+    }
+    if (row.currentNo) {
+      row.currentCourseNo = row.courseNoMap[row.currentNo];
+    } else {
+      row.currentNo = 0;
+    }
+    return row;
+  }
+
+  initData() {
+    const { id } = this.params;
+    Course.detail(id).then(result => {
+      result = this.formatRecord(result);
+      this.setState({ data: result });
+      // 选择课时
+      if (this.state.search.no) {
+        this.onChangeItem(this.state.search.no);
+      } else {
+        this.onChangeItem(1);
+      }
+      this.refreshNote();
+    });
+  }
+
+  refreshAsk(position) {
+    const { id } = this.params;
+    const { item } = this.state;
+    Course.listAsk(Object.assign({ page: 1, size: 1000, courseId: id, courseNoId: item.id, position })).then(result => {
+      this.setState({ asks: result.list });
+    });
+  }
+
+  refreshNote() {
+    const { id } = this.params;
+    const { data } = this.state;
+    if (!data.have) return;
+    My.listCourseNote({ courseId: id, page: 1, size: data.courseNos.length })
+      .then((result) => {
+        this.noteMap = getMap(result.list, 'courseNoId');
+        data.courseNos.forEach((row) => {
+          const note = this.noteMap[row.id];
+          if (note) row.note = true;
+        });
+        this.setState({ data });
+      });
+  }
+
   onChangeRightTab(rightTab) {
     this.setState({ rightTab });
   }
@@ -59,16 +241,77 @@ export default class extends Page {
   }
 
   onChangeItem(key) {
-    this.setState({ key });
+    key = Number(key);
+    this.changeQuery({ no: key });
+    this.setState({ no: key });
+    const { data, item } = this.state;
+    const index = key - 1;
+    const timelineSelect = [];
+    const current = data.courseNos[index];
+    if (current) {
+      const max = current.time;
+      let start = 0;
+      let end = start + 5;
+      while (start < max) {
+        timelineSelect.push({
+          title: `${start} - ${end}min`,
+          key: `${start}`,
+        });
+        start += 5;
+        end = Math.min(start + 5, max);
+      }
+    }
+    // 切换播放,记录进度
+    if (item) {
+      this.updateProgress(item.id, this.lastSecond, item.time, true);
+    }
+    this.setState({ item: current, timelineSelect });
+  }
+
+  onTimeUpdate(second) {
+    const { position, item, data } = this.state;
+    if (!data.have) {
+      // 如果是试用,则按秒数增加
+      second += item.startTrail * 60;
+    }
+    const minute = second / 60;
+    const nowPosition = (minute / 5) * 5;
+    if (nowPosition !== position) {
+      this.refreshAsk(position);
+      this.setState({ position: nowPosition });
+      // 定时更新进度
+      this.updateProgress(item.id, second, item.time);
+    }
+    this.lastSecond = second;
+  }
+
+  playVideo() {
+    // 开始计时
+    this.lastTime = new Date();
+  }
+
+  pauseVideo() {
+    // 停止计时
+    const now = new Date();
+    this.time += (now.getTime() - this.lastTime.getTime()) / 1000;
+    this.lastTime = null;
+  }
+
+  next() {
+    const { data, item } = this.state;
+    if (data.courseNos.length === item.no) {
+      return;
+    }
+    this.onChangeItem(item.no + 1);
   }
 
   onVideoAction(key) {
-    const { rightTab, showTab, showQuestion, showNote } = this.state;
+    const { rightTab, showTab, showAsk, showNote, item } = this.state;
     switch (key) {
-      case 'question':
-        return this.setState({ showQuestion: !showQuestion });
+      case 'ask':
+        return this.setState({ showAsk: !showAsk });
       case 'note':
-        return this.setState({ showNote: !showNote });
+        return this.setState({ showNote: !showNote, note: this.noteMap ? this.noteMap[item.id] || {} : {} });
       case 'answer':
         return this.setState({ showTab: rightTab === '1' ? !showTab : true, rightTab: '1' });
       case 'list':
@@ -78,42 +321,94 @@ export default class extends Page {
     }
   }
 
+  updateProgress(courseNoId, currentTime, totalTime, record) {
+    if (!this.lastTime) return;
+    const { id } = this.params;
+    const now = new Date();
+    this.time += (now.getTime() - this.lastTime.getTime()) / 1000;
+    this.lastTime = now;
+    const progress = formatPercent(currentTime, totalTime);
+    if (record || this.time > 600) {
+      // 最长5分钟记录一次
+      Course.noProgress(id, courseNoId, progress, this.time, courseNoId);
+      this.time = 0;
+    } else {
+      Course.noProgress(id, courseNoId, progress, null, null);
+    }
+  }
+
+  buy() {
+    const { data } = this.props;
+    User.needLogin().then(() => {
+      Order.speedPay({ productType: 'course', productId: data.id }).then(result => {
+        User.needPay(result).then(() => {
+          this.refresh();
+        });
+      });
+    });
+  }
+
+  add() {
+    const { data } = this.props;
+    User.needLogin().then(() => {
+      Order.addCheckout({ productType: 'course', productId: data.id }).then(() => {
+        this.setState({ add: true });
+      });
+    });
+  }
+
+  viewAsk(id) {
+    Course.askView(id);
+  }
+
+  setVideo(video) {
+    this.video = video;
+  }
+
   renderView() {
-    const { base = {}, data = {}, add, progress, rightTab, showTab, showQuestion, showNote } = this.state;
+    const { base = {}, data = {}, item = {}, add, progress, rightTab, showTab, showAsk, showNote, dataStructMap = {}, showComment, comment = {}, showFaq, faq = {}, showFinish, note = {}, ask = {}, timelineSelect = [] } = this.state;
+    const { courseNos = [] } = data;
     return (
       <div>
         <div className="top content t-8">
-          千行课堂 > 全部套餐 > {data.title} > <span className="t-1">课程详情</span>
+          千行课堂 > 全部课程 > {data.parentStructId > 0 ? `${(dataStructMap[data.parentStructId] || {}).title} >` : ''}{' '}
+          {(dataStructMap[data.structId] || {}).title} > {data.title} > <span className="t-1">课程详情</span>
         </div>
         <div className="center content">
           <div className="t-1 t-s-20">
             {data.title}
             <div className="action f-r">
-              <Button className="m-r-1" radius size="lager" onClick={() => this.buy()}>
+              {!data.have && <Button className="m-r-1" radius size="lager" onClick={() => this.buy()}>
                 立即购买
-              </Button>
-              <Button theme="default" radius size="lager" disabled={data.add || add} onClick={() => this.add()}>
+              </Button>}
+              {!data.have && <Button theme="default" radius size="lager" disabled={data.add || add} onClick={() => this.add()}>
                 <Assets name="add" />
-              </Button>
+              </Button>}
+              {data.have && <Button className="m-r-1" radius size="lager" onClick={() => linkTo('/my/course')}>
+                我的课程
+              </Button>}
             </div>
           </div>
-          <div className="t-2 m-b-1">授课老师:李奕都</div>
+          <div className="t-2 m-b-1">授课老师:{data.teacher}</div>
           <div className="detail">
             <div className="left">
-              <div hidden={progress === 0} className="left-top">
+              {data.have && <div hidden={(item.paper && item.paper.times > 0)} className="left-top">
                 <span className="d-i-b m-r-1">预习作业</span>
                 <span className="d-i-b m-r-2">
-                  <ProgressText width={480} size="small" progress={progress} />
+                  <ProgressText width={480} size="small" progress={item.report ? formatPercent(item.report.userNumber, item.report.questionNumber) : 0} />
                 </span>
-                <Button className="f-r" radius>
+                <Button className="f-r" radius onClick={() => (item.report ? Question.continueLink('preview', item) : Question.startLink('preview', item))}>
                   做题
                 </Button>
-              </div>
+              </div>}
               <div className="video-layout">
-                <Video
-                  src="/01.mp4"
+                {item && <Video
+                  key={item.id}
+                  src={item.resource}
+                  duration={10}
+                  ref={ref => this.setVideo(ref)}
                   btnList={[
-                    { title: '提问', key: 'question', active: showQuestion },
+                    { title: '提问', key: 'ask', show: data.have, active: showAsk, pause: true },
                     {
                       key: 'answer',
                       render(active) {
@@ -122,10 +417,14 @@ export default class extends Page {
                       full: true,
                       active: showTab && rightTab === '1',
                     },
-                    { title: '笔记', key: 'note', active: showNote },
+                    { title: '笔记', key: 'note', show: data.have, active: showNote, pause: true },
                     { title: '课表', key: 'list', full: true, active: showTab && rightTab === '2' },
                   ]}
+                  onPlay={() => this.playVideo()}
+                  onPause={() => this.pauseVideo()}
+                  onNext={() => this.next()}
                   onAction={key => this.onVideoAction(key)}
+                  onTimeUpdate={time => this.onTimeUpdate(time)}
                   onFullChange={() => this.setState({ showTab: true, rightTab: '1' })}
                 >
                   <div hidden={!showTab} className="video-fixed tab-warpper">
@@ -139,7 +438,7 @@ export default class extends Page {
                     />
                     <div className="tab-body">{this[`renderRightTab${rightTab}`]()}</div>
                   </div>
-                </Video>
+                </Video>}
               </div>
             </div>
             <div className={`right ${progress > 0 ? 'progress' : ''}  tab-warpper`}>
@@ -154,27 +453,44 @@ export default class extends Page {
               <div className="tab-body">{this[`renderRightTab${rightTab}`]()}</div>
             </div>
           </div>
-          <UserTable columns={this.columns} />
+          {data.have && <UserTable columns={this.columns} list={courseNos} />}
         </div>
-        <div className="bottom">
+        <div hidden={data.have} className="bottom">
           <div className="content">{this.renderTab()}</div>
         </div>
         <Contact data={base.contact} />
         <Footer />
-      </div>
+        <AskCourseModal show={showAsk} defaultData={ask} course={data} courseNo={item} selectList={timelineSelect} onConfirm={() => this.setState({ showAsk: false })} onCancel={() => this.setState({ showAsk: false })} />
+        <CourseNoteModal show={showNote} defaultData={note} course={data} courseNos={courseNos} noteMap={this.noteMap} onConfirm={() => {
+          this.setState({ showNote: false });
+          this.refreshNote();
+        }} onCancel={() => this.setState({ showNote: false })} />
+        <CommentModal
+          show={showComment}
+          defaultData={comment}
+          onConfirm={() => this.setState({ showComment: false, showFinish: true })}
+          onCancel={() => this.setState({ showComment: false })}
+          onClose={() => this.setState({ showComment: false })}
+        />
+        <FaqModal show={showFaq} defaultData={faq} onCancel={() => this.setState({ showFaq: false })} onConfirm={() => this.setState({ showFaq: false, showFinish: true })} />
+        <FinishModal
+          show={showFinish}
+          onConfirm={() => this.setState({ showFinish: false })}
+        />,
+      </div >
     );
   }
 
   renderRightTab1() {
-    const { list = [] } = this.state;
+    const { asks = [], data = {}, position } = this.state;
     return [
       <div className="all-answer">
         <span className="d-i-b b m-r-5" />
-        <span className="d-i-b t-6">35:00 ~ 40:00</span>
-        <span className="f-r d-i-b t-4 c-p">全部问答 ></span>
+        <span className="d-i-b t-6">{position}:00~{position + 5}:00</span>
+        <a className="f-r d-i-b t-4 c-p" href={`/course/answer/${data.id}`} target="_blank">全部问答 ></a>
       </div>,
       <div className="answer-layout">
-        {list.map(item => {
+        {asks.map(item => {
           return (
             <div className="answer-item">
               <div>
@@ -190,7 +506,7 @@ export default class extends Page {
               )}
               {item.answerStatus > 0 && (
                 <div className="desc">
-                  <OpenText>{item.answer}</OpenText>
+                  <OpenText onOpen={() => this.viewAsk(item.id)}>{item.answer}</OpenText>
                 </div>
               )}
             </div>
@@ -201,14 +517,15 @@ export default class extends Page {
   }
 
   renderRightTab2() {
-    const { list = [], key } = this.state;
+    const { data = {}, no } = this.state;
+    const { courseNos = [] } = data;
     return (
       <div className="item-layout">
-        {list.map(item => {
+        {courseNos.map(item => {
           return (
-            <div className={`item ${item.key === key ? 'active' : ''}`} onClick={() => this.onChangeItem(item.key)}>
-              <span className="t-1">课时1</span>
-              <span className="t-2">解读句子结构</span>
+            <div className={`item ${item.no === no ? 'active' : ''}`} onClick={() => this.onChangeItem(item.no)}>
+              <span className="t-1">课时{item.no}</span>
+              <span className="t-2">{item.title}</span>
             </div>
           );
         })}
@@ -242,49 +559,26 @@ export default class extends Page {
   }
 
   renderTab1() {
+    const { data = {} } = this.state;
     return (
       <div className="tab-layout">
         <div className="tab-title">老师资历</div>
-        <div className="tab-desc">
-          已经参加过GMAT基础班,对GMAT考试内容已经有全面认识,已经掌握了GMAT考试所需要的所有语言能力,希望学习具体的做题方法和应试技巧,提高实战做题能力的学员;
-          住宿管理, 让学生高效利用时间在最短的时间内攻破GMAT考试。
-          已经参加过GMAT基础班,对GMAT考试内容已经有全面认识,已经掌握了GMAT考试所需要的所有语言能力,希望学习具体的做题方法和应试技巧,提高实战做题能力的学员;
-          住宿管理, 让学生高效利用时间在最短的时间内攻破GMAT考试。
-        </div>
+        <div className="tab-desc" dangerouslySetInnerHTML={{ __html: data.teacherContent }} />
         <div className="tab-title">基本参数</div>
-        <div className="tab-desc">
-          已经参加过GMAT基础班,对GMAT考试内容已经有全面认识,已经掌握了GMAT考试所需要的所有语言能力,希望学习具体的做题方法和应试技巧,提高实战做题能力的学员;
-          住宿管理, 让学生高效利用时间在最短的时间内攻破GMAT考试。
-          已经参加过GMAT基础班,对GMAT考试内容已经有全面认识,已经掌握了GMAT考试所需要的所有语言能力,希望学习具体的做题方法和应试技巧,提高实战做题能力的学员;
-          住宿管理, 让学生高效利用时间在最短的时间内攻破GMAT考试。
-        </div>
+        <div className="tab-desc" dangerouslySetInnerHTML={{ __html: data.baseContent }} />
         <div className="tab-title">授课重点</div>
-        <div className="tab-desc">
-          已经参加过GMAT基础班,对GMAT考试内容已经有全面认识,已经掌握了GMAT考试所需要的所有语言能力,希望学习具体的做题方法和应试技巧,提高实战做题能力的学员;
-          住宿管理, 让学生高效利用时间在最短的时间内攻破GMAT考试。
-          已经参加过GMAT基础班,对GMAT考试内容已经有全面认识,已经掌握了GMAT考试所需要的所有语言能力,希望学习具体的做题方法和应试技巧,提高实战做题能力的学员;
-          住宿管理, 让学生高效利用时间在最短的时间内攻破GMAT考试。
-        </div>
+        <div className="tab-desc" dangerouslySetInnerHTML={{ __html: data.pointContent }} />
         <div className="tab-title">适合人群</div>
-        <div className="tab-desc">
-          已经参加过GMAT基础班,对GMAT考试内容已经有全面认识,已经掌握了GMAT考试所需要的所有语言能力,希望学习具体的做题方法和应试技巧,提高实战做题能力的学员;
-          住宿管理, 让学生高效利用时间在最短的时间内攻破GMAT考试。
-          已经参加过GMAT基础班,对GMAT考试内容已经有全面认识,已经掌握了GMAT考试所需要的所有语言能力,希望学习具体的做题方法和应试技巧,提高实战做题能力的学员;
-          住宿管理, 让学生高效利用时间在最短的时间内攻破GMAT考试。
-        </div>
+        <div className="tab-desc" dangerouslySetInnerHTML={{ __html: data.crowdContent }} />
       </div>
     );
   }
 
   renderTab2() {
+    const { data } = this.state;
     return (
       <div className="tab-layout">
-        <div className="tab-desc">
-          已经参加过GMAT基础班,对GMAT考试内容已经有全面认识,已经掌握了GMAT考试所需要的所有语言能力,希望学习具体的做题方法和应试技巧,提高实战做题能力的学员;
-          住宿管理, 让学生高效利用时间在最短的时间内攻破GMAT考试。
-          已经参加过GMAT基础班,对GMAT考试内容已经有全面认识,已经掌握了GMAT考试所需要的所有语言能力,希望学习具体的做题方法和应试技巧,提高实战做题能力的学员;
-          住宿管理, 让学生高效利用时间在最短的时间内攻破GMAT考试。
-        </div>
+        <div className="tab-desc" dangerouslySetInnerHTML={{ __html: data.syllabusContent }} />
       </div>
     );
   }
@@ -306,47 +600,40 @@ export default class extends Page {
   }
 
   renderTab4() {
+    const { faqs, data = {}, showFaq, faq } = this.state;
     return (
       <div className="tab-layout">
         <AnswerCarousel
           hideBtn
-          list={[
-            { answer: '123123', content: '12312312' },
-            { answer: '123123', content: '12312312' },
-            { answer: '123123', content: '12312312' },
-            { answer: '123123', content: '12312312' },
-          ]}
+          list={faqs}
+          onFaq={() => this.setState({ showFaq: true, faq: { channel: 'course-video', position: data.id } })}
         />
+        <FaqModal show={showFaq} defaultData={faq} onCancel={() => this.setState({ showFaq: false })} onConfirm={() => this.setState({ showFaq: false })} />
       </div>
     );
   }
 
   renderTab5() {
+    const { data = {} } = this.state;
     return (
       <div className="tab-layout">
-        <div className="tab-desc">
-          已经参加过GMAT基础班,对GMAT考试内容已经有全面认识,已经掌握了GMAT考试所需要的所有语言能力,希望学习具体的做题方法和应试技巧,提高实战做题能力的学员;
-          住宿管理, 让学生高效利用时间在最短的时间内攻破GMAT考试。
-          已经参加过GMAT基础班,对GMAT考试内容已经有全面认识,已经掌握了GMAT考试所需要的所有语言能力,希望学习具体的做题方法和应试技巧,提高实战做题能力的学员;
-          住宿管理, 让学生高效利用时间在最短的时间内攻破GMAT考试。
-        </div>
+        <div className="tab-desc" dangerouslySetInnerHTML={{ __html: data.promoteContent }} />
       </div>
     );
   }
 
   renderTab6() {
+    const { data = {}, comments = [] } = this.state;
     return (
       <div className="tab-layout">
-        <div className="m-b-1 t-r">
-          <Button width={100} radius>
+        {data.have && <div className="m-b-1 t-r">
+          <Button width={100} radius onClick={() => User.needLogin().then(() => this.setState({ showComment: true, comment: { channel: 'course-video', position: data.id } }))}>
             写评论
           </Button>
-        </div>
-        <Comment />
-        <Comment />
-        <Comment />
-        <Comment />
-        <Comment />
+        </div>}
+        {(comments || []).map(item => {
+          return <Comment data={item} />;
+        })}
       </div>
     );
   }

+ 13 - 4
front/project/www/routes/course/main/page.js

@@ -4,7 +4,7 @@ import Assets from '@src/components/Assets';
 import Page from '@src/containers/Page';
 import { getMap } from '@src/services/Tools';
 import Footer from '../../../components/Footer';
-import { FaqModal } from '../../../components/OtherModal';
+import { FaqModal, FinishModal } from '../../../components/OtherModal';
 import { CommentFalls, AnswerCarousel, Consultation, Contact } from '../../../components/Other';
 import Button from '../../../components/Button';
 import { User } from '../../../stores/user';
@@ -12,6 +12,7 @@ import { Main } from '../../../stores/main';
 import { Course } from '../../../stores/course';
 import { ServiceKey, ServiceParamMap } from '../../../../Constant';
 import { Order } from '../../../stores/order';
+import Video from '../../../components/Video';
 
 export default class extends Page {
   initState() {
@@ -40,7 +41,7 @@ export default class extends Page {
   }
 
   renderView() {
-    const { courseIndex = {}, base = {}, packages = [], faqs = [], comments = [], showFaq, faq = {} } = this.state;
+    const { courseIndex = {}, base = {}, packages = [], faqs = [], comments = [], showFaq, faq = {}, showFinish } = this.state;
     return (
       <div>
         <div className="block-1">
@@ -61,11 +62,15 @@ export default class extends Page {
           <div className="main-title">找到你的Style</div>
           <div className="video-list">
             <div className="video-item">
-              <Assets width={70} height={70} name="play" className="play" src={courseIndex.onlineVideo} />
+              <div style={{ width: 70, height: 70 }}>
+                <Video src={courseIndex.onlineVideo} hideAction />
+              </div>
               <div className="name" onClick={() => linkTo('/course/online')}>在线课程 ></div>
             </div>
             <div className="video-item">
-              <Assets width={70} height={70} name="play" className="play" src={courseIndex.vsVideo} />
+              <div style={{ width: 70, height: 70 }}>
+                <Video src={courseIndex.vsVideo} hideAction />
+              </div>
               <div className="name" onClick={() => linkTo('/course/vs')}>1v1私教 ></div>
             </div>
           </div>
@@ -145,6 +150,10 @@ export default class extends Page {
         <Contact data={base.contact} />
         <Footer />
         <FaqModal show={showFaq} defaultData={faq} onCancel={() => this.setState({ showFaq: false })} onConfirm={() => this.setState({ showFaq: false })} />
+        <FinishModal
+          show={showFinish}
+          onConfirm={() => this.setState({ showFinish: false })}
+        />
       </div >
     );
   }

+ 4 - 4
front/project/www/routes/course/online/index.less

@@ -35,8 +35,8 @@
 
       .single-item {
         margin-bottom: 20px;
-        margin-left: 20px;
-        margin-right: 20px;
+        margin-left: 10px;
+        margin-right: 10px;
       }
     }
 
@@ -47,8 +47,8 @@
 
       .package-item {
         margin-bottom: 20px;
-        margin-left: 20px;
-        margin-right: 20px;
+        margin-left: 10px;
+        margin-right: 10px;
       }
     }
   }

+ 7 - 3
front/project/www/routes/course/online/page.js

@@ -4,7 +4,7 @@ import Page from '@src/containers/Page';
 import Assets from '@src/components/Assets';
 import { getMap, formatTreeData } from '@src/services/Tools';
 import Footer from '../../../components/Footer';
-import { FaqModal } from '../../../components/OtherModal';
+import { FaqModal, FinishModal } from '../../../components/OtherModal';
 import { CommentFalls, AnswerCarousel, Consultation, Contact } from '../../../components/Other';
 import Tabs from '../../../components/Tabs';
 import Filter from '../../../components/Filter';
@@ -124,7 +124,7 @@ export default class extends Page {
 
   renderView() {
     const { number } = this.props.order;
-    const { tab, promote = {}, base = {}, comments, faqs, showFaq, faq } = this.state;
+    const { tab, promote = {}, base = {}, comments, faqs, showFaq, faq, showFinish } = this.state;
     return (
       <div>
         <div className="top content">
@@ -156,7 +156,11 @@ export default class extends Page {
         <AnswerCarousel list={faqs} onFaq={() => this.setState({ showFaq: true, faq: { channel: tab === 'single' ? 'course-video_index' : 'course-package_index' } })} />
         <Contact data={base.contact} />
         <Footer />
-        <FaqModal show={showFaq} defaultData={faq} onCancel={() => this.setState({ showFaq: false })} onConfirm={() => this.setState({ showFaq: false })} />
+        <FaqModal show={showFaq} defaultData={faq} onCancel={() => this.setState({ showFaq: false })} onConfirm={() => this.setState({ showFaq: false, showFinish: true })} />
+        <FinishModal
+          show={showFinish}
+          onConfirm={() => this.setState({ showFinish: false })}
+        />
       </div>
     );
   }

+ 38 - 28
front/project/www/routes/course/vs/page.js

@@ -4,6 +4,7 @@ import { Icon } from 'antd';
 import Page from '@src/containers/Page';
 import Assets from '@src/components/Assets';
 import Footer from '../../../components/Footer';
+import { FaqModal, CommentModal, FinishModal } from '../../../components/OtherModal';
 import { Contact, Comment, Consultation, AnswerCarousel } from '../../../components/Other';
 import Tabs from '../../../components/Tabs';
 import Button from '../../../components/Button';
@@ -112,7 +113,7 @@ export default class extends Page {
 
   renderView() {
     const { number } = this.props.order;
-    const { promote = {}, base = {} } = this.state;
+    const { promote = {}, base = {}, showComment, comment = {}, showFaq, faq = {}, showFinish } = this.state;
     return (
       <div>
         <div className="top content">
@@ -133,6 +134,18 @@ export default class extends Page {
         {this.renderDetail()}
         <Contact data={base.contact} />
         <Footer />
+        <CommentModal
+          show={showComment}
+          defaultData={comment}
+          onConfirm={() => this.setState({ showComment: false, showFinish: true })}
+          onCancel={() => this.setState({ showComment: false })}
+          onClose={() => this.setState({ showComment: false })}
+        />
+        <FaqModal show={showFaq} defaultData={faq} onCancel={() => this.setState({ showFaq: false })} onConfirm={() => this.setState({ showFaq: false, showFinish: true })} />
+        <FinishModal
+          show={showFinish}
+          onConfirm={() => this.setState({ showFinish: false })}
+        />
       </div>
     );
   }
@@ -210,12 +223,15 @@ export default class extends Page {
                 <div className="d-i-b t-7 t-s-20 f-w-b"> ¥ {price}</div>
               </div>
               <div className="action">
-                <Button className="m-r-1" radius size="lager" onClick={() => this.buy()}>
+                {data.have && <Button className="m-r-1" radius size="lager" onClick={() => linkTo('/my/course')}>
+                  我的课程
+                </Button>}
+                {!data.have && <Button className="m-r-1" radius size="lager" onClick={() => this.buy()}>
                   立即购买
-                </Button>
-                <Button theme="default" radius size="lager" onClick={() => this.add()}>
+                </Button>}
+                {!data.have && <Button theme="default" disabled={this.state.add || data.add} radius size="lager" onClick={() => this.add()}>
                   <Assets name="add" />
-                </Button>
+                </Button>}
               </div>
             </div>
           </div>
@@ -242,61 +258,55 @@ export default class extends Page {
   }
 
   renderTab1() {
+    const { teachers = [] } = this.state;
+    const [teacher] = teachers;
     return (
       <div className="tab-layout">
         <div className="teach-item">
           <div className="left t-c">
-            <Assets className="m-b-1" />
-            <div className="t-1 t-s-20">李奕都(DUKB24)</div>
-          </div>
-          <div className="right t-1 t-s-16">
-            针对 0G20 的集中训练针对 0G20 的集中训练针对 0G20 的集中训练针对 0G20 的集中训练。针对 0G20 的集中训练针对
-            0G20 的集中训练针对 0G20 的集,中训练针对 0G20 的集中训练针对 0G20 的集中训练针对。 针对 0G20
-            的集中训。练针对 0G20 的集中训练针对 0G20 的集中训练针对 0G20 的集中训练。 针对 0G20 的集中训练针对 0G20
-            的集中训练针对 0G。
+            <Assets className="m-b-1" src={teacher.avatar} />
+            <div className="t-1 t-s-20">{teacher.realname}</div>
           </div>
+          <div className="right t-1 t-s-16 ws-p">{teacher.description}</div>
         </div>
       </div>
     );
   }
 
   renderTab2() {
+    const { base } = this.state;
     return (
       <div className="tab-layout">
-        <Consultation />
+        <Consultation data={base.contact} />
       </div>
     );
   }
 
   renderTab3() {
+    const { faqs, data = {} } = this.state;
     return (
       <div className="tab-layout">
         <AnswerCarousel
           hideBtn
-          list={[
-            { answer: '123123', content: '12312312' },
-            { answer: '123123', content: '12312312' },
-            { answer: '123123', content: '12312312' },
-            { answer: '123123', content: '12312312' },
-          ]}
+          list={faqs}
+          onFaq={() => this.setState({ showFaq: true, faq: { channel: 'course-vs', position: data.id } })}
         />
       </div>
     );
   }
 
   renderTab4() {
+    const { data, comments } = this.state;
     return (
       <div className="tab-layout">
-        <div className="m-b-1 t-r">
-          <Button width={100} radius>
+        {data.have && <div className="m-b-1 t-r">
+          <Button width={100} radius onClick={() => User.needLogin().then(() => this.setState({ showComment: true, comment: { channel: 'course-vs', position: data.id } }))}>
             写评论
           </Button>
-        </div>
-        <Comment />
-        <Comment />
-        <Comment />
-        <Comment />
-        <Comment />
+        </div>}
+        {(comments || []).map(item => {
+          return <Comment data={item} />;
+        })}
       </div>
     );
   }

+ 1 - 1
front/project/www/routes/my/course/page.js

@@ -711,7 +711,7 @@ class CourseOnline extends Component {
         key: 'ask',
         render: (text, record) => {
           return (
-            <Link to={`/course/ask/${record.courseId}?no=${record.id}`}>{`${record.answerNumber ||
+            <Link to={`/course/answer/${record.courseId}?tab=my&courseNoId=${record.id}`}>{`${record.answerNumber ||
               0}/${record.askNumber || 0}`}</Link>
           );
         },

+ 2 - 0
server/gateway-api/src/main/java/com/qxgmat/controller/admin/UserController.java

@@ -209,6 +209,8 @@ public class UserController {
         User entity = Transform.dtoToEntity(dto);
         entity.setRealStatus(1);
         entity.setRealTime(new Date());
+        // 实名认证,半价机经券
+        entity.setTextbookHalf(1);
         usersService.edit(entity);
 
         orderFlowService.giveReal(in);

+ 75 - 3
server/gateway-api/src/main/java/com/qxgmat/controller/api/CourseController.java

@@ -10,6 +10,7 @@ import com.qxgmat.data.constants.enums.ExperienceScoreRange;
 import com.qxgmat.data.constants.enums.module.CourseModule;
 import com.qxgmat.data.constants.enums.module.ProductType;
 import com.qxgmat.data.constants.enums.module.VsCourseType;
+import com.qxgmat.data.constants.enums.status.AnswerStatus;
 import com.qxgmat.data.constants.enums.status.DirectionStatus;
 import com.qxgmat.data.constants.enums.user.DataType;
 import com.qxgmat.data.dao.entity.*;
@@ -20,6 +21,7 @@ import com.qxgmat.dto.request.*;
 import com.qxgmat.dto.response.*;
 import com.qxgmat.help.ShiroHelp;
 import com.qxgmat.service.UserCollectExperienceService;
+import com.qxgmat.service.UserNoteCourseService;
 import com.qxgmat.service.UsersService;
 import com.qxgmat.service.extend.CourseExtendService;
 import com.qxgmat.service.extend.PreviewService;
@@ -97,6 +99,9 @@ public class CourseController {
     private UserAskCourseService userAskCourseService;
 
     @Autowired
+    private UserNoteCourseService userNoteCourseService;
+
+    @Autowired
     private UserOrderRecordService userOrderRecordService;
 
     @Autowired
@@ -115,8 +120,26 @@ public class CourseController {
     @RequestMapping(value = "/vs", method = RequestMethod.GET)
     @ApiOperation(value = "获取1v1课程信息", notes = "获取1v1课程信息", httpMethod = "GET")
     public Response<List<CourseListDto>> vs()  {
+        User user = (User) shiroHelp.getLoginUser();
         List<Course> p = courseService.all(CourseModule.VS);
         List<CourseListDto> pr = Transform.convert(p, CourseListDto.class);
+        Collection ids = Transform.getIds(pr, CourseListDto.class, "id");
+
+        if(user != null){
+            // 已购买: 查看当前服务
+            List<UserCourse> userCourseList = userCourseService.listByCourse(user.getId(), ids);
+            Map userCourseMap = Transform.getMap(userCourseList, UserCourse.class, "courseId");
+
+            // 添加购物车
+            List<UserOrderCheckout> userOrderCheckoutList = userOrderCheckoutService.listWithProduct(user.getId(), ProductType.COURSE, ids);
+            Map userOrderCheckoutMap = Transform.getMap(userOrderCheckoutList, UserOrderCheckout.class, "productId");
+
+            for(CourseListDto dto : pr){
+                dto.setHave(userCourseMap.containsKey(dto.getId()));
+                dto.setAdd(userOrderCheckoutMap.containsKey(dto.getId()));
+            }
+        }
+
         return ResponseHelp.success(pr);
     }
 
@@ -195,9 +218,9 @@ public class CourseController {
         dto.setFaqs(Transform.convert(faqList, FaqExtendDto.class));
 
         // 优质问答
-        UserAskCourseStatRelation relation = userAskCourseService.statCourse(courseId, 1);
-        if (relation != null){
-            dto.setAskNumber(relation.getNumber());
+        UserAskCourseStatRelation askCourseStatRelation = userAskCourseService.statCourse(courseId, 1);
+        if (askCourseStatRelation != null){
+            dto.setAskSpecialNumber(askCourseStatRelation.getNumber());
         }
 
         if(user != null){
@@ -209,6 +232,54 @@ public class CourseController {
 
             dto.setHave(userCourse != null);
             dto.setAdd(userOrderCheckout != null);
+
+            if (userCourse != null){
+
+                // 进度信息
+                Collection<UserCourseProgress> progressList = userCourseProgressService.getByRecordId(userCourse.getRecordId());
+
+                dto.setCurrentNo(courseExtendService.computeCourseNoCurrent(courseNoList, progressList));
+                dto.setProgress(Transform.convert(progressList, UserCourseProgressExtendDto.class));
+
+                // 获取每个科目的所有作业
+                Collection<UserPreviewPaperRelation> previewList = previewService.getByRecordId(user.getId(), userCourse.getRecordId(), 1000);
+                dto.setPapers(Transform.convert(previewList, BasePaperExtendDto.class));
+                int finish = 0;
+                for(UserPreviewPaperRelation relation : previewList){
+                    if (relation.getPaper() == null) continue;
+                    UserPaper paper = relation.getPaper();
+                    if (paper.getTimes() > 0){
+                        finish += 1;
+                    }
+                }
+                dto.setPreviewProgress(previewList.size()> 0 ? finish * 100 / previewList.size(): 0);
+
+                // 提问数、笔记数
+                Collection<UserAskCourse> askList = userAskCourseService.getByRecordId(userCourse.getRecordId());
+                Collection<UserNoteCourse> noteList = userNoteCourseService.getByCourse(user.getId(), courseId);
+                Map<Object, List<UserAskCourse>> askListMap = Transform.getMapList(askList, UserAskCourse.class, "courseNoId");
+                Map notes = Transform.getMap(noteList, UserNoteCourse.class, "courseNoId");
+                Collection<CourseNoExtendDto> courseNos = dto.getCourseNos();
+                int noteNumber = 0;
+                int askNumber = askList == null ? 0: askList.size();
+                int answerNumber = askList == null ? 0 : (int)askList.stream().filter(r->r.getAnswerStatus()== AnswerStatus.ANSWER.index).count();
+                for(CourseNoExtendDto courseNo : courseNos){
+                    if (notes.get(courseNo.getId()) != null){
+                        courseNo.setNote(true);
+                        noteNumber += 1;
+                    }
+                    List<UserAskCourse> askListNo = askListMap.get(courseNo.getId());
+                    if (askListNo != null){
+                        courseNo.setAskNumber(askListNo.size());
+                        courseNo.setAnswerNumber((int)askListNo.stream().filter(r->r.getAnswerStatus()== AnswerStatus.ANSWER.index).count());
+                    }
+                }
+                dto.setNoteNumber(noteNumber);
+                dto.setAskNumber(askNumber);
+                dto.setAnswerNumber(answerNumber);
+            }
+
+
         }
         return ResponseHelp.success(dto);
     }
@@ -273,6 +344,7 @@ public class CourseController {
                     .courseId(dto.getCourseId())
                     .courseNoId(dto.getCurrentCourseNoId())
                     .recordId(userCourse.getRecordId())
+                    .userTime(dto.getTime())
                     .build());
         }
 

+ 1 - 1
server/gateway-api/src/main/java/com/qxgmat/controller/api/MyController.java

@@ -1732,7 +1732,7 @@ public class MyController {
 
         // 提问数、笔记数
         Map<Object, Collection<UserAskCourse>> askMap = userAskCourseService.groupByRecordId(recordIds);
-        Map<Object, Collection<UserNoteCourse>> noteMap = userNoteCourseService.groupByCourse(courseIds);
+        Map<Object, Collection<UserNoteCourse>> noteMap = userNoteCourseService.groupByCourse(user.getId(), courseIds);
         for(UserCourseDetailDto dto : pr){
             Collection<CourseNoExtendDto> courseNos = dto.getCourseNos();
             if (courseNos == null) continue;

+ 72 - 4
server/gateway-api/src/main/java/com/qxgmat/dto/response/CourseDetailDto.java

@@ -2,9 +2,7 @@ package com.qxgmat.dto.response;
 
 import com.nuliji.tools.annotation.Dto;
 import com.qxgmat.data.dao.entity.Course;
-import com.qxgmat.dto.extend.CommentExtendDto;
-import com.qxgmat.dto.extend.CourseNoExtendDto;
-import com.qxgmat.dto.extend.FaqExtendDto;
+import com.qxgmat.dto.extend.*;
 
 import java.math.BigDecimal;
 import java.util.Collection;
@@ -43,7 +41,7 @@ public class CourseDetailDto extends Course {
 
     private Integer useExpireDays;
 
-    private Integer askNumber;
+    private Integer askSpecialNumber;
 
     private Boolean have;
 
@@ -55,6 +53,20 @@ public class CourseDetailDto extends Course {
 
     private Collection<FaqExtendDto> faqs;
 
+    private Integer currentNo;
+
+    private Integer previewProgress;
+
+    private Integer askNumber;
+
+    private Integer answerNumber;
+
+    private Integer noteNumber;
+
+    private Collection<UserCourseProgressExtendDto> progress;
+
+    private Collection<BasePaperExtendDto> papers;
+
     public Integer getId() {
         return id;
     }
@@ -227,6 +239,30 @@ public class CourseDetailDto extends Course {
         this.add = add;
     }
 
+    public Integer getAskSpecialNumber() {
+        return askSpecialNumber;
+    }
+
+    public void setAskSpecialNumber(Integer askSpecialNumber) {
+        this.askSpecialNumber = askSpecialNumber;
+    }
+
+    public Integer getCurrentNo() {
+        return currentNo;
+    }
+
+    public void setCurrentNo(Integer currentNo) {
+        this.currentNo = currentNo;
+    }
+
+    public Integer getPreviewProgress() {
+        return previewProgress;
+    }
+
+    public void setPreviewProgress(Integer previewProgress) {
+        this.previewProgress = previewProgress;
+    }
+
     public Integer getAskNumber() {
         return askNumber;
     }
@@ -234,4 +270,36 @@ public class CourseDetailDto extends Course {
     public void setAskNumber(Integer askNumber) {
         this.askNumber = askNumber;
     }
+
+    public Integer getAnswerNumber() {
+        return answerNumber;
+    }
+
+    public void setAnswerNumber(Integer answerNumber) {
+        this.answerNumber = answerNumber;
+    }
+
+    public Integer getNoteNumber() {
+        return noteNumber;
+    }
+
+    public void setNoteNumber(Integer noteNumber) {
+        this.noteNumber = noteNumber;
+    }
+
+    public Collection<UserCourseProgressExtendDto> getProgress() {
+        return progress;
+    }
+
+    public void setProgress(Collection<UserCourseProgressExtendDto> progress) {
+        this.progress = progress;
+    }
+
+    public Collection<BasePaperExtendDto> getPapers() {
+        return papers;
+    }
+
+    public void setPapers(Collection<BasePaperExtendDto> papers) {
+        this.papers = papers;
+    }
 }

+ 13 - 1
server/gateway-api/src/main/java/com/qxgmat/service/UserNoteCourseService.java

@@ -83,17 +83,29 @@ public class UserNoteCourseService extends AbstractService {
         }
     }
 
+    public List<UserNoteCourse> getByCourse(Integer userId, Integer courseId){
+        Example example = new Example(UserNoteCourse.class);
+        example.and(
+                example.createCriteria()
+                        .andEqualTo("userId", userId)
+                        .andEqualTo("courseId", courseId)
+        );
+        return select(userNoteCourseMapper, example);
+    }
+
+
     /**
      * 获取课程记录分组列表
      * @param courseIds
      * @return
      */
-    public Map<Object, Collection<UserNoteCourse>> groupByCourse(Collection courseIds){
+    public Map<Object, Collection<UserNoteCourse>> groupByCourse(Integer userId, Collection courseIds){
         Map<Object, Collection<UserNoteCourse>> relationMap = new HashMap<>();
         if(courseIds == null || courseIds.size() == 0) return relationMap;
         Example example = new Example(UserNoteCourse.class);
         example.and(
                 example.createCriteria()
+                        .andEqualTo("userId", userId)
                         .andIn("courseId", courseIds)
         );
         List<UserNoteCourse> nos =  select(userNoteCourseMapper, example);

+ 0 - 2
server/gateway-api/src/main/java/com/qxgmat/service/extend/MessageExtendService.java

@@ -1,6 +1,5 @@
 package com.qxgmat.service.extend;
 
-import com.alipay.api.domain.TransOrderDetail;
 import com.nuliji.tools.Transform;
 import com.nuliji.tools.exception.ParameterException;
 import com.qxgmat.data.constants.enums.*;
@@ -19,7 +18,6 @@ import com.qxgmat.service.inline.*;
 import org.apache.logging.log4j.util.Strings;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
-import sun.plugin2.message.Message;
 
 import javax.annotation.Resource;
 import java.text.SimpleDateFormat;

+ 4 - 0
server/gateway-api/src/main/java/com/qxgmat/service/extend/PreviewService.java

@@ -70,6 +70,10 @@ public class PreviewService extends AbstractService {
         return relationMap;
     }
 
+    public List<UserPreviewPaperRelation> getByRecordId(Integer userId, Integer recordId, Integer top){
+        return list(1, top, recordId, userId,  null, 0);
+    }
+
     /**
      * 获取用户分组作业列表
      * @param userId

+ 9 - 0
server/gateway-api/src/main/java/com/qxgmat/service/inline/UserAskCourseService.java

@@ -198,6 +198,15 @@ public class UserAskCourseService extends AbstractService {
         }
     }
 
+    public List<UserAskCourse> getByRecordId(Integer recordId){
+        Example example = new Example(UserAskCourse.class);
+        example.and(
+                example.createCriteria()
+                        .andEqualTo("recordId", recordId)
+        );
+        return select(userAskCourseMapper, example);
+    }
+
     /**
      * 获取课程记录分组列表
      * @param recordIds

+ 10 - 0
server/gateway-api/src/main/java/com/qxgmat/service/inline/UserCourseProgressService.java

@@ -79,6 +79,16 @@ public class UserCourseProgressService extends AbstractService {
         return select(userCourseProgressMapper, example);
     }
 
+    public List<UserCourseProgress> getByRecordId(Integer recordId){
+        Example example = new Example(UserCourseProgress.class);
+        example.and(
+                example.createCriteria()
+                        .andEqualTo("recordId", recordId)
+        );
+        return select(userCourseProgressMapper, example);
+    }
+
+
     /**
      * 获取课程记录分组列表
      * @param recordIds

+ 1 - 1
server/gateway-api/src/main/resources/application.yml

@@ -58,7 +58,7 @@ pay:
     pid: 2088521305942904
 
   wechat:
-    appKey: hanruizhangnachenxuanchenyue1416
+    appKey: 0118wasteless0118wasteless0118wa
     pid: 1531541431
 
 ip: