Bladeren bron

feat(www): 预习作业

Go 5 jaren geleden
bovenliggende
commit
06f9ec413d
54 gewijzigde bestanden met toevoegingen van 1794 en 450 verwijderingen
  1. 3 1
      front/project/Constant.js
  2. 9 0
      front/project/admin/routes/course/detail/page.js
  3. 2 2
      front/project/admin/routes/interaction/index.js
  4. 2 2
      front/project/h5/routes/page/login/page.js
  5. 2 2
      front/project/h5/stores/common.js
  6. 89 88
      front/project/www/components/Card/index.js
  7. 7 0
      front/project/www/components/Card/index.less
  8. 4 4
      front/project/www/components/Login/index.js
  9. 16 2
      front/project/www/components/Login/index.less
  10. 1 1
      front/project/www/local.json
  11. 10 0
      front/project/www/routes/examination/list/index.js
  12. 59 0
      front/project/www/routes/examination/list/index.less
  13. 286 0
      front/project/www/routes/examination/list/page.js
  14. 1 1
      front/project/www/routes/exercise/list/index.js
  15. 18 2
      front/project/www/routes/exercise/list/page.js
  16. 2 0
      front/project/www/routes/exercise/main/index.less
  17. 187 41
      front/project/www/routes/exercise/main/page.js
  18. 1 1
      front/project/www/routes/page/home/page.js
  19. 28 35
      front/project/www/routes/paper/process/base/index.js
  20. 51 10
      front/project/www/routes/paper/process/page.js
  21. 2 2
      front/project/www/routes/paper/question/page.js
  22. 2 1
      front/project/www/routes/preview/index.js
  23. 1 1
      front/project/www/routes/preview/list/index.less
  24. 2 1
      front/project/www/routes/textbook/index.js
  25. 10 0
      front/project/www/routes/textbook/list/index.js
  26. 59 0
      front/project/www/routes/textbook/list/index.less
  27. 286 0
      front/project/www/routes/textbook/list/page.js
  28. 194 186
      front/project/www/static/login.html
  29. 48 4
      front/project/www/stores/course.js
  30. 8 0
      front/project/www/stores/my.js
  31. 32 4
      front/project/www/stores/order.js
  32. 11 12
      front/project/www/stores/question.js
  33. 14 0
      front/project/www/stores/textbook.js
  34. 20 0
      front/src/services/Tools.js
  35. 18 0
      server/data/src/main/java/com/qxgmat/data/constants/enums/module/VideoCourseType.java
  36. 35 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/Course.java
  37. 4 2
      server/data/src/main/java/com/qxgmat/data/dao/mapping/CourseMapper.xml
  38. 7 7
      server/data/src/main/java/com/qxgmat/data/relation/mapping/PreviewAssignRelationMapper.xml
  39. 4 4
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserOrderRecordRelationMapper.xml
  40. 10 0
      server/gateway-api/src/main/java/com/qxgmat/controller/admin/CourseController.java
  41. 19 4
      server/gateway-api/src/main/java/com/qxgmat/controller/api/CourseController.java
  42. 1 1
      server/gateway-api/src/main/java/com/qxgmat/controller/api/TextbookController.java
  43. 40 0
      server/gateway-api/src/main/java/com/qxgmat/dto/extend/CourseExtendDto.java
  44. 69 0
      server/gateway-api/src/main/java/com/qxgmat/dto/extend/CourseTeacherExtendDto.java
  45. 9 9
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserCourseDetailDto.java
  46. 50 9
      server/gateway-api/src/main/java/com/qxgmat/dto/response/UserCourseProgressDto.java
  47. 2 4
      server/gateway-api/src/main/java/com/qxgmat/service/UserPaperService.java
  48. 1 0
      server/gateway-api/src/main/java/com/qxgmat/service/UserQuestionService.java
  49. 3 1
      server/gateway-api/src/main/java/com/qxgmat/service/extend/CourseExtendService.java
  50. 37 0
      server/gateway-api/src/main/java/com/qxgmat/service/extend/PreviewService.java
  51. 2 2
      server/gateway-api/src/main/java/com/qxgmat/service/extend/QuestionFlowService.java
  52. 7 1
      server/gateway-api/src/main/java/com/qxgmat/service/inline/CoursePackageService.java
  53. 7 1
      server/gateway-api/src/main/java/com/qxgmat/service/inline/CourseService.java
  54. 2 2
      server/gateway-api/src/main/java/com/qxgmat/service/inline/UserOrderRecordService.java

+ 3 - 1
front/project/Constant.js

@@ -1,8 +1,10 @@
 export const UserUrl = 'http://www.baidu.com';
 
+export const WechatUserAppId = 'wx324965bb6800f9b9';
+
 export const H5Url = 'http://127.0.0.1:3000';
 
-export const WechatAppId = 'wxbee75af2ece94ed7';
+export const WechatH5AppId = 'wxbee75af2ece94ed7';
 
 export const QuestionDifficult = [{ label: 'easy', value: 'easy' }, { label: 'medium', value: 'medium' }, { label: 'hard', value: 'hard' }];
 

+ 9 - 0
front/project/admin/routes/course/detail/page.js

@@ -468,6 +468,15 @@ export default class extends Page {
             <Input placeholder='请输入课程名称' />,
           )}
         </Form.Item>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='提问权限延迟'>
+          {getFieldDecorator('askExtendDays', {
+            rules: [
+              { required: true, message: '请输入扩展天数' },
+            ],
+          })(
+            <InputNumber placeholder='天' />,
+          )}
+        </Form.Item>
       </Form>
     </Block>;
   }

+ 2 - 2
front/project/admin/routes/interaction/index.js

@@ -1,8 +1,8 @@
 
 import askQuestion from './askQuestion';
 import askQuestionDetail from './askQuestionDetail';
-import faqConsult from './faqConsult';
+import faq from './faq';
 import comment from './comment';
 import feedback from './feedback';
 
-export default [askQuestion, askQuestionDetail, faqConsult, comment, feedback];
+export default [askQuestion, askQuestionDetail, faq, comment, feedback];

+ 2 - 2
front/project/h5/routes/page/login/page.js

@@ -1,7 +1,7 @@
 import React from 'react';
 import './index.less';
 import Page from '@src/containers/Page';
-import { WechatAppId, H5Url } from '../../../../Constant';
+import { WechatH5AppId, H5Url } from '../../../../Constant';
 import { User } from '../../../stores/user';
 
 export default class extends Page {
@@ -16,7 +16,7 @@ export default class extends Page {
         }
       });
     } else {
-      const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${WechatAppId}&redirect_uri=${encodeURIComponent(
+      const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${WechatH5AppId}&redirect_uri=${encodeURIComponent(
         `${H5Url}/login`,
       )}&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect`;
       window.location.href = url;

+ 2 - 2
front/project/h5/stores/common.js

@@ -1,6 +1,6 @@
 import BaseStore from '@src/stores/base';
 import { generateUUID } from '@src/services/Tools';
-import { WechatAppId } from '../../Constant';
+import { WechatH5AppId } from '../../Constant';
 
 export default class CommonStore extends BaseStore {
   /**
@@ -40,7 +40,7 @@ export default class CommonStore extends BaseStore {
       a.push('checkJsApi');
       wx.config({
         debug: false, // 是否打开调试模式,调用的api会被alert出来,在pc端也能看到log信息
-        appid: WechatAppId, // 必填,微信公众号的唯一标识
+        appid: WechatH5AppId, // 必填,微信公众号的唯一标识
         timestamp: time, // 必填,生成签名的时间戳
         nonceStr: nonce, // 必填,生成签名的随机串
         signature: sha1(p), // 必填,用于验证的签名

+ 89 - 88
front/project/www/components/Card/index.js

@@ -3,6 +3,7 @@ import './index.less';
 import { Link } from 'react-router-dom';
 import { Checkbox } from 'antd';
 import Assets from '@src/components/Assets';
+import { formatDate } from '@src/services/Tools';
 import Module from '../Module';
 import Progress from '../Progress';
 import IconButton from '../IconButton';
@@ -52,42 +53,41 @@ export default class Card extends Component {
             <div>好棒!</div>
             <div>近期的作业都完成啦</div>
           </div>
-        ) : (
-          <div className="list">
-            {process.previews.map(item => {
-              return (
-                <div className="item">
-                  <div className="top">
-                    <div className="date">{item.time}</div>
-                    <div className="action">
-                      {!item.repport.id && (
-                        <IconButton
-                          type="start"
-                          tip="Start"
-                          onClick={() => previewAction && previewAction('start', item)}
-                        />
-                      )}
-                      {item.repport.id && (
-                        <IconButton
-                          type="continue"
-                          onClick={() => previewAction && previewAction('continue', item)}
-                          tip="Continue"
-                        />
-                      )}
-                      {item.repport.id && (
-                        <IconButton
-                          type="restart"
-                          onClick={() => previewAction && previewAction('restart', item)}
-                          tip="Restart"
-                        />
-                      )}
-                    </div>
+        ) : (<div className="list">
+          {process.previews.map(item => {
+            return (
+              <div className="item">
+                <div className="top">
+                  <div className="date">{item.time}</div>
+                  <div className="action">
+                    {!item.repport.id && (
+                      <IconButton
+                        type="start"
+                        tip="Start"
+                        onClick={() => previewAction && previewAction('start', item)}
+                      />
+                    )}
+                    {item.repport.id && (
+                      <IconButton
+                        type="continue"
+                        onClick={() => previewAction && previewAction('continue', item)}
+                        tip="Continue"
+                      />
+                    )}
+                    {item.repport.id && (
+                      <IconButton
+                        type="restart"
+                        onClick={() => previewAction && previewAction('restart', item)}
+                        tip="Restart"
+                      />
+                    )}
                   </div>
-                  <Progress progress={item.report.id ? item.repport.userNumber / item.report.questionNumber : 0} />
                 </div>
-              );
-            })}
-          </div>
+                <Progress progress={item.report.id ? item.repport.userNumber / item.report.questionNumber : 0} />
+              </div>
+            );
+          })}
+        </div>
         )}
       </div>
     );
@@ -124,11 +124,16 @@ export default class Card extends Component {
 
 export class Card1 extends Component {
   getEndBody() {
+    const { data, onPreview } = this.props;
+    const { useStartTime, useEndTime } = data;
     return (
       <div className="body">
         <div className="text">
           <div className="t-1">课程已经结束啦</div>
         </div>
+        <div className="bottom">
+          有效期: {formatDate(useStartTime, 'YYYY-MM-DD')}至{formatDate(useEndTime, 'YYYY-MM-DD')} <a onClick={() => onPreview && onPreview()}>全部作业></a>
+        </div>
       </div>
     );
   }
@@ -145,9 +150,9 @@ export class Card1 extends Component {
   }
 
   getOpenBody() {
-    const { data } = this.props;
-    const { qrCode } = data;
-    return !qrCode ? (
+    const { data, onOpen } = this.props;
+    const { teacher, endTime } = data;
+    return !teacher ? (
       <div className="body">
         <div className="text">
           <Checkbox />
@@ -156,25 +161,24 @@ export class Card1 extends Component {
           </span>
         </div>
         <div className="btn">
-          <Button size="lager" radius>
+          <Button size="lager" radius onClick={() => onOpen && onOpen()}>
             开通作业
           </Button>
         </div>
       </div>
-    ) : (
-      <div className="body">
-        <div className="t-1">请尽快与老师预约上课时间</div>
-        <div className="t-2">请于 2019-07-25 前开通课程</div>
-        <div className="qr-code">
-          <Assets name="qrcode" />
-        </div>
+    ) : (<div className="body">
+      <div className="t-1">请尽快与老师预约上课时间</div>
+      <div className="t-2">请于 {formatDate(endTime, 'YYYY-MM-DD')} 前开通课程</div>
+      <div className="qr-code">
+        <Assets name="qrcode" src={teacher.qr} />
       </div>
+    </div>
     );
   }
 
   getIngBody() {
-    const { data, previewAction } = this.props;
-    const { list = [] } = data;
+    const { data, list = [], previewAction, onPreview } = this.props;
+    const { useStartTime, useEndTime } = data;
     return (
       <div className="body">
         {list.length > 0 && <div className="title">近期待完成</div>}
@@ -183,55 +187,53 @@ export class Card1 extends Component {
             <div className="t-1">好棒!</div>
             <div className="t-2">近期的作业都完成啦</div>
           </div>
-        ) : (
-          <div className="list">
-            {list.map(item => {
-              return (
-                <div className="item">
-                  <div className="top">
-                    <div className="title">{item.title}</div>
-                  </div>
-                  <div className="detail">
-                    <Progress size="small" progress={item.progress} />
-                    <div className="action">
-                      {item.status === 'start' && (
-                        <IconButton
-                          type="start"
-                          tip="Start"
-                          onClick={() => previewAction && previewAction('start', item)}
-                        />
-                      )}
-                      {item.status === 'continue' && (
-                        <IconButton
-                          type="continue"
-                          onClick={() => previewAction && previewAction('continue', item)}
-                          tip="Continue"
-                        />
-                      )}
-                      {item.status === 'restart' && (
-                        <IconButton
-                          type="restart"
-                          onClick={() => previewAction && previewAction('restart', item)}
-                          tip="Restart"
-                        />
-                      )}
-                    </div>
+        ) : (<div className="list">
+          {list.map(item => {
+            return (
+              <div className="item">
+                <div className="top">
+                  <div className="title">{item.title}</div>
+                </div>
+                <div className="detail">
+                  <Progress size="small" progress={item.progress} />
+                  <div className="action">
+                    {item.progress === 0 && (
+                      <IconButton
+                        type="start"
+                        tip="Start"
+                        onClick={() => previewAction && previewAction('start', item)}
+                      />
+                    )}
+                    {item.progress > 0 && (
+                      <IconButton
+                        type="continue"
+                        onClick={() => previewAction && previewAction('continue', item)}
+                        tip="Continue"
+                      />
+                    )}
+                    {item.progress > 0 && (
+                      <IconButton
+                        type="restart"
+                        onClick={() => previewAction && previewAction('restart', item)}
+                        tip="Restart"
+                      />
+                    )}
                   </div>
                 </div>
-              );
-            })}
-          </div>
+              </div>
+            );
+          })}
+        </div>
         )}
         <div className="bottom">
-          有效期: 2019-08-20至2020-01-01 <Link to="">全部作业></Link>
+          有效期: {formatDate(useStartTime, 'YYYY-MM-DD')}至{formatDate(useEndTime, 'YYYY-MM-DD')} <a onClick={() => onPreview && onPreview()}>全部作业></a>
         </div>
       </div>
     );
   }
 
   getBody() {
-    const { data } = this.props;
-    const { status } = data;
+    const { status } = this.props;
     if (status === 'end') return this.getEndBody();
     if (status === 'stop') return this.getStopBody();
     if (status === 'open') return this.getOpenBody();
@@ -240,8 +242,7 @@ export class Card1 extends Component {
   }
 
   render() {
-    const { style, data, title, tag } = this.props;
-    const { status } = data;
+    const { style, title, tag, status } = this.props;
     return (
       <Module style={style} className={`card1 ${status}`}>
         <div className="header">

+ 7 - 0
front/project/www/components/Card/index.less

@@ -295,6 +295,13 @@
       color: #5E677B;
     }
   }
+
+  .bottom {
+    color: #8897A8;
+    font-size: 12px;
+    position: absolute;
+    bottom: 20px;
+  }
 }
 
 .module.card1.stop {

+ 4 - 4
front/project/www/components/Login/index.js

@@ -8,7 +8,7 @@ import { Button as GButton } from '../Button';
 import { User } from '../../stores/user';
 import { My } from '../../stores/my';
 import { Common } from '../../stores/common';
-import { MobileArea } from '../../../Constant';
+import { MobileArea, WechatUserAppId } from '../../../Constant';
 
 const LOGIN_PHONE = 'LOGIN_PHONE';
 const LOGIN_WX = 'LOGIN_WX';
@@ -19,7 +19,7 @@ const BIND_WX_ERROR = 'BIND_WX_ERROR';
 export default class Login extends Component {
   constructor(props) {
     super(props);
-    this.state = { type: LOGIN_WX };
+    this.state = { type: LOGIN_WX, data: { area: MobileArea[0].value } };
     window.addEventListener(
       'message',
       event => {
@@ -261,7 +261,7 @@ export default class Login extends Component {
       <div className="body">
         <div className="title">微信扫码登录</div>
         <div className="qr-code">
-          <iframe frameBorder="0" src="/login.html" width="300" height="300" />
+          <iframe frameBorder="0" src={`/login.html?appid=${WechatUserAppId}&redirectUri=${encodeURIComponent('http://www.duoshaojiaoyu.com')}`} width="300" height="300" />
           <div className="text">请使用微信扫描二维码登录</div>
         </div>
         <Tooltip overlayClassName="gray" placement="left" title="手机号登录">
@@ -341,7 +341,7 @@ export default class Login extends Component {
           手机号注册成功!为更好的使用服务,建议您绑定微信号。
         </div>
         <div className="qr-code">
-          <Assets name="qrcode" />
+          <iframe frameBorder="0" src={`/login.html?appid=${WechatUserAppId}&redirectUri=${encodeURIComponent('http://www.duoshaojiaoyu.com')}`} width="300" height="300" />
           <div className="text">请使用微信扫描二维码登录</div>
           <div
             className="jump"

+ 16 - 2
front/project/www/components/Login/index.less

@@ -97,6 +97,7 @@
       }
 
       .g-input-left {
+        position: relative;
         cursor: pointer;
 
         .g-input-left-select {
@@ -136,6 +137,19 @@
       height: 25px;
       line-height: 25px;
     }
+
+    .select-list {
+      top: 36px;
+      width: 61px;
+      border: 1px solid #EAEDF2;
+      max-height: 200px;
+      overflow-y: auto;
+      list-style: none;
+      margin: 0;
+      padding: 5px 10px;
+      position: absolute;
+      background: #fff;
+    }
   }
 }
 
@@ -149,8 +163,8 @@
 
 .LOGIN_WX .body {
   .qr-code {
-    padding-top: 60px;
-    padding-bottom: 120px;
+    padding-top: 20px;
+    padding-bottom: 10px;
   }
 }
 

+ 1 - 1
front/project/www/local.json

@@ -7,7 +7,7 @@
     ],
     "proxy": [
       {
-        "target": "http://qianxing.nuliji.com",
+        "target": "http://127.0.0.1:8888",
         "from": "/api",
         "to": "/api"
       }

+ 10 - 0
front/project/www/routes/examination/list/index.js

@@ -0,0 +1,10 @@
+export default {
+  path: '/examination/list/:id',
+  key: 'examination-list',
+  title: '模考列表',
+  needLogin: false,
+  tab: 'examination',
+  component() {
+    return import('./page');
+  },
+};

+ 59 - 0
front/project/www/routes/examination/list/index.less

@@ -0,0 +1,59 @@
+@charset "utf-8";
+
+#examination-list {
+  .code-module {
+    padding: 80px 250px;
+    text-align: center;
+
+    .title {
+      font-size: 18px;
+      margin-bottom: 24px;
+    }
+
+    .input-block {
+      margin-bottom: 24px;
+
+      .input {
+        width: 350px;
+
+        input {
+          border-top-left-radius: 22px;
+          border-bottom-left-radius: 22px;
+        }
+      }
+
+      .button {
+        width: 150px;
+        border-top-right-radius: 22px;
+        border-bottom-right-radius: 22px;
+      }
+    }
+
+    .tip {
+      .left {
+        float: left;
+      }
+
+      .right {
+        float: right;
+      }
+    }
+  }
+
+  .work-body {
+    .work-nav {
+      margin-bottom: 20px;
+
+      .left {
+        display: inline-block;
+        padding-left: 5px;
+        font-size: 16px;
+        font-weight: 600;
+      }
+
+      .right {
+        float: right;
+      }
+    }
+  }
+}

+ 286 - 0
front/project/www/routes/examination/list/page.js

@@ -0,0 +1,286 @@
+import React from 'react';
+import './index.less';
+import Page from '@src/containers/Page';
+import { asyncConfirm } from '@src/services/AsyncTools';
+import { formatPercent, formatSeconds, formatDate } from '@src/services/Tools';
+import Tabs from '../../../components/Tabs';
+import Module from '../../../components/Module';
+import ListTable from '../../../components/ListTable';
+import ProgressText from '../../../components/ProgressText';
+import IconButton from '../../../components/IconButton';
+import { Main } from '../../../stores/main';
+import { Question } from '../../../stores/question';
+import { QuestionDifficult } from '../../../../Constant';
+
+const LOGIC_NO = 'no';
+const LOGIC_PLACE = 'place';
+const LOGIC_DIFFICULT = 'difficult';
+const LOGIC_ERROR = 'error';
+
+export default class extends Page {
+  initState() {
+    this.columns = [
+      {
+        title: '练习册',
+        width: 250,
+        align: 'left',
+        render: (record) => {
+          let progress = 0;
+          if (record.report) {
+            progress = formatPercent(record.report.userNumber, record.report.questionNumber);
+          }
+          return (
+            <div className="table-row">
+              <div className="night f-s-16">{record.title}</div>
+              <div>
+                <ProgressText progress={progress} size="small" />
+              </div>
+            </div>
+          );
+        },
+      },
+      {
+        title: '正确率',
+        width: 150,
+        align: 'left',
+        render: (record) => {
+          let correct = '--';
+          if (record.report) {
+            correct = formatPercent(record.report.userCorrect, record.report.userNumber, false);
+          }
+          return (
+            <div className="table-row">
+              <div className="night f-s-16 f-w-b">{correct}</div>
+              <div className="f-s-12">全站{formatPercent(record.stat.totalCorrect, record.stat.totalNumber, false)}</div>
+            </div>
+          );
+        },
+      },
+      {
+        title: '全站用时',
+        width: 150,
+        align: 'left',
+        render: (record) => {
+          let time = '--';
+          if (record.paper) {
+            time = formatSeconds(record.paper.report.userTime / record.paper.report.userNumber);
+          }
+          return (
+            <div className="table-row">
+              <div className="night f-s-16 f-w-b">{time}</div>
+              <div className="f-s-12">全站{formatSeconds(record.stat.totalTime / record.stat.totalNumber)}</div>
+            </div>
+          );
+        },
+      },
+      {
+        title: '最近做题',
+        width: 150,
+        align: 'left',
+        render: (record) => {
+          if (!record.report) return null;
+          return (
+            <div className="table-row">
+              <div>{formatDate(record.report.updateTime, 'YYYY-MM-DD')}</div>
+              <div>{formatDate(record.report.updateTime, 'HH:mm')}</div>
+            </div>
+          );
+        },
+      },
+      {
+        title: '操作',
+        width: 180,
+        align: 'left',
+        render: (record) => {
+          return (
+            <div className="table-row p-t-1">
+              {!record.report && <IconButton type="start" tip="Start" onClick={() => {
+                Question.startLink('exercise', record);
+              }} />}
+              {(record.report && !record.report.isFinish) && <IconButton className="m-r-2" type="continue" tip="Continue" onClick={() => {
+                Question.continueLink('exercise', record);
+              }} />}
+              <IconButton type="restart" tip="Restart" onClick={() => {
+                this.restart(record);
+              }} />
+            </div>
+          );
+        },
+      },
+      {
+        title: '报告',
+        width: 30,
+        align: 'right',
+        render: (record) => {
+          if (!record.report || !record.report.isFinish) return null;
+          return (
+            <div className="table-row p-t-1">
+              <IconButton type="report" tip="Report" onClick={() => {
+                Question.reportLink(record);
+              }} />
+            </div>
+          );
+        },
+      },
+    ];
+    this.placeList = [];
+    this.inited = false;
+    return {
+      logic: LOGIC_NO,
+      logicExtend: '',
+      logics: [{
+        key: LOGIC_NO,
+        title: '按顺序练习',
+      }, {
+        key: LOGIC_PLACE,
+        title: '按考点练习',
+      }, {
+        key: LOGIC_DIFFICULT,
+        title: '按难度练习',
+      }, {
+        key: LOGIC_ERROR,
+        title: '按易错度练习',
+      }],
+    };
+  }
+
+  init() {
+    const { id } = this.params;
+    Main.getExerciseParent(id).then(result => {
+      const navs = result;
+      this.inited = true;
+      this.setState({ navs });
+    });
+  }
+
+  initData() {
+    const data = Object.assign(this.state, this.state.search);
+    this.setState(data);
+    this.refreshData();
+  }
+
+  refreshData(newLogic) {
+    const { logic } = this.state;
+    let handler = null;
+    switch (newLogic || logic) {
+      case LOGIC_PLACE:
+        handler = this.refreshPlace();
+        break;
+      case LOGIC_DIFFICULT:
+        handler = this.refreshDifficult();
+        break;
+      default:
+        handler = Promise.resolve();
+    }
+    handler.then(() => {
+      this.refreshExercise();
+    });
+  }
+
+  refreshPlace() {
+    const { id } = this.params;
+    let handler;
+    if (this.placeList.length > 0) {
+      this.setState({ logicExtends: this.placeList });
+      handler = Promise.resolve();
+    } else {
+      handler = Question.getExercisePlace(id).then(result => {
+        this.placeList = result.map(row => {
+          return {
+            name: row,
+            key: row,
+          };
+        });
+        this.setState({ logicExtends: this.placeList });
+      });
+    }
+    return handler.then(() => {
+      let { logicExtend } = this.state;
+      if (logicExtend === '') {
+        logicExtend = this.placeList[0].key;
+        this.setState({ logicExtend });
+      }
+    });
+  }
+
+  refreshDifficult() {
+    let { logicExtend } = this.state;
+    this.setState({
+      logicExtends: QuestionDifficult.map(difficult => {
+        difficult.name = difficult.label;
+        difficult.key = difficult.value;
+        return difficult;
+      }),
+    });
+    return Promise.resolve().then(() => {
+      if (logicExtend === '') {
+        logicExtend = QuestionDifficult[0].key;
+        this.setState({ logicExtend });
+      }
+    });
+  }
+
+  refreshExercise() {
+    const { logic, logicExtend } = this.state;
+    Question.getExerciseList(Object.assign({ structId: this.params.id, logic, logicExtend }, this.state.search))
+      .then((result) => {
+        this.setState({ list: result.list, total: result.total });
+      });
+  }
+
+  onChangeTab(key, value) {
+    const { logic } = this.state;
+    const data = {};
+    if (key === 'logicExtend') {
+      data.logic = logic;
+      data.logicExtend = value;
+    } else {
+      data.logic = value;
+    }
+    // this.refreshData(tab);
+    this.refreshQuery(data);
+  }
+
+  restart(item) {
+    asyncConfirm('提示', '是否重置', () => {
+      Question.restart(item.paper.id).then(() => {
+        this.refresh();
+      });
+    });
+  }
+
+  renderView() {
+    const { logic, logicExtend, logics = [], logicExtends = [], list } = this.state;
+    return (
+      <div>
+        <div className="content">
+          <Module className="m-t-2">
+            <Tabs
+              active={logic}
+              border
+              width="180px"
+              space="0"
+              tabs={logics}
+              onChange={(key) => {
+                this.onChangeTab('logic', key);
+              }}
+            />
+            {logicExtends.length > 0 && <Tabs
+              active={logicExtend}
+              type="text"
+              tabs={logicExtends}
+              onChange={(key) => {
+                this.onChangeTab('logicExtend', key);
+              }}
+            />}
+          </Module>
+
+          <ListTable
+            data={list}
+            columns={this.columns}
+          />
+        </div>
+      </div>
+    );
+  }
+}

+ 1 - 1
front/project/www/routes/exercise/list/index.js

@@ -1,6 +1,6 @@
 export default {
   path: '/exercise/list/:id',
-  key: 'exercise',
+  key: 'exercise-list',
   title: '练习列表',
   needLogin: false,
   tab: 'exercise',

+ 18 - 2
front/project/www/routes/exercise/list/page.js

@@ -250,7 +250,8 @@ export default class extends Page {
   }
 
   renderView() {
-    const { logic, logicExtend, logics = [], logicExtends = [], list } = this.state;
+    const { logic, logicExtend, logics = [], logicExtends = [], list, search } = this.state;
+    const { finish } = search;
     return (
       <div>
         <div className="content">
@@ -274,8 +275,23 @@ export default class extends Page {
               }}
             />}
           </Module>
-
           <ListTable
+            filters={[
+              {
+                type: 'radio',
+                checked: finish,
+                list: [{ key: 0, title: '未完成' }, { key: 1, title: '已完成' }],
+                onChange: item => {
+                  if (item.key === 0) {
+                    this.search({ finish: 0 });
+                  } else if (item.key === 1) {
+                    this.search({ finish: 1 });
+                  } else {
+                    this.search({ finish: null });
+                  }
+                },
+              },
+            ]}
             data={list}
             columns={this.columns}
           />

+ 2 - 0
front/project/www/routes/exercise/main/index.less

@@ -24,6 +24,8 @@
 
       .button {
         width: 150px;
+        height: 44px;
+        line-height: 28px;
         border-top-right-radius: 22px;
         border-bottom-right-radius: 22px;
       }

+ 187 - 41
front/project/www/routes/exercise/main/page.js

@@ -3,8 +3,8 @@ import './index.less';
 import { Modal } from 'antd';
 import { Link } from 'react-router-dom';
 import Page from '@src/containers/Page';
-import { asyncConfirm } from '@src/services/AsyncTools';
-import { formatTreeData, formatSeconds, formatDate, formatPercent } from '@src/services/Tools';
+import { asyncConfirm, asyncSMessage } from '@src/services/AsyncTools';
+import { formatTreeData, formatSeconds, formatDate, formatPercent, getMap } from '@src/services/Tools';
 import Continue from '../../../components/Continue';
 import Step from '../../../components/Step';
 import Panel from '../../../components/Panel';
@@ -15,7 +15,7 @@ import Input from '../../../components/Input';
 import Button from '../../../components/Button';
 import AnswerButton from '../../../components/AnswerButton';
 import Division from '../../../components/Division';
-import Card from '../../../components/Card';
+import { Card1 } from '../../../components/Card';
 import ListTable from '../../../components/ListTable';
 import ProgressText from '../../../components/ProgressText';
 import IconButton from '../../../components/IconButton';
@@ -25,13 +25,16 @@ import { Sentence } from '../../../stores/sentence';
 import { Question } from '../../../stores/question';
 import { Course } from '../../../stores/course';
 import { User } from '../../../stores/user';
-import { CourseModuleShow } from '../../../../Constant';
+import { CourseModuleShow, CourseModule } from '../../../../Constant';
+import { Order } from '../../../stores/order';
 
 const SENTENCE = 'sentence';
 const PREVIEW = 'preview';
 const PREVIEW_COURSE = 'PREVIEW_COURSE';
 const PREVIEW_LIST = 'PREVIEW_LIST';
 
+const CourseModuleMap = getMap(CourseModule, 'value', 'label');
+
 const exerciseColumns = [
   {
     title: '练习册',
@@ -286,18 +289,21 @@ export default class extends Page {
       const tabs = formatTreeData(list, 'id', 'title', 'parentId');
       // 课程顶级分类
       const courseStructs = result.filter(row => row.isCourse && row.level === 1);
+      courseStructs.unshift({ key: '', name: '全部' });
       tabs.push({ key: PREVIEW, name: '预习作业' });
-      this.setState({ tabs, courseStructs });
+      this.courseStructMap = getMap(courseStructs, 'id', 'title');
+      this.setState({
+        tabs,
+        courseStructs,
+        courseTabs: CourseModuleShow.map(row => {
+          row.title = row.label;
+          row.key = row.value;
+          return row;
+        }),
+      });
       this.inited = true;
       this.refreshData();
     });
-    this.setState({
-      courseTabs: CourseModuleShow.map(row => {
-        row.title = row.label;
-        row.key = row.value;
-        return row;
-      }),
-    });
   }
 
   initData() {
@@ -309,6 +315,13 @@ export default class extends Page {
       });
     }
     const data = Object.assign(this.state, this.state.search);
+    if (!data.tab1) {
+      data.tab1 = SENTENCE;
+    }
+    if (data.recordId) {
+      // 作业列表
+      data.previewType = PREVIEW_LIST;
+    }
     this.setState(data);
     if (this.inited) this.refreshData();
   }
@@ -420,7 +433,7 @@ export default class extends Page {
   }
 
   refreshCourseProcess() {
-    const { courseTabs, structId } = this.state;
+    const { courseTabs, courseStructs, struct } = this.state;
     let { tab2 } = this.state;
     let tab;
     if (tab2 === '') {
@@ -430,19 +443,24 @@ export default class extends Page {
     } else {
       ([tab] = courseTabs.filter(row => row.key === tab2));
     }
-    Course.progress(tab.courseModules, structId).then(result => {
-      const courseProgress = {};
-      for (let i = 0; i < result.length; i += 1) {
-        const item = result[i];
-        courseProgress[item.category].push(item);
-      }
-      this.setState({ courseProgress });
+    const [courseStruct] = courseStructs.filter(row => row.key === struct);
+    Course.progress(tab.value, courseStruct ? courseStruct.id : null).then(result => {
+      const courseMap = {};
+      const now = new Date().getTime();
+      courseMap.open = result.filter(row => !row.isUsed);
+      courseMap.end = result.filter(row => row.isUsed && new Date(row.useEndTime).getTime() < now);
+      courseMap.process = result.filter(row => row.isUsed && new Date(row.useEndTime).getTime() >= now);
+      this.setState({ courseMap });
     });
   }
 
   refreshListPreview() {
-    Question.listPreview().then(result => {
-      this.setState({ previews: result });
+    const { recordId, endTime, finish } = this.state;
+    Course.listPreview({ recordId, endTime, finish }).then(result => {
+      this.setState({ previews: result.list });
+    });
+    Course.record(recordId).then(result => {
+      this.setState({ record: result });
     });
   }
 
@@ -499,11 +517,6 @@ export default class extends Page {
     });
   }
 
-  onChangePreviewType(type) {
-    this.setState({ previewType: type });
-    this.refreshPreview();
-  }
-
   onChangeTab(level, tab) {
     const { tab1 } = this.state;
     const data = {};
@@ -517,6 +530,30 @@ export default class extends Page {
     this.refreshQuery(data);
   }
 
+  onChangeCourse(struct) {
+    const { tab1, tab2 } = this.state;
+    const data = {
+      tab1, tab2, struct,
+    };
+    this.refreshQuery(data);
+  }
+
+  onPreviewCourse() {
+    const { tab1, tab2, struct } = this.state;
+    const data = {
+      tab1, tab2, struct,
+    };
+    this.refreshQuery(data);
+  }
+
+  onPreviewList(recordId) {
+    const { tab1, tab2, struct } = this.state;
+    const data = {
+      tab1, tab2, struct, recordId,
+    };
+    this.refreshQuery(data);
+  }
+
   previewAction(type, item) {
     switch (type) {
       case 'start':
@@ -533,6 +570,14 @@ export default class extends Page {
     }
   }
 
+  // 开通课程
+  open(recordId) {
+    Order.useRecord(recordId).then(() => {
+      asyncSMessage('开通成功');
+      this.refresh();
+    });
+  }
+
   restart(item) {
     asyncConfirm('提示', '是否重置', () => {
       Question.restart(item.paper.id).then(() => {
@@ -591,7 +636,7 @@ export default class extends Page {
   renderView() {
     const { tab1, tab2, tabs, latest, sentenceModel, previewType, courseTabs = [] } = this.state;
     const [subject] = tabs.filter(row => row.key === tab1);
-    const children = subject ? subject.children : (tab1 === 'preview' && previewType === 'PREVIEW_COURSE' ? courseTabs : []);
+    const children = (subject && subject.children) ? subject.children : (tab1 === 'preview' && previewType === PREVIEW_COURSE ? courseTabs : []);
     return (
       <div>
         {latest && (
@@ -648,18 +693,74 @@ export default class extends Page {
   }
 
   renderPreviewCourse() {
-    const { allCourse, courseProgress } = this.state;
+    const { courseStructs, struct, tab2, courseTabs, courseMap = {} } = this.state;
     return (
       <div className="work-body">
+        <div className="work-nav" hidden={courseTabs && courseTabs.length > 0 && tab2 !== courseTabs[0].key}>
+          <Tabs
+            type="tag"
+            active={struct || ''}
+            space={5}
+            tabs={courseStructs}
+            onChange={key => {
+              this.onChangeCourse(key);
+            }}
+          />
+        </div>
         <div className="work-nav">
-          <div className="left">完成情况</div>
-          <div className="right theme c-p" onClick={() => this.onChangePreviewType(PREVIEW_LIST)}>
-            全部作业 >
-          </div>
+          <div className="left">学习中</div>
+        </div>
+        <Division col="3">
+          {(courseMap.process || []).map(row => {
+            return <Card1
+              title={`${row.course.title}${row.vsNo > 0 ? `V${row.vsNo}` : ''}${row.number > 0 ? `(${row.number}课时)` : ''}`}
+              tag={CourseModuleMap[row.course.courseModule]}
+              status={row.isStop && !row.isSuspend ? 'stop' : 'ing'}
+              list={row.papers.map(r => {
+                let progress = 0;
+                if (r.report) {
+                  progress = formatPercent(r.report.userNumber, r.report.questionNumber);
+                }
+                r.progress = progress;
+                return r;
+              })}
+              data={row}
+              onPreview={() => {
+                this.onPreviewList(row.id);
+              }}
+            />;
+          })}
+        </Division>
+        <div className="work-nav">
+          <div className="left">待开通</div>
+        </div>
+        <Division col="3">
+          {(courseMap.wait || []).map(row => {
+            return <Card1
+              title={`${row.course.title}${row.vsNo > 0 ? `V${row.vsNo}` : ''}${row.number > 0 ? `(${row.number}课时)` : ''}`}
+              tag={CourseModuleMap[row.course.courseModule]}
+              status='open'
+              data={row}
+              onOpen={() => {
+                this.open(row.id);
+              }}
+            />;
+          })}
+        </Division>
+        <div className="work-nav">
+          <div className="left">已结束</div>
         </div>
         <Division col="3">
-          {allCourse.map(item => {
-            return <Card data={item} process={courseProgress[item.id]} previewAction={this.previewAction} />;
+          {(courseMap.end || []).map(row => {
+            return <Card1
+              title={`${row.course.title}${row.vsNo > 0 ? `V${row.vsNo}` : ''}${row.number > 0 ? `(${row.number}课时)` : ''}`}
+              tag={CourseModuleMap[row.course.courseModule]}
+              status='end'
+              data={row}
+              onPreview={() => {
+                this.onPreviewList(row.id);
+              }}
+            />;
           })}
         </Division>
       </div>
@@ -667,12 +768,23 @@ export default class extends Page {
   }
 
   renderPreviewList() {
-    const { previews } = this.state;
+    const { previews = [], record = {}, search = {} } = this.state;
+    const { finish, endTime } = search;
+    let finishTime = '';
+    if (endTime) {
+      const endTimeD = new Date(endTime);
+      const now = new Date();
+      if (now.getTime() + 86400000 > endTimeD.getTime()) {
+        finishTime = 'today';
+      } else {
+        finishTime = 'tomorrow';
+      }
+    }
     return (
       <div className="work-body">
         <div className="work-nav">
-          <div className="left">全部作业</div>
-          <div className="right theme c-p" onClick={() => this.onChangePreviewType(PREVIEW_COURSE)}>
+          <div className="left">{`${(record.course || {}).title || ''}${record.vsNo > 0 ? `V${record.vsNo}` : ''}${record.number > 0 ? `(${record.number}课时)` : ''}`}全部作业</div>
+          <div className="right theme c-p" onClick={() => this.onPreviewCourse()}>
             我的课程 >
           </div>
         </div>
@@ -680,13 +792,47 @@ export default class extends Page {
           filters={[
             {
               type: 'radio',
-              checked: 'today',
+              checked: finishTime,
               list: [{ key: 'today', title: '今日需完成' }, { key: 'tomorrow', title: '明日需完成' }],
+              onChange: (item) => {
+                if (item.key === finishTime) {
+                  this.search({ endTime: null });
+                } else if (item.key === 'today') {
+                  const a = new Date();
+                  a.setDate(a.getDate() + 1);
+                  a.setHours(0);
+                  a.setMinutes(0);
+                  a.setMilliseconds(0);
+                  a.setSeconds(0);
+                  this.search({ endTime: formatDate(a, 'YYYY-MM-DD') });
+                } else if (item.key === 'tomorrow') {
+                  const a = new Date();
+                  a.setDate(a.getDate() + 2);
+                  a.setHours(0);
+                  a.setMinutes(0);
+                  a.setMilliseconds(0);
+                  a.setSeconds(0);
+                  this.search({ endTime: formatDate(a, 'YYYY-MM-DD') });
+                } else {
+                  this.search({ endTime: null });
+                }
+              },
             },
             {
               type: 'radio',
-              checked: 'unfinish',
-              list: [{ key: 'unfinish', title: '未完成' }, { key: 'finish', title: '已完成' }],
+              checked: finish,
+              list: [{ key: '0', title: '未完成' }, { key: '1', title: '已完成' }],
+              onChange: (item) => {
+                if (item.key === finish) {
+                  this.search({ finish: null });
+                } else if (item.key === '0') {
+                  this.search({ finish: '0' });
+                } else if (item.key === '1') {
+                  this.search({ finish: '1' });
+                } else {
+                  this.search({ finish: null });
+                }
+              },
             },
           ]}
           data={previews}

+ 1 - 1
front/project/www/routes/page/home/page.js

@@ -39,7 +39,7 @@ export default class extends Page {
 
   test() {
     User.needLogin().then(() => {
-      console.log('loginCB test');
+      linkTo('/my');
     });
   }
 

+ 28 - 35
front/project/www/routes/paper/process/base/index.js

@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom';
 import './index.less';
 import { Checkbox, Icon as AntDIcon } from 'antd';
 import Assets from '@src/components/Assets';
-import { formatSeconds, formatSecond, getMap } from '@src/services/Tools';
+import { formatSeconds, formatMinuteSecond, getMap } from '@src/services/Tools';
 import Icon from '../../../../components/Icon';
 import Button from '../../../../components/Button';
 import Navigation from '../../../../components/Navigation';
@@ -224,8 +224,8 @@ export default class extends Component {
               }}
             >
               <Assets name="timeleft_icon" />
-              {showTime && stageTime && `Time left ${formatSecond(stageTime)}`}
-              {showTime && singleTime && `Time cost ${formatSecond(singleTime)}`}
+              {showTime && stageTime && `Time left ${formatMinuteSecond(stageTime)}`}
+              {showTime && !stageTime && singleTime && `Time cost ${formatMinuteSecond(singleTime)}`}
             </div>
             <div
               className="block"
@@ -263,14 +263,14 @@ export default class extends Component {
 
   renderExaminationStart() {
     // const { paper, userQuestion, singleTime, stageTime, flow } = this.props;
-    // const { showTime, showNo } = this.state;
-    const { paper, flow } = this.props;
+    const { showTime } = this.state;
+    const { paper, flow, startTime } = this.props;
     return (
       <div className="layout">
         <div className="fixed" />
         <div className="layout-header">
           <div className="title">{paper.title}</div>
-          {/* <div className="right">
+          <div className="right">
             <div
               className="block"
               onClick={() => {
@@ -278,10 +278,9 @@ export default class extends Component {
               }}
             >
               <Assets name="timeleft_icon" />
-              {showTime && stageTime && `Time left ${formatSecond(stageTime)}`}
-              {showTime && singleTime && `Time cost ${formatSecond(singleTime)}`}
+              {showTime && startTime && `Time left ${formatMinuteSecond(startTime)}`}
             </div>
-            <div
+            {/* <div
               className="block"
               onClick={() => {
                 this.setState({ showNo: !showNo });
@@ -289,8 +288,8 @@ export default class extends Component {
             >
               <Assets name="subjectnumber_icon" />
               {showNo && `${userQuestion.no} of ${paper.questionNumber}`}
-            </div>
-          </div> */}
+            </div> */}
+          </div>
         </div>
         <div className={'layout-body'}>{paper.isAdapt > 1 ? this.renderExaminationStartCAT() : this.renderExaminationStartDefault()}</div>
         <div className="layout-footer">
@@ -311,8 +310,8 @@ export default class extends Component {
   }
 
   renderExerciseStart() {
-    const { disorder } = this.state;
-    const { paper, flow } = this.props;
+    const { paper, flow, setting } = this.props;
+    const { disorder } = setting;
     return (
       <div className="start">
         <div className="bg" />
@@ -335,7 +334,7 @@ export default class extends Component {
             </div>
           </div>
           {paper.times > 0 && <div className="tip">
-            <Checkbox className="m-r-1" checked={!disorder} onChange={() => this.setState({ disorder: !!disorder })} />
+            <Checkbox className="m-r-1" checked={!disorder} onChange={() => flow.setSetting({ disorder: !!disorder })} />
             题目选项乱序显示
           </div>}
           <div className="submit">
@@ -349,8 +348,8 @@ export default class extends Component {
   }
 
   renderExaminationStartCAT() {
-    const { disorder, order, orderIndex } = this.state;
-    const { paper, flow } = this.props;
+    const { paper, flow, setting } = this.props;
+    const { disorder, order, orderIndex } = setting;
     return (
       <div className="exercise-start default">
         <div className="title">Section Ordering</div>
@@ -368,7 +367,7 @@ export default class extends Component {
           {ExaminationOrder.map((row, index) => {
             return <div className="block-item">
               <div className="block-title" onClick={() => {
-                this.setState({ order: row.value, orderIndex: index });
+                flow.setSetting({ order: row.value, orderIndex: index });
               }}>
                 <div className="block-title-border">
                   {orderIndex === index && <AntDIcon type="check" />}
@@ -383,7 +382,7 @@ export default class extends Component {
         </div>
         <div className="bottom">
           {paper.times > 0 && <div className="text">
-            <Checkbox checked={!disorder} onChange={() => this.setState({ disorder: !!disorder })} /> 题目选项乱序显示
+            <Checkbox checked={!disorder} onChange={() => flow.setSetting({ disorder: !!disorder })} /> 题目选项乱序显示
           </div>}
           <div className="text">
             Click{' '}
@@ -399,8 +398,8 @@ export default class extends Component {
   }
 
   renderExaminationStartDefault() {
-    const { disorder, order, orderIndex } = this.state;
-    const { paper, flow } = this.props;
+    const { paper, flow, setting } = this.props;
+    const { disorder, order, orderIndex } = setting;
     return (
       <div className="exercise-start cat">
         <div className="title">Section Ordering</div>
@@ -420,7 +419,7 @@ export default class extends Component {
                       // 选中
                       order[i] = r.value;
                     }
-                    this.setState({ order });
+                    flow.setSetting({ order });
                   }}>
                     <Checkbox checked={orderIndex === index ? order.indexOf(r.value) >= 0 : false} /> {r.label}{' '}
                   </div>;
@@ -431,7 +430,7 @@ export default class extends Component {
         </div>
         <div className="bottom">
           {paper.times > 0 && <div className="text">
-            <Checkbox checked={!disorder} onChange={() => this.setState({ disorder: !!disorder })} /> 题目选项乱序显示
+            <Checkbox checked={!disorder} onChange={() => flow.setSetting({ disorder: !!disorder })} /> 题目选项乱序显示
           </div>}
           <div className="text">
             Click{' '}
@@ -448,8 +447,8 @@ export default class extends Component {
   }
 
   renderRelax() {
-    const { paper, userQuestion, singleTime, stageTime, flow } = this.props;
-    const { showTime, showNo } = this.state;
+    const { paper, stageTime, flow } = this.props;
+    const { showTime } = this.state;
     return (
       <div className="layout">
         <div className="layout-header">
@@ -462,10 +461,10 @@ export default class extends Component {
               }}
             >
               <Assets name="timeleft_icon" />
-              {showTime && stageTime && `Time left ${formatSecond(stageTime)}`}
-              {showTime && singleTime && `Time cost ${formatSecond(singleTime)}`}
+              {showTime && stageTime && `Time left ${formatMinuteSecond(stageTime)}`}
+              {/* {showTime && singleTime && `Time cost ${formatMinuteSecond(singleTime)}`} */}
             </div>
-            <div
+            {/* <div
               className="block"
               onClick={() => {
                 this.setState({ showNo: !showNo });
@@ -473,7 +472,7 @@ export default class extends Component {
             >
               <Assets name="subjectnumber_icon" />
               {showNo && `${userQuestion.no} of ${paper.questionNumber}`}
-            </div>
+            </div> */}
           </div>
         </div>
         <div className={'layout-body'}>
@@ -481,13 +480,7 @@ export default class extends Component {
             <div className="title">
               Optional Break <Icon name="question" />
             </div>
-            <div className="time">
-              <div className="block">0</div>
-              <div className="block">1</div>
-              <div className="div">:</div>
-              <div className="block">2</div>
-              <div className="block">3</div>
-            </div>
+            <div className="time" dangerouslySetInnerHTML={{ __html: formatMinuteSecond(stageTime).split(':').map(row => row.replace(/([0-9])/g, '<div class="block">$1</div>')).join('<div class="div">:</div>') }} />
           </div>
         </div>
         <div className="layout-footer">

+ 51 - 10
front/project/www/routes/paper/process/page.js

@@ -6,6 +6,7 @@ import { randomList, sortListWithOrder, resortListWithOrder } from '@src/service
 import { Question } from '../../../stores/question';
 import Base from './base';
 import Sentence from './sentence';
+import { ExaminationOrder } from '../../../../Constant';
 import { Main } from '../../../stores/main';
 import { My } from '../../../stores/my';
 
@@ -21,6 +22,9 @@ export default class extends Page {
     this.stageNumber = 0;
     this.stageProcess = { number: 0, time: 0 };
     this.relaxProcess = { time: 0 };
+
+    this.startInterval = null;
+    this.startTime = 0;
   }
 
   initState() {
@@ -66,6 +70,10 @@ export default class extends Page {
         });
       } else {
         this.setState({ scene: 'start' });
+        // 模考cat1分钟自动开始
+        if (paper.isAdapt > 1) {
+          this.startWaitTime();
+        }
       }
       handler.catch(() => {
         goBack();
@@ -73,6 +81,11 @@ export default class extends Page {
     });
   }
 
+  setSetting(newSetting) {
+    const { setting } = this.state;
+    this.setState({ setting: Object.assign(setting, newSetting) });
+  }
+
   start(setting) {
     const { type, id } = this.params;
     return Question.start(type, id, setting).then(report => {
@@ -175,14 +188,12 @@ export default class extends Page {
     });
   }
 
+  // 主动进入下一阶段
   stage() {
     const { report } = this.state;
     return Question.stage(report.id)
       .then(() => {
-        return this.next();
-      })
-      .then(() => {
-        this.stageQuestionTime();
+        return this.relaxStage();
       });
   }
 
@@ -213,18 +224,48 @@ export default class extends Page {
     }
   }
 
-  stageQuestionTime(initTime) {
+  stageQuestionTime(initTime, stop) {
     if (this.stageInterval) {
       clearInterval(this.stageInterval);
       this.stageInterval = null;
       this.stageTime = initTime;
     }
-    this.stageInterval = setInterval(() => {
-      this.stageTime += 1;
-      if (this.stageTime >= this.stageProcess.number) {
-        this.nextStage();
+    if (!stop) {
+      this.stageInterval = setInterval(() => {
+        this.stageTime += 1;
+        if (this.stageTime >= this.stageProcess.time) {
+          const { scene } = this.state;
+          if (scene === 'relax') {
+            // 进入下一阶段,获取下一题
+            this.next();
+          } else {
+            // 提交当前阶段
+            this.stage();
+          }
+        }
+        this.setState({ stageTime: this.targetProcess.time - this.stageTime });
+      }, 1000);
+    }
+  }
+
+  startWaitTime() {
+    if (this.startInterval) {
+      clearInterval(this.startInterval);
+      this.startInterval = null;
+    }
+    this.startInterval = setInterval(() => {
+      this.startTime += 1;
+      // 1分钟等待: 自动提交第一选择
+      const { scene } = this.state;
+      if (scene !== 'start') {
+        clearInterval(this.startInterval);
+        this.startInterval = null;
+      } else if (this.startTime >= 60) {
+        clearInterval(this.startInterval);
+        this.startInterval = null;
+        this.start(Object.assign({ order: ExaminationOrder[0].value }, this.state.setting));
       }
-      this.setState({ stageTime: this.targetProcess.time - this.stageTime });
+      this.setState({ startTime: 60 - this.startTime });
     }, 1000);
   }
 

+ 2 - 2
front/project/www/routes/paper/question/page.js

@@ -231,7 +231,7 @@ export default class extends Page {
   }
 
   renderExerciseHeader() {
-    const { userQuestion = {}, questionNo = {}, paper = {}, showIds, questionNos = [] } = this.state;
+    const { userQuestion = {}, questionNo = {}, paper = {}, showIds, questionNos = [], question = {} } = this.state;
     return <div className="layout-header">
       <div className="left">
         <div className="no">No.{userQuestion.stageNo || userQuestion.no}</div>
@@ -249,7 +249,7 @@ export default class extends Page {
           </div>}
         </div>
       </div>
-      <div className="right">
+      <div className="right" hidden={question.questionType === 'awa'}>
         <span className="b">
           用时:<span dangerouslySetInnerHTML={{ __html: formatSeconds(userQuestion.userTime).replace(/([0-9]+)([msh])/g, '<span class="s">$1</span>$2') }} />
           {/* 用时:<span className="s">1</span>m<span className="s">39</span>s */}

+ 2 - 1
front/project/www/routes/preview/index.js

@@ -1,2 +1,3 @@
+import list from './list';
 
-export default [];
+export default [list];

+ 1 - 1
front/project/www/routes/preview/list/index.less

@@ -1,6 +1,6 @@
 @charset "utf-8";
 
-#exercise-list {
+#preview-list {
   .code-module {
     padding: 80px 250px;
     text-align: center;

+ 2 - 1
front/project/www/routes/textbook/index.js

@@ -1,2 +1,3 @@
+import list from './list';
 
-export default [];
+export default [list];

+ 10 - 0
front/project/www/routes/textbook/list/index.js

@@ -0,0 +1,10 @@
+export default {
+  path: '/textbook/list/:id',
+  key: 'textbook',
+  title: '数学机经',
+  needLogin: false,
+  tab: 'examination',
+  component() {
+    return import('./page');
+  },
+};

+ 59 - 0
front/project/www/routes/textbook/list/index.less

@@ -0,0 +1,59 @@
+@charset "utf-8";
+
+#textbook-list {
+  .code-module {
+    padding: 80px 250px;
+    text-align: center;
+
+    .title {
+      font-size: 18px;
+      margin-bottom: 24px;
+    }
+
+    .input-block {
+      margin-bottom: 24px;
+
+      .input {
+        width: 350px;
+
+        input {
+          border-top-left-radius: 22px;
+          border-bottom-left-radius: 22px;
+        }
+      }
+
+      .button {
+        width: 150px;
+        border-top-right-radius: 22px;
+        border-bottom-right-radius: 22px;
+      }
+    }
+
+    .tip {
+      .left {
+        float: left;
+      }
+
+      .right {
+        float: right;
+      }
+    }
+  }
+
+  .work-body {
+    .work-nav {
+      margin-bottom: 20px;
+
+      .left {
+        display: inline-block;
+        padding-left: 5px;
+        font-size: 16px;
+        font-weight: 600;
+      }
+
+      .right {
+        float: right;
+      }
+    }
+  }
+}

+ 286 - 0
front/project/www/routes/textbook/list/page.js

@@ -0,0 +1,286 @@
+import React from 'react';
+import './index.less';
+import Page from '@src/containers/Page';
+import { asyncConfirm } from '@src/services/AsyncTools';
+import { formatPercent, formatSeconds, formatDate } from '@src/services/Tools';
+import Tabs from '../../../components/Tabs';
+import Module from '../../../components/Module';
+import ListTable from '../../../components/ListTable';
+import ProgressText from '../../../components/ProgressText';
+import IconButton from '../../../components/IconButton';
+import { Main } from '../../../stores/main';
+import { Question } from '../../../stores/question';
+import { QuestionDifficult } from '../../../../Constant';
+
+const LOGIC_NO = 'no';
+const LOGIC_PLACE = 'place';
+const LOGIC_DIFFICULT = 'difficult';
+const LOGIC_ERROR = 'error';
+
+export default class extends Page {
+  initState() {
+    this.columns = [
+      {
+        title: '练习册',
+        width: 250,
+        align: 'left',
+        render: (record) => {
+          let progress = 0;
+          if (record.report) {
+            progress = formatPercent(record.report.userNumber, record.report.questionNumber);
+          }
+          return (
+            <div className="table-row">
+              <div className="night f-s-16">{record.title}</div>
+              <div>
+                <ProgressText progress={progress} size="small" />
+              </div>
+            </div>
+          );
+        },
+      },
+      {
+        title: '正确率',
+        width: 150,
+        align: 'left',
+        render: (record) => {
+          let correct = '--';
+          if (record.report) {
+            correct = formatPercent(record.report.userCorrect, record.report.userNumber, false);
+          }
+          return (
+            <div className="table-row">
+              <div className="night f-s-16 f-w-b">{correct}</div>
+              <div className="f-s-12">全站{formatPercent(record.stat.totalCorrect, record.stat.totalNumber, false)}</div>
+            </div>
+          );
+        },
+      },
+      {
+        title: '全站用时',
+        width: 150,
+        align: 'left',
+        render: (record) => {
+          let time = '--';
+          if (record.paper) {
+            time = formatSeconds(record.paper.report.userTime / record.paper.report.userNumber);
+          }
+          return (
+            <div className="table-row">
+              <div className="night f-s-16 f-w-b">{time}</div>
+              <div className="f-s-12">全站{formatSeconds(record.stat.totalTime / record.stat.totalNumber)}</div>
+            </div>
+          );
+        },
+      },
+      {
+        title: '最近做题',
+        width: 150,
+        align: 'left',
+        render: (record) => {
+          if (!record.report) return null;
+          return (
+            <div className="table-row">
+              <div>{formatDate(record.report.updateTime, 'YYYY-MM-DD')}</div>
+              <div>{formatDate(record.report.updateTime, 'HH:mm')}</div>
+            </div>
+          );
+        },
+      },
+      {
+        title: '操作',
+        width: 180,
+        align: 'left',
+        render: (record) => {
+          return (
+            <div className="table-row p-t-1">
+              {!record.report && <IconButton type="start" tip="Start" onClick={() => {
+                Question.startLink('exercise', record);
+              }} />}
+              {(record.report && !record.report.isFinish) && <IconButton className="m-r-2" type="continue" tip="Continue" onClick={() => {
+                Question.continueLink('exercise', record);
+              }} />}
+              <IconButton type="restart" tip="Restart" onClick={() => {
+                this.restart(record);
+              }} />
+            </div>
+          );
+        },
+      },
+      {
+        title: '报告',
+        width: 30,
+        align: 'right',
+        render: (record) => {
+          if (!record.report || !record.report.isFinish) return null;
+          return (
+            <div className="table-row p-t-1">
+              <IconButton type="report" tip="Report" onClick={() => {
+                Question.reportLink(record);
+              }} />
+            </div>
+          );
+        },
+      },
+    ];
+    this.placeList = [];
+    this.inited = false;
+    return {
+      logic: LOGIC_NO,
+      logicExtend: '',
+      logics: [{
+        key: LOGIC_NO,
+        title: '按顺序练习',
+      }, {
+        key: LOGIC_PLACE,
+        title: '按考点练习',
+      }, {
+        key: LOGIC_DIFFICULT,
+        title: '按难度练习',
+      }, {
+        key: LOGIC_ERROR,
+        title: '按易错度练习',
+      }],
+    };
+  }
+
+  init() {
+    const { id } = this.params;
+    Main.getExerciseParent(id).then(result => {
+      const navs = result;
+      this.inited = true;
+      this.setState({ navs });
+    });
+  }
+
+  initData() {
+    const data = Object.assign(this.state, this.state.search);
+    this.setState(data);
+    this.refreshData();
+  }
+
+  refreshData(newLogic) {
+    const { logic } = this.state;
+    let handler = null;
+    switch (newLogic || logic) {
+      case LOGIC_PLACE:
+        handler = this.refreshPlace();
+        break;
+      case LOGIC_DIFFICULT:
+        handler = this.refreshDifficult();
+        break;
+      default:
+        handler = Promise.resolve();
+    }
+    handler.then(() => {
+      this.refreshExercise();
+    });
+  }
+
+  refreshPlace() {
+    const { id } = this.params;
+    let handler;
+    if (this.placeList.length > 0) {
+      this.setState({ logicExtends: this.placeList });
+      handler = Promise.resolve();
+    } else {
+      handler = Question.getExercisePlace(id).then(result => {
+        this.placeList = result.map(row => {
+          return {
+            name: row,
+            key: row,
+          };
+        });
+        this.setState({ logicExtends: this.placeList });
+      });
+    }
+    return handler.then(() => {
+      let { logicExtend } = this.state;
+      if (logicExtend === '') {
+        logicExtend = this.placeList[0].key;
+        this.setState({ logicExtend });
+      }
+    });
+  }
+
+  refreshDifficult() {
+    let { logicExtend } = this.state;
+    this.setState({
+      logicExtends: QuestionDifficult.map(difficult => {
+        difficult.name = difficult.label;
+        difficult.key = difficult.value;
+        return difficult;
+      }),
+    });
+    return Promise.resolve().then(() => {
+      if (logicExtend === '') {
+        logicExtend = QuestionDifficult[0].key;
+        this.setState({ logicExtend });
+      }
+    });
+  }
+
+  refreshExercise() {
+    const { logic, logicExtend } = this.state;
+    Question.getExerciseList(Object.assign({ structId: this.params.id, logic, logicExtend }, this.state.search))
+      .then((result) => {
+        this.setState({ list: result.list, total: result.total });
+      });
+  }
+
+  onChangeTab(key, value) {
+    const { logic } = this.state;
+    const data = {};
+    if (key === 'logicExtend') {
+      data.logic = logic;
+      data.logicExtend = value;
+    } else {
+      data.logic = value;
+    }
+    // this.refreshData(tab);
+    this.refreshQuery(data);
+  }
+
+  restart(item) {
+    asyncConfirm('提示', '是否重置', () => {
+      Question.restart(item.paper.id).then(() => {
+        this.refresh();
+      });
+    });
+  }
+
+  renderView() {
+    const { logic, logicExtend, logics = [], logicExtends = [], list } = this.state;
+    return (
+      <div>
+        <div className="content">
+          <Module className="m-t-2">
+            <Tabs
+              active={logic}
+              border
+              width="180px"
+              space="0"
+              tabs={logics}
+              onChange={(key) => {
+                this.onChangeTab('logic', key);
+              }}
+            />
+            {logicExtends.length > 0 && <Tabs
+              active={logicExtend}
+              type="text"
+              tabs={logicExtends}
+              onChange={(key) => {
+                this.onChangeTab('logicExtend', key);
+              }}
+            />}
+          </Module>
+
+          <ListTable
+            data={list}
+            columns={this.columns}
+          />
+        </div>
+      </div>
+    );
+  }
+}

+ 194 - 186
front/project/www/static/login.html

@@ -1,210 +1,218 @@
 <!DOCTYPE html>
 <html lang="zh">
-  <head>
-    <meta charset="utf-8" />
-    <script src="http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script>
-  </head>
-  <style type="text/css">
-    body {
-      margin: 0;
-      overflow: hidden;
-      width: 300px;
-    }
-    #root {
-      overflow: hidden;
-      margin-top: -45px;
-      height: 350px;
-    }
-    .loading-bg {
-      background: #fff;
-      position: fixed;
-      top: 0;
-      left: 0;
-      right: 0;
-      bottom: 0;
-      z-index: 1;
-      opacity: 0.9;
-    }
-    .page-loading-warp {
-      padding: 130px;
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      position: absolute;
-      top: 0;
-      left: 0;
-      position: fixed;
-      z-index: 2;
-    }
-    .ant-spin {
-      -webkit-box-sizing: border-box;
-      box-sizing: border-box;
-      margin: 0;
-      padding: 0;
-      color: rgba(0, 0, 0, 0.65);
-      font-size: 14px;
-      font-variant: tabular-nums;
-      line-height: 1.5;
-      list-style: none;
-      -webkit-font-feature-settings: 'tnum';
-      font-feature-settings: 'tnum';
-      position: absolute;
-      display: none;
-      color: #1890ff;
-      text-align: center;
-      vertical-align: middle;
-      opacity: 0;
-      -webkit-transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
-      transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
-      transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
-      transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
-        -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
-    }
 
-    .ant-spin-spinning {
-      position: static;
-      display: inline-block;
-      opacity: 1;
-    }
+<head>
+  <meta charset="utf-8" />
+  <script src="http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script>
+</head>
+<style type="text/css">
+  body {
+    margin: 0;
+    overflow: hidden;
+    width: 300px;
+  }
 
-    .ant-spin-dot {
-      position: relative;
-      display: inline-block;
-      font-size: 20px;
-      width: 20px;
-      height: 20px;
-    }
+  #root {
+    overflow: hidden;
+    margin-top: -45px;
+    height: 350px;
+  }
 
-    .ant-spin-dot-item {
-      position: absolute;
-      display: block;
-      width: 9px;
-      height: 9px;
-      background-color: #1890ff;
-      border-radius: 100%;
-      -webkit-transform: scale(0.75);
-      -ms-transform: scale(0.75);
-      transform: scale(0.75);
-      -webkit-transform-origin: 50% 50%;
-      -ms-transform-origin: 50% 50%;
-      transform-origin: 50% 50%;
-      opacity: 0.3;
-      -webkit-animation: antSpinMove 1s infinite linear alternate;
-      animation: antSpinMove 1s infinite linear alternate;
-    }
+  .loading-bg {
+    background: #fff;
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 1;
+    opacity: 0.9;
+  }
 
-    .ant-spin-dot-item:nth-child(1) {
-      top: 0;
-      left: 0;
-    }
+  .page-loading-warp {
+    padding: 130px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    position: absolute;
+    top: 0;
+    left: 0;
+    position: fixed;
+    z-index: 2;
+  }
 
-    .ant-spin-dot-item:nth-child(2) {
-      top: 0;
-      right: 0;
-      -webkit-animation-delay: 0.4s;
-      animation-delay: 0.4s;
-    }
+  .ant-spin {
+    -webkit-box-sizing: border-box;
+    box-sizing: border-box;
+    margin: 0;
+    padding: 0;
+    color: rgba(0, 0, 0, 0.65);
+    font-size: 14px;
+    font-variant: tabular-nums;
+    line-height: 1.5;
+    list-style: none;
+    -webkit-font-feature-settings: 'tnum';
+    font-feature-settings: 'tnum';
+    position: absolute;
+    display: none;
+    color: #1890ff;
+    text-align: center;
+    vertical-align: middle;
+    opacity: 0;
+    -webkit-transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
+    transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
+    transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
+    transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
+      -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
+  }
 
-    .ant-spin-dot-item:nth-child(3) {
-      right: 0;
-      bottom: 0;
-      -webkit-animation-delay: 0.8s;
-      animation-delay: 0.8s;
-    }
+  .ant-spin-spinning {
+    position: static;
+    display: inline-block;
+    opacity: 1;
+  }
 
-    .ant-spin-dot-item:nth-child(4) {
-      bottom: 0;
-      left: 0;
-      -webkit-animation-delay: 1.2s;
-      animation-delay: 1.2s;
-    }
+  .ant-spin-dot {
+    position: relative;
+    display: inline-block;
+    font-size: 20px;
+    width: 20px;
+    height: 20px;
+  }
 
-    .ant-spin-dot-spin {
-      -webkit-transform: rotate(45deg);
-      -ms-transform: rotate(45deg);
-      transform: rotate(45deg);
-      -webkit-animation: antRotate 1.2s infinite linear;
-      animation: antRotate 1.2s infinite linear;
-    }
+  .ant-spin-dot-item {
+    position: absolute;
+    display: block;
+    width: 9px;
+    height: 9px;
+    background-color: #1890ff;
+    border-radius: 100%;
+    -webkit-transform: scale(0.75);
+    -ms-transform: scale(0.75);
+    transform: scale(0.75);
+    -webkit-transform-origin: 50% 50%;
+    -ms-transform-origin: 50% 50%;
+    transform-origin: 50% 50%;
+    opacity: 0.3;
+    -webkit-animation: antSpinMove 1s infinite linear alternate;
+    animation: antSpinMove 1s infinite linear alternate;
+  }
 
-    .ant-spin-lg .ant-spin-dot {
-      font-size: 32px;
-      width: 32px;
-      height: 32px;
-    }
+  .ant-spin-dot-item:nth-child(1) {
+    top: 0;
+    left: 0;
+  }
 
-    .ant-spin-lg .ant-spin-dot i {
-      width: 14px;
-      height: 14px;
-    }
+  .ant-spin-dot-item:nth-child(2) {
+    top: 0;
+    right: 0;
+    -webkit-animation-delay: 0.4s;
+    animation-delay: 0.4s;
+  }
+
+  .ant-spin-dot-item:nth-child(3) {
+    right: 0;
+    bottom: 0;
+    -webkit-animation-delay: 0.8s;
+    animation-delay: 0.8s;
+  }
+
+  .ant-spin-dot-item:nth-child(4) {
+    bottom: 0;
+    left: 0;
+    -webkit-animation-delay: 1.2s;
+    animation-delay: 1.2s;
+  }
+
+  .ant-spin-dot-spin {
+    -webkit-transform: rotate(45deg);
+    -ms-transform: rotate(45deg);
+    transform: rotate(45deg);
+    -webkit-animation: antRotate 1.2s infinite linear;
+    animation: antRotate 1.2s infinite linear;
+  }
+
+  .ant-spin-lg .ant-spin-dot {
+    font-size: 32px;
+    width: 32px;
+    height: 32px;
+  }
+
+  .ant-spin-lg .ant-spin-dot i {
+    width: 14px;
+    height: 14px;
+  }
 
-    @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
-      .ant-spin-blur {
-        background: #fff;
-        opacity: 0.5;
-      }
+  @media all and (-ms-high-contrast: none),
+  (-ms-high-contrast: active) {
+    .ant-spin-blur {
+      background: #fff;
+      opacity: 0.5;
     }
+  }
 
-    @-webkit-keyframes antSpinMove {
-      to {
-        opacity: 1;
-      }
+  @-webkit-keyframes antSpinMove {
+    to {
+      opacity: 1;
     }
+  }
 
-    @keyframes antSpinMove {
-      to {
-        opacity: 1;
-      }
+  @keyframes antSpinMove {
+    to {
+      opacity: 1;
     }
+  }
 
-    @-webkit-keyframes antRotate {
-      to {
-        -webkit-transform: rotate(405deg);
-        transform: rotate(405deg);
-      }
+  @-webkit-keyframes antRotate {
+    to {
+      -webkit-transform: rotate(405deg);
+      transform: rotate(405deg);
     }
+  }
 
-    @keyframes antRotate {
-      to {
-        -webkit-transform: rotate(405deg);
-        transform: rotate(405deg);
-      }
+  @keyframes antRotate {
+    to {
+      -webkit-transform: rotate(405deg);
+      transform: rotate(405deg);
     }
-  </style>
-  <body>
-    <div id="root"></div>
-    <div id="loading">
-      <div class="loading-bg"></div>
-      <div class="page-loading-warp">
-        <div class="ant-spin ant-spin-lg ant-spin-spinning">
-          <span class="ant-spin-dot ant-spin-dot-spin">
-            <i class="ant-spin-dot-item"></i>
-            <i class="ant-spin-dot-item"></i>
-            <i class="ant-spin-dot-item"></i>
-            <i class="ant-spin-dot-item"></i>
-          </span>
-        </div>
+  }
+</style>
+
+<body>
+  <div id="root"></div>
+  <div id="loading">
+    <div class="loading-bg"></div>
+    <div class="page-loading-warp">
+      <div class="ant-spin ant-spin-lg ant-spin-spinning">
+        <span class="ant-spin-dot ant-spin-dot-spin">
+          <i class="ant-spin-dot-item"></i>
+          <i class="ant-spin-dot-item"></i>
+          <i class="ant-spin-dot-item"></i>
+          <i class="ant-spin-dot-item"></i>
+        </span>
       </div>
     </div>
-  </body>
-  <script>
-    function getQuery(name) {
-      var reg = new RegExp(`(^|\\?|&)${name}=([^&]*)(&|$)`);
-      var r = window.location.href.substr(1).match(reg);
-      if (r != null) return unescape(r[2]);
-      return null;
-    }
-    var code = getQuery('code');
-    if (code) {
-      window.parent.postMessage('code:' + code, '*');
-    } else {
-      document.getElementById('loading').style.display = 'none';
-    }
-    new WxLogin({
-      id: 'root',
-      appid: 'wx8fa48dfc3752f0ce',
-      scope: 'snsapi_login',
-      redirect_uri: encodeURIComponent('http://www.scigou.com'),
-    });
-  </script>
-</html>
+  </div>
+</body>
+<script>
+  function getQuery(name) {
+    var reg = new RegExp(`(^|\\?|&)${name}=([^&]*)(&|$)`);
+    var r = window.location.href.substr(1).match(reg);
+    if (r != null) return unescape(r[2]);
+    return null;
+  }
+  var code = getQuery('code');
+  if (code) {
+    window.parent.postMessage('code:' + code, '*');
+  } else {
+    document.getElementById('loading').style.display = 'none';
+  }
+  new WxLogin({
+    id: 'root',
+    appid: getQuery('appid'),
+    scope: 'snsapi_login',
+    redirect_uri: getQuery('redirectUri'),
+  });
+</script>
+
+</html>

+ 48 - 4
front/project/www/stores/course.js

@@ -2,18 +2,62 @@ import BaseStore from '@src/stores/base';
 
 export default class CourseStore extends BaseStore {
   /**
+   * 所有vs课程
+   */
+  allVs() {
+    return this.apiGet('/course/vs');
+  }
+
+  listVideo(params) {
+    return this.apiGet('/course/video/list', params);
+  }
+
+  get(courseId) {
+    return this.apiGet('/course/simple', { courseId });
+  }
+
+  listPackage(params) {
+    return this.apiGet('/course/package/list', params);
+  }
+
+  getPackage(packageId) {
+    return this.apiGet('/course/package/detail', { packageId });
+  }
+
+  listData(params) {
+    return this.apiGet('/course/data/list', params);
+  }
+
+  getData(dataId) {
+    return this.apiGet('/course/data/detail', { dataId });
+  }
+
+  listExperience({ page, size, perpareStatus, experienceDay, experienceScore, experiencePercent, order, direction }) {
+    return this.apiGet('/course/experience/list', { page, size, perpareStatus, experienceDay, experienceScore, experiencePercent, order, direction });
+  }
+
+  /**
    * 获取课程进度
    */
-  progress(courseModules, structId) {
-    return this.apiGet('/course/progress', { courseModules, structId });
+  progress(courseModule, structId, courseId) {
+    return this.apiGet('/course/progress', { courseModule, structId, courseId });
   }
 
+
   /**
    * 获取预习作业列表
    * @param {*} param0
    */
-  listPreview({ page, size, isFinish, endTime }) {
-    return this.apiGet('/course/preview', { page, size, isFinish, endTime });
+  listPreview({ page, size, recordId, finish, endTime }) {
+    return this.apiGet('/course/preview/list', { page, size, recordId, endTime, times: finish });
+  }
+
+  /**
+   * 课程记录信息
+   * @param {*} recordId
+   */
+  record(recordId) {
+    return this.apiGet('/course/record', { recordId });
   }
 }
 

+ 8 - 0
front/project/www/stores/my.js

@@ -84,6 +84,14 @@ export default class MyStore extends BaseStore {
   }
 
   /**
+   * 获取每周学习记录
+   * @param {*} week 0本周,1上周
+   */
+  getStudyWeek(week) {
+    return this.apiGet('/my/study/week', { week });
+  }
+
+  /**
    * 获取总学习记录
    */
   getStudyTotal() {

+ 32 - 4
front/project/www/stores/order.js

@@ -6,8 +6,8 @@ export default class OrderStore extends BaseStore {
     return this.apiGet('/order/checkout/all');
   }
 
-  addCheckout(productType, productId, service, param) {
-    return this.apiPost('/order/checkout/add', { productType, productId, service, param });
+  addCheckout({ productType, productId, service, param, number }) {
+    return this.apiPost('/order/checkout/add', { productType, productId, service, param, number });
   }
 
   removeCheckout(checkoutId) {
@@ -18,8 +18,8 @@ export default class OrderStore extends BaseStore {
     return this.apiPost('/order/pay/confirm');
   }
 
-  speedPay(productType, productId, service, param) {
-    return this.apiPost('/order/pay/speed', { productType, productId, service, param });
+  speedPay({ productType, productId, service, param, number }) {
+    return this.apiPost('/order/pay/speed', { productType, productId, service, param, number });
   }
 
   wechatQr(orderId) {
@@ -33,6 +33,34 @@ export default class OrderStore extends BaseStore {
   alipayQr(orderId) {
     return this.apiPost('/order/alipay/qr', { orderId });
   }
+
+  query(orderId) {
+    return this.apiGet('/order/pay/query', { orderId });
+  }
+
+  /**
+   * 获取所有已购记录
+   * @param {*} param0
+   */
+  listRecord({ page, size }) {
+    return this.apiGet('/my/record/list', { page, size });
+  }
+
+  /**
+   * 获取订单记录
+   * @param {*} id
+   */
+  getRecord(id) {
+    return this.apiGet('/my/record/detail', { id });
+  }
+
+  /**
+   * 开通服务、课程等
+   * @param {*} id
+   */
+  useRecord(id, isSubscribe) {
+    return this.apiPost('/my/record/use', { id, isSubscribe });
+  }
 }
 
 export const Order = new OrderStore({ key: 'order' });

+ 11 - 12
front/project/www/stores/question.js

@@ -34,6 +34,14 @@ export default class QuestionStore extends BaseStore {
   }
 
   /**
+   * 模考进度
+   * @param {*} structId
+   */
+  getExaminationProgress(structId) {
+    return this.apiGet('/question/examination/progress', { structId });
+  }
+
+  /**
    * 练习组卷
    * @param {*} page
    * @param {*} size
@@ -43,16 +51,7 @@ export default class QuestionStore extends BaseStore {
    * @param {*} finish: true完成,false未完成
    */
   getExerciseList({ page, size, structId, logic, logicExtend, finish }) {
-    return this.apiGet('/question/exercise/list', { page, size, structId, logic, logicExtend, times: finish ? 1 : null });
-  }
-
-  /**
-   * 模考进度
-   * @param {*} page
-   * @param {*} size
-   */
-  getExaminationProgress(page, size) {
-    return this.apiGet('/question/examination/progress', { page, size });
+    return this.apiGet('/question/exercise/list', { page, size, structId, logic, logicExtend, times: finish });
   }
 
   /**
@@ -60,8 +59,8 @@ export default class QuestionStore extends BaseStore {
    * @param {*} page
    * @param {*} size
    */
-  getExaminationList(page, size) {
-    return this.apiGet('/question/examination/list', { page, size });
+  getExaminationList(page, size, structId, finish) {
+    return this.apiGet('/question/examination/list', { page, size, structId, times: finish });
   }
 
   /**

+ 14 - 0
front/project/www/stores/textbook.js

@@ -8,6 +8,20 @@ export default class TextbookStore extends BaseStore {
     return this.apiGet('/textbook/info');
   }
 
+  /**
+   * 获取机经进度
+   */
+  progress() {
+    return this.apiGet('/textbook/progress');
+  }
+
+  /**
+   * 机经组卷列表
+   */
+  listPaper(page, size, latest, logic, finish) {
+    return this.apiGet('/textbook/paper', { page, size, latest: !!latest, logic, times: finish });
+  }
+
   listYear(year) {
     return this.apiGet('/textbook/year', { year });
   }

+ 20 - 0
front/src/services/Tools.js

@@ -232,6 +232,26 @@ export function formatSecond(value) {
   return `${hourTime}:${minuteTime}:${secondTime}`;
 }
 
+export function formatMinuteSecond(value) {
+  let secondTime = parseInt(value || 0, 10); // 秒
+  let minuteTime = 0;
+  if (secondTime > 60) {
+    minuteTime = parseInt(secondTime / 60, 10);
+    secondTime = parseInt(secondTime % 60, 10);
+  }
+  if (minuteTime >= 10) {
+    minuteTime = `${minuteTime}`;
+  } else {
+    minuteTime = `0${minuteTime}`;
+  }
+  if (secondTime >= 10) {
+    secondTime = `${secondTime}`;
+  } else {
+    secondTime = `0${secondTime}`;
+  }
+  return `${minuteTime}:${secondTime}`;
+}
+
 export function formatFormError(data, err, prefix = '') {
   const r = {};
   Object.keys(err).forEach(field => {

+ 18 - 0
server/data/src/main/java/com/qxgmat/data/constants/enums/module/VideoCourseType.java

@@ -0,0 +1,18 @@
+package com.qxgmat.data.constants.enums.module;
+
+public enum VideoCourseType {
+    BASE("base"),
+    THINKING("thinking"),
+    SYSTEM("system"),
+
+    ;
+    public String key;
+    private VideoCourseType(String key){
+        this.key = key;
+    }
+
+    public static VideoCourseType ValueOf(String name){
+        if (name == null) return null;
+        return VideoCourseType.valueOf(name.toUpperCase());
+    }
+}

+ 35 - 0
server/data/src/main/java/com/qxgmat/data/dao/entity/Course.java

@@ -115,6 +115,12 @@ public class Course implements Serializable {
     private Integer expireDays;
 
     /**
+     * 提问扩展天数
+     */
+    @Column(name = "`ask_extend_days`")
+    private Integer askExtendDays;
+
+    /**
      * 使用有效时长
      */
     @Column(name = "`use_expire_time`")
@@ -539,6 +545,24 @@ public class Course implements Serializable {
     }
 
     /**
+     * 获取提问扩展天数
+     *
+     * @return ask_extend_days - 提问扩展天数
+     */
+    public Integer getAskExtendDays() {
+        return askExtendDays;
+    }
+
+    /**
+     * 设置提问扩展天数
+     *
+     * @param askExtendDays 提问扩展天数
+     */
+    public void setAskExtendDays(Integer askExtendDays) {
+        this.askExtendDays = askExtendDays;
+    }
+
+    /**
      * 获取使用有效时长
      *
      * @return use_expire_time - 使用有效时长
@@ -878,6 +902,7 @@ public class Course implements Serializable {
         sb.append(", maxNumber=").append(maxNumber);
         sb.append(", expirePreDays=").append(expirePreDays);
         sb.append(", expireDays=").append(expireDays);
+        sb.append(", askExtendDays=").append(askExtendDays);
         sb.append(", useExpireTime=").append(useExpireTime);
         sb.append(", wechatAvatar=").append(wechatAvatar);
         sb.append(", trailNumber=").append(trailNumber);
@@ -1110,6 +1135,16 @@ public class Course implements Serializable {
         }
 
         /**
+         * 设置提问扩展天数
+         *
+         * @param askExtendDays 提问扩展天数
+         */
+        public Builder askExtendDays(Integer askExtendDays) {
+            obj.setAskExtendDays(askExtendDays);
+            return this;
+        }
+
+        /**
          * 设置使用有效时长
          *
          * @param useExpireTime 使用有效时长

+ 4 - 2
server/data/src/main/java/com/qxgmat/data/dao/mapping/CourseMapper.xml

@@ -23,6 +23,7 @@
     <result column="max_number" jdbcType="INTEGER" property="maxNumber" />
     <result column="expire_pre_days" jdbcType="INTEGER" property="expirePreDays" />
     <result column="expire_days" jdbcType="INTEGER" property="expireDays" />
+    <result column="ask_extend_days" jdbcType="INTEGER" property="askExtendDays" />
     <result column="use_expire_time" jdbcType="INTEGER" property="useExpireTime" />
     <result column="wechat_avatar" jdbcType="VARCHAR" property="wechatAvatar" />
     <result column="trail_number" jdbcType="INTEGER" property="trailNumber" />
@@ -53,8 +54,9 @@
     -->
     `id`, `struct_id`, `parent_struct_id`, `course_module`, `no_number`, `vs_type`, `video_type`, 
     `extend`, `title`, `comment`, `crowd`, `price`, `teacher`, `cover`, `min_number`, 
-    `max_number`, `expire_pre_days`, `expire_days`, `use_expire_time`, `wechat_avatar`, 
-    `trail_number`, `sale_number`, `package_sale_number`, `create_time`, `update_time`
+    `max_number`, `expire_pre_days`, `expire_days`, `ask_extend_days`, `use_expire_time`, 
+    `wechat_avatar`, `trail_number`, `sale_number`, `package_sale_number`, `create_time`, 
+    `update_time`
   </sql>
   <sql id="Blob_Column_List">
     <!--

+ 7 - 7
server/data/src/main/java/com/qxgmat/data/relation/mapping/PreviewAssignRelationMapper.xml

@@ -11,7 +11,7 @@
     <!--
       WARNING - @mbg.generated
     -->
-    ep.`id`
+    pa.`id`
   </sql>
 
   <!--
@@ -22,7 +22,7 @@
     <include refid="Id_Column_List" />
     from `preview_assign` pa
     <if test="userId != null">
-    left join `user_paper` up on ep.`id` = up.`origin_id`
+    left join `user_paper` up on pa.`id` = up.`origin_id`
       and up.`paper_origin` = 'preview'
       and up.`user_id` = #{userId,jdbcType=VARCHAR}
       <if test="times != null">
@@ -49,14 +49,14 @@
   </select>
 
   <!--
-   1v1课程作业列表
+   小班课程作业列表
   -->
   <select id="listByAppointment" resultMap="IdMap">
     select
     <include refid="Id_Column_List" />
     from `preview_assign` pa
     <if test="userId != null">
-      left join `user_paper` up on ep.`id` = up.`origin_id`
+      left join `user_paper` up on pa.`id` = up.`origin_id`
       and up.`paper_origin` = 'preview'
       and up.`user_id` = #{userId,jdbcType=VARCHAR}
       <if test="times != null">
@@ -89,14 +89,14 @@
   </select>
 
   <!--
-   小班课程作业列表
+   1v1课程作业列表
   -->
-  <select id="listByAppointment" resultMap="IdMap">
+  <select id="listByTime" resultMap="IdMap">
     select
     <include refid="Id_Column_List" />
     from `preview_assign` pa
     <if test="userId != null">
-      left join `user_paper` up on ep.`id` = up.`origin_id`
+      left join `user_paper` up on pa.`id` = up.`origin_id`
       and up.`paper_origin` = 'preview'
       and up.`user_id` = #{userId,jdbcType=VARCHAR}
       <if test="times != null">

+ 4 - 4
server/data/src/main/java/com/qxgmat/data/relation/mapping/UserOrderRecordRelationMapper.xml

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.qxgmat.data.relation.UserOrderRecordRelationMapper">
-  <resultMap id="IdMap" type="com.qxgmat.data.dao.entity.UserNoteQuestion">
+  <resultMap id="IdMap" type="com.qxgmat.data.dao.entity.UserOrderRecord">
     <!--
       WARNING - @mbg.generated
     -->
@@ -18,7 +18,7 @@
     <!--
       WARNING - @mbg.generated
     -->
-    up.`id`
+    uor.`id`
   </sql>
 
   <!--
@@ -26,7 +26,7 @@
   -->
   <select id="groupByTime" resultMap="NumberMap">
     select
-    count(cs.`id`) as `number`, cs.`no` as `id`
+    count(uor.`id`) as `number`, uor.`no` as `id`
     from `user_order_record` uor
     where
     uor.`no` IN
@@ -44,7 +44,7 @@
     from `user_order_record` uor
     left join `course` c on c.`id` = uor.`product_id` and uor.`product_type` = 'course'
     <if test="courseModules != null">
-      c.`course_module` IN
+      and c.`course_module` IN
       <foreach collection="courseModules" item="item" index="index" open="(" close=")" separator=",">
         #{item, jdbcType=VARCHAR}
       </foreach>

+ 10 - 0
server/gateway-api/src/main/java/com/qxgmat/controller/admin/CourseController.java

@@ -22,6 +22,7 @@ import com.qxgmat.service.ManagerService;
 import com.qxgmat.service.UsersService;
 import com.qxgmat.service.extend.CourseExtendService;
 import com.qxgmat.service.extend.OrderFlowService;
+import com.qxgmat.service.extend.ToolsService;
 import com.qxgmat.service.inline.*;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -31,6 +32,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpSession;
 import javax.xml.transform.TransformerFactory;
+import java.math.BigDecimal;
 import java.util.Collection;
 import java.util.Date;
 import java.util.List;
@@ -93,6 +95,9 @@ public class CourseController {
     @Autowired
     private OrderFlowService orderFlowService;
 
+    @Autowired
+    private ToolsService toolsService;
+
 
     @RequestMapping(value = "/add", method = RequestMethod.POST)
     @ApiOperation(value = "添加课程", httpMethod = "POST")
@@ -528,6 +533,11 @@ public class CourseController {
         entity.setUseTime(new Date());
         entity.setStartTime(courseTime.getStartTime());
         entity.setEndTime(courseTime.getEndTime());
+
+        // 默认回答时间
+        Integer ask = toolsService.getAskTime(BigDecimal.valueOf(0));
+        entity.setAskTime(ask);
+
         entity = userOrderRecordService.addStudent(entity);
         managerLogService.log(request);
         return ResponseHelp.success(Transform.convert(entity, UserOrderRecord.class));

+ 19 - 4
server/gateway-api/src/main/java/com/qxgmat/controller/api/CourseController.java

@@ -13,6 +13,7 @@ import com.qxgmat.data.constants.enums.user.DataType;
 import com.qxgmat.data.dao.entity.*;
 import com.qxgmat.data.relation.entity.UserPreviewPaperRelation;
 import com.qxgmat.dto.extend.CourseExtendDto;
+import com.qxgmat.dto.extend.CourseTeacherExtendDto;
 import com.qxgmat.dto.extend.UserPaperBaseExtendDto;
 import com.qxgmat.dto.response.*;
 import com.qxgmat.help.ShiroHelp;
@@ -123,11 +124,12 @@ public class CourseController {
             @RequestParam(required = false, defaultValue = "1") int page,
             @RequestParam(required = false, defaultValue = "100") int size,
             @RequestParam(required = false) Integer structId,
+            @RequestParam(required = false) Boolean isSpecial,
             @RequestParam(required = false, defaultValue = "id") String order,
             @RequestParam(required = false, defaultValue = "desc") String direction,
             HttpSession session) {
 
-        Page<CoursePackage> p = coursePackageService.list(page, size, structId, order, DirectionStatus.ValueOf(direction));
+        Page<CoursePackage> p = coursePackageService.list(page, size, structId, isSpecial, order, DirectionStatus.ValueOf(direction));
 
         List<CoursePackageListDto> pr = Transform.convert(p, CoursePackageListDto.class);
 
@@ -245,10 +247,14 @@ public class CourseController {
             @RequestParam(required = false) Integer courseId
     )  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null){
+            throw new ParameterException("登录后查看作业");
+        }
         List<UserOrderRecord> userOrderRecordList;
         CourseModule module = CourseModule.ValueOf(courseModule);
-        if (module == CourseModule.VIDEO){
-            // 视频课程包含:小班课程
+
+        if (module == CourseModule.ONLINE){
+            // 在线课程包含:视频课程、小班课程
             userOrderRecordList = userOrderRecordService.listWithStudyAdmin(1, 1000, new String[]{CourseModule.VIDEO.key, CourseModule.ONLINE.key}, structId, courseId, user.getId(), null,null, null);
         } else if (module == CourseModule.VS){
             // 1v1课程:只有系统授课有作业
@@ -268,7 +274,13 @@ public class CourseController {
         List<UserOrderRecord> processList = userOrderRecordList.stream().filter(row-> row.getIsUsed() > 0 && row.getUseEndTime().after(now)).collect(Collectors.toList());
         Collection processIds = Transform.getIds(processList, UserOrderRecord.class, "id");
         Map<Object, Collection<UserPreviewPaperRelation>> previewMap = previewService.groupByCourseId(user.getId(), processIds, 2);
-        Transform.combine(dtos, previewMap, UserCourseProgressDto.class, "productId", "previews", UserPaperBaseExtendDto.class);
+        Transform.combine(dtos, previewMap, UserCourseProgressDto.class, "productId", "papers", UserPaperBaseExtendDto.class);
+
+        // 绑定老师
+        Collection teacherIds = Transform.getIds(userOrderRecordList, UserOrderRecord.class, "teacherId");
+        List<CourseTeacher> teacherList = courseTeacherService.select(teacherIds);
+        Transform.combine(dtos, teacherList, UserCourseProgressDto.class, "teacherId", "teacher", CourseTeacher.class, "id", CourseTeacherExtendDto.class);
+
 
         return ResponseHelp.success(dtos);
     }
@@ -283,6 +295,9 @@ public class CourseController {
             @RequestParam(required = false) Integer times
     )  {
         User user = (User) shiroHelp.getLoginUser();
+        if (user == null){
+            throw new ParameterException("登录后查看作业");
+        }
 
         List<UserPreviewPaperRelation> p = previewService.list(page, size, recordId, user.getId(), endTime, times);
         List<UserExercisePaperDto> pr = Transform.convert(p, UserExercisePaperDto.class);

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

@@ -89,7 +89,7 @@ public class TextbookController
     private TextbookTopicService textbookTopicService;
 
     @RequestMapping(value = "/progress", method = RequestMethod.GET)
-    @ApiOperation(value = "练习进度", httpMethod = "GET")
+    @ApiOperation(value = "机经进度", httpMethod = "GET")
     public Response<List<UserTextbookGroupDto>> progress(HttpSession session) {
         User user = (User) shiroHelp.getLoginUser();
 

+ 40 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/extend/CourseExtendDto.java

@@ -9,10 +9,18 @@ import java.math.BigDecimal;
 public class CourseExtendDto {
     private Integer id;
 
+    private Integer structId;
+
+    private Integer parentStructId;
+
+    private String courseModule;
+
     private String title;
 
     private String teacher;
 
+    private String wechatAvatar;
+
     private BigDecimal price;
 
     private Integer noNumber;
@@ -56,4 +64,36 @@ public class CourseExtendDto {
     public void setPrice(BigDecimal price) {
         this.price = price;
     }
+
+    public String getWechatAvatar() {
+        return wechatAvatar;
+    }
+
+    public void setWechatAvatar(String wechatAvatar) {
+        this.wechatAvatar = wechatAvatar;
+    }
+
+    public Integer getStructId() {
+        return structId;
+    }
+
+    public void setStructId(Integer structId) {
+        this.structId = structId;
+    }
+
+    public Integer getParentStructId() {
+        return parentStructId;
+    }
+
+    public void setParentStructId(Integer parentStructId) {
+        this.parentStructId = parentStructId;
+    }
+
+    public String getCourseModule() {
+        return courseModule;
+    }
+
+    public void setCourseModule(String courseModule) {
+        this.courseModule = courseModule;
+    }
 }

+ 69 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/extend/CourseTeacherExtendDto.java

@@ -0,0 +1,69 @@
+package com.qxgmat.dto.extend;
+
+import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.CourseTeacher;
+
+import java.math.BigDecimal;
+
+@Dto(entity = CourseTeacher.class)
+public class CourseTeacherExtendDto {
+    private Integer id;
+
+    private Integer courseId;
+
+    private String realname;
+
+    private String avatar;
+
+    private String wechat;
+
+    private String qr;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public Integer getCourseId() {
+        return courseId;
+    }
+
+    public void setCourseId(Integer courseId) {
+        this.courseId = courseId;
+    }
+
+    public String getRealname() {
+        return realname;
+    }
+
+    public void setRealname(String realname) {
+        this.realname = realname;
+    }
+
+    public String getAvatar() {
+        return avatar;
+    }
+
+    public void setAvatar(String avatar) {
+        this.avatar = avatar;
+    }
+
+    public String getWechat() {
+        return wechat;
+    }
+
+    public void setWechat(String wechat) {
+        this.wechat = wechat;
+    }
+
+    public String getQr() {
+        return qr;
+    }
+
+    public void setQr(String qr) {
+        this.qr = qr;
+    }
+}

+ 9 - 9
server/gateway-api/src/main/java/com/qxgmat/dto/response/UserCourseDetailDto.java

@@ -24,7 +24,7 @@ public class UserCourseDetailDto {
 
     private Date useEndTime;
 
-    private Integer isUse;
+    private Integer isUsed;
 
     private Integer isStop;
 
@@ -100,14 +100,6 @@ public class UserCourseDetailDto {
         this.useEndTime = useEndTime;
     }
 
-    public Integer getIsUse() {
-        return isUse;
-    }
-
-    public void setIsUse(Integer isUse) {
-        this.isUse = isUse;
-    }
-
     public Integer getIsStop() {
         return isStop;
     }
@@ -139,4 +131,12 @@ public class UserCourseDetailDto {
     public void setRestoreTime(Date restoreTime) {
         this.restoreTime = restoreTime;
     }
+
+    public Integer getIsUsed() {
+        return isUsed;
+    }
+
+    public void setIsUsed(Integer isUsed) {
+        this.isUsed = isUsed;
+    }
 }

+ 50 - 9
server/gateway-api/src/main/java/com/qxgmat/dto/response/UserCourseProgressDto.java

@@ -3,6 +3,7 @@ package com.qxgmat.dto.response;
 import com.nuliji.tools.annotation.Dto;
 import com.qxgmat.data.dao.entity.UserOrderRecord;
 import com.qxgmat.dto.extend.CourseExtendDto;
+import com.qxgmat.dto.extend.CourseTeacherExtendDto;
 import com.qxgmat.dto.extend.UserPaperBaseExtendDto;
 
 import java.util.Date;
@@ -16,6 +17,14 @@ public class UserCourseProgressDto {
 
     private CourseExtendDto course;
 
+    private Integer number;
+
+    private Integer vsNo;
+
+    private Integer teacherId;
+
+    private CourseTeacherExtendDto teacher;
+
     private Date startTime;
 
     private Date endTime;
@@ -24,7 +33,7 @@ public class UserCourseProgressDto {
 
     private Date useEndTime;
 
-    private Integer isUse;
+    private Integer isUsed;
 
     private Integer isStop;
 
@@ -100,14 +109,6 @@ public class UserCourseProgressDto {
         this.useEndTime = useEndTime;
     }
 
-    public Integer getIsUse() {
-        return isUse;
-    }
-
-    public void setIsUse(Integer isUse) {
-        this.isUse = isUse;
-    }
-
     public Integer getIsStop() {
         return isStop;
     }
@@ -139,4 +140,44 @@ public class UserCourseProgressDto {
     public void setRestoreTime(Date restoreTime) {
         this.restoreTime = restoreTime;
     }
+
+    public Integer getNumber() {
+        return number;
+    }
+
+    public void setNumber(Integer number) {
+        this.number = number;
+    }
+
+    public Integer getVsNo() {
+        return vsNo;
+    }
+
+    public void setVsNo(Integer vsNo) {
+        this.vsNo = vsNo;
+    }
+
+    public CourseTeacherExtendDto getTeacher() {
+        return teacher;
+    }
+
+    public void setTeacher(CourseTeacherExtendDto teacher) {
+        this.teacher = teacher;
+    }
+
+    public Integer getTeacherId() {
+        return teacherId;
+    }
+
+    public void setTeacherId(Integer teacherId) {
+        this.teacherId = teacherId;
+    }
+
+    public Integer getIsUsed() {
+        return isUsed;
+    }
+
+    public void setIsUsed(Integer isUsed) {
+        this.isUsed = isUsed;
+    }
 }

+ 2 - 4
server/gateway-api/src/main/java/com/qxgmat/service/UserPaperService.java

@@ -27,10 +27,7 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 @Service
 public class UserPaperService extends AbstractService {
@@ -79,6 +76,7 @@ public class UserPaperService extends AbstractService {
      * @return
      */
     public List<UserPaper> listWithOrigin(Integer userId, PaperOrigin origin, Collection ids, Integer recordId){
+        if (ids == null || ids.size() == 0) return new ArrayList<>();
         Example example = new Example(UserPaper.class);
         example.and(
                 example.createCriteria()

+ 1 - 0
server/gateway-api/src/main/java/com/qxgmat/service/UserQuestionService.java

@@ -73,6 +73,7 @@ public class UserQuestionService extends AbstractService {
     }
 
     public List<UserQuestion> listByQuestion(Integer userId, Collection questionIds){
+        if (questionIds == null || questionIds.size() == 0) return new ArrayList<>();
         Example example = new Example(UserQuestion.class);
         example.and(
                 example.createCriteria()

+ 3 - 1
server/gateway-api/src/main/java/com/qxgmat/service/extend/CourseExtendService.java

@@ -5,6 +5,7 @@ import com.nuliji.tools.Transform;
 import com.qxgmat.data.constants.enums.QuestionSubject;
 import com.qxgmat.data.constants.enums.QuestionType;
 import com.qxgmat.data.constants.enums.module.ProductType;
+import com.qxgmat.data.constants.enums.module.VideoCourseType;
 import com.qxgmat.data.dao.entity.Course;
 import com.qxgmat.data.dao.entity.UserCourse;
 import com.qxgmat.data.dao.entity.UserOrderRecord;
@@ -91,7 +92,8 @@ public class CourseExtendService {
      */
     public Integer questionRelationCourse(Integer userId, QuestionType questionType){
         QuestionSubject subject = QuestionSubject.FromType(questionType);
-        List<Course> courseList = courseService.listByExtend(questionType.key, subject.key);
+        // 只查询系统授课记录
+        List<Course> courseList = courseService.listByExtend(questionType.key, subject.key, VideoCourseType.SYSTEM.key);
         if (courseList.size()==0)return null;
         Collection ids = Transform.getIds(courseList, Course.class, "id");
         List<UserCourse> userCourseList = userCourseService.listByCourse(userId, ids);

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

@@ -1,6 +1,7 @@
 package com.qxgmat.service.extend;
 
 import com.nuliji.tools.AbstractService;
+import com.nuliji.tools.Tools;
 import com.nuliji.tools.Transform;
 import com.nuliji.tools.exception.ParameterException;
 import com.qxgmat.data.constants.enums.module.*;
@@ -175,7 +176,43 @@ public class PreviewService extends AbstractService {
             default:
                 return 0;
         }
+    }
 
+    /**
+     * 获取预习作业关联的提问权限记录
+     * @param assignId
+     * @return
+     */
+    public Integer questionCourse(Integer userId, Integer assignId){
+        PreviewAssign assign = previewAssignService.get(assignId);
+        if (assign == null){
+            throw new ParameterException("记录不存在");
+        }
+        Course course = courseService.get(assign.getCourseId());
+        CourseModule courseModule = CourseModule.ValueOf(course.getCourseModule());
+        switch(courseModule){
+            case VS:
+                // 默认没有?
+                return 0;
+//                UserCourseAppointment appointment = userCourseAppointmentService.get(assign.getCourseAppointment());
+//                return appointment.getRecordId();
+            case ONLINE:
+                UserOrderRecord timeRecord = userOrderRecordService.getByUserAndTime(userId, assign.getCourseId(), assign.getCourseTime());
+                // 有效期延长到n天
+                if (Tools.addDate(timeRecord.getUseEndTime(), course.getAskExtendDays()).before(new Date())){
+                    return 0;
+                }
+                return timeRecord.getId();
+            case VIDEO:
+                // 只有系统授课允许
+                VideoCourseType videoCourseType = VideoCourseType.ValueOf(course.getVideoType());
+                if (videoCourseType != VideoCourseType.SYSTEM) return 0;
+                // 获取当前该课程记录
+                UserCourse record = userCourseService.getCourseBase(userId, assign.getCourseId());
+                return record != null ? record.getRecordId() : 0;
+            default:
+                return 0;
+        }
     }
 
     private void replaceTitle(List<PreviewAssign> previewAssignList){

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

@@ -720,13 +720,13 @@ public class QuestionFlowService {
     }
 
     /**
-     * 根据题目类型获取课程信息
+     * 获取提问权限记录
      * @param questionType
      * @return
      */
     public Integer questionRelationCourse(Integer userId, Integer assignId, QuestionType questionType){
         if (assignId != null){
-            Integer assignRecordId = previewService.getRecord(userId, assignId);
+            Integer assignRecordId = previewService.questionCourse(userId, assignId);
             if (assignRecordId != null) return assignRecordId;
         }
         return courseExtendService.questionRelationCourse(userId, questionType);

+ 7 - 1
server/gateway-api/src/main/java/com/qxgmat/service/inline/CoursePackageService.java

@@ -33,7 +33,7 @@ public class CoursePackageService extends AbstractService {
     @Resource
     private CoursePackageRelationMapper coursePackageRelationMapper;
 
-    public Page<CoursePackage> list(int page, int size, Integer structId, String order, DirectionStatus direction){
+    public Page<CoursePackage> list(int page, int size, Integer structId, Boolean isSpecial, String order, DirectionStatus direction){
         Example example = new Example(Course.class);
         if(structId != null){
             example.and(
@@ -41,6 +41,12 @@ public class CoursePackageService extends AbstractService {
                             .orEqualTo("structId", structId)
             );
         }
+        if (isSpecial != null){
+            example.and(
+                    example.createCriteria()
+                            .orEqualTo("isSpecial", isSpecial ? 1: 0)
+            );
+        }
         if(order == null || order.isEmpty()) order = "id";
         if (direction != null){
             switch(direction){

+ 7 - 1
server/gateway-api/src/main/java/com/qxgmat/service/inline/CourseService.java

@@ -118,13 +118,19 @@ public class CourseService extends AbstractService {
         return select(courseMapper, example);
     }
 
-    public List<Course> listByExtend(String questionType, String questionSubject){
+    public List<Course> listByExtend(String questionType, String questionSubject, String videoType){
         Example example = new Example(Course.class);
         example.and(
                 example.createCriteria()
                         .orEqualTo("extend", questionType)
                         .orEqualTo("extend", questionSubject)
         );
+        if (videoType != null){
+            example.and(
+                    example.createCriteria()
+                        .andEqualTo("videoType", videoType)
+            );
+        }
         return select(courseMapper, example);
     }
 

+ 2 - 2
server/gateway-api/src/main/java/com/qxgmat/service/inline/UserOrderRecordService.java

@@ -96,10 +96,10 @@ public class UserOrderRecordService extends AbstractService {
         if (process != null){
              example.and(
                     process?  example.createCriteria()
-                    .andEqualTo("isUse", 1)
+                    .andEqualTo("isUsed", 1)
                     .andGreaterThanOrEqualTo("useEndTime", new Date()):
                             example.createCriteria()
-                     .andEqualTo("isUse", 1)
+                     .andEqualTo("isUsed", 1)
                      .andLessThanOrEqualTo("useEndTime", new Date())
 
             );