Browse Source

feat(front): 长难句考试流程

Go 5 years ago
parent
commit
043561f4bf
100 changed files with 3770 additions and 1634 deletions
  1. 66 61
      front/package-lock.json
  2. 1 0
      front/package.json
  3. 2 2
      front/project/Constant.js
  4. 1 0
      front/project/admin/local.json
  5. 3 0
      front/project/admin/routes/setting/struct/page.js
  6. 35 31
      front/project/admin/routes/setting/time/page.js
  7. 201 1
      front/project/admin/routes/subject/examination/page.js
  8. 8 7
      front/project/admin/routes/subject/exercise/page.js
  9. 1 2
      front/project/admin/routes/subject/question/page.js
  10. 1 1
      front/project/admin/routes/subject/sentence/index.js
  11. 25 1
      front/project/admin/routes/subject/sentence/page.js
  12. 3 3
      front/project/admin/routes/subject/sentenceArticle/page.js
  13. 4 1
      front/project/admin/routes/user/askDetail/page.js
  14. 8 0
      front/project/admin/stores/examination.js
  15. 14 12
      front/project/www/app.js
  16. 1 0
      front/project/www/app.less
  17. 4 0
      front/project/www/assets/close.svg
  18. 13 4
      front/project/www/assets/right.svg
  19. 23 5
      front/project/www/components/AnswerCheckbox/index.js
  20. 1 1
      front/project/www/components/Continue/index.js
  21. 19 4
      front/project/www/components/HardInput/index.js
  22. 1 1
      front/project/www/components/HardInput/index.less
  23. 2 2
      front/project/www/components/Icon/index.js
  24. 2 2
      front/project/www/components/Icon/index.less
  25. 1 1
      front/project/www/components/IconButton/index.js
  26. 3 3
      front/project/www/components/List/index.js
  27. 1 1
      front/project/www/components/List/index.less
  28. 2 2
      front/project/www/components/ListTable/index.js
  29. 16 23
      front/project/www/components/Panel/index.js
  30. 4 1
      front/project/www/components/ProgressButton/index.js
  31. 2 2
      front/project/www/components/ProgressText/index.js
  32. 3 3
      front/project/www/components/ProgressText/index.less
  33. 1 1
      front/project/www/components/RadioButton/index.js
  34. 8 6
      front/project/www/components/Select/index.js
  35. 62 2
      front/project/www/components/Select/index.less
  36. 3 3
      front/project/www/components/Step/index.js
  37. 4 0
      front/project/www/components/Step/index.less
  38. 4 2
      front/project/www/components/Switch/index.js
  39. 6 5
      front/project/www/index.js
  40. 186 131
      front/project/www/routes/exercise/list/page.js
  41. 69 0
      front/project/www/routes/exercise/main/index.less
  42. 155 70
      front/project/www/routes/exercise/main/page.js
  43. 314 0
      front/project/www/routes/paper/process/base/index.js
  44. 265 0
      front/project/www/routes/paper/process/base/index.less
  45. 0 257
      front/project/www/routes/paper/process/index.less
  46. 234 252
      front/project/www/routes/paper/process/page.js
  47. 288 0
      front/project/www/routes/paper/process/sentence/index.js
  48. 30 5
      front/project/www/routes/sentence/process/index.less
  49. 380 264
      front/project/www/routes/paper/question/index.less
  50. 425 116
      front/project/www/routes/paper/question/page.js
  51. 67 0
      front/project/www/routes/paper/report/page.js
  52. 1 2
      front/project/www/routes/sentence/index.js
  53. 2 2
      front/project/www/routes/sentence/process/index.js
  54. 0 92
      front/project/www/routes/sentence/process/page.js
  55. 1 2
      front/project/www/routes/sentence/read/page.js
  56. 5 5
      front/project/www/stores/my.js
  57. 22 10
      front/project/www/stores/question.js
  58. 2 2
      front/src/layouts/TabRightLayout/index.js
  59. 41 8
      front/src/services/Tools.js
  60. 20 0
      server/data/src/main/java/com/qxgmat/data/constants/enums/module/PaperModule.java
  61. 12 12
      server/data/src/main/java/com/qxgmat/data/dao/entity/PreviewPaper.java
  62. 35 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/UserAskQuestion.java
  63. 31 31
      server/data/src/main/java/com/qxgmat/data/dao/entity/UserNoteQuestion.java
  64. 35 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/UserPaper.java
  65. 38 3
      server/data/src/main/java/com/qxgmat/data/dao/entity/UserSentenceProgress.java
  66. 2 2
      server/data/src/main/java/com/qxgmat/data/dao/mapping/PreviewPaperMapper.xml
  67. 2 1
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserAskQuestionMapper.xml
  68. 4 4
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserNoteQuestionMapper.xml
  69. 3 2
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserPaperMapper.xml
  70. 2 1
      server/data/src/main/java/com/qxgmat/data/dao/mapping/UserSentenceProgressMapper.xml
  71. 18 0
      server/data/src/main/java/com/qxgmat/data/relation/ExaminationPaperRelationMapper.java
  72. 8 0
      server/data/src/main/java/com/qxgmat/data/relation/ExercisePaperRelationMapper.java
  73. 1 1
      server/data/src/main/java/com/qxgmat/data/relation/QuestionNoRelationMapper.java
  74. 1 1
      server/data/src/main/java/com/qxgmat/data/relation/QuestionRelationMapper.java
  75. 1 1
      server/data/src/main/java/com/qxgmat/data/relation/SentenceQuestionRelationMapper.java
  76. 1 1
      server/data/src/main/java/com/qxgmat/data/relation/TextbookQuestionRelationMapper.java
  77. 4 10
      server/data/src/main/java/com/qxgmat/data/relation/UserPaperRelationMapper.java
  78. 1 1
      server/data/src/main/java/com/qxgmat/data/relation/UserReportRelationMapper.java
  79. 40 0
      server/data/src/main/java/com/qxgmat/data/relation/mapping/ExaminationPaperRelationMapper.xml
  80. 32 2
      server/data/src/main/java/com/qxgmat/data/relation/mapping/ExercisePaperRelationMapper.xml
  81. 7 7
      server/data/src/main/java/com/qxgmat/data/relation/mapping/QuestionNoRelationMapper.xml
  82. 1 1
      server/data/src/main/java/com/qxgmat/data/relation/mapping/QuestionRelationMapper.xml
  83. 1 1
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserAskCourseRelationMapper.xml
  84. 1 1
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserAskQuestionRelationMapper.xml
  85. 2 2
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserCollectQuestionRelationMapper.xml
  86. 2 2
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserNoteQuestionRelationMapper.xml
  87. 5 34
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserPaperRelationMapper.xml
  88. 2 2
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserRelationMapper.xml
  89. 10 8
      server/data/src/main/java/com/qxgmat/data/relation/mapping/UserReportRelationMapper.xml
  90. 25 0
      server/gateway-api/src/main/java/com/qxgmat/controller/admin/ExaminationController.java
  91. 2 2
      server/gateway-api/src/main/java/com/qxgmat/controller/admin/ExerciseController.java
  92. 3 2
      server/gateway-api/src/main/java/com/qxgmat/controller/admin/PreviewController.java
  93. 2 3
      server/gateway-api/src/main/java/com/qxgmat/controller/admin/SentenceController.java
  94. 8 7
      server/gateway-api/src/main/java/com/qxgmat/controller/api/MyController.java
  95. 282 54
      server/gateway-api/src/main/java/com/qxgmat/controller/api/QuestionController.java
  96. 21 21
      server/gateway-api/src/main/java/com/qxgmat/controller/api/SentenceController.java
  97. 39 0
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/response/ExaminationPaperListDto.java
  98. 10 0
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/response/SentenceQuestionListDto.java
  99. 10 0
      server/gateway-api/src/main/java/com/qxgmat/dto/admin/response/UserAskQuestionDetailDto.java
  100. 0 0
      server/gateway-api/src/main/java/com/qxgmat/dto/extend/QuestionBaseExtendDto.java

+ 66 - 61
front/package-lock.json

@@ -609,7 +609,7 @@
     },
     "array-find-index": {
       "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
+      "resolved": "http://registry.npm.taobao.org/array-find-index/download/array-find-index-1.0.2.tgz",
       "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
       "dev": true
     },
@@ -621,7 +621,7 @@
     },
     "array-includes": {
       "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz",
+      "resolved": "http://registry.npm.taobao.org/array-includes/download/array-includes-3.0.3.tgz",
       "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=",
       "dev": true,
       "requires": {
@@ -636,7 +636,7 @@
     },
     "array-union": {
       "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
+      "resolved": "http://registry.npm.taobao.org/array-union/download/array-union-1.0.2.tgz",
       "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
       "dev": true,
       "requires": {
@@ -645,7 +645,7 @@
     },
     "array-uniq": {
       "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
+      "resolved": "http://registry.npm.taobao.org/array-uniq/download/array-uniq-1.0.3.tgz",
       "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=",
       "dev": true
     },
@@ -1862,7 +1862,7 @@
     },
     "batch": {
       "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
+      "resolved": "http://registry.npm.taobao.org/batch/download/batch-0.6.1.tgz",
       "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=",
       "dev": true
     },
@@ -1944,7 +1944,7 @@
     },
     "bonjour": {
       "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz",
+      "resolved": "http://registry.npm.taobao.org/bonjour/download/bonjour-3.5.0.tgz",
       "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=",
       "dev": true,
       "requires": {
@@ -2196,7 +2196,7 @@
     },
     "camelcase-keys": {
       "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+      "resolved": "http://registry.npm.taobao.org/camelcase-keys/download/camelcase-keys-2.1.0.tgz",
       "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
       "dev": true,
       "requires": {
@@ -2206,7 +2206,7 @@
       "dependencies": {
         "camelcase": {
           "version": "2.1.1",
-          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+          "resolved": "http://registry.npm.taobao.org/camelcase/download/camelcase-2.1.1.tgz",
           "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
           "dev": true
         }
@@ -3506,7 +3506,7 @@
     },
     "currently-unhandled": {
       "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
+      "resolved": "http://registry.npm.taobao.org/currently-unhandled/download/currently-unhandled-0.4.1.tgz",
       "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
       "dev": true,
       "requires": {
@@ -3725,7 +3725,7 @@
     },
     "dns-equal": {
       "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
+      "resolved": "http://registry.npm.taobao.org/dns-equal/download/dns-equal-1.0.0.tgz",
       "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=",
       "dev": true
     },
@@ -3741,7 +3741,7 @@
     },
     "dns-txt": {
       "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz",
+      "resolved": "http://registry.npm.taobao.org/dns-txt/download/dns-txt-2.0.2.tgz",
       "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=",
       "dev": true,
       "requires": {
@@ -4568,7 +4568,7 @@
     },
     "eventsource": {
       "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-0.1.6.tgz",
+      "resolved": "http://registry.npm.taobao.org/eventsource/download/eventsource-0.1.6.tgz",
       "integrity": "sha1-Cs7ehJ7X3RzMMsgRuxG5RNTykjI=",
       "dev": true,
       "requires": {
@@ -4879,7 +4879,7 @@
     },
     "faye-websocket": {
       "version": "0.10.0",
-      "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
+      "resolved": "http://registry.npm.taobao.org/faye-websocket/download/faye-websocket-0.10.0.tgz",
       "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=",
       "dev": true,
       "requires": {
@@ -5069,7 +5069,7 @@
     },
     "for-in": {
       "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+      "resolved": "http://registry.npm.taobao.org/for-in/download/for-in-1.0.2.tgz",
       "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
       "dev": true
     },
@@ -5161,7 +5161,7 @@
     },
     "get-stdin": {
       "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
+      "resolved": "http://registry.npm.taobao.org/get-stdin/download/get-stdin-4.0.1.tgz",
       "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
       "dev": true
     },
@@ -5500,7 +5500,7 @@
     },
     "hpack.js": {
       "version": "2.1.6",
-      "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
+      "resolved": "http://registry.npm.taobao.org/hpack.js/download/hpack.js-2.1.6.tgz",
       "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=",
       "dev": true,
       "requires": {
@@ -5600,7 +5600,7 @@
     },
     "http-deceiver": {
       "version": "1.2.7",
-      "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
+      "resolved": "http://registry.npm.taobao.org/http-deceiver/download/http-deceiver-1.2.7.tgz",
       "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=",
       "dev": true
     },
@@ -5723,7 +5723,7 @@
       "dependencies": {
         "find-up": {
           "version": "2.1.0",
-          "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
+          "resolved": "http://registry.npm.taobao.org/find-up/download/find-up-2.1.0.tgz",
           "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
           "dev": true,
           "requires": {
@@ -5732,7 +5732,7 @@
         },
         "pkg-dir": {
           "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
+          "resolved": "http://registry.npm.taobao.org/pkg-dir/download/pkg-dir-2.0.0.tgz",
           "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=",
           "dev": true,
           "requires": {
@@ -5770,7 +5770,7 @@
     },
     "indent-string": {
       "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
+      "resolved": "http://registry.npm.taobao.org/indent-string/download/indent-string-2.1.0.tgz",
       "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
       "dev": true,
       "requires": {
@@ -5920,7 +5920,7 @@
     },
     "internal-ip": {
       "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-1.2.0.tgz",
+      "resolved": "http://registry.npm.taobao.org/internal-ip/download/internal-ip-1.2.0.tgz",
       "integrity": "sha1-rp+/k7mEh4eF1QqN4bNWlWBYz1w=",
       "dev": true,
       "requires": {
@@ -5949,7 +5949,7 @@
     },
     "ip": {
       "version": "1.1.5",
-      "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
+      "resolved": "http://registry.npm.taobao.org/ip/download/ip-1.1.5.tgz",
       "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=",
       "dev": true
     },
@@ -5982,7 +5982,7 @@
     },
     "is-binary-path": {
       "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
+      "resolved": "http://registry.npm.taobao.org/is-binary-path/download/is-binary-path-1.0.1.tgz",
       "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
       "dev": true,
       "requires": {
@@ -6116,7 +6116,7 @@
     },
     "is-path-cwd": {
       "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
+      "resolved": "http://registry.npm.taobao.org/is-path-cwd/download/is-path-cwd-1.0.0.tgz",
       "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=",
       "dev": true
     },
@@ -6131,7 +6131,7 @@
     },
     "is-path-inside": {
       "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
+      "resolved": "http://registry.npm.taobao.org/is-path-inside/download/is-path-inside-1.0.1.tgz",
       "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=",
       "dev": true,
       "requires": {
@@ -6205,7 +6205,7 @@
     },
     "is-utf8": {
       "version": "0.2.1",
-      "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+      "resolved": "http://registry.npm.taobao.org/is-utf8/download/is-utf8-0.2.1.tgz",
       "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
       "dev": true
     },
@@ -6217,7 +6217,7 @@
     },
     "is-wsl": {
       "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
+      "resolved": "http://registry.npm.taobao.org/is-wsl/download/is-wsl-1.1.0.tgz",
       "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=",
       "dev": true
     },
@@ -6733,7 +6733,7 @@
     },
     "loud-rejection": {
       "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
+      "resolved": "http://registry.npm.taobao.org/loud-rejection/download/loud-rejection-1.6.0.tgz",
       "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
       "dev": true,
       "requires": {
@@ -6765,7 +6765,7 @@
     },
     "map-obj": {
       "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+      "resolved": "http://registry.npm.taobao.org/map-obj/download/map-obj-1.0.1.tgz",
       "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
       "dev": true
     },
@@ -6822,7 +6822,7 @@
     },
     "meow": {
       "version": "3.7.0",
-      "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+      "resolved": "http://registry.npm.taobao.org/meow/download/meow-3.7.0.tgz",
       "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
       "dev": true,
       "requires": {
@@ -6840,7 +6840,7 @@
       "dependencies": {
         "load-json-file": {
           "version": "1.1.0",
-          "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+          "resolved": "http://registry.npm.taobao.org/load-json-file/download/load-json-file-1.1.0.tgz",
           "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
           "dev": true,
           "requires": {
@@ -6853,13 +6853,13 @@
         },
         "minimist": {
           "version": "1.2.0",
-          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "resolved": "http://registry.npm.taobao.org/minimist/download/minimist-1.2.0.tgz",
           "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
           "dev": true
         },
         "path-type": {
           "version": "1.1.0",
-          "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+          "resolved": "http://registry.npm.taobao.org/path-type/download/path-type-1.1.0.tgz",
           "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
           "dev": true,
           "requires": {
@@ -6870,7 +6870,7 @@
         },
         "read-pkg": {
           "version": "1.1.0",
-          "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
+          "resolved": "http://registry.npm.taobao.org/read-pkg/download/read-pkg-1.1.0.tgz",
           "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
           "dev": true,
           "requires": {
@@ -6881,7 +6881,7 @@
         },
         "read-pkg-up": {
           "version": "1.0.1",
-          "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
+          "resolved": "http://registry.npm.taobao.org/read-pkg-up/download/read-pkg-up-1.0.1.tgz",
           "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
           "dev": true,
           "requires": {
@@ -6891,7 +6891,7 @@
         },
         "strip-bom": {
           "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+          "resolved": "http://registry.npm.taobao.org/strip-bom/download/strip-bom-2.0.0.tgz",
           "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
           "dev": true,
           "requires": {
@@ -7073,7 +7073,7 @@
     },
     "multicast-dns-service-types": {
       "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
+      "resolved": "http://registry.npm.taobao.org/multicast-dns-service-types/download/multicast-dns-service-types-1.1.0.tgz",
       "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
       "dev": true
     },
@@ -7616,7 +7616,7 @@
     },
     "path-dirname": {
       "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
+      "resolved": "http://registry.npm.taobao.org/path-dirname/download/path-dirname-1.0.2.tgz",
       "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=",
       "dev": true
     },
@@ -7729,7 +7729,7 @@
       "dependencies": {
         "async": {
           "version": "1.5.2",
-          "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+          "resolved": "http://registry.npm.taobao.org/async/download/async-1.5.2.tgz",
           "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
           "dev": true
         }
@@ -9189,6 +9189,11 @@
       "resolved": "http://registry.npm.taobao.org/react-dom-factories/download/react-dom-factories-1.0.2.tgz",
       "integrity": "sha1-63cFxNs2+1AbOqOP91lhaqD/luA="
     },
+    "react-fullscreen-crossbrowser": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/react-fullscreen-crossbrowser/-/react-fullscreen-crossbrowser-1.0.9.tgz",
+      "integrity": "sha512-O8OqVca/i3pZWlBT5bnNJ55SYhTOsdwlXu0ZUi514ZrzbQKhUpz0ux1sh++1caEd2rtGP2OXCTwjj/0DfNi1+w=="
+    },
     "react-hot-loader": {
       "version": "4.8.0",
       "resolved": "http://registry.npm.taobao.org/react-hot-loader/download/react-hot-loader-4.8.0.tgz",
@@ -9464,7 +9469,7 @@
     },
     "redent": {
       "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
+      "resolved": "http://registry.npm.taobao.org/redent/download/redent-1.0.0.tgz",
       "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
       "dev": true,
       "requires": {
@@ -9604,7 +9609,7 @@
     },
     "remove-trailing-separator": {
       "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+      "resolved": "http://registry.npm.taobao.org/remove-trailing-separator/download/remove-trailing-separator-1.1.0.tgz",
       "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
       "dev": true
     },
@@ -9723,7 +9728,7 @@
     },
     "requires-port": {
       "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+      "resolved": "http://registry.npm.taobao.org/requires-port/download/requires-port-1.0.0.tgz",
       "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
       "dev": true
     },
@@ -9743,7 +9748,7 @@
     },
     "resolve-cwd": {
       "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz",
+      "resolved": "http://registry.npm.taobao.org/resolve-cwd/download/resolve-cwd-2.0.0.tgz",
       "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=",
       "dev": true,
       "requires": {
@@ -9752,7 +9757,7 @@
       "dependencies": {
         "resolve-from": {
           "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
+          "resolved": "http://registry.npm.taobao.org/resolve-from/download/resolve-from-3.0.0.tgz",
           "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
           "dev": true
         }
@@ -10075,7 +10080,7 @@
     },
     "select-hose": {
       "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
+      "resolved": "http://registry.npm.taobao.org/select-hose/download/select-hose-2.0.0.tgz",
       "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=",
       "dev": true
     },
@@ -10125,7 +10130,7 @@
     },
     "serve-index": {
       "version": "1.9.1",
-      "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
+      "resolved": "http://registry.npm.taobao.org/serve-index/download/serve-index-1.9.1.tgz",
       "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=",
       "dev": true,
       "requires": {
@@ -10170,7 +10175,7 @@
       "dependencies": {
         "extend-shallow": {
           "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "resolved": "http://registry.npm.taobao.org/extend-shallow/download/extend-shallow-2.0.1.tgz",
           "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
           "dev": true,
           "requires": {
@@ -10751,7 +10756,7 @@
     },
     "strip-indent": {
       "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
+      "resolved": "http://registry.npm.taobao.org/strip-indent/download/strip-indent-1.0.1.tgz",
       "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
       "dev": true,
       "requires": {
@@ -11088,7 +11093,7 @@
     },
     "trim-newlines": {
       "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
+      "resolved": "http://registry.npm.taobao.org/trim-newlines/download/trim-newlines-1.0.0.tgz",
       "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
       "dev": true
     },
@@ -11659,13 +11664,13 @@
       "dependencies": {
         "camelcase": {
           "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
+          "resolved": "http://registry.npm.taobao.org/camelcase/download/camelcase-3.0.0.tgz",
           "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
           "dev": true
         },
         "cliui": {
           "version": "3.2.0",
-          "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
+          "resolved": "http://registry.npm.taobao.org/cliui/download/cliui-3.2.0.tgz",
           "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
           "dev": true,
           "requires": {
@@ -11685,13 +11690,13 @@
         },
         "has-flag": {
           "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+          "resolved": "http://registry.npm.taobao.org/has-flag/download/has-flag-3.0.0.tgz",
           "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
           "dev": true
         },
         "load-json-file": {
           "version": "1.1.0",
-          "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+          "resolved": "http://registry.npm.taobao.org/load-json-file/download/load-json-file-1.1.0.tgz",
           "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
           "dev": true,
           "requires": {
@@ -11710,7 +11715,7 @@
         },
         "os-locale": {
           "version": "1.4.0",
-          "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
+          "resolved": "http://registry.npm.taobao.org/os-locale/download/os-locale-1.4.0.tgz",
           "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
           "dev": true,
           "requires": {
@@ -11719,7 +11724,7 @@
         },
         "path-type": {
           "version": "1.1.0",
-          "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+          "resolved": "http://registry.npm.taobao.org/path-type/download/path-type-1.1.0.tgz",
           "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
           "dev": true,
           "requires": {
@@ -11730,7 +11735,7 @@
         },
         "read-pkg": {
           "version": "1.1.0",
-          "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
+          "resolved": "http://registry.npm.taobao.org/read-pkg/download/read-pkg-1.1.0.tgz",
           "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
           "dev": true,
           "requires": {
@@ -11741,7 +11746,7 @@
         },
         "read-pkg-up": {
           "version": "1.0.1",
-          "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
+          "resolved": "http://registry.npm.taobao.org/read-pkg-up/download/read-pkg-up-1.0.1.tgz",
           "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
           "dev": true,
           "requires": {
@@ -11751,7 +11756,7 @@
         },
         "strip-bom": {
           "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+          "resolved": "http://registry.npm.taobao.org/strip-bom/download/strip-bom-2.0.0.tgz",
           "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
           "dev": true,
           "requires": {
@@ -11769,13 +11774,13 @@
         },
         "which-module": {
           "version": "1.0.0",
-          "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
+          "resolved": "http://registry.npm.taobao.org/which-module/download/which-module-1.0.0.tgz",
           "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=",
           "dev": true
         },
         "yargs": {
           "version": "6.6.0",
-          "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz",
+          "resolved": "http://registry.npm.taobao.org/yargs/download/yargs-6.6.0.tgz",
           "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=",
           "dev": true,
           "requires": {
@@ -11796,7 +11801,7 @@
         },
         "yargs-parser": {
           "version": "4.2.1",
-          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz",
+          "resolved": "http://registry.npm.taobao.org/yargs-parser/download/yargs-parser-4.2.1.tgz",
           "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=",
           "dev": true,
           "requires": {

+ 1 - 0
front/package.json

@@ -82,6 +82,7 @@
     "react-addons-css-transition-group": "^15.6.2",
     "react-contextmenu": "^2.10",
     "react-dom": "^16.6.3",
+    "react-fullscreen-crossbrowser": "^1.0.9",
     "react-quill": "^1.3.3",
     "react-redux": "^4.4.5",
     "react-router-dom": "^4.3.1",

+ 2 - 2
front/project/Constant.js

@@ -2,13 +2,13 @@ export const UserUrl = 'http://www.baidu.com/';
 
 export const QuestionDifficult = [{ label: 'easy', value: 'easy' }, { label: 'medium', value: 'medium' }, { label: 'hard', value: 'hard' }];
 
-export const QuestionType = [{ label: 'SC/语法', value: 'sc', long: 'Sentence Correnction' }, { label: 'RC/阅读', value: 'rc', long: '' }, { label: 'CR/逻辑', value: 'cr' }, { label: 'PS/数学', value: 'ps' }, { label: 'DS/数学', value: 'ds' }, { label: 'IR/综合推理', value: 'ir' }, { label: 'AWA/作文', value: 'awa' }];
+export const QuestionType = [{ label: 'SC/语法', value: 'sc', long: 'Sentence Correnction' }, { label: 'RC/阅读', value: 'rc', long: '' }, { label: 'CR/逻辑', value: 'cr' }, { label: 'PS/数学', value: 'ps' }, { label: 'DS/数学', value: 'ds' }, { label: 'IR/综合推理', value: 'ir' }, { label: 'AWA/作文', value: 'awa', long: 'Analytical Writing Assessment' }];
 
 export const TextbookType = [{ label: 'PS/数学', value: 'ps' }, { label: 'DS/数学', value: 'ds' }];
 
 export const MoneyRange = [{ label: '0', value: 0 }, { label: '1-1000', value: 1 }, { label: '1000-5000', value: 2 }, { label: '5000-10000', value: 3 }, { label: '10000以上', value: 4 }];
 
-export const AskTarget = [{ label: '题目', value: 'question' }, { label: '官方', value: 'official' }, { label: '千行解析', value: 'qx' }, { label: '题源联想', value: 'association' }, { label: '相关问答', value: 'qa' }];
+export const AskTarget = [{ label: '题目', value: 'question', title: '题目', key: 'question' }, { label: '官方解析', value: 'official', title: '官方解析', key: 'official' }, { label: '千行解析', value: 'qx', title: '千行解析', key: 'qx' }, { label: '题源联想', value: 'association', title: '题源联想', key: 'association' }, { label: '相关问答', value: 'qa', title: '相关问答', key: 'qa' }];
 
 export const PreviewStatus = [{ label: '全部', value: 0 }, { label: '未开始', value: 1 }, { label: '进行中', value: 2 }, { label: '已结束', value: 3 }];
 

+ 1 - 0
front/project/admin/local.json

@@ -1,5 +1,6 @@
 {
   "development": {
+    "serverPort": 2999,
     "scripts": [],
     "proxy": [
       {

+ 3 - 0
front/project/admin/routes/setting/struct/page.js

@@ -179,6 +179,9 @@ export default class extends Page {
   }
 
   refresh(tab) {
+    if (!tab) {
+      ({ tab } = this.state);
+    }
     if (tab === 'exercise') {
       return this.refreshExercise();
     }

+ 35 - 31
front/project/admin/routes/setting/time/page.js

@@ -19,12 +19,12 @@ export default class extends Page {
       title: '学科',
       dataIndex: 'title',
     }].concat(QuestionDifficult.map(row => {
-      const { exercise = {} } = this.state;
       return {
         title: row.label,
         dataIndex: row.value,
         render: (text, result) => {
-          return <EditTableCell value={(exercise[result.id] || {})[text] || 0} onChange={(v) => {
+          const { exercise = {} } = this.state;
+          return <EditTableCell value={(exercise[result.id] || {})[row.value] || 0} onChange={(v) => {
             this.changeMapValue('exercise', result.id, row.value, v);
           }} />;
         },
@@ -39,18 +39,20 @@ export default class extends Page {
       dataIndex: 'number',
       render: (text, result) => {
         const { examination = {} } = this.state;
-        return <EditTableCell value={(examination[result.extend] || {}).number || 0} onChange={(v) => {
-          this.changeMapValue('examination', result.extend, 'number', v);
-        }} />;
+        return (examination[result.extend] || {}).number || 0;
+        // return <EditTableCell value={(examination[result.extend] || {}).number || 0} onChange={(v) => {
+        //   this.changeMapValue('examination', result.extend, 'number', v);
+        // }} />;
       },
     }, {
       title: '做题时间',
       dataIndex: 'time',
       render: (text, result) => {
         const { examination = {} } = this.state;
-        return <EditTableCell value={(examination[result.extend] || {}).time || 0} onChange={(v) => {
-          this.changeMapValue('examination', result.extend, 'time', v);
-        }} />;
+        return Number((examination[result.extend] || {}).time || 0) / 60;
+        // return <EditTableCell value={(examination[result.extend] || {}).time || 0} onChange={(v) => {
+        //   this.changeMapValue('examination', result.extend, 'time', v);
+        // }} />;
       },
     }];
     this.state.tab = 'exercise';
@@ -84,7 +86,7 @@ export default class extends Page {
 
   refreshExercise() {
     return System.getExerciseTime().then((result) => {
-      this.setState({ exercise: result || {} });
+      this.setState({ exercise: result });
     });
   }
 
@@ -108,7 +110,7 @@ export default class extends Page {
 
   refreshExamination() {
     return System.getExaminationTime().then((result) => {
-      this.setState({ examination: result || {} });
+      this.setState({ examination: result });
     });
   }
 
@@ -221,12 +223,12 @@ export default class extends Page {
   }
 
   renderAll() {
+    const { score = {} } = this.state;
     return <Form>
       <Row>
         <Col span={8} offset={16}>
-          <Form.Item labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} label='是否启动全站数据'>
-            <Switch checked={this.state.score.all} checkedChildren='启用' unCheckedChildren='未启用' onChange={(value) => {
-              const { score } = this.state;
+          <Form.Item labelCol={{ span: 12 }} wrapperCol={{ span: 10 }} label='是否启动全站数据'>
+            <Switch checked={score.all} checkedChildren='启用' unCheckedChildren='未启用' onChange={(value) => {
               score.all = value;
               this.setState({ score });
               this.submitScore();
@@ -326,29 +328,31 @@ export default class extends Page {
 
   renderView() {
     const { tab } = this.state;
-    return <Block><Tabs activeKey={tab} onChange={(value) => {
-      this.setState({ tab: value, selectedKeys: [], checkedKeys: [] });
-      this.refresh(value);
-    }}>
-      <Tabs.TabPane tab="预估做题时间" key="exercise">
-        {this.renderAll()}
-        {this.renderExercise()}
-        {this.renderOther()}
-      </Tabs.TabPane>
-      <Tabs.TabPane tab="数据剔除时间" key="filter">
-        {this.renderFilterTime()}
-      </Tabs.TabPane>
-      <Tabs.TabPane tab="预估考试时间" key="examination">
-        {this.renderExamination()}
-      </Tabs.TabPane>
-    </Tabs>
-      <Row type="flex" justify="center">
+    return <Block>
+      <Tabs activeKey={tab} onChange={(value) => {
+        this.setState({ tab: value, selectedKeys: [], checkedKeys: [] });
+        this.refresh(value);
+      }}>
+        <Tabs.TabPane tab="预估做题时间" key="exercise">
+          {this.renderAll()}
+          {this.renderExercise()}
+          {this.renderOther()}
+        </Tabs.TabPane>
+        <Tabs.TabPane tab="数据剔除时间" key="filter">
+          {this.renderFilterTime()}
+        </Tabs.TabPane>
+        <Tabs.TabPane tab="预估考试时间" key="examination">
+          {this.renderExamination()}
+        </Tabs.TabPane>
+      </Tabs>
+      {tab !== 'examination' && <Row type="flex" justify="center">
         <Col>
           <Button type="primary" onClick={() => {
             this.submit(tab);
           }}>保存</Button>
         </Col>
-      </Row>
+      </Row>}
+
     </Block>;
   }
 }

+ 201 - 1
front/project/admin/routes/subject/examination/page.js

@@ -1,10 +1,210 @@
 import React from 'react';
+import { Button } from 'antd';
+import { Link } from 'react-router-dom';
 import './index.less';
 import Page from '@src/containers/Page';
 import Block from '@src/components/Block';
+import FilterLayout from '@src/layouts/FilterLayout';
+import ActionLayout from '@src/layouts/ActionLayout';
+import TableLayout from '@src/layouts/TableLayout';
+import { getMap, formatTreeData, bindSearch, formatDate, formatPercent } from '@src/services/Tools';
+import { asyncSMessage, asyncDelConfirm } from '@src/services/AsyncTools';
+import { QuestionType, QuestionDifficult } from '../../../../Constant';
+import { Question } from '../../../stores/question';
+import { Examination } from '../../../stores/examination';
 
+const QuestionTypeMap = getMap(QuestionType, 'value', 'label');
+
+const filterForm = [
+  {
+    key: 'structId',
+    type: 'tree',
+    allowClear: true,
+    name: '出题原理',
+    select: [],
+    placeholder: '请选择',
+    number: true,
+  },
+  {
+    key: 'paperId',
+    type: 'select',
+    allowClear: true,
+    name: '练习册',
+    select: [],
+    placeholder: '请选择',
+    number: true,
+  },
+  {
+    key: 'difficult',
+    type: 'select',
+    allowClear: true,
+    name: '难度',
+    select: QuestionDifficult,
+    placeholder: '请选择',
+    number: true,
+  },
+  {
+    key: 'questionType',
+    type: 'select',
+    allowClear: true,
+    name: '题型',
+    select: QuestionType,
+    placeholder: '请选择',
+    number: true,
+  },
+  {
+    key: 'questionNoId',
+    type: 'select',
+    allowClear: true,
+    name: '题目ID',
+    select: [],
+    number: true,
+    placeholder: '请输入',
+  },
+];
 export default class extends Page {
+  constructor(props) {
+    super(props);
+    this.actionList = [{
+      key: 'add',
+      name: '新建',
+      render: (item) => {
+        return <Button onClick={() => {
+          linkTo('/subject/question');
+        }}>{item.name}</Button>;
+      },
+    }, {
+      key: 'delete',
+      name: '批量删除',
+      needSelect: 1,
+    }];
+    this.categoryMap = {};
+    this.columns = [{
+      title: '出题原理',
+      dataIndex: 'first',
+      render: (text, record) => {
+        return this.categoryMap[record.questionNo.moduleStruct[0]] || text;
+      },
+    }, {
+      title: '类别',
+      dataIndex: 'second',
+      render: (text, record) => {
+        return QuestionTypeMap[record.question.questionType] || text;
+      },
+    }, {
+      title: '试卷',
+      dataIndex: 'three',
+      render: (text, record) => {
+        return QuestionTypeMap[record.question.questionType] || text;
+      },
+    }, {
+      title: '题型',
+      dataIndex: 'questionType',
+      render: (text, record) => {
+        return QuestionTypeMap[record.question.questionType] || text;
+      },
+    }, {
+      title: '难度',
+      dataIndex: 'difficlut',
+      render: (text, record) => {
+        return record.question.difficult;
+      },
+    }, {
+      title: '错误率',
+      dataIndex: 'correct',
+      render: (text, record) => {
+        return formatPercent(record.questionNo.totalNumber - record.questionNo.totalCorrect, record.questionNo.totalNumber, false);
+      },
+    }, {
+      title: '修改时间',
+      dataIndex: 'updateTime',
+      render: (text, record) => {
+        return formatDate(record.question.updateTime);
+      },
+    }, {
+      title: '操作',
+      dataIndex: 'handler',
+      render: (text, record) => {
+        return <div className="table-button">
+          {(
+            <Link to={`/subject/question/${record.question_id}`}>编辑</Link>
+          )}
+        </div>;
+      },
+    }];
+  }
+
+  init() {
+    Examination.allStruct().then(result => {
+      const list = result.filter(row => row.level < 3).map(row => { row.title = row.titleZh; row.value = row.id; return row; });
+      filterForm[0].tree = formatTreeData(list, 'id', 'title', 'parentId');
+      this.categoryMap = getMap(result.map(row => { row.title = row.titleZh; row.value = row.id; return row; }), 'id', 'title');
+      this.setState({ examination: result });
+    });
+    bindSearch(filterForm, 'paperId', this, (search) => {
+      return Examination.listPaper(search);
+    }, (row) => {
+      return {
+        title: row.title,
+        value: row.id,
+      };
+    }, this.state.search.paperId ? Number(this.state.search.paperId) : null, null);
+    bindSearch(filterForm, 'questionNoId', this, (search) => {
+      return Question.searchNo(search);
+    }, (row) => {
+      return {
+        title: row.title,
+        value: row.id,
+      };
+    }, this.state.search.questionNoId ? Number(this.state.search.questionNoId) : null, null);
+  }
+
+  initData() {
+    const { search } = this.state;
+    const data = Object.assign({}, search);
+    Examination.listQuestion(data).then(result => {
+      this.setTableData(result.list, result.total);
+    });
+  }
+
+  deleteAction() {
+    const { selectedRows } = this.state;
+    asyncDelConfirm('删除确认', '是否删除选中题目?', () => {
+      return Promise.all(selectedRows.map(row => Question.del({ id: row.question_id }))).then(() => {
+        asyncSMessage('删除成功!');
+        this.refresh();
+      });
+    });
+  }
+
   renderView() {
-    return <Block flex />;
+    const { examination } = this.state;
+    return <Block flex>
+      {examination && <FilterLayout
+        show
+        itemList={filterForm}
+        data={this.state.search}
+        onChange={data => {
+          // if (data.time.length > 0) {
+          //   data.time = [data.time[0].format('YYYY-MM-DD HH:mm:ss'), data.time[1].format('YYYY-MM-DD HH:mm:ss')];
+          // }
+          this.search(data);
+        }} />}
+      <ActionLayout
+        itemList={this.actionList}
+        selectedKeys={this.state.selectedKeys}
+        onAction={key => this.onAction(key)}
+      />
+      <TableLayout
+        select
+        columns={this.columns}
+        list={this.state.list}
+        pagination={this.state.page}
+        loading={this.props.core.loading}
+        onChange={(pagination, filters, sorter) => this.tableChange(pagination, filters, sorter)}
+        onSelect={(keys, rows) => this.tableSelect(keys, rows)}
+        selectedKeys={this.state.selectedKeys}
+      />
+    </Block>;
   }
 }

+ 8 - 7
front/project/admin/routes/subject/exercise/page.js

@@ -7,7 +7,7 @@ import Block from '@src/components/Block';
 import FilterLayout from '@src/layouts/FilterLayout';
 import ActionLayout from '@src/layouts/ActionLayout';
 import TableLayout from '@src/layouts/TableLayout';
-import { getMap, formatTreeData, bindSearch, formatDate } from '@src/services/Tools';
+import { getMap, formatTreeData, bindSearch, formatDate, formatSeconds, formatPercent } from '@src/services/Tools';
 import { asyncSMessage, asyncDelConfirm, asyncGet } from '@src/services/AsyncTools';
 import { QuestionType, QuestionDifficult } from '../../../../Constant';
 import { Exercise } from '../../../stores/exercise';
@@ -115,7 +115,7 @@ export default class extends Page {
       title: '练习册',
       dataIndex: 'paper',
       render: (text) => {
-        return text.title;
+        return (text || {}).title;
       },
     }, {
       title: '考点',
@@ -133,13 +133,13 @@ export default class extends Page {
       title: '易错度',
       dataIndex: 'correct',
       render: (text, record) => {
-        return `${record.questionNo.totalCorrect * 100 / record.questionNo.totalNumber}%`;
+        return formatPercent(record.questionNo.totalNumber - record.questionNo.totalCorrect, record.questionNo.totalNumber, false);
       },
     }, {
       title: '平均时间',
       dataIndex: 'time',
       render: (text, record) => {
-        return `${record.questionNo.totalTime / record.questionNo.totalNumber}s`;
+        return formatSeconds(record.questionNo.totalTime / record.questionNo.totalNumber);
       },
     }, {
       title: '序号',
@@ -206,7 +206,7 @@ export default class extends Page {
 
   init() {
     Exercise.allStruct().then(result => {
-      const list = result.filter(row => row.level > 2).map(row => { row.title = `${row.titleZh}/${row.titleEn}`; row.value = row.id; return row; });
+      const list = result.filter(row => row.level >= 2).map(row => { row.title = `${row.titleZh}/${row.titleEn}`; row.value = row.id; return row; });
       filterForm[1].tree = formatTreeData(list, 'id', 'title', 'parentId');
       this.categoryMap = getMap(result.map(row => { row.title = `${row.titleZh}/${row.titleEn}`; row.value = row.id; return row; }), 'id', 'title');
       this.setState({ exercise: result });
@@ -266,8 +266,9 @@ export default class extends Page {
   }
 
   renderView() {
+    const { exercise } = this.state;
     return <Block flex>
-      <FilterLayout
+      {exercise && <FilterLayout
         show
         itemList={filterForm}
         data={this.state.search}
@@ -276,7 +277,7 @@ export default class extends Page {
             data.time = [data.time[0].format('YYYY-MM-DD HH:mm:ss'), data.time[1].format('YYYY-MM-DD HH:mm:ss')];
           }
           this.search(data);
-        }} />
+        }} />}
       <ActionLayout
         itemList={this.actionList}
         selectedKeys={this.state.selectedKeys}

+ 1 - 2
front/project/admin/routes/subject/question/page.js

@@ -54,7 +54,7 @@ export default class extends Page {
         return result;
       });
     } else {
-      handler = Promise.resolve({ content: { number: 1, type: 'single', typeset: 'one', questions: [], steps: [] } });
+      handler = Promise.resolve({ content: { number: 1, type: 'single', typeset: 'one', questions: [], steps: [], table: {} } });
     }
     handler.then(result => {
       this.uuid[0] = -1;
@@ -80,7 +80,6 @@ export default class extends Page {
       });
       const { row, col } = result.content.table;
       for (let i = 0; i < col; i += 1) {
-        console.log(`content.table.header[${i}]`);
         form.getFieldDecorator(`content.table.header[${i}]`);
         for (let j = 0; j < row; j += 1) {
           console.log(`content.table.data[${j}][${i}]`);

+ 1 - 1
front/project/admin/routes/subject/sentence/index.js

@@ -4,7 +4,7 @@ import group from '../group';
 export default {
   path: '/subject/sentence',
   key: 'subject-sentence',
-  title: '长难句教材',
+  title: '长难句',
   needLogin: true,
   module,
   group,

+ 25 - 1
front/project/admin/routes/subject/sentence/page.js

@@ -12,7 +12,9 @@ import { getMap } from '@src/services/Tools';
 import { asyncSMessage, asyncDelConfirm } from '@src/services/AsyncTools';
 import { Sentence } from '../../../stores/sentence';
 import { Slient } from '../../../stores/slient';
+import { SwitchSelect } from '../../../../Constant';
 
+const SwitchSelectMap = getMap(SwitchSelect, 'value', 'label');
 const filterForm = [
   {
     key: 'chapter',
@@ -22,6 +24,15 @@ const filterForm = [
     placeholder: '请选择',
     number: true,
   },
+  {
+    key: 'isTrail',
+    type: 'select',
+    name: '试用',
+    allowClear: true,
+    placeholder: '请选择',
+    number: true,
+    select: SwitchSelect,
+  },
   // {
   //   key: 'part',
   //   type: 'select',
@@ -71,9 +82,22 @@ export default class extends Page {
       title: 'part',
       dataIndex: 'part',
     }, {
-      title: 'title',
+      title: '名称',
       dataIndex: 'title',
     }, {
+      title: '试用',
+      dataIndex: 'isTrail',
+      render: (text) => {
+        return SwitchSelectMap[text ? 1 : 0] || '';
+      },
+    }, {
+      title: '练习',
+      dataIndex: 'exercise',
+      render: () => {
+        const item = this.structMap[this.state.search.chapter];
+        return SwitchSelectMap[item.exercise ? 1 : 0] || '';
+      },
+    }, {
       title: '操作',
       dataIndex: 'handler',
       render: (text, record) => {

+ 3 - 3
front/project/admin/routes/subject/sentenceArticle/page.js

@@ -76,7 +76,7 @@ export default class extends Page {
         handler = Sentence.listArticle({ chapter: 0 })
           .then(result => {
             if (result.total > 0) {
-              return result[0];
+              return result.list[0];
             }
             return { part: 1, chapter: 0, title: '前言' };
           });
@@ -108,7 +108,7 @@ export default class extends Page {
         data.isTrail = data.isTrail ? 1 : 0;
         data.pages = this.computePages();
         let handler;
-        if (!data.id) {
+        if (data.id) {
           handler = Sentence.editArticle(data);
         } else {
           handler = Sentence.addArticle(data);
@@ -136,7 +136,7 @@ export default class extends Page {
       <Form>
         {getFieldDecorator('id')(<input hidden />)}
         {getFieldDecorator('chapter')(<input hidden />)}
-        {getFieldDecorator('pater')(<input hidden />)}
+        {getFieldDecorator('part')(<input hidden />)}
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='前言名称'>
           {getFieldDecorator('title')(
             <Input placeholder='请输入名称' />,

+ 4 - 1
front/project/admin/routes/user/askDetail/page.js

@@ -97,7 +97,7 @@ export default class extends Page {
 
   renderAsk() {
     const { data } = this.state;
-    const { user = {}, createTime, target, content } = data;
+    const { user = {}, createTime, target, originContent, content } = data;
     return <Block>
       <h1>提问信息</h1>
       <Form>
@@ -110,6 +110,9 @@ export default class extends Page {
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='提问模块'>
           {AskTargetMap[target]}
         </Form.Item>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='提问内容'>
+          {originContent}
+        </Form.Item>
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='提问详情'>
           {content}
         </Form.Item>

+ 8 - 0
front/project/admin/stores/examination.js

@@ -16,6 +16,14 @@ export default class ExaminationStore extends BaseStore {
   delStruct(params) {
     return this.apiDel('/examination/struct/delete', params);
   }
+
+  listPaper(params) {
+    return this.apiGet('/examination/paper/list', params);
+  }
+
+  listQuestion(params) {
+    return this.apiGet('/examination/question/list', params);
+  }
 }
 
 export const Examination = new ExaminationStore({ key: 'examination' });

+ 14 - 12
front/project/www/app.js

@@ -3,27 +3,29 @@ import { LocaleProvider } from 'antd';
 import zhCN from 'antd/lib/locale-provider/zh_CN';
 import './app.less';
 import Header from './components/Header';
+import { User } from './stores/user';
 
 export default class extends Component {
   constructor(props) {
     super(props);
     const state = { routes: [] };
     this.state = state;
+    // 初始化登录
+    User.token().then(() => {
+      this.setState({ show: true });
+    });
   }
 
   render() {
     const { children, project, config } = this.props;
-    return (
-      <LocaleProvider locale={zhCN}>
-        {config.hideHeader ? (
-          <div id="full-page">{children}</div>
-        ) : (
-          <div className={`${config.tab}`} id="page">
-            <Header tabs={project.tabs} active={config.tab} />
-            {children}
-          </div>
-        )}
-      </LocaleProvider>
-    );
+    const { show } = this.state;
+    return (show ? <LocaleProvider locale={zhCN}>
+      {config.hideHeader ? (
+        <div id="full-page">{children}</div>
+      ) : (<div className={`${config.tab}`} id="page">
+        <Header tabs={project.tabs} active={config.tab} />
+        {children}
+      </div>)}
+    </LocaleProvider> : null);
   }
 }

+ 1 - 0
front/project/www/app.less

@@ -250,6 +250,7 @@ body,
 
   #full-page {
     height: 100%;
+    background: #fff;
   }
 
   .content {

+ 4 - 0
front/project/www/assets/close.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none" viewBox="0 0 32 32">
+    <circle cx="16" cy="16" r="16" fill="#FDF7F7"/>
+    <path stroke="#F36565" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M11 21l10-10M11 11l10 10"/>
+</svg>

+ 13 - 4
front/project/www/assets/right.svg

@@ -1,4 +1,13 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none" viewBox="0 0 32 32">
-    <circle cx="16" cy="16" r="16" fill="#FDF7F7"/>
-    <path stroke="#F36565" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M11 21l10-10M11 11l10 10"/>
-</svg>
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 55.2 (78181) - https://sketchapp.com -->
+    <title>right</title>
+    <desc>Created with Sketch.</desc>
+    <g id="5-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="5-图标" transform="translate(-193.000000, -106.000000)" fill="#6EC64B">
+            <g id="right" transform="translate(193.000000, 106.000000)">
+                <path d="M3,11.3978271 C6.07356771,15.5225016 7.95532227,17.5848389 8.64526367,17.5848389 C9.33520508,17.5848389 12.6957194,13.3898926 18.7268066,5 L14.2478027,5.35632324 C10.5932583,11.7386475 8.72574527,14.9298096 8.64526367,14.9298096 C8.46516927,14.9298096 7.50512695,13.4373658 5.76513672,10.4524782 L3,11.3978271 Z" id="路径-4"></path>
+            </g>
+        </g>
+    </g>
+</svg>

+ 23 - 5
front/project/www/components/AnswerCheckbox/index.js

@@ -3,14 +3,32 @@ import './index.less';
 import CheckboxItem from '../CheckboxItem';
 
 function AnswerCheckbox(props) {
-  const { selected, answer, show, list = [] } = props;
+  const { selected = [], answer = [], show, list = [], onChange } = props;
+  // const trueList = [];
+  const falseList = [];
+  const map = {};
+  answer.forEach(row => {
+    map[row] = true;
+  });
+  selected.forEach(row => {
+    if (!map[row]) falseList.push(row);
+  });
+
   return (
     <div className="answer-checkbox">
-      {list.map((item, index) => {
+      {list.map((item) => {
         return (
-          <div className={`item ${index === answer ? 'true' : 'false'} ${show ? 'show' : ''}`}>
-            <CheckboxItem checked={index === selected} />
-            <div className="text">{item.text}</div>
+          <div className={`item ${answer.indexOf(item.value) >= 0 ? 'true' : ''} ${falseList.indexOf(item.value) >= 0 ? 'false' : ''} ${show ? 'show' : ''}`} onClick={() => {
+            const index = selected.indexOf(item.value);
+            if (index >= 0) {
+              selected.splice(index, 1);
+            } else {
+              selected.push(item.value);
+            }
+            if (onChange) onChange(selected);
+          }}>
+            <CheckboxItem checked={selected.indexOf(item.value) >= 0} />
+            <div className="text">{item.label}</div>
             <div className="icon" />
           </div>
         );

+ 1 - 1
front/project/www/components/Continue/index.js

@@ -38,7 +38,7 @@ function Continue(props) {
           </div>
         </div>
         <div className="right">
-          <Assets name="right" svg onClick={() => {
+          <Assets name="close" svg onClick={() => {
             onClose();
           }} />
         </div>

+ 19 - 4
front/project/www/components/HardInput/index.js

@@ -3,12 +3,27 @@ import './index.less';
 
 export default class HardInput extends Component {
   render() {
-    const { selected, answer, show, list = [], otherList = [] } = this.props;
+    const { focus, answer = [], show, list = [], correct, onClick, onDelete } = this.props;
+    const a = answer.length > 0 ? answer[0] : [];
+    const otherList = [];
+    if (show) {
+      const map = {};
+      list.forEach((row) => {
+        map[row.uuid] = row;
+      });
+      a.forEach((row) => {
+        if (!map[row.uuid]) otherList.push(row);
+      });
+    }
     return (
-      <div className={`hard-input ${selected ? 'selected' : ''}`}>
-        {list.map((item, index) => {
+      <div className={`hard-input ${focus ? 'focus' : ''}`} onClick={() => {
+        if (onClick) onClick();
+      }}>
+        {list.map((item) => {
           return (
-            <div className={`item ${index === answer ? 'true' : 'false'} ${show ? 'show' : ''}`}>
+            <div className={`item ${a.indexOf(item.uuid) >= 0 || correct ? 'true' : 'false'} ${show ? 'show' : ''}`} onClick={() => {
+              if (onDelete) onDelete(item);
+            }}>
               <div className="text">{item.text}</div>
               <div className="icon" />
             </div>

+ 1 - 1
front/project/www/components/HardInput/index.less

@@ -72,7 +72,7 @@
   }
 }
 
-.hard-input.selected {
+.hard-input.focus {
   background: rgba(244, 249, 254, 1);
   border-left: 4px solid rgba(66, 146, 240, 1);
 }

+ 2 - 2
front/project/www/components/Icon/index.js

@@ -2,8 +2,8 @@ import React from 'react';
 import './index.less';
 
 function Icon(props) {
-  const { active, name, onClick } = props;
-  return <div className={`icon ${name} ${active ? 'active' : ''}`} onClick={() => onClick && onClick()} />;
+  const { active, name, onClick, children } = props;
+  return <div className={`icon ${name} ${active ? 'active' : ''}`} onClick={() => onClick && onClick()}>{children}</div>;
 }
 Icon.propTypes = {};
 export default Icon;

+ 2 - 2
front/project/www/components/Icon/index.less

@@ -84,8 +84,8 @@
 }
 
 .icon.question {
-  width: 16px;
-  height: 16px;
+  width: 18px;
+  height: 18px;
   background: url('/assets/header_question_normal.png') no-repeat center;
 }
 

+ 1 - 1
front/project/www/components/IconButton/index.js

@@ -8,7 +8,7 @@ function IconButton(props) {
   return (
     <Tooltip placement="top" title={tip}>
       <div className={`icon-button ${className}`} onClick={() => {
-        onClick();
+        if (onClick) onClick();
       }}>
         <Assets name={`ico_24_${type}`} svg />
       </div>

+ 3 - 3
front/project/www/components/List/index.js

@@ -9,7 +9,7 @@ function List(props) {
   return (
     <Module style={style} className="list">
       {title && <div className="header">
-        {position && <span className="title">{position}:</span>}
+        {position && <span className="title">{position}</span>}
         <span className="sub-title">{title}</span>
       </div>}
 
@@ -20,11 +20,11 @@ function List(props) {
               <div className="col part">{item.position}</div>
               <div className="col title">{item.title}</div>
               <div className="col pg">
-                <ProgressText progress={item.progress || 0} size="small" />
+                <ProgressText progress={item.progress || 0} times={item.times || 0} size="small" />
               </div>
               <div className="col action">
                 <IconButton type="view" tip="View" onClick={() => {
-                  onClick(item);
+                  if (onClick) onClick(item);
                 }} />
               </div>
             </div>

+ 1 - 1
front/project/www/components/List/index.less

@@ -57,7 +57,7 @@
         flex: 3;
 
         .progress-text {
-          width: 180px;
+          width: 240px;
           margin-top: 20px;
         }
       }

+ 2 - 2
front/project/www/components/ListTable/index.js

@@ -20,10 +20,10 @@ function ListTable(props) {
   const { style, position, title, filters = [], columns = [], data = [] } = props;
   return (
     <Module style={style} className="list-table">
-      <div hidden={!title} className="header">
+      {title && <div className="header">
         <span className="title">{position}</span>
         <span className="sub-title">{title}</span>
-      </div>
+      </div>}
       {filters.length > 0 && <div className="filter">
         <span className="text">筛选</span>
         <div className="filter-list">

+ 16 - 23
front/project/www/components/Panel/index.js

@@ -1,45 +1,38 @@
 import React from 'react';
+import { Tooltip } from 'antd';
 import './index.less';
 import Assets from '@src/components/Assets';
 import Module from '../Module';
 import ProgressButton from '../ProgressButton';
 
 function Panel(props) {
-  const { style, list = [], col = 3, title } = props;
+  const { style, message, data = {}, col = 3, title, onClick } = props;
   return (
     <Module style={style} className="panel">
       <div className="header">
-        {title}
-        <Assets name="QA" svg />
+        <span>{title}</span>
+        <Tooltip title={message} trigger='click'><Assets name="QA" svg /></Tooltip>
       </div>
       <div className="body">
         <div className="chart-info">
           <div className="chart" />
           <div className="info">
-            <div className="item">
-              <div className="title">已做</div>
-              <div className="data">
-                <span className="text">123</span>
-              </div>
-            </div>
-            <div className="item">
-              <div className="title">剩余</div>
-              <div className="data">
-                <span className="text">123</span>
-              </div>
-            </div>
-            <div className="item">
-              <div className="title">总计</div>
-              <div className="data">
-                <span className="text">123</span>
-              </div>
-            </div>
+            {(data.info || []).map(row => {
+              return <div className="item">
+                <div className="title">{row.title}</div>
+                <div className="data">
+                  <span className="text">{row.number}</span>{row.unit}
+                </div>
+              </div>;
+            })}
           </div>
         </div>
         <div className={`list col-${col}`}>
-          {list.map(item => {
+          {(data.children || []).map(item => {
             return (
-              <ProgressButton className="item" progress={item.progress}>
+              <ProgressButton className="item" progress={item.progress} onClick={() => {
+                if (onClick) onClick(item);
+              }}>
                 {item.title}
               </ProgressButton>
             );

+ 4 - 1
front/project/www/components/ProgressButton/index.js

@@ -2,11 +2,14 @@ import React from 'react';
 import './index.less';
 
 function ProgressButton(props) {
-  const { children, className = '', progress, width } = props;
+  const { children, className = '', progress, width, onClick } = props;
   return (
     <div
       style={{ width: width || '' }}
       className={`progress-button ${className} ${progress > 0 ? 'theme' : 'default'}`}
+      onClick={() => {
+        if (onClick) onClick();
+      }}
     >
       <div className="progress" style={{ width: `${progress}%` }} />
       {children}

+ 2 - 2
front/project/www/components/ProgressText/index.js

@@ -3,13 +3,13 @@ import './index.less';
 import Progress from '../Progress';
 
 function ProgressText(props) {
-  const { progress, size } = props;
+  const { progress, times, size } = props;
   return (
     <div className="progress-text">
       <div className="p">
         <Progress progress={progress} size={size} />
       </div>
-      <div className="t">{progress}%</div>
+      <div className="t">{times > 0 ? `${times}遍+` : ''}{progress}%</div>
     </div>
   );
 }

+ 3 - 3
front/project/www/components/ProgressText/index.less

@@ -2,16 +2,16 @@
 
 .progress-text {
   display: flex;
-  width: 160px;
   line-height: 24px;
 
   .p {
-    flex: 1;
+    width: 140px;
     margin-top: 8px;
   }
 
   .t {
-    width: 40px;
+    margin-left: 10px;
+    min-width: 40px;
     text-align: right;
   }
 }

+ 1 - 1
front/project/www/components/RadioButton/index.js

@@ -9,7 +9,7 @@ function RadioButton(props) {
       {list.map(item => {
         return (
           <Button theme={item.key === checked ? 'theme' : 'default'} size="small" radius onClick={() => {
-            onChange(item);
+            if (onChange) onChange(item);
           }}>
             {item.title}
           </Button>

+ 8 - 6
front/project/www/components/Select/index.js

@@ -22,7 +22,7 @@ export default class Select extends Component {
 
   render() {
     const { selecting } = this.state;
-    const { value, list = [], size = 'small', theme = 'theme', onChange } = this.props;
+    const { value, list = [], size = 'small', theme = 'theme', excludeSelf, onChange } = this.props;
     let index = 0;
     for (let i = 0; i < list.length; i += 1) {
       if (list[i].key === value) {
@@ -32,16 +32,18 @@ export default class Select extends Component {
     }
     const title = list.length > 0 ? list[index].title : '';
     return (
-      <div className="select">
+      <div className={`select ${theme || ''}`}>
         <div hidden={!selecting} className="mask" onClick={() => this.close()} />
-        <div className="select-warp">
+        <div className={`select-warp ${selecting ? 'active' : ''}`}>
           <Button size={size} theme={theme} radius onClick={() => this.open()}>
-            {title} <i className="right-arrow" />
+            {title} <i className={selecting ? 'up-arrow' : 'down-arrow'} />
           </Button>
           <div className={`select-body ${selecting ? 'select' : ''}`}>
-            {list.map(item => {
+            {list.map((item, i) => {
+              if (excludeSelf && index === i) return null;
               return <div className="select-option" onClick={() => {
-                onChange(item);
+                if (onChange) onChange(item);
+                this.close();
               }}>{item.title}</div>;
             })}
           </div>

+ 62 - 2
front/project/www/components/Select/index.less

@@ -62,14 +62,14 @@
     }
   }
 
-  .right-arrow {
+  .down-arrow {
     display: inline-block;
     position: relative;
     width: 16px;
     height: 16px;
   }
 
-  .right-arrow::after {
+  .down-arrow::after {
     display: inline-block;
     content: " ";
     height: 8px;
@@ -82,4 +82,64 @@
     top: 50%;
     margin-top: -1px;
   }
+
+  .up-arrow {
+    display: inline-block;
+    position: relative;
+    width: 16px;
+    height: 16px;
+  }
+
+  .up-arrow::after {
+    display: inline-block;
+    content: " ";
+    height: 8px;
+    width: 8px;
+    border-width: 2px 2px 0 0;
+    border-color: #fff;
+    border-style: solid;
+    transform: matrix(0.71, 0.71, -0.71, 0.71, 0, 0);
+    position: absolute;
+    top: 50%;
+    margin-top: -1px;
+  }
+
+  &.white {
+    cursor: pointer;
+    text-align: center;
+
+    .select-warp.active {
+      background: rgba(241, 243, 246, 1);
+    }
+
+    .button {
+      width: 98px;
+      padding: 0px;
+      border-radius: 0;
+      background: none;
+      color: #686872;
+      line-height: 30px;
+    }
+
+    .up-arrow {
+      display: none;
+    }
+
+    .down-arrow {
+      display: none;
+    }
+
+    color: #686872;
+
+    .select-body {
+      border: none;
+    }
+
+    .select-option {
+      padding: 0;
+      text-align: center;
+      line-height: 30px;
+      border: none;
+    }
+  }
 }

+ 3 - 3
front/project/www/components/Step/index.js

@@ -7,15 +7,15 @@ function Step(props) {
   return (
     <div className="step">
       {list.map((item, index) => {
-        const info = <div className={`item ${index === step - 1 ? 'active' : ''}`} onClick={() => {
-          if ((maxStep && index < maxStep) || !maxStep) onClick(index + 1);
+        const info = <div className={`item ${index === step - 1 ? 'active' : ''} ${maxStep && index >= maxStep ? 'trail' : ''}`} onClick={() => {
+          if ((maxStep && index < maxStep) || !maxStep) if (onClick) onClick(index + 1);
         }}>
           <span className="text">{item}</span>
         </div>;
         if ((maxStep && index < maxStep) || !maxStep) {
           return info;
         }
-        return <Tooltip title={message}>
+        return <Tooltip title={message} trigger='click'>
           {info}
         </Tooltip>;
       })}

+ 4 - 0
front/project/www/components/Step/index.less

@@ -15,6 +15,10 @@
     .text {
       margin-left: 5px;
     }
+
+    &.trail {
+      color: #ccc;
+    }
   }
 
   .item:before {

+ 4 - 2
front/project/www/components/Switch/index.js

@@ -3,9 +3,11 @@ import './index.less';
 import Assets from '@src/components/Assets';
 
 function Switch(props) {
-  const { checked, children } = props;
+  const { checked, children, onChange } = props;
   return (
-    <div className="switch">
+    <div className="switch" onClick={() => {
+      if (onChange) onChange(!checked);
+    }}>
       <Assets name={checked ? 'swich_on' : 'swich_off'} />
       {children}
     </div>

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

@@ -7,10 +7,11 @@ export default {
   },
   tabs: [
     { key: 'main', name: '首页', path: '/' },
-    { key: 'ready', name: 'GetReady', path: '/' },
-    { key: 'practise', name: '练习', path: '/exercise' },
-    { key: 'cat', name: 'CAT模考', path: '/examination' },
-    { key: 'item', name: '题库', path: '/' },
-    { key: 'machine', name: '换库机经', path: '/textbook' },
+    { key: 'ready', name: 'GetReady', path: '/ready' },
+    { key: 'exercise', name: '练习', path: '/exercise' },
+    { key: 'examination', name: 'CAT模考', path: '/examination' },
+    { key: 'questions', name: '题库', path: '/questions' },
+    { key: 'textbook', name: '换库机经', path: '/textbook' },
+    { key: 'course', name: '课堂', path: '/course' },
   ],
 };

+ 186 - 131
front/project/www/routes/exercise/list/page.js

@@ -2,8 +2,10 @@ 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';
@@ -15,111 +17,118 @@ const LOGIC_PLACE = 'place';
 const LOGIC_DIFFICULT = 'difficult';
 const LOGIC_ERROR = 'error';
 
-const columns = [
-  {
-    title: '练习册',
-    width: 250,
-    align: 'left',
-    render: item => {
-      return (
-        <div className="table-row">
-          <div className="night f-s-16">{item.title}</div>
-          <div>
-            <ProgressText
-              progress={item.report.id ? item.repport.userNumber / item.report.questionNumber : 0}
-              size="small"
-            />
-          </div>
-        </div>
-      );
-    },
-  },
-  {
-    title: '正确率',
-    width: 150,
-    align: 'left',
-    render: item => {
-      return (
-        <div className="table-row">
-          <div className="night f-s-16 f-w-b">--</div>
-          <div className="f-s-12">{item.stat.totalCorrect / item.stat.totalNumber}</div>
-        </div>
-      );
-    },
-  },
-  {
-    title: '全站用时',
-    width: 150,
-    align: 'left',
-    render: item => {
-      return (
-        <div className="table-row">
-          <div className="night f-s-16 f-w-b">--</div>
-          <div className="f-s-12">全站{item.stat.totalTime / item.stat.totalNumber}s</div>
-        </div>
-      );
-    },
-  },
-  {
-    title: '最近做题',
-    width: 150,
-    align: 'left',
-    render: () => {
-      return (
-        <div className="table-row">
-          <div>2019-04-28</div>
-          <div>07:30</div>
-        </div>
-      );
-    },
-  },
-  {
-    title: '操作',
-    width: 180,
-    align: 'left',
-    render: item => {
-      return (
-        <div className="table-row p-t-1">
-          {!item.repport.id && (
-            <IconButton type="start" tip="Start" onClick={() => this.previewAction('start', item)} />
-          )}
-          {item.repport.id && (
-            <IconButton
-              className="m-r-2"
-              type="continue"
-              tip="Continue"
-              onClick={() => this.previewAction('continue', item)}
-            />
-          )}
-          {item.repport.id && (
-            <IconButton type="restart" tip="Restart" onClick={() => this.previewAction('restart', item)} />
-          )}
-        </div>
-      );
-    },
-  },
-  {
-    title: '报告',
-    width: 30,
-    align: 'right',
-    render: item => {
-      return (
-        <div className="table-row p-t-1">
-          {item.report.userNumber === item.report.questionNumber && <IconButton type="report" tip="Report" />}
-        </div>
-      );
-    },
-  },
-];
-
 export default class extends Page {
   initState() {
-    this.columns = columns;
+    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: '',
-      tabs: [{
+      logics: [{
         key: LOGIC_NO,
         title: '按顺序练习',
       }, {
@@ -139,18 +148,21 @@ export default class extends Page {
     const { id } = this.params;
     Main.getExerciseParent(id).then(result => {
       const navs = result;
+      this.inited = true;
       this.setState({ navs });
     });
   }
 
   initData() {
-    this.refresh();
+    const data = Object.assign(this.state, this.state.search);
+    this.setState(data);
+    this.refreshData();
   }
 
-  refresh() {
+  refreshData(newLogic) {
     const { logic } = this.state;
     let handler = null;
-    switch (logic) {
+    switch (newLogic || logic) {
       case LOGIC_PLACE:
         handler = this.refreshPlace();
         break;
@@ -166,64 +178,107 @@ export default class extends Page {
   }
 
   refreshPlace() {
+    const { id } = this.params;
+    let handler;
     if (this.placeList.length > 0) {
-      this.setState({ logicExtend: this.placeList[0] });
-      return Promise.resolve();
+      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 });
+      });
     }
-    const { id } = this.params;
-    return Question.getExercisePlace(id).then(result => {
-      this.placeList = result;
-      this.setState({ logicExtend: this.placeList[0] });
+    return handler.then(() => {
+      let { logicExtend } = this.state;
+      if (logicExtend === '') {
+        logicExtend = this.placeList[0].key;
+        this.setState({ logicExtend });
+      }
     });
   }
 
   refreshDifficult() {
-    this.setState({ logicExtend: QuestionDifficult[0].value });
-    return Promise.resolve();
+    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() {
-    Question.getExerciseList(this.state.search)
+    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(level, tab) {
-    const state = {};
-    state[`level${level}Tab`] = tab;
-    this.setState(state);
-    this.refresh();
+  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.report.id).then(() => {
+      Question.restart(item.paper.id).then(() => {
         this.refresh();
       });
     });
   }
 
-  start(type, item) {
-    linkTo(`/paper/process/${type}/${item.id}`);
-  }
-
-  continue(type, item) {
-    linkTo(`/paper/process/${type}/${item.id}?r=${item.report.id}`);
-  }
-
   renderView() {
-    const { level1Tab = {}, level2Tab = {}, tabs } = this.state;
+    const { logic, logicExtend, logics = [], logicExtends = [], list } = this.state;
     return (
       <div>
         <div className="content">
           <Module className="m-t-2">
-            <Tabs type="card" active={level1Tab.key} tabs={tabs} onChange={key => {
-              this.onChangeTab(1, key);
-            }} />
-            {level1Tab.children > 1 && <Tabs active={level2Tab.key} tabs={level1Tab.children} onChange={key => this.onChangeTab(2, key)} />}
-
+            <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>
     );

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

@@ -27,6 +27,11 @@
         border-top-right-radius: 22px;
         border-bottom-right-radius: 22px;
       }
+
+      .error {
+        color: #ff7554;
+        text-align: left;
+      }
     }
 
     .tip {
@@ -40,6 +45,7 @@
     }
   }
 
+
   .work-body {
     .work-nav {
       margin-bottom: 20px;
@@ -65,4 +71,67 @@
       margin: 3px;
     }
   }
+}
+
+.code-module-modal {
+  .title {
+    height: 38px;
+    line-height: 38px;
+    font-size: 18px;
+    border-bottom: 1px solid #fff;
+    font-weight: bold;
+  }
+
+  .desc {
+    text-align: center;
+    margin-top: 20px;
+    margin-bottom: 20px;
+    height: 100px;
+
+    .input {
+      width: 100%;
+      background: #fff;
+
+      input {
+        background: #f7f7f7;
+        border: none;
+      }
+    }
+
+    .tip {
+      .left {
+        float: left;
+      }
+
+      .right {
+        float: right;
+      }
+    }
+
+    .error {
+      color: #ff7554;
+      text-align: left;
+      width: 50%;
+      float: left;
+    }
+  }
+
+  .btn-list {
+    text-align: center;
+    margin-bottom: 15px;
+
+    .btn {
+      display: inline-block;
+      width: 70px;
+      line-height: 35px;
+      height: 35px;
+      border: 1px solid #fff;
+      background: #006DAA;
+      cursor: pointer;
+    }
+
+    .btn:hover {
+      background: darken(#006DAA, 5);
+    }
+  }
 }

+ 155 - 70
front/project/www/routes/exercise/main/page.js

@@ -1,16 +1,19 @@
 import React from 'react';
 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, getMap, formatSeconds, formatDate } from '@src/services/Tools';
+import { formatTreeData, formatSeconds, formatDate, formatPercent } from '@src/services/Tools';
 import Continue from '../../../components/Continue';
 import Step from '../../../components/Step';
+import Panel from '../../../components/Panel';
 import List from '../../../components/List';
 import Tabs from '../../../components/Tabs';
 import Module from '../../../components/Module';
 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 ListTable from '../../../components/ListTable';
@@ -39,7 +42,7 @@ const exerciseColumns = [
           <div className="night f-s-16">{item.title}</div>
           <div>
             <ProgressText
-              progress={item.report.id ? item.repport.userNumber / item.report.questionNumber : 0}
+              progress={item.report.id ? formatPercent(item.repport.userNumber, item.report.questionNumber) : 0}
               size="small"
             />
           </div>
@@ -55,7 +58,7 @@ const exerciseColumns = [
       return (
         <div className="table-row">
           <div className="night f-s-16 f-w-b">--</div>
-          <div className="f-s-12">{item.stat.totalCorrect / item.stat.totalNumber}</div>
+          <div className="f-s-12">{formatPercent(item.stat.totalCorrect, item.stat.totalNumber, false)}</div>
         </div>
       );
     },
@@ -68,7 +71,7 @@ const exerciseColumns = [
       return (
         <div className="table-row">
           <div className="night f-s-16 f-w-b">--</div>
-          <div className="f-s-12">全站{item.stat.totalTime / item.stat.totalNumber}s</div>
+          <div className="f-s-12">全站{formatSeconds(item.stat.totalTime / item.stat.totalNumber)}</div>
         </div>
       );
     },
@@ -94,14 +97,14 @@ const exerciseColumns = [
       return (
         <div className="table-row p-t-1">
           {!item.report && (
-            <IconButton type="start" tip="Start" onClick={() => this.start('preview', item)} />
+            <IconButton type="start" tip="Start" onClick={() => Question.startLink('preview', item)} />
           )}
           {item.report.id && !item.report.isFinish && (
             <IconButton
               className="m-r-2"
               type="continue"
               tip="Continue"
-              onClick={() => this.continue('preview', item)}
+              onClick={() => Question.continueLink('preview', item)}
             />
           )}
           {item.report.id && (
@@ -118,7 +121,7 @@ const exerciseColumns = [
     render: item => {
       return (
         <div className="table-row p-t-1">
-          {item.report.isFinish && <IconButton type="report" tip="Report" onClick={() => this.viewReport(item)} />}
+          {item.report.isFinish && <IconButton type="report" tip="Report" onClick={() => Question.reportLink(item)} />}
         </div>
       );
     },
@@ -136,7 +139,7 @@ export default class extends Page {
         render: (record) => {
           let progress = 0;
           if (record.report) {
-            progress = record.report.userNumber * 100 / record.report.questionNumber;
+            progress = formatPercent(record.report.userNumber, record.report.questionNumber);
           }
           return (
             <div className="table-row">
@@ -155,12 +158,12 @@ export default class extends Page {
         render: (record) => {
           let correct = '--';
           if (record.report) {
-            correct = `${record.report.userCorrect * 100 / record.report.userNumber}%`;
+            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">全站{record.stat.totalCorrect * 100 / record.stat.totalNumber}%</div>
+              <div className="f-s-12">全站{formatPercent(record.stat.totalCorrect, record.stat.totalNumber, false)}</div>
             </div>
           );
         },
@@ -171,12 +174,12 @@ export default class extends Page {
         align: 'left',
         render: (record) => {
           let time = '--';
-          if (record.paper) {
-            time = record.paper.report.userTime / record.paper.report.userNumber;
+          if (record.report) {
+            time = formatSeconds(record.report.userTime / record.report.userNumber);
           }
           return (
             <div className="table-row">
-              <div className="night f-s-16 f-w-b">{formatSeconds(time)}</div>
+              <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>
           );
@@ -187,11 +190,12 @@ export default class extends Page {
         width: 150,
         align: 'left',
         render: (record) => {
-          if (!record.report) return null;
+          const time = record.report ? record.report.updateTime : record.paper ? record.paper.latestTime : null;
+          if (!time) 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>{formatDate(time, 'YYYY-MM-DD')}</div>
+              <div>{formatDate(time, 'HH:mm')}</div>
             </div>
           );
         },
@@ -204,29 +208,28 @@ export default class extends Page {
           return (
             <div className="table-row p-t-1">
               {!record.report && <IconButton type="start" tip="Start" onClick={() => {
-                this.start('sentence', record);
+                Question.startLink('sentence', record);
               }} />}
-              {!record.report.isFinish && <IconButton className="m-r-2" type="continue" tip="Continue" onClick={() => {
-                this.continue('sentence', record);
+              {(record.report && !record.report.isFinish) && <IconButton className="m-r-2" type="continue" tip="Continue" onClick={() => {
+                Question.continueLink('sentence', record);
               }} />}
-              <IconButton type="restart" tip="Restart" onClick={() => {
+              {(record.report && !!record.report.isFinish) && <IconButton type="restart" tip="Restart" onClick={() => {
                 this.restart(record);
-              }} />
+              }} />}
             </div>
           );
         },
       },
       {
         title: '报告',
-        dataIndex: 'report',
         width: 30,
         align: 'right',
-        render: (text, record) => {
+        render: (record) => {
           if (!record.report || !record.report.isFinish) return null;
           return (
             <div className="table-row p-t-1">
               <IconButton type="report" tip="Report" onClick={() => {
-                this.viewReport(record);
+                Question.reportLink(record);
               }} />
             </div>
           );
@@ -257,10 +260,9 @@ export default class extends Page {
         row.key = row.extend;
         return row;
       });
-      const map = getMap(list, 'key');
       const tabs = formatTreeData(list, 'id', 'title', 'parentId');
       tabs.push({ key: PREVIEW, name: '预习作业' });
-      this.setState({ tabs, map });
+      this.setState({ tabs });
       this.inited = true;
       this.refreshData();
     });
@@ -296,10 +298,10 @@ export default class extends Page {
   refreshSentence() {
     const { sentence } = this.state;
     if (!sentence) {
-      // User.clearSentenceTrail();
+      User.clearSentenceTrail();
       Sentence.getInfo().then(result => {
         // result.code = '123123';
-        result.trailPages = 20;
+        // result.trailPages = 20;
         this.setState({ sentence: result });
         return result;
       })
@@ -328,7 +330,7 @@ export default class extends Page {
               article.startPage = totalPage + 1;
               article.endPage = totalPage + article.pages;
               if (article.chapter) {
-                article.position = `${article.chapter}.${article.part}`;
+                article.position = `Part ${article.part}`;
               } else {
                 // 设置list中的样式
                 article.style = 'introduction';
@@ -345,12 +347,11 @@ export default class extends Page {
             });
 
             if (!code) {
-              chapterSteps.push(`「${index}」试用`);
+              chapterSteps.push('试用');
             }
             // 添加前言
             if (introduction) {
-              index += 1;
-              chapterSteps.push(`「${index}${introduction.title}`);
+              chapterSteps.push(`${introduction.title}`);
               chapterMap[0] = {
                 title: introduction.title,
                 value: 0,
@@ -403,26 +404,51 @@ export default class extends Page {
   }
 
   refreshExercise(tab) {
-    const { map, tab1 } = this.state;
+    const { tabs, tab1 } = this.state;
     let { tab2 } = this.state;
-    if (!map) {
+    if (!tabs) {
       // 等待数据加载
       return;
     }
-    const subject = map[tab];
+    const [subject] = tabs.filter(row => row.key === tab);
     // 切换tab1的情况
     if (tab2 === '' || tab1 !== tab) {
       tab2 = subject.children[0].key;
       this.setState({ tab2 });
     }
-    const type = map[tab2];
-    Main.getExerciseChildren(type.id, true).then(result => {
-      const exerciseChild = result;
-      this.setState({ exerciseChild });
-    });
-    Question.getExerciseProgress(type.id).then((r => {
-      const exerciseProgress = getMap(r, 'id');
-      this.setState({ exerciseProgress });
+    const [type] = subject.children.filter(row => row.key === tab2);
+    Question.getExerciseProgress(type.id).then((result => {
+      // const exerciseProgress = getMap(r, 'id');
+      result = result.map(row => {
+        row.info = [{
+          title: '已做',
+          number: row.userNumber || 0,
+          unit: '题',
+        }, {
+          title: '剩余',
+          number: row.questionNumber - row.userNumber || 0,
+          unit: '题',
+        }, {
+          title: '总计',
+          number: row.questionNumber || 0,
+          unit: '题',
+        }];
+        if (row.userStat) {
+          row.correct = formatPercent(row.userStat.userCorrect, row.userStat.userNumber, false);
+        } else {
+          row.correct = '--';
+        }
+        row.progress = formatPercent(row.questionNumber - row.userNumber || 0, row.questionNumber);
+        row.totalCorrect = formatPercent(row.stat.totalCorrect, row.stat.totalNumber, false);
+
+        row.children = row.children.map((r) => {
+          r.title = r.title || r.titleZh;
+          r.progress = formatPercent(r.userNumber, r.questionNumber);
+          return r;
+        });
+        return row;
+      });
+      this.setState({ exerciseProgress: result });
     }));
   }
 
@@ -432,21 +458,28 @@ export default class extends Page {
   }
 
   onChangeTab(level, tab) {
-    const { tab1, tab2 } = this.state;
+    const { tab1 } = this.state;
+    const data = {};
+    if (level > 1) {
+      data.tab1 = tab1;
+      data.tab2 = tab;
+    } else {
+      data.tab1 = tab;
+    }
     // this.refreshData(tab);
-    this.refreshQuery(Object.assign({ tab1, tab2 }, { [`tab${level}`]: tab }));
+    this.refreshQuery(data);
   }
 
   previewAction(type, item) {
     switch (type) {
       case 'start':
-        this.start('preview', item);
+        Question.startLink('preview', item);
         break;
       case 'restart':
         this.restart(item);
         break;
       case 'continue':
-        this.continue('preview', item);
+        Question.continueLink('preview', item);
         break;
       default:
         break;
@@ -455,22 +488,14 @@ export default class extends Page {
 
   restart(item) {
     asyncConfirm('提示', '是否重置', () => {
-      Question.restart(item.report.id).then(() => {
+      Question.restart(item.paper.id).then(() => {
         this.refresh();
       });
     });
   }
 
-  start(type, item) {
-    linkTo(`/paper/process/${type}/${item.id}`);
-  }
-
-  continue(type, item) {
-    linkTo(`/paper/process/${type}/${item.id}?r=${item.report.id}`);
-  }
-
-  viewReport(item) {
-    linkTo(`/paper/report/${item.report.id}`);
+  exerciseList(item) {
+    linkTo(`/exercise/list/${item.id}`);
   }
 
   activeSentence() {
@@ -480,12 +505,15 @@ export default class extends Page {
         User.clearSentenceTrail();
         this.setState({ sentence: null, articleMap: null, paperList: null });
         this.refresh();
+      })
+      .catch(err => {
+        this.setState({ sentenceError: err.message });
       });
   }
 
   trailSentence() {
-    this.setState({ sentenceInput: false });
     User.sentenceTrail();
+    this.setState({ sentenceError: null });
   }
 
   sentenceRead(article) {
@@ -514,8 +542,9 @@ export default class extends Page {
   }
 
   renderView() {
-    const { tab1 = {}, tab2 = {}, tabs, map = {}, latest } = this.state;
-    const children = (map[tab1] || {}).children || [];
+    const { tab1, tab2, tabs, latest, sentenceModel } = this.state;
+    const [subject] = tabs.filter(row => row.key === tab1);
+    const children = subject ? subject.children : [];
     return (
       <div>
         {latest && <Continue
@@ -524,13 +553,13 @@ export default class extends Page {
             this.clearExercise();
           }}
           onContinue={() => {
-
+            Question.continueLink('exercise', latest);
           }}
           onRestart={() => {
-
+            this.restart(latest);
           }}
           onNext={() => {
-
+            Question.continueLink('exercise', latest);
           }} />}
         <div className="content">
           <Module className="m-t-2">
@@ -538,12 +567,12 @@ export default class extends Page {
               this.onChangeTab(1, key);
             }} />
             {children.length > 1 && <Tabs active={tab2} tabs={children} onChange={key => this.onChangeTab(2, key)} />}
-
           </Module>
           {tab1 !== SENTENCE && tab1 !== PREVIEW && this.renderExercise()}
           {tab1 === SENTENCE && this.renderSentence()}
           {tab1 === PREVIEW && this.renderPreview()}
         </div>
+        {sentenceModel && this.renderInputCodeModel()}
       </div>
     );
   }
@@ -611,9 +640,9 @@ export default class extends Page {
   }
 
   renderSentence() {
-    const { sentence = {}, sentenceInput } = this.state;
+    const { sentence = {} } = this.state;
     const { sentenceTrail } = this.props.user;
-    if (sentenceInput !== true && (sentence.code || sentenceTrail)) {
+    if (sentence.code || sentenceTrail) {
       return this.renderSentenceArticle();
     }
     return this.renderInputCode();
@@ -638,7 +667,7 @@ export default class extends Page {
     return <div>
       {sentence.code && <div className='sentence-code'>CODE: {sentence.code}</div>}
       {sentenceTrail && <div className='sentence-code'>CODE: <Link to=''>去获取</Link><a onClick={() => {
-        this.setState({ sentenceInput: true });
+        this.setState({ sentenceModel: true });
       }}>输入</a></div>}
       <Module>
         <Step
@@ -660,7 +689,7 @@ export default class extends Page {
         }}
       />}
       {/* 正常文章 */}
-      {sentence.code && chapter && !isExercise && <List
+      {sentence.code && chapter > 0 && !isExercise && <List
         position={`Chapter${chapter}`}
         title={chapterInfo.title}
         list={articleMap[chapter]}
@@ -705,6 +734,7 @@ export default class extends Page {
   }
 
   renderInputCode() {
+    const { sentenceError } = this.state;
     return (
       <Module className="code-module">
         <div className="title">输入《千行GMAT长难句》专属 Code,解锁在线练习功能。</div>
@@ -715,6 +745,7 @@ export default class extends Page {
           <Button size="lager" onClick={() => {
             this.activeSentence();
           }}>解锁</Button>
+          {sentenceError && <div className='error'>{sentenceError}</div>}
         </div>
         <div className="tip">
           <Link to="/" className="left link">
@@ -734,7 +765,61 @@ export default class extends Page {
     );
   }
 
+  renderInputCodeModel() {
+    const { sentenceError } = this.state;
+    return <Modal visible closable={false} footer={false} title={false}>
+      <div className="code-module-modal">
+        <div className="title">请输入CODE</div>
+        <div className="desc">
+          <Input onChange={(value) => {
+            this.code = value;
+          }} />
+          {sentenceError && <div className='error'>{sentenceError}</div>}
+          <div className='tip'>
+            <Link to="/" className="right link">
+              什么是CODE?
+          </Link>
+          </div>
+        </div>
+        <div className="btn-list">
+          <AnswerButton size="lager" theme="confirm" width={150} onClick={() => this.activeSentence()}>确认</AnswerButton>
+          <AnswerButton size="lager" theme="cancel" width={150} onClick={() => this.setState({ sentenceModel: false })}>取消</AnswerButton>
+        </div>
+      </div>
+    </Modal>;
+  }
+
   renderExercise() {
-    return <div />;
+    const { exerciseProgress = [] } = this.state;
+    return <div >
+      <Division col="2">
+        {(exerciseProgress || []).map((struct) => {
+          const [first] = struct.children;
+          let col = 3;
+          if (first && first.type === 'paper') {
+            col = 5;
+          }
+          return <Panel
+            title={struct.titleEn}
+            message={struct.description}
+            data={struct}
+            col={col}
+            onClick={(item) => {
+              if (item.type === 'paper') {
+                if (item.progress === 0) {
+                  Question.startLink('exercise', item);
+                } else if (item.progress === 100) {
+                  Question.startLink('exercise', item);
+                } else {
+                  Question.continueLink('exercise', item);
+                }
+              } else {
+                this.exerciseList(item);
+              }
+            }}
+          />;
+        })}
+      </Division>
+    </div>;
   }
 }

+ 314 - 0
front/project/www/routes/paper/process/base/index.js

@@ -0,0 +1,314 @@
+import React, { Component } from 'react';
+import ReactDOM from 'react-dom';
+import './index.less';
+import { Checkbox } from 'antd';
+import Assets from '@src/components/Assets';
+import { formatSeconds, formatSecond, getMap } from '@src/services/Tools';
+import Button from '../../../../components/Button';
+import Navigation from '../../../../components/Navigation';
+import Answer from '../../../../components/Answer';
+import Calculator from '../../../../components/Calculator';
+import AnswerSelect from '../../../../components/AnswerSelect';
+import AnswerTable from '../../../../components/AnswerTable';
+import Editor from '../../../../components/Editor';
+import { QuestionType } from '../../../../../Constant';
+
+const QuestionTypeMap = getMap(QuestionType, 'value');
+
+export default class extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      showCalculator: false,
+      disorder: false,
+      order: [],
+      step: 0,
+      answer: {},
+      modal: null,
+    };
+  }
+
+  onChangeQuestion(index, value) {
+    const { question = {}, answer = {} } = this.state;
+    answer.questions[index] = { [question.type]: value };
+    this.setState({ answer });
+  }
+
+  onChangeAwa(value) {
+    const { answer = {} } = this.state;
+    answer.awa = value;
+    this.setState({ answer });
+  }
+
+  showConfirm(title, desc, cb) {
+    this.showModal('confirm', title, desc, cb);
+  }
+
+  showToast(title, desc, cb) {
+    this.showModal('toast', title, desc, cb);
+  }
+
+  showModal(type, title, desc, cb) {
+    this.setState({ modal: { type, title, desc, cb } });
+  }
+
+  checkAnswer() {
+    const { question, answer } = this.state;
+    let result = null;
+    if (question.type === 'awa' && !answer.awa) result = 'Please answer the question first.';
+    if (result) return this.showToast(null, result);
+    return true;
+  }
+
+  hideModal(b) {
+    if (b) {
+      const { modal = {} } = this.state;
+      if (modal.cb) modal.cb();
+    }
+    this.setState({ modal: null });
+  }
+
+  formatStrem(text) {
+    if (!text) return '';
+    const { question = { content: {} } } = this.state;
+    const { table = {}, questions = [] } = question.content;
+    text = text.replace(/#select#/g, "<span class='#select#' />");
+    text = text.replace(/#table#/g, "<span class='#table#' />");
+    setTimeout(() => {
+      const selectList = document.getElementsByClassName('#select#');
+      const tableList = document.getElementsByClassName('#table#');
+      for (let i = 0; i < selectList.length; i += 1) {
+        ReactDOM.render(
+          <AnswerSelect list={questions[i].select} onChange={v => this.onChangeQuestion(i, v)} />,
+          selectList[i],
+        );
+      }
+      for (let i = 0; i < tableList.length; i += 1) {
+        ReactDOM.render(<AnswerTable list={table.header} columns={table.header} data={table.data} />, tableList[i]);
+      }
+    }, 1);
+    return text;
+  }
+
+  next() {
+    const { flow } = this.props;
+
+    if (this.checkAnswer()) {
+      flow.next();
+    }
+  }
+
+  render() {
+    const { modal } = this.state;
+    const { scene, paper } = this.props;
+    let content = null;
+    switch (scene) {
+      case 'start':
+        content = paper.paperModule === 'examination' ? this.renderExaminationStart() : this.renderExerciseStart();
+        break;
+      case 'relax':
+        content = this.renderRelax();
+        break;
+      default:
+        content = this.renderDetail();
+        break;
+    }
+    return <div id='paper-process-base'>
+      {content}
+      {modal ? this.renderModal() : ''}
+    </div>;
+  }
+
+  renderContent() {
+    const { question = { content: {} } } = this.props;
+    const { step } = this.state;
+    const { steps = [] } = question.content;
+    return (
+      <div className="block block-content">
+        {steps.length > 0 && <Navigation list={question.content.steps} active={step} onChange={() => { }} />}
+        <div className="text">{this.formatStrem(steps.length > 0 ? steps[step].stem : question.stem)}</div>
+      </div>
+    );
+  }
+
+  renderAnswer() {
+    const { question = { content: {} } } = this.props;
+    const { questions = [], type } = question.content;
+    if (type === 'inline') return '';
+    return (
+      <div className="block block-answer">
+        {question.questionType === 'awa' && <Editor onChange={v => this.onChangeAwa(v)} />}
+        {questions.map((item, index) => {
+          return (
+            <div>
+              <div className="text m-b-2">{item.description}</div>
+              <Answer
+                list={item.select}
+                type={type}
+                direction={question.direction}
+                onChange={v => this.onChangeQuestion(index, v)}
+              />
+            </div>
+          );
+        })}
+      </div>
+    );
+  }
+
+  renderDetail() {
+    const { paper, userQuestion, question = { content: {} }, singleTime, stageTime, flow } = this.state;
+    const { showCalculator } = this.state;
+    const { typeset = 'one' } = question.content;
+    return (
+      <div className="layout">
+        <div className="fixed">
+          {QuestionTypeMap[question.questionType].long}
+          <Assets
+            className="calculator-icon"
+            name="calculator_icon"
+            onClick={() => this.setState({ showCalculator: !showCalculator })}
+          />
+          <Assets className="collect-icon" name="collect_icon" />
+        </div>
+        <Calculator show={showCalculator} />
+        <div className="layout-header">
+          <div className="title">{paper.title}</div>
+          <div className="right">
+            <div className="block">
+              <Assets name="timeleft_icon" />
+              Time left {formatSecond(stageTime || singleTime)}
+            </div>
+            <div className="block">
+              <Assets name="subjectnumber_icon" />
+              {userQuestion.no} of {paper.questionNumer}
+            </div>
+          </div>
+        </div>
+        <div className={`layout-body ${typeset}`}>
+          {this.renderContent()}
+          {this.renderAnswer()}
+        </div>
+        <div className="layout-footer">
+          <div className="help">
+            <Assets name="help_icon" />
+            Help
+          </div>
+          <div className="full">
+            <Assets name="fullscreen_icon" onClick={() => flow.toggleFullscreen()} />
+          </div>
+          <div className="next" onClick={() => this.next()}>
+            Next
+            <Assets name="next_icon" />
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  renderExaminationStart() {
+    const { disorder } = this.state;
+    const { paper, flow } = this.props;
+    return (
+      <div className="start">
+        <div className="bg" />
+        <div className="fixed-content">
+          <div className="title">{paper.title}</div>
+          <div className="desc">
+            <div className="block">
+              <div className="desc-title">
+                <Assets name="subject_icon" />
+                题目总数
+              </div>
+              <div className="desc-info">{paper.questionNumer}</div>
+            </div>
+            <div className="block">
+              <div className="desc-title">
+                <Assets name="time_icon" />
+                建议用时
+              </div>
+              <div className="desc-info">{formatSeconds(paper.time)}</div>
+            </div>
+          </div>
+          <div className="tip">
+            <Checkbox className="m-r-1" checked={disorder} onChange={() => this.setState({ disorder: !disorder })} />
+            题目选项乱序显示
+          </div>
+          <div className="submit">
+            <Button size="lager" radius onClick={() => flow.start({ disorder })}>
+              开始练习
+            </Button>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  renderExerciseStart() {
+    const { disorder } = this.state;
+    const { paper, flow } = this.props;
+    return (
+      <div className="start">
+        <div className="bg" />
+        <div className="fixed-content">
+          <div className="title">{paper.title}</div>
+          <div className="desc">
+            <div className="block">
+              <div className="desc-title">
+                <Assets name="subject_icon" />
+                题目总数
+              </div>
+              <div className="desc-info">{paper.questionNumber}</div>
+            </div>
+            <div className="block">
+              <div className="desc-title">
+                <Assets name="time_icon" />
+                建议用时
+              </div>
+              <div className="desc-info">{formatSeconds(paper.time)}</div>
+            </div>
+          </div>
+          <div className="tip">
+            <Checkbox className="m-r-1" checked={disorder} onChange={() => this.setState({ disorder: !disorder })} />
+            题目选项乱序显示
+          </div>
+          <div className="submit">
+            <Button size="lager" radius onClick={() => flow.start({ disorder })}>
+              开始练习
+            </Button>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  renderRelax() {
+    return <div />;
+  }
+
+  renderModal() {
+    const { modal } = this.state;
+    return (
+      <div className="modal">
+        <div className="mask" />
+        <div className="body">
+          <div className="title">{modal.title}</div>
+          <div className="desc">{modal.desc}</div>
+          {modal.type === 'confirm' ? (
+            <div className="btn-list">
+              <div className="btn" onClick={() => this.hideModal(true)}>
+                <span className="t-d-l">Y</span>es
+              </div>
+              <div className="btn" onClick={() => this.hideModal(false)}>
+                <span className="t-d-l">N</span>o
+              </div>
+            </div>
+          ) : (<div className="btn-list">
+            <div className="btn" onClick={() => this.hideModal(true)}>
+              <span className="t-d-l">O</span>k
+              </div>
+          </div>)}
+        </div>
+      </div>
+    );
+  }
+}

+ 265 - 0
front/project/www/routes/paper/process/base/index.less

@@ -0,0 +1,265 @@
+@charset "utf-8";
+
+#paper-process-base {
+  height: 100%;
+  color: #000;
+
+  .start {
+    background: #7775CA;
+    height: 100%;
+    padding: 40px 20px;
+
+    .bg {
+      height: 100%;
+      width: 100%;
+      background: #fff;
+    }
+
+    .fixed-content {
+      position: fixed;
+      left: 50%;
+      top: 50%;
+      transform: translate(-50%, -50%);
+
+      .title {
+        text-align: center;
+        font-size: 40px;
+        font-weight: 600;
+        margin-bottom: 100px;
+      }
+
+      .desc {
+        text-align: center;
+        margin-bottom: 80px;
+        display: flex;
+        flex-direction: row;
+
+        .block {
+          display: inline-block;
+          width: 300px;
+
+          .desc-title {
+            margin-bottom: 20px;
+
+            .assets {
+              margin-right: 10px;
+            }
+          }
+
+          .desc-info {
+            font-size: 40px;
+            font-weight: 600;
+          }
+        }
+      }
+
+      .tip {
+        text-align: center;
+        margin-bottom: 20px;
+      }
+
+      .submit {
+        text-align: center;
+      }
+    }
+  }
+
+  .layout {
+    background: #006DAA;
+    height: 100%;
+    padding: 0 25px;
+    display: flex;
+    flex-direction: column;
+    position: relative;
+
+    .calculator {
+      position: absolute;
+      z-index: 9;
+      top: 60px;
+    }
+
+    .fixed {
+      position: absolute;
+      top: 62px;
+      left: 0;
+      right: 0;
+      height: 30px;
+      line-height: 30px;
+      background: #7EAFE0;
+      color: #fff;
+      padding: 0 25px;
+
+      .calculator-icon {
+        margin-left: 50px;
+        cursor: pointer;
+      }
+
+      .collect-icon {
+        float: right;
+        cursor: pointer;
+        transform: translateY(5px);
+      }
+    }
+
+    .layout-header {
+      height: 60px;
+      color: #fff;
+
+      .title {
+        font-size: 20px;
+        line-height: 60px;
+        display: inline-block;
+      }
+
+      .right {
+        float: right;
+        text-align: right;
+
+        .block {
+          line-height: 30px;
+
+          .assets {
+            margin-right: 10px;
+          }
+        }
+      }
+    }
+
+    .layout-footer {
+      height: 35px;
+      line-height: 35px;
+      color: #fff;
+      margin-right: -25px;
+
+      .help {
+        padding: 0 10px;
+        border-left: 1px solid #fff;
+        border-right: 1px solid #fff;
+        display: inline-block;
+        cursor: pointer;
+
+        .assets {
+          margin-right: 10px;
+        }
+      }
+
+      .help:hover {
+        background: darken(#006DAA, 10);
+      }
+
+      .full {
+        display: inline-block;
+        padding: 0 10px;
+        border-right: 1px solid #fff;
+        cursor: pointer;
+      }
+
+      .full:hover {
+        background: darken(#006DAA, 10);
+      }
+
+      .next {
+        float: right;
+        padding: 0 10px;
+        border-left: 1px solid #fff;
+        border-top: 1px solid #fff;
+        cursor: pointer;
+        box-sizing: border-box;
+        height: 35px;
+
+        .assets {
+          margin-left: 20px;
+        }
+      }
+
+      .next:hover {
+        background: darken(#006DAA, 10);
+      }
+    }
+
+    .layout-body {
+      background: #fff;
+      flex: 1;
+      overflow: hidden;
+      overflow-y: auto;
+
+      .block {
+        padding: 60px 20px 20px;
+      }
+    }
+
+    .layout-body.two {
+      display: flex;
+
+      .block {
+        overflow: hidden;
+        overflow-y: auto;
+        flex: 1;
+      }
+
+      .block-content {
+        border-right: 4px solid #006DAA;
+
+        .navigation {
+          margin-bottom: 80px;
+        }
+      }
+
+      .block-answer {
+        border-left: 4px solid #006DAA;
+      }
+    }
+  }
+
+  .modal {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+
+    .body {
+      position: absolute;
+      left: 50%;
+      top: 50%;
+      transform: translate(-50%, -50%);
+      background: #006DAA;
+      width: 400px;
+      color: #fff;
+
+      .title {
+        height: 38px;
+        line-height: 38px;
+        font-size: 18px;
+        padding-left: 25px;
+        border-bottom: 1px solid #fff;
+      }
+
+      .desc {
+        text-align: center;
+        margin-top: 20px;
+        margin-bottom: 20px;
+
+      }
+
+      .btn-list {
+        text-align: center;
+        margin-bottom: 15px;
+
+        .btn {
+          display: inline-block;
+          width: 70px;
+          line-height: 35px;
+          height: 35px;
+          border: 1px solid #fff;
+          background: #006DAA;
+          cursor: pointer;
+        }
+
+        .btn:hover {
+          background: darken(#006DAA, 5);
+        }
+      }
+    }
+  }
+}

+ 0 - 257
front/project/www/routes/paper/process/index.less

@@ -3,261 +3,4 @@
 #paper-process {
   height: 100%;
   color: #000;
-
-  .start {
-    background: #3A4287;
-    height: 100%;
-    padding: 40px 20px;
-
-    .bg {
-      height: 100%;
-      width: 100%;
-      background: #fff;
-    }
-
-    .fixed-content {
-      position: fixed;
-      left: 50%;
-      top: 50%;
-      transform: translate(-50%, -50%);
-
-      .title {
-        text-align: center;
-        font-size: 40px;
-        font-weight: 600;
-        margin-bottom: 100px;
-      }
-
-      .desc {
-        text-align: center;
-        margin-bottom: 80px;
-
-        .block {
-          display: inline-block;
-          width: 300px;
-
-          .desc-title {
-            margin-bottom: 20px;
-
-            .assets {
-              margin-right: 10px;
-            }
-          }
-
-          .desc-info {
-            font-size: 40px;
-            font-weight: 600;
-          }
-        }
-      }
-
-      .tip {
-        text-align: center;
-        margin-bottom: 20px;
-      }
-
-      .submit {
-        text-align: center;
-      }
-    }
-  }
-
-  .layout {
-    background: #006DAA;
-    height: 100%;
-    padding: 0 25px;
-    display: flex;
-    flex-direction: column;
-    position: relative;
-
-    .calculator {
-      position: absolute;
-      z-index: 9;
-      top: 60px;
-    }
-
-    .fixed {
-      position: absolute;
-      top: 62px;
-      left: 0;
-      right: 0;
-      height: 30px;
-      line-height: 30px;
-      background: #7EAFE0;
-      color: #fff;
-      padding: 0 25px;
-
-      .calculator-icon {
-        margin-left: 50px;
-        cursor: pointer;
-      }
-
-      .collect-icon {
-        float: right;
-        cursor: pointer;
-        transform: translateY(5px);
-      }
-    }
-
-    .layout-header {
-      height: 60px;
-      color: #fff;
-
-      .title {
-        font-size: 20px;
-        line-height: 60px;
-        display: inline-block;
-      }
-
-      .right {
-        float: right;
-        text-align: right;
-
-        .block {
-          line-height: 30px;
-
-          .assets {
-            margin-right: 10px;
-          }
-        }
-      }
-    }
-
-    .layout-footer {
-      height: 35px;
-      line-height: 35px;
-      color: #fff;
-      margin-right: -25px;
-
-      .help {
-        padding: 0 10px;
-        border-left: 1px solid #fff;
-        border-right: 1px solid #fff;
-        display: inline-block;
-        cursor: pointer;
-
-        .assets {
-          margin-right: 10px;
-        }
-      }
-
-      .help:hover {
-        background: darken(#006DAA, 10);
-      }
-
-      .full {
-        display: inline-block;
-        padding: 0 10px;
-        border-right: 1px solid #fff;
-        cursor: pointer;
-      }
-
-      .full:hover {
-        background: darken(#006DAA, 10);
-      }
-
-      .next {
-        float: right;
-        padding: 0 10px;
-        border-left: 1px solid #fff;
-        border-top: 1px solid #fff;
-        cursor: pointer;
-        box-sizing: border-box;
-        height: 35px;
-
-        .assets {
-          margin-left: 20px;
-        }
-      }
-
-      .next:hover {
-        background: darken(#006DAA, 10);
-      }
-    }
-
-    .layout-body {
-      background: #fff;
-      flex: 1;
-      overflow: hidden;
-      overflow-y: auto;
-
-      .block {
-        padding: 60px 20px 20px;
-      }
-    }
-
-    .layout-body.two {
-      display: flex;
-
-      .block {
-        overflow: hidden;
-        overflow-y: auto;
-        flex: 1;
-      }
-
-      .block-content {
-        border-right: 4px solid #006DAA;
-
-        .navigation {
-          margin-bottom: 80px;
-        }
-      }
-
-      .block-answer {
-        border-left: 4px solid #006DAA;
-      }
-    }
-  }
-
-  .modal {
-    position: fixed;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-
-    .body {
-      position: absolute;
-      left: 50%;
-      top: 50%;
-      transform: translate(-50%, -50%);
-      background: #006DAA;
-      width: 400px;
-      color: #fff;
-
-      .title {
-        height: 38px;
-        line-height: 38px;
-        font-size: 18px;
-        padding-left: 25px;
-        border-bottom: 1px solid #fff;
-      }
-
-      .desc {
-        text-align: center;
-        margin-top: 20px;
-        margin-bottom: 20px;
-
-      }
-
-      .btn-list {
-        text-align: center;
-        margin-bottom: 15px;
-
-        .btn {
-          display: inline-block;
-          width: 70px;
-          line-height: 35px;
-          height: 35px;
-          border: 1px solid #fff;
-          background: #006DAA;
-          cursor: pointer;
-        }
-
-        .btn:hover {
-          background: darken(#006DAA, 5);
-        }
-      }
-    }
-  }
 }

+ 234 - 252
front/project/www/routes/paper/process/page.js

@@ -1,306 +1,288 @@
 import React from 'react';
-import ReactDOM from 'react-dom';
+import Fullscreen from 'react-fullscreen-crossbrowser';
 import './index.less';
-import { Checkbox } from 'antd';
-import Assets from '@src/components/Assets';
 import Page from '@src/containers/Page';
-import Button from '../../../components/Button';
-import Navigation from '../../../components/Navigation';
-import Answer from '../../../components/Answer';
-import Calculator from '../../../components/Calculator';
-import AnswerSelect from '../../../components/AnswerSelect';
-import AnswerTable from '../../../components/AnswerTable';
+import { randomList, sortListWithOrder, resortListWithOrder } from '@src/services/Tools';
 import { Question } from '../../../stores/question';
-import Editor from '../../../components/Editor';
+import Base from './base';
+import Sentence from './sentence';
+import { Main } from '../../../stores/main';
+import { My } from '../../../stores/my';
 
 export default class extends Page {
+  init() {
+    this.singleTime = null;
+    this.singleInterval = null;
+
+    this.stages = {};
+    this.stage = '';
+    this.stageInterval = null;
+    this.stageTime = 0;
+    this.stageNumber = 0;
+    this.stageProcess = { number: 0, time: 0 };
+    this.relaxProcess = { time: 0 };
+  }
+
   initState() {
     return {
-      showCalculator: false,
-      start: !this.props.core.query.r,
-      reportId: this.props.core.query.r,
-      type: this.props.core.query.type,
-      disorder: false,
-      step: 0,
-      info: {},
-      reportInfo: {},
-      questionInfo: {},
-      answer: {},
-      modal: null,
+      setting: {},
+      report: {},
+      question: {},
+      userQuestion: {},
+      paper: {},
     };
   }
 
   initData() {
-    const { start } = this.state;
-    Question.getPaper(this.params.id).then(result => {
-      this.setState({ info: result });
+    const { type, id } = this.params;
+    // type 是获取基础paper的表信息
+    // 等同于PaperOrigin
+    Question.getPaper(type, id).then(paper => {
+      this.setState({ paper });
+      let handler = null;
+      if (paper.paperModule === 'examination') {
+        // 模考获取配置信息
+        handler = Main.getExaminationNumber().then((result) => {
+          Object.keys(result).forEach((key) => {
+            result[key].time = Number(result[key].time);
+            result[key].number = Number(result[key].number);
+          });
+          this.stages = result;
+          this.relaxProcess = { time: 8 * 60 };
+        });
+      } else {
+        handler = Promise.resolve();
+      }
+      const { r } = this.state.search;
+      if (r) {
+        handler.then(() => {
+          this.continue(r);
+        });
+      } else if (paper.paperModule === 'sentence') {
+        // 长难句没有设置,直接开始
+        handler.then(() => {
+          this.start({});
+        });
+      } else {
+        this.setState({ scene: 'start' });
+      }
+      handler.catch(() => {
+        goBack();
+      });
     });
-    if (!start) {
-      this.continue();
-    }
-  }
-
-  onChangeQuestion(index, value) {
-    const { question = {}, answer = {} } = this.state;
-    answer.questions[index] = { [question.type]: value };
-    this.setState({ answer });
-  }
-
-  onChangeAwa(value) {
-    const { answer = {} } = this.state;
-    answer.awa = value;
-    this.setState({ answer });
   }
 
-  start() {
-    const { type, info, disorder } = this.state;
-    Question.start(type, info.id, disorder).then(result => {
-      this.setState({ reportInfo: result });
-      this.next();
+  start(setting) {
+    const { type, id } = this.params;
+    return Question.start(type, id, setting).then(report => {
+      if (report.isFinish) {
+        return this.finish();
+      }
+      this.setState({ report, scene: 'question' });
+      // 开始统计做题进度
+      if (report.paperModule === 'examination') {
+        const { order } = setting;
+        this.initStage(order[0], 0, 0);
+      }
+      return this.next();
     });
   }
 
-  continue() {
-    const { reportId } = this.state;
-    Question.continue(reportId).then(result => {
-      this.setState({ reportInfo: result });
-      this.next();
+  continue(reportId) {
+    return Question.continue(reportId).then(report => {
+      if (report.isFinish) {
+        throw new Error('做题结束,请先重置');
+      }
+      this.setState({ report, scene: 'questionn' });
+      // 更新当前做题进度
+      if (report.paperModule === 'examination') {
+        const { stage, time, number } = report.setting;
+        this.initStage(stage, time[stage], number[stage]);
+      }
+      return this.next();
+    }).catch(() => {
+      Question.reportLink({ report: { id: reportId } });
     });
   }
 
   next() {
-    const { reportInfo } = this.state;
-    Question.next(reportInfo.id).then(result => {
+    const { report, scene } = this.state;
+    const { setting = {} } = report;
+    if (scene === 'relax') {
+      this.nextStage();
+    }
+    return Question.next(report.id).then(userQuestion => {
+      const questionSetting = {};
+      if (setting.disorder) {
+        const { questions } = userQuestion.question.content;
+        if (questions) {
+          // 乱序显示选项
+          questionSetting.questions = [];
+          questions.forEach((q) => {
+            const order = randomList(q.select.length);
+            q.select = sortListWithOrder(q.select, order);
+            questionSetting.questions.push(order);
+          });
+        }
+      }
       this.setState({
-        questionInfo: result,
-        question: result.question,
-        answer: { questions: [], subject: [], predicate: [], object: [], options: [], awa: '' },
+        userQuestion,
+        question: userQuestion.question,
+        questionSetting,
+        scene: 'question',
       });
+      this.singleQuestionTime();
+      return true;
+    }).catch((err) => {
+      if (err.message === 'finish') {
+        // 考试结束
+        return this.finish();
+      }
+      return false;
     });
   }
 
-  submit() {
-    const { question, answer } = this.state;
-    if (!this.checkAnswer()) return;
-    Question.submit(question.questionNoId, answer).then(() => {
-      this.next();
+  submit(answer) {
+    const { report, userQuestion, questionSetting, singleTime } = this.state;
+    const { setting = {} } = report;
+    if (setting.disorder) {
+      const { questions } = answer;
+      if (questions) {
+        // 还原乱序选项
+        answer.questions = questions.forEach((q, index) => {
+          const order = questionSetting.questions[index];
+          Object.keys(q).forEach((k) => {
+            if (q[k]) q[k] = resortListWithOrder(q[k], order);
+          });
+        });
+      }
+    }
+    return Question.submit(userQuestion.id, answer, singleTime, questionSetting).then(() => {
+      this.singleQuestionTime(true);
+      // 更新模考做题进度
+      if (report.paperModule === 'examination') {
+        this.stageNumber += 1;
+        if (this.stageNumber >= this.stageProcess.number) {
+          // 进入休息
+          this.relaxStage();
+        }
+      }
     });
   }
 
-  finish() {
-    const { reportInfo } = this.state;
-    Question.finish(reportInfo.id).then(() => {
-      this.showToast(null, 'Complete!', () => {
-        goBack();
-      });
+  stage() {
+    const { report } = this.state;
+    return Question.stage(report.id).then(() => {
+      return this.next();
+    }).then(() => {
+      this.stageQuestionTime();
     });
   }
 
-  showConfirm(title, desc, cb) {
-    this.showModal('confirm', title, desc, cb);
-  }
-
-  showToast(title, desc, cb) {
-    this.showModal('toast', title, desc, cb);
-  }
-
-  showModal(type, title, desc, cb) {
-    this.setState({ modal: { type, title, desc, cb } });
+  finish() {
+    const { report } = this.state;
+    return Question.finish(report.id).then(() => {
+      // 跳转到报告页
+      Question.reportLink({ report });
+      // this.setState({ scene: 'finish' });
+    }).catch(() => {
+      Question.reportLink({ report });
+    });
   }
 
-  checkAnswer() {
-    const { question, answer } = this.state;
-    let result = null;
-    if (question.type === 'awa' && !answer.awa) result = 'Please answer the question first.';
-    if (result) return this.showToast(null, result);
-    return true;
+  singleQuestionTime(stop) {
+    if (this.singleInterval) {
+      clearInterval(this.singleInterval);
+      this.singleInterval = null;
+    }
+    if (!stop) {
+      this.singleTime = 0;
+      this.singleInterval = setInterval(() => {
+        this.singleTime += 1;
+        this.setState({ singleTime: this.singleTime });
+      }, 1000);
+    }
   }
 
-  hideModal(b) {
-    if (b) {
-      const { modal = {} } = this.state;
-      if (modal.cb) modal.cb();
+  stageQuestionTime(initTime) {
+    if (this.stageInterval) {
+      clearInterval(this.stageInterval);
+      this.stageInterval = null;
+      this.stageTime = initTime;
     }
-    this.setState({ modal: null });
+    this.stageInterval = setInterval(() => {
+      this.stageTime += 1;
+      if (this.stageTime >= this.stageProcess.number) {
+        this.nextStage();
+      }
+      this.setState({ stageTime: this.targetProcess.time - this.stageTime });
+    }, 1000);
   }
 
-  formatStrem(text) {
-    if (!text) return '';
-    const { question = { content: {} } } = this.state;
-    const { table = {}, questions = [] } = question.content;
-    text = text.replace(/#select#/g, "<span class='#select#' />");
-    text = text.replace(/#table#/g, "<span class='#table#' />");
-    setTimeout(() => {
-      const selectList = document.getElementsByClassName('#select#');
-      const tableList = document.getElementsByClassName('#table#');
-      for (let i = 0; i < selectList.length; i += 1) {
-        ReactDOM.render(
-          <AnswerSelect list={questions[i].select} onChange={v => this.onChangeQuestion(i, v)} />,
-          selectList[i],
-        );
-      }
-      for (let i = 0; i < tableList.length; i += 1) {
-        ReactDOM.render(<AnswerTable list={table.header} columns={table.header} data={table.data} />, tableList[i]);
-      }
-    }, 1);
-    return text;
+  nextStage() {
+    const { report } = this.state;
+    // 进入下一阶段
+    const { order } = report.setting;
+    this.stage = order[order.indexOf(this.stage) + 1];
+    this.stageProcess = this.stages[this.stage];
+    this.stageNumber = 0;
+    this.stageQuestionTime(0);
   }
 
-  renderView() {
-    const { start } = this.state;
-    if (start) return this.renderStart();
-    return this.renderDetail();
+  relaxStage() {
+    this.stageProcess = this.relaxProcess;
+    this.stageNumber = 0;
+    this.stageQuestionTime(0);
+    this.setState({
+      scene: 'relax',
+    });
   }
 
-  renderContent() {
-    const { question = { content: {} }, step } = this.state;
-    const { steps = [] } = question.content;
-    return (
-      <div className="block block-content">
-        {steps.length > 0 && <Navigation list={question.content.steps} active={step} onChange={() => {}} />}
-        <div className="text">{this.formatStrem(steps.length > 0 ? steps[step].stem : question.stem)}</div>
-      </div>
-    );
+  initStage(stage, time, number) {
+    this.stage = stage;
+    this.stageProcess = this.stages[stage];
+    this.stageTime = time;
+    this.stageNumber = number;
+    this.stageQuestionTime(time);
   }
 
-  renderAnswer() {
-    const { question = { content: {} } } = this.state;
-    const { questions = [] } = question.content;
-    if (question.type === 'inline') return '';
-    return (
-      <div className="block block-answer">
-        {question.type === 'awa' && <Editor onChange={v => this.onChangeAwa(v)} />}
-        {questions.map((item, index) => {
-          return (
-            <div>
-              <div className="text m-b-2">{item.description}</div>
-              <Answer
-                list={item.select}
-                type={question.type}
-                direction={question.direction}
-                onChange={v => this.onChangeQuestion(index, v)}
-              />
-            </div>
-          );
-        })}
-      </div>
-    );
+  toggleFullscreen() {
+    const { isFullscreenEnabled } = this.state;
+    this.setState({ isFullscreenEnabled: !isFullscreenEnabled });
   }
 
-  renderDetail() {
-    const { modal, showCalculator, info, question = { content: {} } } = this.state;
-    const { typeset = 'one' } = question.content;
-    return (
-      <div className="layout">
-        <div className="fixed">
-          Analytical Writing Assessment
-          <Assets
-            className="calculator-icon"
-            name="calculator_icon"
-            onClick={() => this.setState({ showCalculator: !showCalculator })}
-          />
-          <Assets className="collect-icon" name="collect_icon" />
-        </div>
-        <Calculator show={showCalculator} />
-        <div className="layout-header">
-          <div className="title">{info.title}</div>
-          <div className="right">
-            <div className="block">
-              <Assets name="timeleft_icon" />
-              Time left 00:02
-            </div>
-            <div className="block">
-              <Assets name="subjectnumber_icon" />
-              {question.no} of {info.questionNumer}
-            </div>
-          </div>
-        </div>
-        <div className={`layout-body ${typeset}`}>
-          {this.renderContent()}
-          {this.renderAnswer()}
-        </div>
-        <div className="layout-footer">
-          <div className="help">
-            <Assets name="help_icon" />
-            Help
-          </div>
-          <div className="full">
-            <Assets name="fullscreen_icon" />
-          </div>
-          <div className="next" onClick={() => this.next()}>
-            Next
-            <Assets name="next_icon" />
-          </div>
-        </div>
-        {modal ? this.renderModal() : ''}
-      </div>
-    );
+  toggleCollect() {
+    const { userQuestion = {} } = this.state;
+    if (!userQuestion.collect) {
+      My.addQuestionCollect(userQuestion.questionModule, userQuestion.questionNoId).then(() => {
+        userQuestion.collect = true;
+        this.setState({ userQuestion });
+      });
+    } else {
+      My.delQuestionCollect(userQuestion.questionModule, userQuestion.questionNoId).then(() => {
+        userQuestion.collect = false;
+        this.setState({ userQuestion });
+      });
+    }
   }
 
-  renderStart() {
-    const { info, disorder, modal } = this.state;
-    return (
-      <div className="start">
-        <div className="bg" />
-        <div className="fixed-content">
-          <div className="title">{info.title}</div>
-          <div className="desc">
-            <div className="block">
-              <div className="desc-title">
-                <Assets name="subject_icon" />
-                题目总数
-              </div>
-              <div className="desc-info">{info.questionNumer}</div>
-            </div>
-            <div className="block">
-              <div className="desc-title">
-                <Assets name="time_icon" />
-                建议用时
-              </div>
-              <div className="desc-info">{info.time}</div>
-            </div>
-          </div>
-          <div className="tip">
-            <Checkbox className="m-r-1" checked={disorder} onChange={() => this.setState({ disorder: !disorder })} />
-            题目选项乱序显示
-          </div>
-          <div className="submit">
-            <Button size="lager" radius onClick={() => this.start()}>
-              开始练习
-            </Button>
-          </div>
-        </div>
-        {modal ? this.renderModal() : ''}
-      </div>
-    );
+  renderView() {
+    return <Fullscreen
+      enabled={this.state.isFullscreenEnabled}
+      onChange={isFullscreenEnabled => this.setState({ isFullscreenEnabled })}
+    >
+      {this.renderDetail()}
+    </Fullscreen>;
   }
 
-  renderModal() {
-    const { modal } = this.state;
-    return (
-      <div className="modal">
-        <div className="mask" />
-        <div className="body">
-          <div className="title">{modal.title}</div>
-          <div className="desc">{modal.desc}</div>
-          {modal.type === 'confirm' ? (
-            <div className="btn-list">
-              <div className="btn" onClick={() => this.hideModal(true)}>
-                <span className="t-d-l">Y</span>es
-              </div>
-              <div className="btn" onClick={() => this.hideModal(false)}>
-                <span className="t-d-l">N</span>o
-              </div>
-            </div>
-          ) : (
-            <div className="btn-list">
-              <div className="btn" onClick={() => this.hideModal(true)}>
-                <span className="t-d-l">O</span>k
-              </div>
-            </div>
-          )}
-        </div>
-      </div>
-    );
+  renderDetail() {
+    const { scene, paper, userQuestion } = this.state;
+    if (!paper.id || !scene) return null;
+    switch (paper.paperModule) {
+      case 'sentence':
+        return <Sentence key={userQuestion.id} {...this.state} flow={this} />;
+      default:
+        return <Base key={userQuestion.id} {...this.state} flow={this} />;
+    }
   }
 }

+ 288 - 0
front/project/www/routes/paper/process/sentence/index.js

@@ -0,0 +1,288 @@
+import React, { Component } from 'react';
+import './index.less';
+import Assets from '@src/components/Assets';
+import { formatSecond, formatPercent } from '@src/services/Tools';
+import Icon from '../../../../components/Icon';
+import Button from '../../../../components/Button';
+import Tabs from '../../../../components/Tabs';
+import Progress from '../../../../components/Progress';
+import HardInput from '../../../../components/HardInput';
+import AnswerCheckbox from '../../../../components/AnswerCheckbox';
+import { SentenceOption } from '../../../../../Constant';
+import { Question } from '../../../../stores/question';
+
+export default class extends Component {
+  constructor(props) {
+    super(props);
+    // 确保可以自身进行答案显示,外部也可以直接显示答案
+    // 将props转入state
+    this.state = {
+      analysisTab: 'qx',
+      focusKey: 'subject',
+      scene: this.props.scene || 'answer',
+      userQuestion: this.props.userQuestion,
+      question: this.props.question,
+    };
+    const { question, userQuestion } = this.props;
+    if (this.state.scene === 'answer') {
+      this.state.stem = this.formatStem(question.stem, userQuestion.userAnswer, question.answer);
+    } else {
+      this.state.stem = question.stem;
+    }
+  }
+
+  showAnswer() {
+    const { userQuestion } = this.state;
+    Question.getDetailById(userQuestion.id).then(result => {
+      const { question } = result;
+      this.setState({
+        userQuestion: result,
+        question: result.question,
+        scene: 'answer',
+        stem: this.formatStem(question.stem, result.userAnswer, question.answer),
+      });
+    });
+  }
+
+  addTarget(target) {
+    const uuid = target.getAttribute('uuid');
+    if (!uuid) return;
+    const text = target.innerText;
+    const { focusKey, answer = {}, question } = this.state;
+    if (!answer[focusKey]) answer[focusKey] = [];
+    if (answer[focusKey].filter(row => row.uuid === uuid).length > 0) return;
+    answer[focusKey].push({
+      text,
+      uuid,
+    });
+    this.setState({
+      answer,
+      stem: this.formatStem(question.stem, answer),
+    });
+  }
+
+  removeTarget(key, target) {
+    const { answer = {}, question } = this.state;
+    if (!answer[key]) return;
+    answer[key] = answer[key].filter(row => row.uuid !== target.uuid);
+    this.setState({
+      answer,
+      stem: this.formatStem(question.stem, answer),
+    });
+  }
+
+  next() {
+    const { flow } = this.props;
+    const { scene } = this.state;
+    if (scene === 'question') {
+      const { answer } = this.state;
+      flow.submit(answer).then(() => {
+        this.showAnswer();
+      });
+    } else if (scene === 'answer') {
+      flow.next();
+    }
+  }
+
+  formatStem(stem, userAnswer, answer) {
+    // userAnswer 添加蓝色字, 错误的添加红色背景
+    // answer 添加绿色背景
+    const uuidMap = {};
+    const show = !!answer;
+    answer = answer || {};
+    userAnswer = userAnswer || {};
+    Object.keys(userAnswer).forEach((key) => {
+      if (key === 'options') return;
+      const u = userAnswer[key];
+      const a = answer[key] && answer[key].length > 0 ? answer[key][0] : [];
+      const map = {};
+      a.forEach((row) => {
+        if (!uuidMap[row.uuid]) uuidMap[row.uuid] = [];
+        uuidMap[row.uuid].push('true');
+        map[row.uuid] = row;
+      });
+      u.forEach((row) => {
+        if (!uuidMap[row.uuid]) uuidMap[row.uuid] = [];
+        uuidMap[row.uuid].push('user');
+        if (show && !map[row.uuid]) uuidMap[row.uuid].push('false');
+      });
+    });
+    Object.keys(uuidMap).forEach(uuid => {
+      stem = stem.replace(`uuid='${uuid}'`, `uuid='${uuid}' class='${uuidMap[uuid].join(' ')}'`);
+    });
+    return stem;
+  }
+
+  render() {
+    const { flow, paper, userQuestion, singleTime } = this.props;
+    return (
+      <div id='paper-process-sentence'>
+        <div className="layout">
+          <div className="layout-header">
+            <div className="left">
+              <div className="title">{paper.title}</div>
+            </div>
+            <div className="right">
+              <div className="text"><Assets name='timecost_icon' />Time cost {formatSecond(userQuestion.userTime || singleTime)}</div>
+              <Icon name="star" active={userQuestion.collect} onClick={() => flow.toggleCollect()} />
+            </div>
+          </div>
+          {this.renderBody()}
+          <div className="layout-footer">
+            <div className="left">
+              <Icon name={this.props.isFullscreenEnabled ? 'sceen-restore' : 'sceen-full'} onClick={() => flow.toggleFullscreen()} />
+            </div>
+            <div className="center">
+              <div className="p">
+                <Progress theme="theme" progress={formatPercent(userQuestion.no, paper.questionNumber)} />
+              </div>
+              <div className="t">{userQuestion.no}/{paper.questionNumber}</div>
+            </div>
+            <div className="right">
+              <Button size="lager" radius onClick={() => {
+                this.next();
+              }}>
+                Next <Assets name="next_icon" />
+              </Button>
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  renderBody() {
+    const { scene } = this.state;
+    switch (scene) {
+      case 'question':
+        return this.renderQuestion();
+      case 'answer':
+        return this.renderAnswer();
+      default:
+        return null;
+    }
+  }
+
+  renderQuestion() {
+    const { focusKey, answer = {}, stem } = this.state;
+    return (
+      <div className="layout-body">
+        <div className="title"><Icon name="question" />请分别找出句子中的主语,谓语和宾语,并做出逻辑关系判断。</div>
+        <div className="desc" dangerouslySetInnerHTML={{ __html: stem }} onClick={(e) => {
+          this.addTarget(e.target);
+        }} />
+        <div className="label">主语</div>
+        <div className="input">
+          <HardInput
+            focus={focusKey === 'subject'}
+            list={answer.subject || []}
+            onClick={() => {
+              this.setState({ focusKey: 'subject' });
+            }}
+            onDelete={(item) => {
+              this.removeTarget('subject', item);
+            }}
+          />
+        </div>
+        <div className="label">谓语</div>
+        <div className="input">
+          <HardInput
+            focus={focusKey === 'predicate'}
+            list={answer.predicate || []}
+            onClick={() => {
+              this.setState({ focusKey: 'predicate' });
+            }}
+            onDelete={(item) => {
+              this.removeTarget('predicate', item);
+            }}
+          />
+        </div>
+        <div className="label">宾语</div>
+        <div className="input">
+          <HardInput
+            focus={focusKey === 'object'}
+            list={answer.object || []}
+            onClick={() => {
+              this.setState({ focusKey: 'object' });
+            }}
+            onDelete={(item) => {
+              this.removeTarget('object', item);
+            }}
+          />
+        </div>
+        <div className="select">
+          <div className="select-title">本句存在以下哪种逻辑关系?(可多选)</div>
+          <AnswerCheckbox list={SentenceOption} selected={answer.options} onChange={(values) => {
+            answer.options = values;
+            this.setState({ answer });
+          }} />
+        </div>
+      </div>
+    );
+  }
+
+  renderAnswer() {
+    const { analysisTab, question, userQuestion, stem } = this.state;
+    const { userAnswer = {} } = userQuestion;
+    const { answer } = question;
+    return <div className="layout-body">
+      <div className="title"><Icon name="question" />请分别找出句子中的主语,谓语和宾语,并做出逻辑关系判断。</div>
+      <div className="desc" dangerouslySetInnerHTML={{ __html: stem }} />
+      <div className="label">主语</div>
+      <div className="input">
+        <HardInput
+          show
+          list={userAnswer.subject || []}
+          answer={answer.subject}
+        />
+      </div>
+      <div className="label">谓语</div>
+      <div className="input">
+        <HardInput
+          show
+          list={userAnswer.predicate || []}
+          answer={answer.predicate}
+        />
+      </div>
+      <div className="label">宾语</div>
+      <div className="input">
+        <HardInput
+          show
+          list={userAnswer.object || []}
+          answer={answer.object}
+        />
+      </div>
+      <div className="select">
+        <div className="select-title">本句存在以下哪种逻辑关系?(可多选)</div>
+        <AnswerCheckbox show list={SentenceOption} selected={userAnswer.options} answer={answer.options} />
+      </div>
+      <div className="analysis">
+        <Tabs
+          type="division"
+          active={analysisTab}
+          tabs={[{ key: 'qx', name: '解析详情' }, { key: 'chinese', name: '中文语意' }]}
+          onChange={(key) => {
+            this.setState({ analysisTab: key });
+          }}
+        />
+        {this.renderText()}
+      </div>
+    </div>;
+  }
+
+  renderText() {
+    const { analysisTab, question = {}, questionNo = {} } = this.state;
+    let content;
+    switch (analysisTab) {
+      case 'chinese':
+        content = <div className="detail-block text-block" dangerouslySetInnerHTML={{ __html: questionNo.chineseContent }} />;
+        break;
+      case 'qx':
+        content = <div className="detail-block text-block" dangerouslySetInnerHTML={{ __html: question.qxContent }} />;
+        break;
+      default:
+        break;
+    }
+    return content;
+  }
+}

+ 30 - 5
front/project/www/routes/sentence/process/index.less

@@ -1,6 +1,6 @@
 @charset "utf-8";
 
-#sentence {
+#paper-process-sentence {
   height: 100%;
 
   .layout {
@@ -36,6 +36,10 @@
 
         .text {
           line-height: 22px;
+
+          img {
+            margin-right: 5px;
+          }
         }
       }
     }
@@ -49,7 +53,6 @@
     height: 60px;
     line-height: 60px;
     box-shadow: 0px -4px 14px 0px rgba(189, 199, 215, 0.16);
-    position: relative;
 
     .left {
       position: absolute;
@@ -99,6 +102,7 @@
     overflow-y: auto;
     position: relative;
     padding: 20px 30px;
+    margin-bottom: 60px;
 
     .title {
       color: #050930;
@@ -108,6 +112,21 @@
     .desc {
       color: #050930;
       margin-bottom: 30px;
+      user-select: none;
+      cursor: pointer;
+      font-size: 14px;
+
+      span.user {
+        color: #217DFF;
+      }
+
+      span.true {
+        background-color: rgba(23, 165, 27, 0.06);
+      }
+
+      span.false {
+        background-color: rgba(253, 247, 247, 1);
+      }
     }
 
     .label {
@@ -118,6 +137,7 @@
 
     .input {
       margin-bottom: 20px;
+      display: block;
     }
 
     .select {
@@ -133,17 +153,22 @@
     }
 
     .analysis {
-      margin-bottom: 40px;
 
       .tabs {
         width: 320px;
+        margin: 0 -2px;
+
+        .tab {
+          margin: 0 2px;
+        }
 
-        .tab.active {
+        .tab.active,
+        .tab:hover {
           background: #F4F9FE;
         }
       }
 
-      .result {
+      .text-block {
         padding: 20px;
         background: #F4F9FE;
       }

+ 380 - 264
front/project/www/routes/paper/question/index.less

@@ -3,152 +3,274 @@
 #paper-question {
   height: 100%;
 
-  .layout {
-    background: #fff;
+  .base {
     height: 100%;
-    display: flex;
-    flex-direction: column;
-    position: relative;
 
-    .layout-header {
-      height: 60px;
-      line-height: 60px;
-      padding: 0 50px;
-      position: fixed;
-      top: 0;
-      left: 0;
-      right: 0;
-      box-shadow: 0px 4px 14px 0px rgba(189, 199, 215, 0.16);
-      text-align: center;
+    .layout {
+      background: #fff;
+      height: 100%;
+      display: flex;
+      flex-direction: row;
+
+      .layout-header {
+        height: 60px;
+        line-height: 60px;
+        margin: 0 50px;
+        position: fixed;
+        top: 0;
+        left: 0;
+        right: 0;
+        box-shadow: 0px 4px 14px 0px rgba(189, 199, 215, 0.16);
+        text-align: center;
+        z-index: 80;
 
-      .left {
-        position: absolute;
+        .left {
+          position: absolute;
 
-        .no {
-          font-size: 20px;
-          display: inline-block;
-          color: #303036;
-          font-size: 20px;
-          margin-right: 25px;
+          .no {
+            font-size: 20px;
+            display: inline-block;
+            color: #303036;
+            font-size: 20px;
+            margin-right: 25px;
+          }
+
+          .title {
+            color: #A7A7B7;
+            display: inline-block;
+            font-size: 20px;
+
+            img {
+              margin-top: -3px;
+              margin-right: 5px;
+            }
+          }
         }
 
-        .title {
-          color: #A7A7B7;
-          display: inline-block;
-          font-size: 20px;
+
+        .menu-wrap {
+          position: absolute;
+          right: 0;
+          text-align: left;
+          padding: 0 10px;
+
+          .menu-content {
+            position: absolute;
+            background: #fff;
+            text-align: left;
+            top: 50px;
+            right: 10px;
+            border: 1px solid #EAEDF2;
+            padding: 10px 20px;
+            min-width: 150px;
+
+            p {
+              line-height: 30px;
+              height: 30px;
+              text-align: left;
+              margin: 0;
+            }
+          }
         }
-      }
 
-      .center {
-        display: inline-block;
-        transform: translateX(100%);
+        .center {
+          position: absolute;
+          right: 50%;
+          transform: translateX(100%);
 
-        .icon {
-          margin-left: 20px;
+          .icon {
+            margin-left: 20px;
+          }
         }
-      }
 
-      .right {
-        float: right;
-        text-align: right;
+        .right {
+          position: absolute;
+          right: 0;
 
-        .b {
-          margin-left: 30px;
+          .b {
+            margin-left: 30px;
 
-          .s {
-            color: #4299FF;
+            .s {
+              color: #4299FF;
+            }
           }
-        }
 
-        .icon {
-          margin-left: 10px;
+          .icon {
+            margin-left: 10px;
+          }
         }
       }
     }
-  }
 
-  .layout-footer {
-    position: fixed;
-    bottom: 0;
-    left: 0;
-    right: 0;
-    height: 60px;
-    line-height: 60px;
-    box-shadow: 0px -4px 14px 0px rgba(189, 199, 215, 0.16);
-
-    .left {
-      width: 30%;
-      display: inline-block;
-      padding-left: 50px;
-    }
+    .layout-footer {
+      position: fixed;
+      bottom: 0;
+      left: 0;
+      right: 0;
+      height: 60px;
+      line-height: 60px;
+      box-shadow: 0px -4px 14px 0px rgba(189, 199, 215, 0.16);
 
-    .right {
-      width: 30%;
-      display: inline-block;
-      text-align: right;
-      padding-right: 50px;
+      .left {
+        width: 30%;
+        display: inline-block;
+        padding-left: 50px;
+      }
+
+      .right {
+        width: 30%;
+        display: inline-block;
+        text-align: right;
+        padding-right: 50px;
 
-      .icon {
-        margin-left: 10px;
+        .icon {
+          margin-left: 10px;
+        }
       }
-    }
 
-    .center {
-      width: 40%;
-      display: inline-block;
-      text-align: center;
+      .center {
+        width: 40%;
+        display: inline-block;
+        text-align: center;
 
-      .item {
-        margin: 0 10px;
+        .item {
+          margin: 0 10px;
+        }
       }
     }
-  }
 
-  .layout-body {
-    background: #fff;
-    flex: 1;
-    overflow: hidden;
-    overflow-y: auto;
-    margin: 60px 0;
-    position: relative;
+    .layout-body {
+      background: #fff;
+      flex: 1;
+      overflow: hidden;
+      margin: 60px 0;
 
-    .layout-content {
-      display: flex;
-      height: 100%;
+      .layout-content {
+        height: 100%;
+        position: relative;
 
-      .one {
-        flex: 1;
-        display: flex;
-        flex-direction: column;
-      }
+        .one {
+          flex: 1;
+          display: flex;
+          flex-direction: column;
+        }
 
-      .two {
-        flex: 1;
-        display: flex;
-      }
+        .two {
+          flex: 1;
+          display: flex;
+          flex-direction: row;
+          overflow: hidden;
+          height: 100%;
+        }
 
-      .block {
-        flex: 1;
-      }
+        .block {
+          flex: 1;
+        }
 
-      .block-content,
-      .block-answer {
-        padding: 30px 60px;
-        color: #303036;
-      }
+        .block-content,
+        .block-answer,
+        .block-awa {
+          padding: 30px 60px;
+          color: #303036;
+          height: 100%;
+          overflow: hidden;
+          overflow-y: auto;
+        }
+
+        .block-content {
+          h2 {
+            padding: 65px 0 20px 0px;
+            font-size: 20px;
+            color: #303036;
+          }
+        }
 
-      .block-analysis {
-        background: #EFF3F7;
-        padding: 20px 20px 0 20px;
-        display: flex;
-        flex-direction: column;
+        .block-awa {
+          background: #EFF3F7;
 
-        .detail {
-          flex: 1;
+          h2 {
+            font-size: 20px;
+            color: #303036;
+            margin-top: 37px;
+            margin-bottom: 23px;
+          }
+
+          .detail {
+            .info {
+              font-weight: bold;
+              font-size: 18px;
+              color: #303036;
+
+              span.b {
+                margin-right: 80px;
+
+                .s {
+                  color: #4299FF;
+                }
+              }
+            }
+          }
+
+          .content-awa {
+            padding-top: 50px;
+            color: #686872;
+            font-size: 16px;
+          }
+
+          .show-awa {
+            font-size: 12px;
+            width: 100%;
+            height: 100%;
+            margin: 50% 0;
+            text-align: center;
+            line-height: 20px;
+            color: #A7A7B7;
+          }
+        }
+
+        .block-analysis {
+          background: #EFF3F7;
+          padding: 25px 25px 0 20px;
           display: flex;
           flex-direction: column;
 
-          .detail-block {
+          .tabs.division {
+            margin: 0 -2px;
+
+            .tab {
+              margin: 0 2px;
+            }
+          }
+
+          .block-answer {
+            padding: 38px 50px;
+          }
+
+          .block {
+            background: #fff;
+          }
+
+          .detail {
+            flex: 1;
+            display: flex;
+            flex-direction: column;
+
+            .detail-block {
+              margin-top: 5px;
+              flex: 1;
+              padding: 30px 50px;
+              overflow: hidden;
+              overflow-y: auto;
+              font-size: 16px;
+              color: #686872;
+            }
+
+            .answer-block {
+              margin-bottom: 5px;
+            }
+          }
+
+          .other {
             flex: 1;
             background: #fff;
             padding: 30px 50px;
@@ -156,205 +278,199 @@
             overflow-y: auto;
             font-size: 16px;
             color: #686872;
-          }
 
-          .answer-block {
-            margin-bottom: 5px;
+            .other-answer {
+              margin-bottom: 30px;
+            }
           }
         }
 
-        .other {
-          flex: 1;
-          background: #fff;
-          padding: 30px 50px;
-          overflow: hidden;
-          overflow-y: auto;
-          font-size: 16px;
-          color: #686872;
+        .two-analysis {
+          position: absolute;
+          height: 100%;
+          top: 0;
+          left: 0;
+          width: 50%;
+          transition: all 0.3s;
+          transform: translateX(200%);
+        }
 
-          .other-answer {
-            margin-bottom: 30px;
-          }
+        .two-analysis.show {
+          transform: translateX(100%);
         }
-      }
 
-      .two-analysis {
-        position: absolute;
-        height: 100%;
-        width: 50%;
-        transition: all 0.3s;
-        transform: translateX(200%);
+        .fixed-analysis {
+          height: 110px;
+          line-height: 20px;
+          position: absolute;
+          width: 35px;
+          padding: 5px;
+          right: 0;
+          top: 50%;
+          transform: translateY(-50%);
+          border: 1px solid #E7E7E7;
+          background: #fff;
+          z-index: 9;
+          color: #787883;
+          cursor: pointer;
+          text-align: center;
+        }
       }
+    }
+
+    .modal {
+      position: fixed;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
 
-      .two-analysis.hide {
-        transform: translateX(100%);
+      >.mask {
+        background: #000;
+        opacity: .2;
+        width: 100%;
+        height: 100%;
       }
 
-      .fixed-analysis {
+      .body {
         position: absolute;
-        width: 35px;
-        padding: 5px;
-        right: 0;
+        left: 50%;
         top: 50%;
-        transform: translateY(-50%);
-        border: 1px solid #E7E7E7;
+        transform: translate(-50%, -50%);
         background: #fff;
-        z-index: 9;
-        color: #787883;
-        cursor: pointer;
-        text-align: center;
-      }
-    }
-  }
+        width: 630px;
+        color: #686872;
+        padding: 20px 30px;
 
-  .modal {
-    position: fixed;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-
-    .mask {
-      background: #000;
-      opacity: .2;
-      width: 100%;
-      height: 100%;
-    }
+        .title {
+          color: #303036;
+          font-size: 20px;
+          font-weight: 600;
+        }
 
-    .body {
-      position: absolute;
-      left: 50%;
-      top: 50%;
-      transform: translate(-50%, -50%);
-      background: #fff;
-      width: 630px;
-      color: #686872;
-      padding: 20px 30px;
-
-      .title {
-        color: #303036;
-        font-size: 20px;
-        font-weight: 600;
-      }
+        .desc {
+          color: #686872;
+          font-size: 16px;
+          padding: 20px 0;
 
-      .desc {
-        color: #686872;
-        font-size: 16px;
-        padding: 20px 0;
+          .select-inline {
+            margin-bottom: 15px;
 
-        .select {
-          margin-bottom: 15px;
-        }
+            .select {
+              display: inline-block;
+            }
+          }
 
-        .label {
-          margin-bottom: 5px;
+          .label {
+            margin-bottom: 5px;
+          }
         }
-      }
 
-      .textarea {
-        width: 570px;
-        height: 80px;
-        background: rgba(247, 247, 247, 1);
-        margin-bottom: 15px;
-        border: none;
-        padding: 5px 10px;
-      }
+        .textarea {
+          width: 570px;
+          height: 80px;
+          background: rgba(247, 247, 247, 1);
+          margin-bottom: 15px;
+          border: none;
+          padding: 5px 10px;
+        }
 
-      .textarea::placeholder {
-        color: #A7A7B7;
-      }
+        .textarea::placeholder {
+          color: #A7A7B7;
+        }
 
-      .bottom {
-        border-top: 1px solid #E1E1E1;
-        padding-top: 10px;
-        text-align: right;
+        .bottom {
+          border-top: 1px solid #E1E1E1;
+          padding-top: 10px;
+          text-align: right;
+        }
       }
     }
-  }
 
-  .modal.ask-ok,
-  .modal.error-ok {
-    .body {
-      .content {
-        width: 100%;
-        padding-top: 20px;
-        padding-bottom: 40px;
-        color: #686872;
-        overflow: hidden;
+    .modal.ask-ok,
+    .modal.error-ok {
+      .body {
+        .content {
+          width: 100%;
+          padding-top: 20px;
+          padding-bottom: 40px;
+          color: #686872;
+          overflow: hidden;
 
-        .left {
-          float: left;
-          width: 360px;
-          font-size: 18px;
+          .left {
+            float: left;
+            width: 360px;
+            font-size: 18px;
 
-          a {
-            padding-top: 30px;
-            display: inline-block;
-            font-size: 14px;
+            a {
+              padding-top: 30px;
+              display: inline-block;
+              font-size: 14px;
+            }
           }
-        }
 
-        .right {
-          float: right;
-          text-align: right;
-          font-size: 12px;
+          .right {
+            float: right;
+            text-align: right;
+            font-size: 12px;
+          }
         }
-      }
 
-      .confirm {
-        text-align: center;
-        padding-bottom: 10px;
+        .confirm {
+          text-align: center;
+          padding-bottom: 10px;
 
-        .answer-button.lager {
-          font-size: 16px;
+          .answer-button.lager {
+            font-size: 16px;
+          }
         }
       }
     }
-  }
 
-  .modal.note {
-    .body {
-      width: 720px;
+    .modal.note {
+      .body {
+        width: 720px;
 
-      .content {
-        padding-top: 20px;
+        .content {
+          padding-top: 20px;
 
-        .tabs {
-          display: inline-block;
-          width: 170px;
-          vertical-align: top;
-          margin-left: -30px;
-          margin-right: 30px;
-
-          .tab {
-            padding: 5px 0px 5px 40px;
-            line-height: 20px;
-            color: #686872;
-            margin-bottom: 30px;
-            cursor: pointer;
-            transition: all 0.3s;
-            border-top-right-radius: 25px;
-            border-bottom-right-radius: 25px;
-
-            .date {
-              font-size: 12px;
+          .tabs {
+            display: inline-block;
+            width: 170px;
+            vertical-align: top;
+            margin-left: -30px;
+            margin-right: 30px;
+
+            .tab {
+              padding: 5px 0px 5px 40px;
+              line-height: 20px;
+              color: #686872;
+              margin-bottom: 30px;
+              cursor: pointer;
+              transition: all 0.3s;
+              border-top-right-radius: 25px;
+              border-bottom-right-radius: 25px;
+
+              .date {
+                font-size: 12px;
+              }
             }
-          }
 
-          .tab.active,
-          .tab:hover {
-            color: #fff;
-            background: #4299FF;
+            .tab.active,
+            .tab:hover {
+              color: #fff;
+              background: #4299FF;
+            }
           }
-        }
 
-        .input {
-          display: inline-block;
+          .input {
+            display: inline-block;
 
-          .textarea {
-            width: 490px;
-            height: 350px;
-            margin-bottom: 20px;
+            .textarea {
+              width: 490px;
+              height: 350px;
+              margin-bottom: 20px;
+            }
           }
         }
       }

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

@@ -1,63 +1,267 @@
 import React from 'react';
+import ReactDOM from 'react-dom';
+import { Carousel } from 'antd';
 import { Link } from 'react-router-dom';
+import Fullscreen from 'react-fullscreen-crossbrowser';
 import './index.less';
 import Page from '@src/containers/Page';
+import { formatSeconds, formatPercent, formatDate, sortListWithOrder } from '@src/services/Tools';
+import Assets from '@src/components/Assets';
+import Navigation from '../../../components/Navigation';
 import Tabs from '../../../components/Tabs';
 import Icon from '../../../components/Icon';
 import Switch from '../../../components/Switch';
+import Select from '../../../components/Select';
+import AnswerSelect from '../../../components/AnswerSelect';
 import AnswerList from '../../../components/AnswerList';
 import AnswerButton from '../../../components/AnswerButton';
+import AnswerTable from '../../../components/AnswerTable';
 import OtherAnswer from '../../../components/OtherAnswer';
+import { AskTarget } from '../../../../Constant';
+import { Question } from '../../../stores/question';
+import { My } from '../../../stores/my';
+import Sentence from '../process/sentence';
 
 export default class extends Page {
-  constructor(props) {
-    super(props);
-    this.state = { hideAnalysis: true };
+  initState() {
+    return {
+      hideAnalysis: true,
+      analysisTab: 'official',
+      showAnswer: false,
+      noteField: AskTarget[0].key,
+      showIds: false,
+
+      question: {
+        content: {
+          typeset: 'one',
+        },
+        // questionType: 'awa',
+        answer: {
+          subject: [[{ text: 'like', uuid: 'hKyz' }]],
+          options: ['parallel'],
+        },
+        stem: "<p><span uuid='kBJe'>I</span> <span uuid='hKyz'>like</span> <span uuid='fQXh'>book</span></p>",
+      },
+      userQuestion: {
+        userAnswer: {
+          subject: [{ text: 'I', uuid: 'kBJe' }],
+          options: ['compare'],
+        },
+        no: 2,
+      },
+      paper: {
+        title: '长难句练习',
+        questionNumber: 20,
+      },
+      report: {
+        paperModule: 'sentence',
+      },
+
+    };
+  }
+
+  initData() {
+    const { id } = this.params;
+    Question.getDetailById(id).then(userQuestion => {
+      const { question, questionNos, paper, note, report, answer, setting } = userQuestion;
+      let { questionNo } = userQuestion;
+      if (!questionNo) ([questionNo] = questionNos);
+      if ((report.setting || {}).disorder) {
+        const { content } = question;
+        // 还原做题顺序
+        content.questions.forEach((q, i) => {
+          q.select = sortListWithOrder(question.select, setting.questions[i]);
+        });
+        answer.questions.forEach((q, i) => {
+          Object.keys(q).forEach((k) => {
+            if (q[k]) q[k] = sortListWithOrder(q[k], setting.questions[i]);
+          });
+        });
+        question.answerDistributed.forEach((q, i) => {
+          Object.keys(q).forEach((k) => {
+            if (q[k]) q[k] = sortListWithOrder(q[k], setting.questions[i]);
+          });
+        });
+      }
+      this.setState({ userQuestion, question, questionNo, note, paper });
+    });
+  }
+
+  prevQuestion() {
+    const { userQuestion } = this.state;
+    if (userQuestion.no === 1) return;
+    Question.getDetailByNo(userQuestion.reportId, userQuestion.no - 1).then((r) => {
+      linkTo(`/paper/question/${r.id}`);
+    });
+  }
+
+  nextQuestion() {
+    const { userQuestion } = this.state;
+    if (userQuestion.questionNumber === userQuestion.no) return;
+    Question.getDetailByNo(userQuestion.reportId, userQuestion.no + 1).then((r) => {
+      linkTo(`/paper/question/${r.id}`);
+    });
+  }
+
+  submitAsk() {
+    const { userQuestion = {}, ask = {} } = this.state;
+    if (ask.originContent === '' || ask.content === '' || ask.target === '') return;
+    My.addQuestionAsk(ask.target, userQuestion.questionModule, ask.originContent, ask.content).then(() => {
+      this.setState({ askModal: false, askOkModal: true });
+    }).catch(err => {
+      this.setState({ askError: err.message });
+    });
+  }
+
+  submitFeedbackError() {
+    const { feedback = {}, question = {}, questionNo = {} } = this.state;
+    if (feedback.originContent === '' || feedback.content === '' || feedback.target === '') return;
+    My.addFeedbackErrorQuestion(question.id, questionNo.title, feedback.target, feedback.originContent, feedback.content).then(() => {
+      this.setState({ feedbackModal: false, feedbackOkModal: true });
+    }).catch(err => {
+      this.setState({ feedbackError: err.message });
+    });
+  }
+
+  submitNote(close) {
+    const { userQuestion = {}, note = {} } = this.state;
+    My.updateQuestionNote(userQuestion.questionModule, userQuestion.questionNoId, note).then(() => {
+      if (close) this.setState({ noteModal: false });
+    }).catch(err => {
+      this.setState({ noteError: err.message });
+    });
+  }
+
+  toggleFullscreen() {
+    const { isFullscreenEnabled } = this.state;
+    this.setState({ isFullscreenEnabled: !isFullscreenEnabled });
+  }
+
+  toggleCollect() {
+    const { userQuestion = {} } = this.state;
+    if (!userQuestion.collect) {
+      My.addQuestionCollect(userQuestion.questionModule, userQuestion.questionNoId).then(() => {
+        userQuestion.collect = true;
+        this.setState({ userQuestion });
+      });
+    } else {
+      My.delQuestionCollect(userQuestion.questionModule, userQuestion.questionNoId).then(() => {
+        userQuestion.collect = false;
+        this.setState({ userQuestion });
+      });
+    }
+  }
+
+  formatStem(text) {
+    if (!text) return '';
+    const { question = { content: {} } } = this.state;
+    const { table = {}, questions = [] } = question.content;
+    text = text.replace(/#select#/g, "<span class='#select#' />");
+    text = text.replace(/#table#/g, "<span class='#table#' />");
+    setTimeout(() => {
+      const selectList = document.getElementsByClassName('#select#');
+      const tableList = document.getElementsByClassName('#table#');
+      for (let i = 0; i < selectList.length; i += 1) {
+        ReactDOM.render(
+          <AnswerSelect list={questions[i].select} onChange={v => this.onChangeQuestion(i, v)} />,
+          selectList[i],
+        );
+      }
+      for (let i = 0; i < tableList.length; i += 1) {
+        ReactDOM.render(<AnswerTable list={table.header} columns={table.header} data={table.data} />, tableList[i]);
+      }
+    }, 1);
+    return text;
   }
 
   renderView() {
     return (
-      <div className="layout">
-        <div className="layout-header">
-          <div className="left">
-            <div className="no">No.36</div>
-            <div className="title">OG18 - Easy (21-40) </div>
-          </div>
-          <div className="center">
-            ID:PREP 07-124
-            <Icon name="more" />
-          </div>
-          <div className="right">
-            <span className="b">
-              用时:<span className="s">1</span>m<span className="s">39</span>s
-            </span>
-            <span className="b">
-              全站:<span className="s">1</span>m<span className="s">39</span>s
-            </span>
-            <span className="b">
-              <span className="s">80</span>%
-            </span>
-            <Icon name="question" />
-            <Icon name="star" />
-          </div>
+      <Fullscreen
+        enabled={this.state.isFullscreenEnabled}
+        onChange={isFullscreenEnabled => this.setState({ isFullscreenEnabled })}
+      >
+        {this.renderDetail()}
+      </Fullscreen>
+    );
+  }
+
+  renderDetail() {
+    const { report = {} } = this.state;
+    switch (report.paperModule) {
+      case 'sentence':
+        return <Sentence {...this.state} flow={this} scene='answer' />;
+      default:
+        return <div className='base'>{this.renderBase()}</div>;
+    }
+  }
+
+  renderBase() {
+    const { questionStatus, userQuestion = {}, questionNo = {}, paper = {}, showIds, questionNos = [] } = this.state;
+
+    return <div className="layout" onClick={() => {
+      if (showIds) this.setState({ showIds: false });
+    }}>
+      <div className="layout-header">
+        <div className="left">
+          <div className="no">No.{userQuestion.no}</div>
+          <div className="title"><Assets name='book' />{paper.title}13</div>
         </div>
-        <div className="layout-body">{this.renderBody()}</div>
-        <div className="layout-footer">
-          <div className="left">
-            <Icon name="sceen-full" />
-          </div>
-          <div className="center">
-            <AnswerButton className="item">笔记</AnswerButton>
-            <AnswerButton className="item">提问</AnswerButton>
-            <AnswerButton className="item">纠错</AnswerButton>
-          </div>
-          <div className="right">
-            <Icon name="prev" />
-            <Icon name="next" />
+        <div className="center">
+          <div className="menu-wrap">
+            ID:{questionNo.title}
+            {questionNos && questionNos.length > 0 && <Icon name="more" onClick={() => {
+              this.setState({ showIds: true });
+            }} />}
+            {showIds && <div className='menu-content'>
+              <p>题源汇总</p>
+              {(questionNos || []).map((row) => <p>ID:{row.title}</p>)}
+            </div>}
           </div>
         </div>
+        <div className="right">
+          <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 */}
+          </span>
+          <span className="b">
+            全站:<span dangerouslySetInnerHTML={{ __html: formatSeconds(questionNo.totalTime / questionNo.totalNumber).replace(/([0-9]+)([msh])/g, '<span class="s">$1</span>$2') }} />
+            {/* 全站:<span className="s">1</span>m<span className="s">39</span>s */}
+          </span>
+          <span className="b">
+            <span className="s">{formatPercent(questionNo.totalCorrect, questionNo.totalNumber)}</span>%
+          </span>
+          <Icon name="question" />
+          <Icon name="star" active={userQuestion.collect} onClick={() => this.toggleCollect()} />
+        </div>
       </div>
-    );
+      <div className="layout-body">{this.renderBody()}</div>
+      <div className="layout-footer">
+        <div className="left">
+          <Icon name={this.state.isFullscreenEnabled ? 'sceen-restore' : 'sceen-full'} onClick={() => this.toggleFullscreen()} />
+        </div>
+        <div className="center">
+          <AnswerButton className="item" onClick={() => this.setState({ noteModal: true })}>笔记</AnswerButton>
+          <AnswerButton className="item" onClick={() => {
+            if (questionStatus) {
+              this.setState({ askModal: true });
+            } else {
+              this.setState({ askFailModal: true });
+            }
+          }}>提问</AnswerButton>
+          <AnswerButton className="item" onClick={() => this.setState({ feedbackModal: true })}>纠错</AnswerButton>
+        </div>
+        <div className="right">
+          <Icon name="prev" onClick={() => this.prevQuestion()} />
+          <Icon name="next" onClick={() => this.nextQuestion()} />
+        </div>
+      </div>
+      {this.state.askModal && this.renderAsk()}
+      {this.state.askOkModal && this.renderAskOk()}
+      {this.state.askFailModal && this.renderAskFail()}
+      {this.state.feedbackModal && this.renderFeedbackError()}
+      {this.state.feedbackOkModal && this.renderFeedbackErrorOk()}
+      {this.state.noteModal && this.renderNote()}
+    </div>;
   }
 
   renderBody() {
@@ -67,14 +271,15 @@ export default class extends Page {
     const show = typeset === 'one' ? true : !hideAnalysis;
     return (
       <div className="layout-content">
-        <div className={`${typeset}`}>
+        <div className='two'>
           {this.renderContent()}
-          {this.renderAnswer()}
+          {question.questionType !== 'awa' && this.renderAnswer()}
+          {question.questionType === 'awa' && this.renderAWA()}
         </div>
-        {this.renderAnalysis()}
-        {typeset === 'two' && (
+        {question.questionType !== 'awa' && this.renderAnalysis()}
+        {typeset === 'two' && question.questionType !== 'awa' && (
           <div className="fixed-analysis" onClick={() => this.setState({ hideAnalysis: !hideAnalysis })}>
-            {show ? '查看解析>' : '收起解析>'}
+            {show ? '收起解析 >' : '查看解析 <'}
           </div>
         )}
       </div>
@@ -82,39 +287,64 @@ export default class extends Page {
   }
 
   renderAnalysis() {
-    const { question = { content: {} } } = this.state;
+    const { question = { content: {} }, analysisTab } = this.state;
     const { typeset = 'one' } = question.content;
     const { hideAnalysis } = this.state;
     const show = typeset === 'one' ? true : !hideAnalysis;
     return (
-      <div className={`block block-analysis ${typeset}-analysis ${!show ? 'hide' : ''}`}>
+      <div className={`block block-analysis two-analysis ${show ? 'show' : ''}`}>
         <Tabs
           type="division"
-          active="1"
+          active={analysisTab}
           tabs={[
-            { key: '1', name: '官方解析', path: '/' },
-            { key: '2', name: '千行解析', path: '/' },
-            { key: '3', name: '题源联想', path: '/' },
-            { key: '4', name: '相关回答', path: '/' },
+            { key: 'official', name: '官方解析' },
+            { key: 'qx', name: '千行解析' },
+            { key: 'association', name: '题源联想' },
+            { key: 'qa', name: '相关回答' },
           ]}
+          onChange={(key) => {
+            this.setState({ analysisTab: key });
+          }}
         />
-        {this.renderText()}
+        <div className="detail">
+          {typeset === 'two' && this.renderAnswer()}
+          {this.renderText()}
+        </div>
       </div>
     );
   }
 
   renderText() {
-    return (
-      <div className="detail">
-        <div className="detail-block answer-block" />
-        <div className="detail-block text-block">
-          “Offering support services to spouses caring for their other halves may reduce martial stress and prevent
-          divorce at older ages,” she said. “But it’s also important to recognize that the pressure to divorce may be
-          health-related and that sick ex-wives may need additional care and services to prevent worsening health and
-          increased health costs.”
-        </div>
-      </div>
-    );
+    const { analysisTab, question = {}, userQuestion = {} } = this.state;
+    const { asks = [], associations = [] } = userQuestion;
+    let content;
+    switch (analysisTab) {
+      case 'official':
+        content = <div className="detail-block text-block" dangerouslySetInnerHTML={{ __html: question.officialContent }} />;
+        break;
+      case 'qx':
+        content = <div className="detail-block text-block" dangerouslySetInnerHTML={{ __html: question.qxContent }} />;
+        break;
+      case 'association':
+        content = <div className="detail-block">
+          <Carousel>
+            {associations.map(association => {
+              return <div className="text-block" dangerouslySetInnerHTML={{ __html: association.stem }} />;
+            })}
+          </Carousel>
+        </div>;
+        break;
+      case 'qa':
+        content = <div className="detail-block answer-block">
+          {asks.map(ask => {
+            return ask;
+          })}
+        </div>;
+        break;
+      default:
+        break;
+    }
+    return content;
   }
 
   renderOtherAnswer() {
@@ -129,50 +359,99 @@ export default class extends Page {
   }
 
   renderAnswer() {
-    const { question = { content: {} } } = this.state;
+    const { question = { content: {} }, showAnswer } = this.state;
+    const { questions = [] } = question.content;
     const { typeset = 'one' } = question.content;
-    return <div className="block block-answer">{typeset === 'two' ? <Switch>显示答案</Switch> : ''}</div>;
+    return <div className="block block-answer">
+      {typeset === 'two' ? <Switch checked={showAnswer} onChange={(value) => {
+        this.setState({ showAnswer: value });
+      }}>{showAnswer ? '显示答案' : '关闭答案'}</Switch> : ''}
+      {questions.map((item) => {
+        return (
+          <div>
+            <div className="text m-b-2">{item.description}</div>
+            <AnswerList
+              list={item.select}
+              type={question.type}
+              direction={question.direction}
+            />
+          </div>
+        );
+      })}
+    </div>;
   }
 
   renderContent() {
-    const { question = { content: {} } } = this.state;
+    const { question = { content: {} }, userQuestion = {}, showAnswer, step } = this.state;
+    const { userAnswer } = userQuestion;
     const { typeset = 'one' } = question.content;
+    const { steps = [] } = question.content;
+    console.log(userAnswer);
     return (
       <div className="block block-content">
-        {typeset === 'one' ? <Switch>显示答案</Switch> : ''}
-        <AnswerList
-          selected={1}
-          answer={2}
-          show
-          list={[
-            { text: ' They can become increasingly vulnerable to serious illness.' },
-            { text: ' They can become increasingly vulnerable to serious illness.' },
-            { text: ' They can become increasingly vulnerable to serious illness.' },
-            { text: ' They can become increasingly vulnerable to serious illness.' },
-          ]}
-        />
+        {typeset === 'one' && question.questionType !== 'awa' ? <Switch checked={showAnswer} onChange={(value) => {
+          this.setState({ showAnswer: value });
+        }}>{showAnswer ? '显示答案' : '关闭答案'}</Switch> : ''}
+        {question.questionType === 'awa' && <h2>Analytical Writing Assessment</h2>}
+        {steps.length > 0 && <Navigation list={question.content.steps} active={step} onChange={() => { }} />}
+        <div className="text" style={{ height: 2000 }} dangerouslySetInnerHTML={{ __html: this.formatStem(steps.length > 0 ? steps[step].stem : question.stem) }} />
       </div>
     );
   }
 
+  renderAWA() {
+    const { showAnswer, userQuestion = { detail: {}, userAnswer: {} } } = this.state;
+    return <div className="block block-awa">
+      <Switch checked={showAnswer} onChange={(value) => {
+        this.setState({ showAnswer: value });
+      }}>{showAnswer ? '显示答案' : '关闭答案'}</Switch>
+      <div className="body">
+        <h2>Your Response</h2>
+        {showAnswer && <div className='detail'>
+          <div className='info'>
+            <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 */}
+            </span>
+            <span className="b">
+              单词数:<span className="s">{Number((userQuestion.detail || {}).words || 0)}</span>词
+            </span>
+          </div>
+          <div className='content-awa' dangerouslySetInnerHTML={{ __html: userQuestion.userAnswer.awa || '' }} />
+        </div>}
+        {!showAnswer && <div className='show-awa'>选择「显示答案」查看自己的作文</div>}
+      </div>
+    </div>;
+  }
+
   renderAsk() {
+    const { ask = {} } = this.state;
     return (
       <div className="modal ask">
         <div className="mask" />
         <div className="body">
           <div className="title">提问</div>
           <div className="desc">
-            <div className="select">我想对进行提问</div>
+            <div className="select-inline">我想对<Select excludeSelf size="small" theme="white" value={ask.target} list={AskTarget} onChange={(item) => {
+              ask.target = item.value;
+              this.setState({ ask });
+            }} />进行提问</div>
             <div className="label">有疑问的具体内容是:</div>
-            <textarea className="textarea" placeholder="请复制粘贴有疑问的内容。" />
+            <textarea className="textarea" value={ask.originContent} placeholder="请复制粘贴有疑问的内容。" onChange={(e) => {
+              ask.originContent = e.target.value;
+              this.setState({ ask });
+            }} />
             <div className="label">针对以上内容的问题是:</div>
-            <textarea className="textarea" placeholder="提问频率高的问题会被优先回答哦。" />
+            <textarea className="textarea" value={ask.content} placeholder="提问频率高的问题会被优先回答哦。" onChange={(e) => {
+              ask.content = e.target.value;
+              this.setState({ ask });
+            }} />
           </div>
           <div className="bottom">
-            <AnswerButton theme="cancel" size="lager">
+            <AnswerButton theme="cancel" size="lager" onClick={() => this.setState({ askModal: false })}>
               取消
             </AnswerButton>
-            <AnswerButton size="lager">提交</AnswerButton>
+            <AnswerButton size="lager" onClick={() => this.submitAsk()}>提交</AnswerButton>
           </div>
         </div>
       </div>
@@ -188,8 +467,9 @@ export default class extends Page {
           <div className="content">
             <div className="left">
               <div className="text">已提交成功!</div>
-              <div className="text">感谢您的耐心反馈,我们会尽快核实并以站内信的方式告知结果。</div>
-              <div className="text">您也可以关注公众号及时获取结果。</div>
+              <div className="text">关注公众号,老师回答后会立即收到通知。</div>
+              <div className="text">我们也会通过站内信的方式通知你。</div>
+              <div className="small">成为学员享受极速答疑特权。<Link>了解更多</Link></div>
             </div>
             <div className="right">
               <div className="text">扫码关注公众号</div>
@@ -197,7 +477,9 @@ export default class extends Page {
             </div>
           </div>
           <div className="confirm">
-            <AnswerButton size="lager" theme="confirm">
+            <AnswerButton size="lager" theme="confirm" onClick={() => {
+              this.setState({ askOkModal: false });
+            }}>
               好的,知道了
             </AnswerButton>
           </div>
@@ -224,7 +506,9 @@ export default class extends Page {
             </div>
           </div>
           <div className="confirm">
-            <AnswerButton size="lager" theme="confirm">
+            <AnswerButton size="lager" theme="confirm" onClick={() => {
+              this.setState({ askFailModal: false });
+            }}>
               好的,知道了
             </AnswerButton>
           </div>
@@ -233,48 +517,61 @@ export default class extends Page {
     );
   }
 
-  renderError() {
+  renderFeedbackError() {
+    const { feedback = {} } = this.state;
     return (
       <div className="modal error">
         <div className="mask" />
         <div className="body">
           <div className="title">纠错</div>
           <div className="desc">
-            <div className="select">我想对进行提问</div>
-            <div className="label">有疑问的具体内容是:</div>
-            <textarea className="textarea" placeholder="请复制粘贴有疑问的内容。" />
-            <div className="label">针对以上内容的问题是:</div>
-            <textarea className="textarea" placeholder="提问频率高的问题会被优先回答哦。" />
+            <div className="select-inline">我想对<Select excludeSelf size="small" theme="white" value={feedback.target} list={AskTarget} onChange={(item) => {
+              feedback.target = item.value;
+              this.setState({ feedback });
+            }} />进行提问</div>
+            <div className="label">错误内容是:</div>
+            <textarea className="textarea" value={feedback.originContent} placeholder="你可以适当扩大复制范围以使我们准确定位,感谢。" />
+            <div className="label">应该改为:</div>
+            <textarea className="textarea" placeholder="只需提供正确内容即可" />
           </div>
           <div className="bottom">
-            <AnswerButton theme="cancel" size="lager">
+            <AnswerButton theme="cancel" size="lager" onClick={() => {
+              this.setState({ feedbackModal: false });
+            }}>
               取消
             </AnswerButton>
-            <AnswerButton size="lager">提交</AnswerButton>
+            <AnswerButton size="lager" onClick={() => {
+              this.submitFeedbackError();
+            }}>提交</AnswerButton>
           </div>
         </div>
       </div>
     );
   }
 
-  renderErrorOk() {
+  renderFeedbackErrorOk() {
     return (
       <div className="modal error-ok">
         <div className="mask" />
         <div className="body">
           <div className="title">纠错</div>
-          <div className="desc">
-            <div className="select">我想对进行提问</div>
-            <div className="label">有疑问的具体内容是:</div>
-            <textarea className="textarea" placeholder="请复制粘贴有疑问的内容。" />
-            <div className="label">针对以上内容的问题是:</div>
-            <textarea className="textarea" placeholder="提问频率高的问题会被优先回答哦。" />
+          <div className="content">
+            <div className="left">
+              <div className="text"><Assets name='right' svg />已提交成功!</div>
+              <div className="text">感谢您的耐心反馈,我们会尽快核实并以站内信的方式告知结果。</div>
+              <div className="text">您也可以关注公众号及时获取结果。</div>
+            </div>
+            <div className="right">
+              <div className="text">扫码关注公众号</div>
+              <div className="text">千行GMAT</div>
+            </div>
           </div>
-          <div className="bottom">
-            <AnswerButton theme="cancel" size="lager">
-              取消
+          <div className="confirm">
+            <AnswerButton size="lager" theme="confirm" onClick={() => {
+              this.setState({ feedbackOkModal: false });
+            }}>
+              好的,知道了
             </AnswerButton>
-            <AnswerButton size="lager">提交</AnswerButton>
           </div>
         </div>
       </div>
@@ -282,7 +579,7 @@ export default class extends Page {
   }
 
   renderNote() {
-    const { note = ['题目', '官方解析'] } = this.state;
+    const { noteField, note = {} } = this.state;
     return (
       <div className="modal note">
         <div className="mask" />
@@ -290,23 +587,35 @@ export default class extends Page {
           <div className="title">笔记</div>
           <div className="content">
             <div className="tabs">
-              {note.map(item => {
+              {AskTarget.map(item => {
                 return (
-                  <div className="tab">
-                    <div className="text">{item}</div>
-                    <div className="date">2019.05.13 15:30</div>
+                  <div className={`tab ${noteField === item.key ? 'active' : ''}`} onClick={() => {
+                    this.setState({ noteField: item.key });
+                  }}>
+                    <div className="text">{item.label}</div>
+                    <div className="date">{note[`${item.key}Time`] ? formatDate(note[`${item.key}Time`]) : ''}</div>
                   </div>
                 );
               })}
             </div>
             <div className="input">
-              <textarea className="textarea" placeholder="记下笔记,方便以后复习" />
+              <textarea className="textarea" value={note[`${noteField}Content`] || ''} placeholder="记下笔记,方便以后复习" onChange={(e) => {
+                note[`${noteField}Time`] = new Date();
+                note[`${noteField}Content`] = e.target.value;
+                this.setState({ note });
+              }} />
               <div className="bottom">
-                <AnswerButton theme="cancel" size="lager">
+                <AnswerButton theme="cancel" size="lager" onClick={() => {
+                  this.setState({ noteModal: false });
+                }}>
                   取消
                 </AnswerButton>
-                <AnswerButton size="lager">编辑</AnswerButton>
-                <AnswerButton size="lager">保存</AnswerButton>
+                <AnswerButton size="lager" onClick={() => {
+                  this.submitNote();
+                }}>编辑</AnswerButton>
+                <AnswerButton size="lager" onClick={() => {
+                  this.submitNote(true);
+                }}>保存</AnswerButton>
               </div>
             </div>
           </div>

+ 67 - 0
front/project/www/routes/paper/report/page.js

@@ -4,6 +4,7 @@ import LineChart from '@src/components/LineChart';
 import BarChart from '@src/components/BarChart';
 import PieChart from '@src/components/PieChart';
 import Page from '@src/containers/Page';
+import { Question } from '../../../stores/question';
 
 const lineOption = {
   title: {
@@ -209,7 +210,69 @@ const pieOption = {
 };
 
 export default class extends Page {
+  initData() {
+    const { id } = this.params;
+    Question.detailReport(id).then(result => {
+      switch (result.paperModule) {
+        case 'sentence':
+          this.refreshSentence(result);
+          break;
+        case 'textbook':
+          this.refreshTextbook(result);
+          break;
+        case 'exercise':
+          this.refreshExercise(result);
+          break;
+        case 'examination':
+          this.refreshExamination(result);
+          break;
+        default:
+      }
+      this.setState({ report: result });
+    });
+  }
+
+  refreshSentence() {
+
+  }
+
+  refreshTextbook() {
+
+  }
+
+  refreshExamination() {
+
+  }
+
+  refreshExercise() {
+
+  }
+
   renderView() {
+    const { report = {} } = this.state;
+    switch (report.paperModule) {
+      case 'sentence':
+        return this.renderSentence();
+      case 'textbook':
+        return this.renderTextbook();
+      case 'exercise':
+        return this.renderExercise();
+      case 'examination':
+        return this.renderExamination();
+      default:
+        return <div />;
+    }
+  }
+
+  renderSentence() {
+    return <div />;
+  }
+
+  renderTextbook() {
+    return <div />;
+  }
+
+  renderExercise() {
     return (
       <div>
         <div className="content">
@@ -223,4 +286,8 @@ export default class extends Page {
       </div>
     );
   }
+
+  renderExamination() {
+    return <div />;
+  }
 }

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

@@ -1,4 +1,3 @@
 import read from './read';
-import process from './process';
 
-export default [read, process];
+export default [read];

+ 2 - 2
front/project/www/routes/sentence/process/index.js

@@ -1,6 +1,6 @@
 export default {
-  path: '/sentence/:id',
-  key: 'sentence',
+  path: '/sentence/process/:id',
+  key: 'sentence-process',
   title: '长难句',
   needLogin: true,
   hideHeader: true,

+ 0 - 92
front/project/www/routes/sentence/process/page.js

@@ -1,92 +0,0 @@
-import React from 'react';
-import './index.less';
-import Page from '@src/containers/Page';
-import Assets from '@src/components/Assets';
-import Icon from '../../../components/Icon';
-import Button from '../../../components/Button';
-import Tabs from '../../../components/Tabs';
-import Progress from '../../../components/Progress';
-import HardInput from '../../../components/HardInput';
-import AnswerCheckbox from '../../../components/AnswerCheckbox';
-
-export default class extends Page {
-  constructor(props) {
-    super(props);
-    this.state = { hideAnalysis: true };
-  }
-
-  renderView() {
-    return (
-      <div className="layout">
-        <div className="layout-header">
-          <div className="left">
-            <div className="title">长难句练习 · Part2</div>
-          </div>
-          <div className="right">
-            <div className="text">Time cost 00:02</div>
-            <Icon name="star" />
-          </div>
-        </div>
-        {this.renderBody()}
-        <div className="layout-footer">
-          <div className="left">
-            <Icon name="sceen-full" />
-          </div>
-          <div className="center">
-            <div className="p">
-              <Progress theme="theme" progress={20} />
-            </div>
-            <div className="t">5/20</div>
-          </div>
-          <div className="right">
-            <Button size="lager" radius>
-              Next <Assets name="next_icon" />
-            </Button>
-          </div>
-        </div>
-      </div>
-    );
-  }
-
-  renderBody() {
-    return (
-      <div className="layout-body">
-        <div className="title">请分别找出句子中的主语,谓语和,并做出逻辑关系判断。</div>
-        <div className="desc">
-          of so—called cybersquatters, people who register the Internet domain names of high—profile companies in hopes
-          of reselling the rights to those names for a profit,
-        </div>
-        <div className="label">主语</div>
-        <div className="input">
-          <HardInput
-            selected
-            answer={0}
-            show
-            list={[{ text: 123 }, { text: 321 }]}
-            otherList={[{ text: 123 }, { text: 321 }]}
-          />
-        </div>
-        <div className="label">谓语</div>
-        <div className="input">
-          <HardInput />
-        </div>
-        <div className="label">宾语</div>
-        <div className="input">
-          <HardInput />
-        </div>
-        <div className="select">
-          <div className="select-title">本句存在以下哪种逻辑关系?(可多选)</div>
-          <AnswerCheckbox show list={[{ text: 123 }, { text: 321 }]} selected={1} answer={0} />
-        </div>
-        <div className="analysis">
-          <Tabs
-            type="division"
-            active="1"
-            tabs={[{ key: '1', name: '解析详情', path: '/' }, { key: '2', name: '中文语意', path: '/' }]}
-          />
-          <div className="result" />
-        </div>
-      </div>
-    );
-  }
-}

+ 1 - 2
front/project/www/routes/sentence/read/page.js

@@ -108,7 +108,6 @@ export default class extends Page {
   jumpPage(targetPage) {
     // 计算哪篇文章
     const { target, index, allow } = this.computeArticle(targetPage);
-    console.log(targetPage, target, index);
     if (!allow) {
       // todo 无法访问:非试用
       return;
@@ -182,7 +181,7 @@ export default class extends Page {
     const now = new Date();
     const time = (now.getTime() - this.lastTime.getTime()) / 1000;
     this.lastTime = now;
-    const progress = index + 1 * 100 / target.pages;
+    const progress = (index + 1) * 100 / target.pages;
     Sentence.updateProgress(target.chapter, target.part, progress, time, current.chapter, current.page);
     this.timeout = setTimeout(() => {
       // 最长5分钟阅读时间

+ 5 - 5
front/project/www/stores/my.js

@@ -187,7 +187,7 @@ export default class MyStore extends BaseStore {
    * @param {*} associationContent
    * @param {*} qaContent
    */
-  updateQuestionNote(questionModule, questionNoId, content, qxContent, officialContent, associationContent, qaContent) {
+  updateQuestionNote(questionModule, questionNoId, { content, qxContent, officialContent, associationContent, qaContent }) {
     return this.apiPut('/my/note/question', { questionModule, questionNoId, content, qxContent, officialContent, associationContent, qaContent });
   }
 
@@ -202,7 +202,7 @@ export default class MyStore extends BaseStore {
    * @param {*} order
    * @param {*} direction
    */
-  noteList(questionModule, questionType, page, size, startTime, endTime, order, direction) {
+  questionNoteList(questionModule, questionType, page, size, startTime, endTime, order, direction) {
     return this.apiGet('/my/note/question/list', { questionModule, questionType, page, size, startTime, endTime, order, direction });
   }
 
@@ -228,8 +228,8 @@ export default class MyStore extends BaseStore {
    * @param {*} questionNoId
    * @param {*} content
    */
-  addQuestionAsk(target, questionModule, questionNoId, content) {
-    return this.apiPost('/my/ask/question', { target, questionModule, questionNoId, content });
+  addQuestionAsk(target, questionModule, questionNoId, originContent, content) {
+    return this.apiPost('/my/ask/question', { target, questionModule, questionNoId, originContent, content });
   }
 
   /**
@@ -240,7 +240,7 @@ export default class MyStore extends BaseStore {
    * @param {*} originContent
    * @param {*} content
    */
-  addErrorQuestion(moduleId, title, position, originContent, content) {
+  addFeedbackErrorQuestion(moduleId, title, position, originContent, content) {
     return this.apiPost('/my/feedback/error/question', { moduleId, title, position, originContent, content });
   }
 

+ 22 - 10
front/project/www/stores/question.js

@@ -1,6 +1,18 @@
 import BaseStore from '@src/stores/base';
 
 export default class QuestionStore extends BaseStore {
+  startLink(type, item) {
+    linkTo(`/paper/process/${type}/${item.id}`);
+  }
+
+  continueLink(type, item) {
+    linkTo(`/paper/process/${type}/${item.id}?r=${item.report.id}`);
+  }
+
+  reportLink(item) {
+    linkTo(`/paper/report/${item.report.id}`);
+  }
+
   /**
    * 练习进度
    * @param {*} structId
@@ -26,7 +38,7 @@ export default class QuestionStore extends BaseStore {
    * @param {*} logicExtend
    * @param {*} finish: true完成,false未完成
    */
-  getExerciseList(page, size, structId, logic, logicExtend, finish) {
+  getExerciseList({ page, size, structId, logic, logicExtend, finish }) {
     return this.apiGet('/question/exercise/list', { page, size, structId, logic, logicExtend, times: finish ? 1 : null });
   }
 
@@ -57,12 +69,12 @@ export default class QuestionStore extends BaseStore {
   }
 
   /**
-   * 通过记录及序号获取详情
+   * 通过记录及序号获取基础信息
    * @param {*} userReportId
    * @param {*} no
    */
   getDetailByNo(userReportId, no) {
-    return this.apiGet('/question/detail', { userReportId, no });
+    return this.apiGet('/question/base', { userReportId, no });
   }
 
   /**
@@ -129,7 +141,7 @@ export default class QuestionStore extends BaseStore {
    * @param {*} disorder
    * @param {*} order: 模考
    */
-  start(type, paperId, disorder, order) {
+  start(type, paperId, { disorder, order }) {
     return this.apiPost(`/question/${type}/start`, { paperId, disorder, order });
   }
 
@@ -162,18 +174,18 @@ export default class QuestionStore extends BaseStore {
 
   /**
    * 继续考试
-   * @param {*} userPaperId
+   * @param {*} userReportId
    */
-  continue(userPaperId) {
-    return this.apiPost('/question/continue', { userPaperId });
+  continue(userReportId) {
+    return this.apiPost('/question/continue', { userReportId });
   }
 
   /**
    * 模考:下一阶段
    * @param {*} userPaperId
    */
-  stage(userPaperId) {
-    return this.apiPost('/question/stage', { userPaperId });
+  stage(userReportId) {
+    return this.apiPost('/question/stage', { userReportId });
   }
 
   /**
@@ -181,7 +193,7 @@ export default class QuestionStore extends BaseStore {
    * @param {*} userPaperId
    */
   restart(userPaperId) {
-    return this.apiPost('/question/restart', { userPaperId });
+    return this.apiPost('/question/restart/paper', { userPaperId });
   }
 
   /**

+ 2 - 2
front/src/layouts/TabRightLayout/index.js

@@ -1,8 +1,8 @@
 import React from 'react';
 import { Tabs } from 'antd';
-import { linkTo, getMap } from '../../services/Tools';
+import { getMap } from '../../services/Tools';
 
-export default function(props) {
+export default function (props) {
   const { tabs = [], active, rightElement } = props;
   const tabMap = getMap(tabs, 'key');
   return (

+ 41 - 8
front/src/services/Tools.js

@@ -193,7 +193,7 @@ export function search(data = [], key, value) {
 }
 
 export function formatSecond(value) {
-  let secondTime = parseInt(value, 10); // 秒
+  let secondTime = parseInt(value || 0, 10); // 秒
   let minuteTime = 0;
   let hourTime = 0;
   if (secondTime > 60) {
@@ -249,7 +249,7 @@ export function formatDate(time, format = 'YYYY-MM-DD HH:mm:ss') {
 }
 
 export function formatSeconds(seconds) {
-  const time = parseInt(seconds, 10);
+  const time = parseInt(seconds || 0, 10);
   if (time < 60) {
     return `${time}s`;
   }
@@ -259,9 +259,9 @@ export function formatSeconds(seconds) {
   return `${parseInt(time / 3600, 10)}h${formatSecond(time % 3600)}`;
 }
 
-export function formatPercent(child, mother) {
-  if (!mother || !child) return '0%';
-  return `${Math.floor((child * 100) / mother)}% `;
+export function formatPercent(child, mother, number = true) {
+  if (!mother || !child) return number ? 0 : '0%';
+  return number ? Math.floor((child * 100) / mother) : `${Math.floor((child * 100) / mother)}%`;
 }
 
 export function formatTreeData(list, key = 'id', title = 'title', index = 'parent_id') {
@@ -270,7 +270,7 @@ export function formatTreeData(list, key = 'id', title = 'title', index = 'paren
   list.forEach(row => {
     row.children = [];
     row.title = row[title];
-    if (!row.key) row.key = `${row[key]} `;
+    if (!row.key) row.key = `${row[key]}`;
     row.value = row[key];
   });
   list.forEach(row => {
@@ -291,10 +291,10 @@ export function flattenObject(ob, prefix = '') {
     if (typeof ob[i] === 'object' && ob[i] !== null) {
       const flatObject = flattenObject(ob[i]);
       Object.keys(flatObject).forEach(x => {
-        toReturn[`${prefix} ${i}.${x} `] = flatObject[x];
+        toReturn[`${prefix}${i}.${x}`] = flatObject[x];
       });
     } else {
-      toReturn[`${prefix} ${i} `] = ob[i];
+      toReturn[`${prefix}${i}`] = ob[i];
     }
   });
   return toReturn;
@@ -438,3 +438,36 @@ export function getSimpleText(html) {
   text = text.replace(new RegExp('<.+?>', 'g'), '');
   return text;
 }
+
+export function randomList(length) {
+  const list = [];
+  for (let i = 0; i < length; i += 1) {
+    list.push(i);
+  }
+  for (let i = 0; i < length; i += 1) {
+    const o = Math.floor(Math.random() * length);
+    const tmp = list[o];
+    list[o] = list[i];
+    list[i] = tmp;
+  }
+  return list;
+}
+
+export function sortListWithOrder(target, order) {
+  const list = [];
+  order.forEach((t) => {
+    list.push(target[t]);
+  });
+  return list;
+}
+
+export function resortListWithOrder(target, order) {
+  const list = [];
+  for (let i = 0; i < order.length; i += 1) {
+    list.push('');
+  }
+  order.forEach((t, i) => {
+    list[t] = target[i];
+  });
+  return list;
+}

+ 20 - 0
server/data/src/main/java/com/qxgmat/data/constants/enums/module/PaperModule.java

@@ -18,6 +18,26 @@ public enum PaperModule {
     }
 
     /**
+     * 根据origin类型判断组卷类型
+     * @param origin
+     * @return
+     */
+    public static PaperModule WithOrigin(PaperOrigin origin){
+        switch(origin){
+            case SENTENCE:
+                return SENTENCE;
+            case TEXTBOOK:
+                return TEXTBOOK;
+            case EXERCISE:
+                return EXERCISE;
+            case EXAMINATION:
+                return EXAMINATION;
+            default:
+                return null;
+        }
+    }
+
+    /**
      * 根据question模块判断paper模块
      * @param module
      * @return

+ 12 - 12
server/data/src/main/java/com/qxgmat/data/dao/entity/PreviewPaper.java

@@ -44,8 +44,8 @@ public class PreviewPaper implements Serializable {
     /**
      * 题目类型
      */
-    @Column(name = "`question_type`")
-    private String questionType;
+    @Column(name = "`paper_module`")
+    private String paperModule;
 
     /**
      * 制定作业用户id:json
@@ -192,19 +192,19 @@ public class PreviewPaper implements Serializable {
     /**
      * 获取题目类型
      *
-     * @return question_type - 题目类型
+     * @return paper_module - 题目类型
      */
-    public String getQuestionType() {
-        return questionType;
+    public String getPaperModule() {
+        return paperModule;
     }
 
     /**
      * 设置题目类型
      *
-     * @param questionType 题目类型
+     * @param paperModule 题目类型
      */
-    public void setQuestionType(String questionType) {
-        this.questionType = questionType;
+    public void setPaperModule(String paperModule) {
+        this.paperModule = paperModule;
     }
 
     /**
@@ -337,7 +337,7 @@ public class PreviewPaper implements Serializable {
         sb.append(", courseNo=").append(courseNo);
         sb.append(", questionNoIds=").append(questionNoIds);
         sb.append(", mode=").append(mode);
-        sb.append(", questionType=").append(questionType);
+        sb.append(", paperModule=").append(paperModule);
         sb.append(", userIds=").append(userIds);
         sb.append(", time=").append(time);
         sb.append(", startTime=").append(startTime);
@@ -421,10 +421,10 @@ public class PreviewPaper implements Serializable {
         /**
          * 设置题目类型
          *
-         * @param questionType 题目类型
+         * @param paperModule 题目类型
          */
-        public Builder questionType(String questionType) {
-            obj.setQuestionType(questionType);
+        public Builder paperModule(String paperModule) {
+            obj.setPaperModule(paperModule);
             return this;
         }
 

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

@@ -78,6 +78,12 @@ public class UserAskQuestion implements Serializable {
     private Date updateTime;
 
     /**
+     * 问题内容
+     */
+    @Column(name = "`origin_content`")
+    private String originContent;
+
+    /**
      * 提问
      */
     @Column(name = "`content`")
@@ -314,6 +320,24 @@ public class UserAskQuestion implements Serializable {
     }
 
     /**
+     * 获取问题内容
+     *
+     * @return origin_content - 问题内容
+     */
+    public String getOriginContent() {
+        return originContent;
+    }
+
+    /**
+     * 设置问题内容
+     *
+     * @param originContent 问题内容
+     */
+    public void setOriginContent(String originContent) {
+        this.originContent = originContent;
+    }
+
+    /**
      * 获取提问
      *
      * @return content - 提问
@@ -368,6 +392,7 @@ public class UserAskQuestion implements Serializable {
         sb.append(", order=").append(order);
         sb.append(", createTime=").append(createTime);
         sb.append(", updateTime=").append(updateTime);
+        sb.append(", originContent=").append(originContent);
         sb.append(", content=").append(content);
         sb.append(", answer=").append(answer);
         sb.append("]");
@@ -520,6 +545,16 @@ public class UserAskQuestion implements Serializable {
         }
 
         /**
+         * 设置问题内容
+         *
+         * @param originContent 问题内容
+         */
+        public Builder originContent(String originContent) {
+            obj.setOriginContent(originContent);
+            return this;
+        }
+
+        /**
          * 设置提问
          *
          * @param content 提问

+ 31 - 31
server/data/src/main/java/com/qxgmat/data/dao/entity/UserNoteQuestion.java

@@ -35,8 +35,8 @@ public class UserNoteQuestion implements Serializable {
     @Column(name = "`question_no_id`")
     private Integer questionNoId;
 
-    @Column(name = "`content_time`")
-    private Date contentTime;
+    @Column(name = "`question_time`")
+    private Date questionTime;
 
     @Column(name = "`official_time`")
     private Date officialTime;
@@ -59,8 +59,8 @@ public class UserNoteQuestion implements Serializable {
     /**
      * 笔记内容
      */
-    @Column(name = "`content`")
-    private String content;
+    @Column(name = "`question_content`")
+    private String questionContent;
 
     @Column(name = "`official_content`")
     private String officialContent;
@@ -163,17 +163,17 @@ public class UserNoteQuestion implements Serializable {
     }
 
     /**
-     * @return content_time
+     * @return question_time
      */
-    public Date getContentTime() {
-        return contentTime;
+    public Date getQuestionTime() {
+        return questionTime;
     }
 
     /**
-     * @param contentTime
+     * @param questionTime
      */
-    public void setContentTime(Date contentTime) {
-        this.contentTime = contentTime;
+    public void setQuestionTime(Date questionTime) {
+        this.questionTime = questionTime;
     }
 
     /**
@@ -263,19 +263,19 @@ public class UserNoteQuestion implements Serializable {
     /**
      * 获取笔记内容
      *
-     * @return content - 笔记内容
+     * @return question_content - 笔记内容
      */
-    public String getContent() {
-        return content;
+    public String getQuestionContent() {
+        return questionContent;
     }
 
     /**
      * 设置笔记内容
      *
-     * @param content 笔记内容
+     * @param questionContent 笔记内容
      */
-    public void setContent(String content) {
-        this.content = content;
+    public void setQuestionContent(String questionContent) {
+        this.questionContent = questionContent;
     }
 
     /**
@@ -345,14 +345,14 @@ public class UserNoteQuestion implements Serializable {
         sb.append(", questionModule=").append(questionModule);
         sb.append(", questionId=").append(questionId);
         sb.append(", questionNoId=").append(questionNoId);
-        sb.append(", contentTime=").append(contentTime);
+        sb.append(", questionTime=").append(questionTime);
         sb.append(", officialTime=").append(officialTime);
         sb.append(", qxTime=").append(qxTime);
         sb.append(", associationTime=").append(associationTime);
         sb.append(", qaTime=").append(qaTime);
         sb.append(", createTime=").append(createTime);
         sb.append(", updateTime=").append(updateTime);
-        sb.append(", content=").append(content);
+        sb.append(", questionContent=").append(questionContent);
         sb.append(", officialContent=").append(officialContent);
         sb.append(", qxContent=").append(qxContent);
         sb.append(", associationContent=").append(associationContent);
@@ -421,20 +421,10 @@ public class UserNoteQuestion implements Serializable {
         }
 
         /**
-         * 设置笔记内容
-         *
-         * @param content 笔记内容
-         */
-        public Builder content(String content) {
-            obj.setContent(content);
-            return this;
-        }
-
-        /**
-         * @param contentTime
+         * @param questionTime
          */
-        public Builder contentTime(Date contentTime) {
-            obj.setContentTime(contentTime);
+        public Builder questionTime(Date questionTime) {
+            obj.setQuestionTime(questionTime);
             return this;
         }
 
@@ -487,6 +477,16 @@ public class UserNoteQuestion implements Serializable {
         }
 
         /**
+         * 设置笔记内容
+         *
+         * @param questionContent 笔记内容
+         */
+        public Builder questionContent(String questionContent) {
+            obj.setQuestionContent(questionContent);
+            return this;
+        }
+
+        /**
          * @param officialContent
          */
         public Builder officialContent(String officialContent) {

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

@@ -66,6 +66,12 @@ public class UserPaper implements Serializable {
     private Integer time;
 
     /**
+     * 最近一次做题记录
+     */
+    @Column(name = "`latest_time`")
+    private Date latestTime;
+
+    /**
      * 总作答时间
      */
     @Column(name = "`total_time`")
@@ -274,6 +280,24 @@ public class UserPaper implements Serializable {
     }
 
     /**
+     * 获取最近一次做题记录
+     *
+     * @return latest_time - 最近一次做题记录
+     */
+    public Date getLatestTime() {
+        return latestTime;
+    }
+
+    /**
+     * 设置最近一次做题记录
+     *
+     * @param latestTime 最近一次做题记录
+     */
+    public void setLatestTime(Date latestTime) {
+        this.latestTime = latestTime;
+    }
+
+    /**
      * 获取总作答时间
      *
      * @return total_time - 总作答时间
@@ -379,6 +403,7 @@ public class UserPaper implements Serializable {
         sb.append(", questionNumber=").append(questionNumber);
         sb.append(", times=").append(times);
         sb.append(", time=").append(time);
+        sb.append(", latestTime=").append(latestTime);
         sb.append(", totalTime=").append(totalTime);
         sb.append(", totalNumber=").append(totalNumber);
         sb.append(", totalCorrect=").append(totalCorrect);
@@ -498,6 +523,16 @@ public class UserPaper implements Serializable {
         }
 
         /**
+         * 设置最近一次做题记录
+         *
+         * @param latestTime 最近一次做题记录
+         */
+        public Builder latestTime(Date latestTime) {
+            obj.setLatestTime(latestTime);
+            return this;
+        }
+
+        /**
          * 设置总作答时间
          *
          * @param totalTime 总作答时间

+ 38 - 3
server/data/src/main/java/com/qxgmat/data/dao/entity/UserSentenceProgress.java

@@ -3,7 +3,7 @@ package com.qxgmat.data.dao.entity;
 import java.io.Serializable;
 import javax.persistence.*;
 
-@Table(name = "user_sentence_process")
+@Table(name = "user_sentence_progress")
 public class UserSentenceProgress implements Serializable {
     @Id
     @Column(name = "`id`")
@@ -34,6 +34,12 @@ public class UserSentenceProgress implements Serializable {
     @Column(name = "`progress`")
     private Integer progress;
 
+    /**
+     * 次数
+     */
+    @Column(name = "`times`")
+    private Integer times;
+
     private static final long serialVersionUID = 1L;
 
     /**
@@ -107,7 +113,7 @@ public class UserSentenceProgress implements Serializable {
     /**
      * 获取进度:0-100
      *
-     * @return process - 进度:0-100
+     * @return progress - 进度:0-100
      */
     public Integer getProgress() {
         return progress;
@@ -122,6 +128,24 @@ public class UserSentenceProgress implements Serializable {
         this.progress = progress;
     }
 
+    /**
+     * 获取次数
+     *
+     * @return times - 次数
+     */
+    public Integer getTimes() {
+        return times;
+    }
+
+    /**
+     * 设置次数
+     *
+     * @param times 次数
+     */
+    public void setTimes(Integer times) {
+        this.times = times;
+    }
+
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();
@@ -132,7 +156,8 @@ public class UserSentenceProgress implements Serializable {
         sb.append(", userId=").append(userId);
         sb.append(", chapter=").append(chapter);
         sb.append(", part=").append(part);
-        sb.append(", process=").append(progress);
+        sb.append(", progress=").append(progress);
+        sb.append(", times=").append(times);
         sb.append("]");
         return sb.toString();
     }
@@ -196,6 +221,16 @@ public class UserSentenceProgress implements Serializable {
             return this;
         }
 
+        /**
+         * 设置次数
+         *
+         * @param times 次数
+         */
+        public Builder times(Integer times) {
+            obj.setTimes(times);
+            return this;
+        }
+
         public UserSentenceProgress build() {
             return this.obj;
         }

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

@@ -11,7 +11,7 @@
     <result column="course_no" jdbcType="INTEGER" property="courseNo" />
     <result column="question_no_ids" jdbcType="VARCHAR" property="questionNoIds" typeHandler="com.nuliji.tools.mybatis.handler.IntegerArrayWithJsonHandler" />
     <result column="mode" jdbcType="INTEGER" property="mode" />
-    <result column="question_type" jdbcType="VARCHAR" property="questionType" />
+    <result column="paper_module" jdbcType="VARCHAR" property="paperModule" />
     <result column="user_ids" jdbcType="VARCHAR" property="userIds" typeHandler="com.nuliji.tools.mybatis.handler.IntegerArrayWithJsonHandler" />
     <result column="time" jdbcType="INTEGER" property="time" />
     <result column="start_time" jdbcType="TIMESTAMP" property="startTime" />
@@ -24,7 +24,7 @@
     <!--
       WARNING - @mbg.generated
     -->
-    `id`, `title`, `course_id`, `course_no`, `question_no_ids`, `mode`, `question_type`, 
+    `id`, `title`, `course_id`, `course_no`, `question_no_ids`, `mode`, `paper_module`, 
     `user_ids`, `time`, `start_time`, `end_time`, `finish`, `create_time`, `update_time`
   </sql>
 </mapper>

+ 2 - 1
server/data/src/main/java/com/qxgmat/data/dao/mapping/UserAskQuestionMapper.xml

@@ -23,6 +23,7 @@
     <!--
       WARNING - @mbg.generated
     -->
+    <result column="origin_content" jdbcType="LONGVARCHAR" property="originContent" />
     <result column="content" jdbcType="LONGVARCHAR" property="content" />
     <result column="answer" jdbcType="LONGVARCHAR" property="answer" />
   </resultMap>
@@ -37,6 +38,6 @@
     <!--
       WARNING - @mbg.generated
     -->
-    `content`, `answer`
+    `origin_content`, `content`, `answer`
   </sql>
 </mapper>

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

@@ -10,7 +10,7 @@
     <result column="question_module" jdbcType="VARCHAR" property="questionModule" />
     <result column="question_id" jdbcType="INTEGER" property="questionId" />
     <result column="question_no_id" jdbcType="INTEGER" property="questionNoId" />
-    <result column="content_time" jdbcType="TIMESTAMP" property="contentTime" />
+    <result column="question_time" jdbcType="TIMESTAMP" property="questionTime" />
     <result column="official_time" jdbcType="TIMESTAMP" property="officialTime" />
     <result column="qx_time" jdbcType="TIMESTAMP" property="qxTime" />
     <result column="association_time" jdbcType="TIMESTAMP" property="associationTime" />
@@ -22,7 +22,7 @@
     <!--
       WARNING - @mbg.generated
     -->
-    <result column="content" jdbcType="LONGVARCHAR" property="content" />
+    <result column="question_content" jdbcType="LONGVARCHAR" property="questionContent" />
     <result column="official_content" jdbcType="LONGVARCHAR" property="officialContent" />
     <result column="qx_content" jdbcType="LONGVARCHAR" property="qxContent" />
     <result column="association_content" jdbcType="LONGVARCHAR" property="associationContent" />
@@ -32,13 +32,13 @@
     <!--
       WARNING - @mbg.generated
     -->
-    `id`, `user_id`, `question_module`, `question_id`, `question_no_id`, `content_time`, 
+    `id`, `user_id`, `question_module`, `question_id`, `question_no_id`, `question_time`, 
     `official_time`, `qx_time`, `association_time`, `qa_time`, `create_time`, `update_time`
   </sql>
   <sql id="Blob_Column_List">
     <!--
       WARNING - @mbg.generated
     -->
-    `content`, `official_content`, `qx_content`, `association_content`, `qa_content`
+    `question_content`, `official_content`, `qx_content`, `association_content`, `qa_content`
   </sql>
 </mapper>

+ 3 - 2
server/data/src/main/java/com/qxgmat/data/dao/mapping/UserPaperMapper.xml

@@ -15,6 +15,7 @@
     <result column="question_number" jdbcType="INTEGER" property="questionNumber" />
     <result column="times" jdbcType="INTEGER" property="times" />
     <result column="time" jdbcType="INTEGER" property="time" />
+    <result column="latest_time" jdbcType="DATE" property="latestTime" />
     <result column="total_time" jdbcType="INTEGER" property="totalTime" />
     <result column="total_number" jdbcType="INTEGER" property="totalNumber" />
     <result column="total_correct" jdbcType="INTEGER" property="totalCorrect" />
@@ -26,7 +27,7 @@
       WARNING - @mbg.generated
     -->
     `id`, `user_id`, `title`, `paper_module`, `paper_origin`, `origin_id`, `question_no_ids`, 
-    `question_number`, `times`, `time`, `total_time`, `total_number`, `total_correct`, 
-    `delete_time`, `is_reset`
+    `question_number`, `times`, `time`, `latest_time`, `total_time`, `total_number`, 
+    `total_correct`, `delete_time`, `is_reset`
   </sql>
 </mapper>

+ 2 - 1
server/data/src/main/java/com/qxgmat/data/dao/mapping/UserSentenceProgressMapper.xml

@@ -10,11 +10,12 @@
     <result column="chapter" jdbcType="INTEGER" property="chapter" />
     <result column="part" jdbcType="INTEGER" property="part" />
     <result column="progress" jdbcType="INTEGER" property="progress" />
+    <result column="times" jdbcType="INTEGER" property="times" />
   </resultMap>
   <sql id="Base_Column_List">
     <!--
       WARNING - @mbg.generated
     -->
-    `id`, `user_id`, `chapter`, `part`, `progress`
+    `id`, `user_id`, `chapter`, `part`, `progress`, `times`
   </sql>
 </mapper>

+ 18 - 0
server/data/src/main/java/com/qxgmat/data/relation/ExaminationPaperRelationMapper.java

@@ -0,0 +1,18 @@
+package com.qxgmat.data.relation;
+
+import com.qxgmat.data.dao.entity.ExercisePaper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * Created by gaojie on 2017/11/9.
+ */
+public interface ExaminationPaperRelationMapper {
+
+    List<ExercisePaper> listWithUser(
+            @Param("structId") Number structId,
+            @Param("userId") Number userId,
+            @Param("times") Integer times
+    );
+}

+ 8 - 0
server/data/src/main/java/com/qxgmat/data/relation/ExercisePaperRelationMapper.java

@@ -13,4 +13,12 @@ public interface ExercisePaperRelationMapper {
     List<ExercisePaper> groupPlace(
             @Param("structId") Number structId
     );
+
+    List<ExercisePaper> listWithUser(
+            @Param("structId") Number structId,
+            @Param("userId") Number userId,
+            @Param("logic") String logic,
+            @Param("logicExtend") String logicExtend,
+            @Param("times") Integer times
+    );
 }

+ 1 - 1
server/data/src/main/java/com/qxgmat/data/relation/QuestionNoRelationMapper.java

@@ -18,7 +18,7 @@ public interface QuestionNoRelationMapper {
             @Param("id") Number questionNoId,
             @Param("number") Integer number,
             @Param("time") Integer time,
-            @Param("current") Integer current
+            @Param("correct") Integer correct
     );
 
     List<QuestionNo> searchStem(

+ 1 - 1
server/data/src/main/java/com/qxgmat/data/relation/QuestionRelationMapper.java

@@ -14,6 +14,6 @@ public interface QuestionRelationMapper {
             @Param("id") Number questionId,
             @Param("number") Integer number,
             @Param("time") Integer time,
-            @Param("current") Integer current
+            @Param("correct") Integer correct
     );
 }

+ 1 - 1
server/data/src/main/java/com/qxgmat/data/relation/SentenceQuestionRelationMapper.java

@@ -11,6 +11,6 @@ public interface SentenceQuestionRelationMapper {
             @Param("id") Number sentenceQuestionId,
             @Param("number") Integer number,
             @Param("time") Integer time,
-            @Param("current") Integer current
+            @Param("correct") Integer correct
     );
 }

+ 1 - 1
server/data/src/main/java/com/qxgmat/data/relation/TextbookQuestionRelationMapper.java

@@ -14,7 +14,7 @@ public interface TextbookQuestionRelationMapper {
             @Param("id") Number textbookQuestionId,
             @Param("number") Integer number,
             @Param("time") Integer time,
-            @Param("current") Integer current
+            @Param("correct") Integer correct
     );
 
     List<TextbookQuestion> listAdmin(

+ 4 - 10
server/data/src/main/java/com/qxgmat/data/relation/UserPaperRelationMapper.java

@@ -5,6 +5,7 @@ import com.qxgmat.data.relation.entity.UserStudyStatRelation;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.Collection;
+import java.util.Date;
 import java.util.List;
 
 /**
@@ -32,19 +33,12 @@ public interface UserPaperRelationMapper {
             @Param("top") Number top
     );
 
-    List<UserPaper> listExercisePaper(
-            @Param("structId") Number structId,
-            @Param("userId") Number userId,
-            @Param("logic") String logic,
-            @Param("logicExtend") String logicExtend,
-            @Param("times") Integer times
-    );
-
     void accumulation(
             @Param("id") Number userPaperId,
             @Param("number") Integer number,
             @Param("time") Integer time,
-            @Param("current") Integer current,
-            @Param("times") Integer times
+            @Param("correct") Integer correct,
+            @Param("times") Integer times,
+            @Param("latestTime") Date latestTime
     );
 }

+ 1 - 1
server/data/src/main/java/com/qxgmat/data/relation/UserReportRelationMapper.java

@@ -18,7 +18,7 @@ public interface UserReportRelationMapper {
             @Param("id") Number reportId,
             @Param("number") Integer number,
             @Param("time") Integer time,
-            @Param("current") Integer current
+            @Param("correct") Integer correct
     );
 
     List<UserReport> listFinishLast(

+ 40 - 0
server/data/src/main/java/com/qxgmat/data/relation/mapping/ExaminationPaperRelationMapper.xml

@@ -0,0 +1,40 @@
+<?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.ExaminationPaperRelationMapper">
+  <resultMap id="IdMap" type="com.qxgmat.data.dao.entity.ExaminationPaper">
+    <!--
+      WARNING - @mbg.generated
+    -->
+    <id column="id" jdbcType="INTEGER" property="id" />
+  </resultMap>
+  <sql id="Id_Column_List">
+    <!--
+      WARNING - @mbg.generated
+    -->
+    ep.`id`
+  </sql>
+
+  <!--
+    用户练习-练习册列表: 用户端
+  -->
+  <select id="listWithUser" resultMap="IdMap">
+    select
+    <include refid="Id_Column_List" />
+    from `examination_paper` ep
+    <if test="userId != null">
+    left join `user_paper` up on ep.`id` = up.`origin_id`
+      and up.`paper_origin` = 'examination'
+      and up.`user_id` = #{userId,jdbcType=VARCHAR}
+      <if test="times != null">
+        and up.`times` >= #{times,jdbcType=VARCHAR}
+      </if>
+    </if>
+    where 1
+    <if test="structId != null">
+      and ep.`struct_three` = #{structId,jdbcType=VARCHAR} or ep.`struct_four` = #{structId,jdbcType=VARCHAR}
+    </if>
+    <if test="userId != null">
+      and up.`id` != null
+    </if>
+  </select>
+</mapper>

+ 32 - 2
server/data/src/main/java/com/qxgmat/data/relation/mapping/ExercisePaperRelationMapper.xml

@@ -17,12 +17,42 @@
   <!--获取考点分组所有考点信息-->
   <select id="groupPlace" resultMap="IdMap">
     select
-    <include refid="Id_Column_List" />
+    ep.`logic_extend`
     from `exercise_paper` ep
     where ep.`logic` = "place" and ep.`status` = 1
     <if test="structId != null">
-      and ep.`struct_three` = #{structId,jdbcType=VARCHAR}
+      and (ep.`struct_three` = #{structId,jdbcType=VARCHAR} or ep.`struct_four` = #{structId,jdbcType=VARCHAR})
     </if>
     group by ep.`logic_extend`
   </select>
+
+  <!--
+    用户练习-练习册列表: 用户端
+  -->
+  <select id="listWithUser" resultMap="IdMap">
+    select
+    <include refid="Id_Column_List" />
+    from `exercise_paper` ep
+    <if test="userId != null">
+    left join `user_paper` up on ep.`id` = up.`origin_id`
+      and up.`paper_origin` = 'exercise'
+      and up.`user_id` = #{userId,jdbcType=VARCHAR}
+      <if test="times != null">
+        and up.`times` >= #{times,jdbcType=VARCHAR}
+      </if>
+    </if>
+    where 1
+    <if test="structId != null">
+      and ep.`struct_three` = #{structId,jdbcType=VARCHAR} or ep.`struct_four` = #{structId,jdbcType=VARCHAR}
+    </if>
+    <if test="userId != null">
+      and up.`id` &gt; 0
+    </if>
+    <if test="logic != null">
+      and ep.`logic` = #{logic,jdbcType=VARCHAR}
+      <if test="logicExtend != null">
+        and ep.`logic_extend` = #{logicExtend,jdbcType=VARCHAR}
+      </if>
+    </if>
+  </select>
 </mapper>

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

@@ -43,7 +43,7 @@
     left join `question` q on q.`id` = qn.`question_id`
       and MATCH (q.`description`) AGAINST (#{stem, jdbcType=VARCHAR} IN NATURAL LANGUAGE MODE) > 0.8
     where
-      q.`id` != null
+      q.`id` &gt; 0
   </select>
 
   <!--
@@ -102,15 +102,15 @@
   <select id="listExerciseAdmin" resultMap="IdMap">
     select
     <include refid="Id_Column_List" />
-    form `question_no` qn
+    from `question_no` qn
     left join `question` q on q.`id` = qn.`question_id`
     <if test="paperId != null">
       left join `exercise_paper` ep on find_in_set(qn.`id`, trim(TRAILING ']' from trim(LEADING '[' from eq.`question_no_ids`)))
       and ep.`id` = #{paperId,jdbcType=VARCHAR}
     </if>
-    where qn.`module` = "exercise"
+    where qn.`module` = "exercise" and q.`id` &gt; 0 and qn.`delete_time` = null
     <if test="paperId != null">
-      and ep.`id` != null
+      and ep.`id` &gt; 0
     </if>
     <if test="structId != null">
       and find_in_set(#{structId,jdbcType=VARCHAR}, qn.`module_struct`)
@@ -142,15 +142,15 @@
   <select id="listExaminationAdmin" resultMap="IdMap">
     select
     <include refid="Id_Column_List" />
-    form `question_no` qn
+    from `question_no` qn
     left join `question` q on q.`id` = qn.`question_id`
     <if test="paperId != null">
       left join `examination_paper` ep on find_in_set(qn.`id`, trim(TRAILING ']' from trim(LEADING '[' from eq.`question_no_ids`)))
       and ep.`id` = #{paperId,jdbcType=VARCHAR}
     </if>
-    where qn.`module` = "examination"
+    where qn.`module` = "examination" and q.`id` &gt; 0 and qn.`delete_time` = null
     <if test="paperId != null">
-      and ep.`id` != null
+      and ep.`id` &gt; 0
     </if>
     <if test="structId != null">
       and find_in_set(#{structId,jdbcType=VARCHAR}, qn.`module_struct`)

+ 1 - 1
server/data/src/main/java/com/qxgmat/data/relation/mapping/QuestionRelationMapper.xml

@@ -16,7 +16,7 @@
 
   <!--累加做题记录-->
   <update id="accumulation">
-    UPDATE `question_no`
+    UPDATE `question`
     <trim prefix="set" suffixOverrides=",">
       `total_number`=`total_number`+#{number, jdbcType=INTEGER},
       `total_time`=`total_time`+#{time, jdbcType=INTEGER},

+ 1 - 1
server/data/src/main/java/com/qxgmat/data/relation/mapping/UserAskCourseRelationMapper.xml

@@ -40,7 +40,7 @@
       </if>
     left join `question` q on q.`id` = ua.`question_id`
     where
-    u.`id` != null
+    u.`id` &gt; 0
     <if test="questionNoId != null">
       and ua.`question_no_id` = #{questionNoId,jdbcType=VARCHAR}
     </if>

+ 1 - 1
server/data/src/main/java/com/qxgmat/data/relation/mapping/UserAskQuestionRelationMapper.xml

@@ -40,7 +40,7 @@
       </if>
     left join `question` q on q.`id` = ua.`question_id`
     where
-    u.`id` != null
+    u.`id` &gt; 0
     <if test="questionNoId != null">
       and ua.`question_no_id` = #{questionNoId,jdbcType=VARCHAR}
     </if>

+ 2 - 2
server/data/src/main/java/com/qxgmat/data/relation/mapping/UserCollectQuestionRelationMapper.xml

@@ -30,8 +30,8 @@
         and q.`question_type` = #{questionType,jdbcType=VARCHAR}
       </if>
     where
-    qn.`id` != null
-    and q.`id` != null
+    qn.`id` &gt; 0
+    and q.`id` &gt; 0
     <if test="userId != null">
       and ucq.`user_id` = #{userId,jdbcType=VARCHAR}
     </if>

+ 2 - 2
server/data/src/main/java/com/qxgmat/data/relation/mapping/UserNoteQuestionRelationMapper.xml

@@ -30,8 +30,8 @@
       and q.`question_type` = #{questionType,jdbcType=VARCHAR}
     </if>
     where
-    qn.`id` != null
-    and q.`id` != null
+    qn.`id` &gt; 0
+    and q.`id` &gt; 0
     <if test="userId != null">
       and ucq.`user_id` = #{userId,jdbcType=VARCHAR}
     </if>

+ 5 - 34
server/data/src/main/java/com/qxgmat/data/relation/mapping/UserPaperRelationMapper.xml

@@ -22,6 +22,7 @@
       `total_time`=`total_time`+#{time, jdbcType=INTEGER},
       `total_correct`=`total_correct`+#{correct, jdbcType=INTEGER},
       `times` = `times`+#{times, jdbcType=INTEGER},
+      `latest_time` = #{latestTime, jdbcType=DATE},
     </trim>
     WHERE `id` = #{id, jdbcType=VARCHAR}
   </update>
@@ -43,7 +44,7 @@
         and hp.`category` = #{category,jdbcType=VARCHAR}
       </if>
     where
-    hp.`id` != null
+    hp.`id` &gt; 0
     <if test="userId != null">
       and up.`user_id` = #{userId,jdbcType=VARCHAR}
     </if>
@@ -71,13 +72,13 @@
         and hp.`endTime` &lt; #{endTime,jdbcType=VARCHAR}
       </if>
     where
-    hp.`id` != null
+    hp.`id` &gt; null
     <if test="userId != null">
       and up.`user_id` = #{userId,jdbcType=VARCHAR}
     </if>
     <if test="finish != null">
       <if test="finish == true">
-      and up.`number` > 1
+      and up.`number` &gt; 1
       </if>
       <if test="finish == false">
         and up.`number` = 0
@@ -93,7 +94,7 @@
     <include refid="Id_Column_List" />
     from `user_paper` up
     where
-    hp.`id` != null
+    hp.`id` &gt; 0
     and up.`module` = "preview"
     <if test="userId != null">
       and up.`user_id` = #{userId,jdbcType=VARCHAR}
@@ -105,34 +106,4 @@
         and up.`module` = "preview"
         and up1.create_time &gt; up.create_time) &lt; ${top}
   </select>
-
-  <!--
-    用户练习-练习册列表: 用户端
-  -->
-  <select id="listExercisePaper" resultMap="IdMap">
-    select
-    <include refid="Id_Column_List" />
-    from `user_paper` up
-    left join `exercise_paper` hp on hp.`id` = up.`module_id` and ep.`status` = 1
-    and up.`module` = "exercise"
-    <if test="category != null">
-      and hp.`category` = #{category,jdbcType=VARCHAR}
-    </if>
-    <if test="endTime != null">
-      and hp.`endTime` &lt; #{endTime,jdbcType=VARCHAR}
-    </if>
-    where
-    hp.`id` != null
-    <if test="userId != null">
-      and up.`user_id` = #{userId,jdbcType=VARCHAR}
-    </if>
-    <if test="finish != null">
-      <if test="finish == true">
-        and up.`number` > 1
-      </if>
-      <if test="finish == false">
-        and up.`number` = 0
-      </if>>
-    </if>
-  </select>
 </mapper>

+ 2 - 2
server/data/src/main/java/com/qxgmat/data/relation/mapping/UserRelationMapper.xml

@@ -31,7 +31,7 @@
     from `user`
     where
       `prepare_goal` > 0
-    and `${field}` != null and `${field}` != ""
+    and `${field}` &gt; 0 and `${field}` != ""
     group by `${field}`
   </select>
 
@@ -60,7 +60,7 @@
     <if test="courseId != null">
       and uc.`course_id` = #{courseId,jdbcType=VARCHAR}
     </if>
-    where uc.`id` != null
+    where uc.`id` &gt; 0
     <if test="keyword != null">
       and (u.`mobile` like #{keywordLike,jdbcType=VARCHAR}
         or u.`id` like #{keywordLike,jdbcType=VARCHAR})

+ 10 - 8
server/data/src/main/java/com/qxgmat/data/relation/mapping/UserReportRelationMapper.xml

@@ -54,10 +54,10 @@
   -->
   <select id="listFinishLast" resultMap="IdMap">
     select
-      SUBSTRING_INDEX(GROUP_CONCAT(ur.`id` ORDER BY ur.`user_number` desc, ur.`create_time` desc),',',1)
+      SUBSTRING_INDEX(GROUP_CONCAT(ur.`id` ORDER BY ur.`user_number` desc, ur.`create_time` desc),',',1) as `id`
     from `user_report` ur
     where
-    up.`paper_id` IN
+    ur.`paper_id` IN
     <foreach collection="paperIds" item="item" index="index" open="(" close=")" separator=",">
       #{item}
     </foreach>
@@ -68,7 +68,7 @@
     <!--<if test="endTime != null">-->
       <!--and up.`createTime` &lt; #{endTime,jdbcType=VARCHAR}-->
     <!--</if>-->
-    group by up.`paper_id`
+    group by ur.`paper_id`
   </select>
 
   <!--
@@ -77,10 +77,12 @@
   -->
   <select id="listLast" resultMap="IdMap">
     select
-    SUBSTRING_INDEX(GROUP_CONCAT(ur.`id` ORDER BY ur.`create_time` desc),',',1)
+    SUBSTRING_INDEX(GROUP_CONCAT(ur.`id` ORDER BY ur.`create_time` desc),',',1) as `id`
     from `user_report` ur
+    left join `user_paper` up on up.`id` = ur.`paper_id` and up.`is_reset` = 0
     where
-    up.`paper_id` IN
+    up.`id` &gt; 0
+    and ur.`paper_id` IN
     <foreach collection="paperIds" item="item" index="index" open="(" close=")" separator=",">
       #{item}
     </foreach>
@@ -91,7 +93,7 @@
     <!--<if test="endTime != null">-->
     <!--and up.`createTime` &lt; #{endTime,jdbcType=VARCHAR}-->
     <!--</if>-->
-    group by up.`paper_id`
+    group by ur.`paper_id`
   </select>
 
   <!--
@@ -131,7 +133,7 @@
     where
     ur.`user_id` = #{userId,jdbcType=VARCHAR}
     and ur.`paper_origin` = 'exercise'
-    and ep.`id` != null
+    and ep.`id` &gt; 0
     <if test="startTime != null">
       and ur.`update_time` &gt; #{startTime,jdbcType=VARCHAR}
     </if>
@@ -152,7 +154,7 @@
     where
     ur.`user_id` = #{userId,jdbcType=VARCHAR}
     and ur.`paper_origin` = 'preview'
-    and pp.`id` != null
+    and pp.`id` &gt; 0
     <if test="startTime != null">
       and ur.`update_time` &gt; #{startTime,jdbcType=VARCHAR}
     </if>

+ 25 - 0
server/gateway-api/src/main/java/com/qxgmat/controller/admin/ExaminationController.java

@@ -7,11 +7,14 @@ import com.nuliji.tools.Response;
 import com.nuliji.tools.ResponseHelp;
 import com.nuliji.tools.Transform;
 import com.qxgmat.data.constants.enums.status.DirectionStatus;
+import com.qxgmat.data.dao.entity.ExaminationPaper;
 import com.qxgmat.data.dao.entity.ExaminationStruct;
 import com.qxgmat.data.relation.entity.QuestionNoRelation;
 import com.qxgmat.dto.admin.request.ExaminationStructDto;
+import com.qxgmat.dto.admin.response.ExaminationPaperListDto;
 import com.qxgmat.dto.admin.response.ExaminationQuestionListDto;
 import com.qxgmat.service.extend.ExaminationService;
+import com.qxgmat.service.inline.ExaminationPaperService;
 import com.qxgmat.service.inline.ExaminationStructService;
 import com.qxgmat.service.inline.ManagerLogService;
 import io.swagger.annotations.Api;
@@ -37,6 +40,9 @@ public class ExaminationController {
     private ExaminationStructService examinationStructService;
 
     @Autowired
+    private ExaminationPaperService examinationPaperService;
+
+    @Autowired
     private ExaminationService examinationService;
 
     @RequestMapping(value = "/struct/add", method = RequestMethod.POST)
@@ -71,6 +77,25 @@ public class ExaminationController {
         return ResponseHelp.success(p);
     }
 
+    @RequestMapping(value = "/paper/list", method = RequestMethod.GET)
+    @ApiOperation(value = "练习册列表", httpMethod = "GET")
+    public Response<PageMessage<ExaminationPaperListDto>> listPaper(
+            @RequestParam(required = false, defaultValue = "1") int page,
+            @RequestParam(required = false, defaultValue = "100") int size,
+            @RequestParam(required = false, defaultValue = "") String keyword,
+            @RequestParam(required = false) Integer[] ids,
+            HttpSession session) {
+        Page<ExaminationPaper> p;
+        if (ids != null && ids.length > 0){
+            p = examinationPaperService.select(ids);
+        }else{
+            p = examinationPaperService.select(page, size, keyword);
+        }
+        List<ExaminationPaperListDto> pr = Transform.convert(p, ExaminationPaperListDto.class);
+
+        return ResponseHelp.success(pr, page, size, p.getTotal());
+    }
+
     @RequestMapping(value = "/question/list", method = RequestMethod.GET)
     @ApiOperation(value = "模考试题列表", httpMethod = "GET")
     public Response<PageMessage<ExaminationQuestionListDto>> listQuestion(

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

@@ -155,11 +155,11 @@ public class ExerciseController {
 
     @RequestMapping(value = "/paper/auto", method = RequestMethod.POST)
     @ApiOperation(value = "自动组卷", httpMethod = "POST")
-    public Response<Boolean> autoPaper(@RequestBody @Validated ExerciseStructDto dto, HttpServletRequest request) {
+    public Response<Boolean> autoPaper(HttpServletRequest request) {
         // 判断当前组卷状态
         Setting setting = settingService.getByKey(SettingKey.EXERCISE_PAPER_STATUS);
         JSONObject status = setting.getValue();
-        if (status.getInteger("progress")<100){
+        if (status != null && status.getInteger("progress") != null && status.getInteger("progress")<100){
             throw new ParameterException("组卷进行中,请稍后再试");
         }
         asyncTask.autoExercisePaper();

+ 3 - 2
server/gateway-api/src/main/java/com/qxgmat/controller/admin/PreviewController.java

@@ -6,6 +6,7 @@ import com.nuliji.tools.Response;
 import com.nuliji.tools.ResponseHelp;
 import com.nuliji.tools.Transform;
 import com.qxgmat.data.constants.enums.QuestionType;
+import com.qxgmat.data.constants.enums.module.PaperModule;
 import com.qxgmat.data.constants.enums.module.QuestionModule;
 import com.qxgmat.data.constants.enums.status.PreviewStatus;
 import com.qxgmat.data.dao.entity.PreviewPaper;
@@ -54,7 +55,7 @@ public class PreviewController {
         PreviewPaper entity = Transform.dtoToEntity(dto);
 
         // 获取考题模块
-        QuestionModule module = QuestionModule.WithQuestionType(QuestionType.ValueOf(entity.getQuestionType()));
+        QuestionModule module = QuestionModule.WithPaper(PaperModule.ValueOf(entity.getPaperModule()));
 
         UserPaper tmpPaper = UserPaper.builder()
                 .questionNoIds(entity.getQuestionNoIds())
@@ -75,7 +76,7 @@ public class PreviewController {
         PreviewPaper entity = Transform.dtoToEntity(dto);
 
         // 获取考题模块
-        QuestionModule module = QuestionModule.WithQuestionType(QuestionType.ValueOf(entity.getQuestionType()));
+        QuestionModule module = QuestionModule.WithPaper(PaperModule.ValueOf(entity.getPaperModule()));
 
         UserPaper tmpPaper = UserPaper.builder()
                 .questionNoIds(entity.getQuestionNoIds())

+ 2 - 3
server/gateway-api/src/main/java/com/qxgmat/controller/admin/SentenceController.java

@@ -163,11 +163,11 @@ public class SentenceController {
 
     @RequestMapping(value = "/paper/auto", method = RequestMethod.POST)
     @ApiOperation(value = "自动组卷", httpMethod = "POST")
-    public Response<Boolean> autoPaper(@RequestBody @Validated ExerciseStructDto dto, HttpServletRequest request) {
+    public Response<Boolean> autoPaper(HttpServletRequest request) {
         // 判断当前组卷状态
         Setting setting = settingService.getByKey(SettingKey.SENTENCE_PAPER_STATUS);
         JSONObject status = setting.getValue();
-        if (status.getInteger("progress")<100){
+        if (status != null && status.getInteger("progress") != null && status.getInteger("progress")<100){
             throw new ParameterException("组卷进行中,请稍后再试");
         }
         asyncTask.autoSentencePaper();
@@ -181,7 +181,6 @@ public class SentenceController {
         // 判断当前组卷状态
         Setting setting = settingService.getByKey(SettingKey.SENTENCE_PAPER_STATUS);
         JSONObject status = setting.getValue();
-
         return ResponseHelp.success(status);
     }
 }

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

@@ -17,7 +17,7 @@ import com.qxgmat.data.inline.UserQuestionStat;
 import com.qxgmat.data.relation.entity.*;
 import com.qxgmat.dto.extend.*;
 import com.qxgmat.dto.request.*;
-import com.qxgmat.dto.request.UserNoteDto;
+import com.qxgmat.dto.request.UserNoteQuestionDto;
 import com.qxgmat.dto.response.*;
 import com.qxgmat.help.ShiroHelp;
 import com.qxgmat.service.*;
@@ -547,7 +547,8 @@ public class MyController {
         }
 
         // 绑定题目统计
-        Map<Object, UserQuestionStat> stats = userQuestionService.statQuestionMap(questionIds);
+        List<UserQuestion> userQuestionList = userQuestionService.listByQuestion(user.getId(), questionIds);
+        Map<Object, UserQuestionStat> stats = userQuestionService.statQuestionMap(userQuestionList);
         Transform.combine(pr, stats, UserCollectQuestionDto.class, "questionId", "stat");
 
         return ResponseHelp.success(pr, page, size, p.getTotal());
@@ -633,7 +634,7 @@ public class MyController {
 
     @RequestMapping(value = "/note/question", method = RequestMethod.PUT)
     @ApiOperation(value = "更新笔记", notes = "更新笔记", httpMethod = "PUT")
-    public Response<Boolean> updateNoteQuestion(@RequestBody @Validated UserNoteDto dto)  {
+    public Response<Boolean> updateNoteQuestion(@RequestBody @Validated UserNoteQuestionDto dto)  {
         UserNoteQuestion entity = Transform.dtoToEntity(dto);
         User user = (User) shiroHelp.getLoginUser();
         entity.setUserId(user.getId());
@@ -665,7 +666,7 @@ public class MyController {
 
     @RequestMapping(value = "/note/question/list", method = RequestMethod.GET)
     @ApiOperation(value = "获取题目笔记列表", notes = "获取笔记列表", httpMethod = "GET")
-    public Response<PageMessage<UserNoteQuestionDto>> listNoteQuestion(
+    public Response<PageMessage<com.qxgmat.dto.response.UserNoteQuestionDto>> listNoteQuestion(
             @RequestParam(required = false, defaultValue = "1") int page,
             @RequestParam(required = false, defaultValue = "100") int size,
             @RequestParam(required = true) String questionModule,
@@ -678,12 +679,12 @@ public class MyController {
         User user = (User) shiroHelp.getLoginUser();
         QuestionModule qm = QuestionModule.ValueOf(questionModule);
         PageResult<UserNoteQuestionRelation> p = userNoteQuestionService.list(page, size, user.getId(), qm, QuestionType.ValueOf(questionType), startTime, endTime, order, DirectionStatus.ValueOf(direction));
-        List<UserNoteQuestionDto> pr = Transform.convert(p, UserNoteQuestionDto.class);
+        List<com.qxgmat.dto.response.UserNoteQuestionDto> pr = Transform.convert(p, com.qxgmat.dto.response.UserNoteQuestionDto.class);
 
         // 获取题目信息
-        Collection questionIds = Transform.getIds(pr, UserNoteQuestionDto.class, "questionId");
+        Collection questionIds = Transform.getIds(pr, com.qxgmat.dto.response.UserNoteQuestionDto.class, "questionId");
         List<Question> questionList = questionService.select(questionIds);
-        Transform.combine(pr, questionList, UserNoteQuestionDto.class, "questionId", "question", Question.class, "id", QuestionExtendDto.class);
+        Transform.combine(pr, questionList, com.qxgmat.dto.response.UserNoteQuestionDto.class, "questionId", "question", Question.class, "id", QuestionExtendDto.class);
 
         Collection questionNoIds = Transform.getIds(pr, UserQuestionErrorListDto.class, "questionNoId");
         switch(qm){

+ 282 - 54
server/gateway-api/src/main/java/com/qxgmat/controller/api/QuestionController.java

@@ -7,13 +7,13 @@ import com.nuliji.tools.*;
 import com.nuliji.tools.exception.ParameterException;
 import com.qxgmat.data.constants.enums.logic.ExerciseLogic;
 import com.qxgmat.data.constants.enums.module.PaperOrigin;
+import com.qxgmat.data.constants.enums.module.QuestionModule;
+import com.qxgmat.data.constants.enums.module.StructModule;
 import com.qxgmat.data.dao.entity.*;
-import com.qxgmat.data.relation.entity.UserExercisePaperRelation;
+import com.qxgmat.data.inline.UserQuestionStat;
+import com.qxgmat.data.relation.entity.QuestionNoRelation;
 import com.qxgmat.data.relation.entity.UserReportRelation;
-import com.qxgmat.dto.extend.QuestionExtendDto;
-import com.qxgmat.dto.extend.QuestionNoExtendDto;
-import com.qxgmat.dto.extend.UserExercisePaperExtendDto;
-import com.qxgmat.dto.extend.UserPreviewPaperExtendDto;
+import com.qxgmat.dto.extend.*;
 import com.qxgmat.dto.request.*;
 import com.qxgmat.dto.response.*;
 import com.qxgmat.help.ShiroHelp;
@@ -25,15 +25,13 @@ import com.qxgmat.service.extend.QuestionFlowService;
 import com.qxgmat.service.inline.*;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
-import net.bytebuddy.asm.Advice;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpSession;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
+import java.util.stream.Collectors;
 
 @RestController
 @RequestMapping("/api/question")
@@ -50,6 +48,9 @@ public class QuestionController {
     private ExercisePaperService exercisePaperService;
 
     @Autowired
+    private ExerciseStructService exerciseStructService;
+
+    @Autowired
     private ExerciseService exerciseService;
 
     @Autowired
@@ -65,6 +66,13 @@ public class QuestionController {
     private QuestionService questionService;
 
     @Autowired
+    private SentenceQuestionService sentenceQuestionService;
+
+    @Autowired
+    private TextbookQuestionService textbookQuestionService;
+
+
+    @Autowired
     private SentencePaperService sentencePaperService;
 
     @Autowired
@@ -77,6 +85,12 @@ public class QuestionController {
     private UserCollectQuestionService userCollectQuestionService;
 
     @Autowired
+    private UserNoteQuestionService userNoteQuestionService;
+
+    @Autowired
+    private UserAskQuestionService userAskQuestionService;
+
+    @Autowired
     private UserClassService userClassService;
 
     @Autowired
@@ -97,9 +111,127 @@ public class QuestionController {
     public Response<List<UserExerciseGroupDto>> exerciseProgress(
             @RequestParam(required = true) Integer structId, // 第二层,查询第4层,以及第三层汇总
             HttpSession session) {
-        Page<UserExerciseGroupDto> p=null;
+        User user = (User) shiroHelp.getLoginUser();
 
-        // todo 获取数据
+        List<ExerciseStruct> three = exerciseStructService.children(structId, 0);
+        List<UserExerciseGroupDto> p = new ArrayList<>(three.size());
+        for(ExerciseStruct struct : three){
+            UserExerciseGroupDto dto = Transform.convert(struct, UserExerciseGroupDto.class);
+            // 获取第三层所有题目,并获取题目统计
+            List<QuestionNo> list = questionNoService.listByStruct(StructModule.EXERCISE, struct.getId());
+            dto.setStat(questionNoService.statPaper(list));
+            dto.setQuestionNumber(list.size());
+            Map<Object, UserQuestionStat> userQuestionStatMap = null;
+            if(user != null){
+                Collection questionNoIds = Transform.getIds(list, QuestionNo.class, "id");
+                List<UserQuestion> userQuestionList = userQuestionService.listByQuestionNo(user.getId(), questionNoIds);
+                userQuestionStatMap = userQuestionService.statQuestionNoMap(userQuestionList);
+
+                dto.setUserStat(userQuestionService.statQuestion(userQuestionList));
+
+                if (list.size() > userQuestionStatMap.size()){
+                    dto.setUserNumber(userQuestionStatMap.size());
+                    dto.setMinTimes(0);
+                }else{
+                    int minTimes = 0;
+                    // 统计最小轮的已做题数
+                    for(UserQuestionStat stat : userQuestionStatMap.values()){
+                        if(stat.getUserNumber() < minTimes || minTimes == 0) minTimes = stat.getUserNumber();
+                    }
+                    int userNumber = 0;
+                    for(UserQuestionStat stat : userQuestionStatMap.values()){
+                        if(stat.getUserNumber() > minTimes) userNumber += 1;
+                    }
+                    dto.setUserNumber(userNumber);
+                }
+            }
+
+            // 作文、阅读没有第4层
+            // 获取第四层节点
+            List<ExerciseStruct> children = exerciseStructService.children(struct.getId(), 0);
+            if (children == null || children.size() == 0){
+                // 以下属的paper作为children
+                List<ExercisePaper> paperList = exercisePaperService.listByLogic(struct.getId(), 0, ExerciseLogic.NO, null);
+                List<UserExerciseGroupExtendDto> childrenDtos = new ArrayList<>(paperList.size());
+
+                for(ExercisePaper child : paperList){
+                    UserExerciseGroupExtendDto extendDto = new UserExerciseGroupExtendDto();
+                    extendDto.setId(child.getId());
+                    extendDto.setTitle(child.getNo().toString());
+                    extendDto.setQuestionNumber(child.getQuestionNumber());
+                    if(user != null){
+                        int minTimes = 0;
+                        int userQuestionNumber = 0;
+                        boolean flag = true;
+                        for(int questionNoId : child.getQuestionNoIds()){
+                            UserQuestionStat stat = userQuestionStatMap.get(questionNoId);
+                            if (stat == null) {
+                                flag = false;
+                                break;
+                            }
+                            if (stat.getUserNumber() < minTimes || minTimes == 0) minTimes = stat.getUserNumber();
+                        }
+                        if (!flag) minTimes = 0;
+                        for(int questionNoId : child.getQuestionNoIds()){
+                            UserQuestionStat stat = userQuestionStatMap.get(questionNoId);
+                            if (stat != null && stat.getUserNumber() > minTimes)  userQuestionNumber += 1;
+                        }
+                        extendDto.setUserNumber(userQuestionNumber);
+                        extendDto.setMinTimes(minTimes);
+                    }
+                    extendDto.setType("paper");
+                    childrenDtos.add(extendDto);
+                }
+
+                Collection ids = Transform.getIds(p, ExercisePaper.class, "id");
+                List<UserPaper> userPaperList = userPaperService.listWithOrigin(user.getId(), PaperOrigin.EXERCISE, ids);
+                // 绑定userPaperId,用于关联report
+                Map userPaperMap = Transform.getMap(userPaperList, UserPaper.class, "originId", "id");
+                Transform.combine(childrenDtos, userPaperMap, UserSentencePaperDto.class, "id", "userPaperId");
+
+                // 获取最后一次作业结果
+                Collection paperIds = Transform.getIds(paperList, ExercisePaper.class, "id");
+                List<UserReport> reportList = userReportService.listWithLater(paperIds);
+                Transform.combine(childrenDtos, reportList, UserSentencePaperDto.class, "userPaperId", "report", UserReport.class, "paperId", UserReportExtendDto.class);
+
+                dto.setChildren(childrenDtos);
+            }else{
+                // 以struct作为children
+                List<UserExerciseGroupExtendDto> childrenDtos = new ArrayList<>(children.size());
+                for(ExerciseStruct child : children){
+                    UserExerciseGroupExtendDto extendDto = new UserExerciseGroupExtendDto();
+                    extendDto.setId(child.getId());
+                    extendDto.setTitleEn(child.getTitleEn());
+                    extendDto.setTitleZh(child.getTitleZh());
+                    List<QuestionNo> childQuestionList = list.stream().filter((q)-> Collections.singletonList(q.getModuleStruct()).contains(child.getId().intValue())).collect(Collectors.toList());
+                    extendDto.setQuestionNumber(childQuestionList.size());
+                    if (user != null){
+                        int minTimes = 0;
+                        int userQuestionNumber = 0;
+                        boolean flag = true;
+                        for(QuestionNo questionNo : childQuestionList){
+                            UserQuestionStat stat = userQuestionStatMap.get(questionNo.getId());
+                            if (stat == null) {
+                                flag = false;
+                                break;
+                            }
+                            if (stat.getUserNumber() < minTimes || minTimes == 0) minTimes = stat.getUserNumber();
+                        }
+                        if (!flag) minTimes = 0;
+                        for(QuestionNo questionNo : childQuestionList){
+                            UserQuestionStat stat = userQuestionStatMap.get(questionNo.getId());
+                            if (stat != null && stat.getUserNumber() > minTimes)  userQuestionNumber += 1;
+                        }
+                        extendDto.setUserNumber(userQuestionNumber);
+                        extendDto.setMinTimes(minTimes);
+                    }
+                    extendDto.setType("struct");
+                    childrenDtos.add(extendDto);
+                }
+                dto.setChildren(childrenDtos);
+            }
+            p.add(dto);
+        }
 
         return ResponseHelp.success(p);
     }
@@ -114,7 +246,7 @@ public class QuestionController {
 
     @RequestMapping(value = "/exercise/list", method = RequestMethod.GET)
     @ApiOperation(value = "练习组卷列表", httpMethod = "GET")
-    public Response<PageMessage<UserExercisePaperExtendDto>> listExercisePaper(
+    public Response<PageMessage<UserExercisePaperDto>> listExercisePaper(
             @RequestParam(required = false, defaultValue = "1") int page,
             @RequestParam(required = false, defaultValue = "100") int size,
             @RequestParam(required = true) Integer structId,
@@ -123,24 +255,37 @@ public class QuestionController {
             @RequestParam(required = false)  Integer times,
             HttpSession session) {
         User user = (User) shiroHelp.getLoginUser();
-        PageResult<UserExercisePaperRelation> p = exerciseService.list(page, size, structId, user.getId(), ExerciseLogic.ValueOf(logic), logicExtend, times);
+        PageResult<ExercisePaper> p = exerciseService.list(page, size, structId, user != null ? user.getId():null, ExerciseLogic.ValueOf(logic), logicExtend, times);
 
-        List<UserExercisePaperExtendDto> pr = Transform.convert(p, UserExercisePaperExtendDto.class);
+        List<UserExercisePaperDto> pr = Transform.convert(p, UserExercisePaperDto.class);
 
         // 获取试卷统计信息
-        Map map = Transform.getMap(p, UserExercisePaperRelation.class, "id", "paper");
         Map<Integer, Integer[]> questionNoIdsMap = new HashMap<>();
-        for(Object value : map.keySet()){
-            Integer key = (Integer) value;
-            ExercisePaper paper = (ExercisePaper) map.get(key);
-            questionNoIdsMap.put(key, paper.getQuestionNoIds());
+        for(ExercisePaper paper : p){
+            questionNoIdsMap.put(paper.getId(), paper.getQuestionNoIds());
         }
         Map statMap = questionNoService.statPaperMap(questionNoIdsMap);
-        Transform.combine(pr, statMap, UserPreviewPaperExtendDto.class, "id", "stat");
+        Transform.combine(pr, statMap, UserExercisePaperDto.class, "id", "stat");
+
+        if (user != null){
+            // 获取做题记录
+            Collection ids = Transform.getIds(p, ExercisePaper.class, "id");
+            List<UserPaper> paperList = userPaperService.listWithOrigin(user.getId(), PaperOrigin.EXERCISE, ids);
+            Transform.combine(pr, paperList, UserExercisePaperDto.class, "id", "paper", UserPaper.class, "originId", UserPaperBaseExtendDto.class);
+            // 绑定userPaperId,用于关联report
+            Map userPaperMap = Transform.getMap(paperList, UserPaper.class, "originId", "id");
+            Transform.combine(pr, userPaperMap, UserExercisePaperDto.class, "id", "paperId");
+
+            // 获取最后一次作业结果
+            Collection paperIds = Transform.getIds(paperList, UserPaper.class, "id");
+            List<UserReport> reportList = userReportService.listWithLater(paperIds);
+            Transform.combine(pr, reportList, UserExercisePaperDto.class, "id", "report", UserReport.class, "paperId", UserReportExtendDto.class);
+        }
 
         return ResponseHelp.success(pr, page, size, p.getTotal());
     }
 
+
     @RequestMapping(value = "/examination/progress", method = RequestMethod.GET)
     @ApiOperation(value = "模考进度", httpMethod = "GET")
     public Response<PageMessage<ExercisePaper>> examinationProgress(
@@ -156,44 +301,110 @@ public class QuestionController {
 
     @RequestMapping(value = "/examination/list", method = RequestMethod.GET)
     @ApiOperation(value = "模考组卷列表", httpMethod = "GET")
-    public Response<PageMessage<ExercisePaper>> examinationPaperList(
+    public Response<PageMessage<UserExaminationPaperDto>> examinationPaperList(
             @RequestParam(required = false, defaultValue = "1") int page,
             @RequestParam(required = false, defaultValue = "100") int size,
+            @RequestParam(required = true) Integer structId,
+            @RequestParam(required = false)  Integer times,
             HttpSession session) {
-        Page<ExercisePaper> p = null;
+        User user = (User) shiroHelp.getLoginUser();
+        PageResult<ExaminationPaper> p = examinationService.list(page, size, structId, user != null ? user.getId():null, times);
+
+        List<UserExaminationPaperDto> pr = Transform.convert(p, UserExaminationPaperDto.class);
+
+        if (user != null){
+            // 获取做题记录
+            Collection ids = Transform.getIds(p, ExaminationPaper.class, "id");
+            List<UserPaper> paperList = userPaperService.listWithOrigin(user.getId(), PaperOrigin.EXAMINATION, ids);
+            Transform.combine(pr, paperList, UserExaminationPaperDto.class, "id", "paper", UserPaper.class, "originId", UserPaperBaseExtendDto.class);
+            // 绑定userPaperId,用于关联report
+            Map userPaperMap = Transform.getMap(paperList, UserPaper.class, "originId", "id");
+            Transform.combine(pr, userPaperMap, UserExaminationPaperDto.class, "id", "paperId");
+
+            // 获取最后一次作业结果
+            Collection paperIds = Transform.getIds(paperList, UserPaper.class, "id");
+            List<UserReport> reportList = userReportService.listWithLater(paperIds);
+            Transform.combine(pr, reportList, UserExaminationPaperDto.class, "id", "report", UserReport.class, "paperId", UserReportExtendDto.class);
+        }
 
-        // todo
+        return ResponseHelp.success(pr, page, size, p.getTotal());
+    }
 
-        return ResponseHelp.success(p, page, size, p.getTotal());
+    @RequestMapping(value = "/base", method = RequestMethod.GET)
+    @ApiOperation(value = "获取题目详情", notes = "根据题目序号获取题目", httpMethod = "GET")
+    public Response<UserQuestionBaseDto> base(
+            @RequestParam(required = true) Integer userReportId,
+            @RequestParam(required = true) Integer no
+    )  {
+        User user = (User) shiroHelp.getLoginUser();
+        if (no == null || no == 0){
+            no = 1;
+        }
+        UserQuestion userQuestion = userQuestionService.getByReportAndNo(user.getId(), userReportId, no);
+        UserQuestionBaseDto dto = Transform.convert(userQuestion, UserQuestionBaseDto.class);
+
+        return ResponseHelp.success(dto);
     }
 
-    @RequestMapping(value = "/question/detail", method = RequestMethod.GET)
+    @RequestMapping(value = "/detail", method = RequestMethod.GET)
     @ApiOperation(value = "获取题目详情", notes = "获取题目详情", httpMethod = "GET")
     public Response<UserQuestionDetailDto> detail(
-            @RequestParam(required = true) Integer userQuestionId,
-            @RequestParam(required = true) Integer userReportId,
-            @RequestParam(required = true) Integer no
+            @RequestParam(required = true) Integer userQuestionId
     )  {
         User user = (User) shiroHelp.getLoginUser();
-        if (userQuestionId != null && userQuestionId > 0){
+        UserQuestion userQuestion = userQuestionService.get(userQuestionId);
 
-        }else if(userReportId != null && userReportId > 0){
-            if (no == null || no == 0){
-                no = 1;
-            }
-            // 根据题目序号获取
+        UserQuestionDetailDto dto = Transform.convert(userQuestion, UserQuestionDetailDto.class);
+
+        UserReport userReport = userReportService.get(userQuestion.getReportId());
+        dto.setReport(Transform.convert(userReport, UserReportExtendDto.class));
+
+        UserPaper userPaper = userPaperService.get(userReport.getPaperId());
+        dto.setPaper(Transform.convert(userPaper, UserPaperBaseExtendDto.class));
+
+        Question question = questionService.get(userQuestion.getQuestionId());
+        dto.setQuestion(Transform.convert(question, QuestionDetailExtendDto.class));
+
+        UserCollectQuestion collect = userCollectQuestionService.getByUserAndQuestion(user.getId(), userQuestion.getQuestionId());
+        dto.setCollect(collect != null);
+
+        UserNoteQuestion userNoteQuestion = userNoteQuestionService.getByUserAndQuestion(user.getId(), userQuestion.getQuestionId());
+        dto.setNote(Transform.convert(userNoteQuestion, UserNoteQuestionExtendDto.class));
+
+        List<UserAskQuestion> userAskQuestionList = userAskQuestionService.listByQuestion(userQuestion.getQuestionId(), true);
+        dto.setAsks(Transform.convert(userAskQuestionList, UserAskQuestionExtendDto.class));
+
+        if (question.getAssociationContent() != null){
+            List<QuestionNoRelation> associations = questionNoService.listWithRelationByIds(question.getAssociationContent());
+            Collection questions = Transform.getIds(associations, QuestionNoRelation.class, "question");
+            dto.setAssociations(Transform.convert(questions, QuestionBaseExtendDto.class));
+        }
+
+        switch (QuestionModule.ValueOf(userQuestion.getQuestionModule())){
+            case BASE:
+                List<QuestionNo> questionNoList = questionNoService.listByQuestion(userQuestion.getQuestionId());
+                dto.setQuestionNos(Transform.convert(questionNoList, QuestionNoExtendDto.class));
+                break;
+            case SENTENCE:
+                SentenceQuestion sentenceQuestion = sentenceQuestionService.get(userQuestion.getQuestionNoId());
+                dto.setQuestionNo(Transform.convert(sentenceQuestion, QuestionNoExtendDto.class));
+                break;
+            case TEXTBOOK:
+                TextbookQuestion textbookQuestion = textbookQuestionService.get(userQuestion.getQuestionNoId());
+                dto.setQuestionNo(Transform.convert(textbookQuestion, QuestionNoExtendDto.class));
+                break;
         }
 
-        return ResponseHelp.success(null);
+        return ResponseHelp.success(dto);
     }
 
     @RequestMapping(value = "/exercise/paper", method = RequestMethod.GET)
     @ApiOperation(value = "获取练习卷", notes = "获取练习卷", httpMethod = "GET")
     public Response<PaperBaseDto> detailExercise(
-            @RequestParam(required = true, name="id") Integer paperId
+            @RequestParam(required = true) Integer paperId
     )  {
         User user = (User) shiroHelp.getLoginUser();
-        ExercisePaper paper = exercisePaperService.get(paperId);
+        UserPaper paper = questionFlowService.paper(user.getId(), PaperOrigin.EXERCISE, paperId);
         PaperBaseDto paperDto = Transform.convert(paper, PaperBaseDto.class);
 
         return ResponseHelp.success(paperDto);
@@ -202,10 +413,10 @@ public class QuestionController {
     @RequestMapping(value = "/examination/paper", method = RequestMethod.GET)
     @ApiOperation(value = "获取模考卷", notes = "获取模考卷", httpMethod = "GET")
     public Response<PaperBaseDto> detailExamination(
-            @RequestParam(required = true, name="id") Integer paperId
+            @RequestParam(required = true) Integer paperId
     )  {
         User user = (User) shiroHelp.getLoginUser();
-        ExaminationPaper paper = examinationPaperService.get(paperId);
+        UserPaper paper = questionFlowService.paper(user.getId(), PaperOrigin.EXAMINATION, paperId);
         PaperBaseDto paperDto = Transform.convert(paper, PaperBaseDto.class);
 
         return ResponseHelp.success(paperDto);
@@ -214,7 +425,7 @@ public class QuestionController {
     @RequestMapping(value = "/report/base", method = RequestMethod.GET)
     @ApiOperation(value = "获取练习记录", notes = "获取练习记录", httpMethod = "GET")
     public Response<UserReportBaseDto> baseReport(
-            @RequestParam(required = true, name="id") Integer userReportId
+            @RequestParam(required = true) Integer userReportId
     )  {
         User user = (User) shiroHelp.getLoginUser();
         UserReportRelation report = questionFlowService.baseReport(user.getId(), userReportId);
@@ -226,28 +437,42 @@ public class QuestionController {
     @RequestMapping(value = "/report/detail", method = RequestMethod.GET)
     @ApiOperation(value = "获取练习详细记录", notes = "获取练习卷", httpMethod = "GET")
     public Response<UserReportDetailDto> detailReport(
-            @RequestParam(required = true, name="id") Integer userReportId
+            @RequestParam(required = true) Integer userReportId
     )  {
         User user = (User) shiroHelp.getLoginUser();
         UserReportRelation report = questionFlowService.baseReport(user.getId(), userReportId);
 
         UserReportDetailDto userReportDto = Transform.convert(report, UserReportDetailDto.class);
-        // todo 绑定数据
+
+        // 用户paper
+        UserPaper userPaper = userPaperService.get(report.getPaperId());
+        userReportDto.setPaper(Transform.convert(userPaper, UserPaperBaseExtendDto.class));
 
         return ResponseHelp.success(userReportDto);
     }
 
     @RequestMapping(value = "/report/question", method = RequestMethod.GET)
-    @ApiOperation(value = "获取练习做题记录", notes = "获取练习卷", httpMethod = "GET")
-    public Response<List<UserQuestionDetailDto>> detailReportQuestion(
-            @RequestParam(required = true, name="id") Integer userReportId
+    @ApiOperation(value = "获取做题记录", notes = "获取做题记录", httpMethod = "GET")
+    public Response<List<UserQuestionExtendDto>> detailReportQuestion(
+            @RequestParam(required = true) Integer userReportId
     )  {
         User user = (User) shiroHelp.getLoginUser();
         List<UserQuestion> userQuestionList = questionFlowService.listByReport(user.getId(), userReportId);
+        List<UserQuestionExtendDto> userQuestionDtos = Transform.convert(userQuestionList, UserQuestionExtendDto.class);
+
+        Collection ids = Transform.getIds(userQuestionList, UserQuestion.class, "questionId");
+        List<UserCollectQuestion> userCollectQuestionList = userCollectQuestionService.listByUserAndQuestions(user.getId(), ids);
+        Map collectMap = Transform.getMap(userCollectQuestionList, UserCollectQuestion.class, "questionId", "id");
 
-        List<UserQuestionDetailDto> userQuestionDetailDtos = Transform.convert(userQuestionList, UserQuestionDetailDto.class);
+        List<UserNoteQuestion> userNoteQuestionList = userNoteQuestionService.listByUserAndQuestions(user.getId(), ids);
+        Map noteMap = Transform.getMap(userNoteQuestionList, UserNoteQuestion.class, "questionId", "id");
 
-        return ResponseHelp.success(userQuestionDetailDtos);
+        for(UserQuestionExtendDto dto : userQuestionDtos){
+            dto.setCollect(collectMap.containsKey(dto.getQuestionId()));
+            dto.setNote(noteMap.containsKey(dto.getQuestionId()));
+        }
+
+        return ResponseHelp.success(userQuestionDtos);
     }
 
     @RequestMapping(value = "/examination/start", method = RequestMethod.POST)
@@ -289,10 +514,10 @@ public class QuestionController {
     @RequestMapping(value = "/textbook/paper", method = RequestMethod.GET)
     @ApiOperation(value = "获取机经练习卷", notes = "获取练习卷", httpMethod = "GET")
     public Response<PaperBaseDto> detailTextbookPaper(
-            @RequestParam(required = true, name="id") Integer paperId
+            @RequestParam(required = true) Integer paperId
     )  {
         User user = (User) shiroHelp.getLoginUser();
-        TextbookPaper paper = textbookPaperService.get(paperId);
+        UserPaper paper = questionFlowService.paper(user.getId(), PaperOrigin.TEXTBOOK, paperId);
 
         PaperBaseDto paperDto = Transform.convert(paper, PaperBaseDto.class);
 
@@ -315,10 +540,10 @@ public class QuestionController {
     @RequestMapping(value = "/sentence/paper", method = RequestMethod.GET)
     @ApiOperation(value = "获取长难句练习卷", notes = "获取练习卷", httpMethod = "GET")
     public Response<PaperBaseDto> detailSentencePaper(
-            @RequestParam(required = true, name="id") Integer paperId
+            @RequestParam(required = true) Integer paperId
     )  {
         User user = (User) shiroHelp.getLoginUser();
-        SentencePaper paper = sentencePaperService.get(paperId);
+        UserPaper paper = questionFlowService.paper(user.getId(), PaperOrigin.SENTENCE, paperId);
 
         PaperBaseDto paperDto = Transform.convert(paper, PaperBaseDto.class);
 
@@ -340,7 +565,7 @@ public class QuestionController {
     @RequestMapping(value = "/error/paper", method = RequestMethod.GET)
     @ApiOperation(value = "获取错题组卷", notes = "获取错题组卷", httpMethod = "GET")
     public Response<PaperBaseDto> detailError(
-            @RequestParam(required = true, name="id") Integer paperId
+            @RequestParam(required = true) Integer paperId
     )  {
         User user = (User) shiroHelp.getLoginUser();
         UserPaper paper = userPaperService.get(paperId);
@@ -371,7 +596,7 @@ public class QuestionController {
     @RequestMapping(value = "/collect/paper", method = RequestMethod.GET)
     @ApiOperation(value = "获取收藏组卷", notes = "获取收藏组卷", httpMethod = "GET")
     public Response<PaperBaseDto> detailCollect(
-            @RequestParam(required = true, name="id") Integer paperId
+            @RequestParam(required = true) Integer paperId
     )  {
         User user = (User) shiroHelp.getLoginUser();
         UserPaper paper = userPaperService.get(paperId);
@@ -415,13 +640,16 @@ public class QuestionController {
     public Response<UserQuestionBaseDto> next(@RequestBody @Validated ReportNextDto dto)  {
         User user = (User) shiroHelp.getLoginUser();
         UserQuestion userQuestion = questionFlowService.next(user.getId(), dto.getUserReportId());
+        if (userQuestion == null) {
+            throw new ParameterException("finish");
+        }
         UserQuestionBaseDto baseDto = Transform.convert(userQuestion, UserQuestionBaseDto.class);
 
         // 绑定questionNos
         baseDto.setQuestionNos(Transform.convert(questionNoService.listByQuestion(userQuestion.getQuestionId()), QuestionNoExtendDto.class));
 
         // 绑定question
-        baseDto.setQuestion(Transform.convert(questionService.get(userQuestion.getQuestionId()), QuestionExtendDto.class));
+        baseDto.setQuestion(Transform.convert(questionService.get(userQuestion.getQuestionId()), QuestionBaseExtendDto.class));
 
         // 绑定questionNo
         baseDto.setQuestionNo(Transform.convert(questionNoService.get(userQuestion.getQuestionNoId()), QuestionNoExtendDto.class));

+ 21 - 21
server/gateway-api/src/main/java/com/qxgmat/controller/api/SentenceController.java

@@ -8,6 +8,7 @@ import com.nuliji.tools.Transform;
 import com.nuliji.tools.exception.AuthException;
 import com.qxgmat.data.constants.enums.SettingKey;
 import com.qxgmat.data.constants.enums.logic.SentenceLogic;
+import com.qxgmat.data.constants.enums.module.PaperOrigin;
 import com.qxgmat.data.dao.entity.*;
 import com.qxgmat.data.relation.entity.UserSentencePaperRelation;
 import com.qxgmat.dto.extend.UserPaperBaseExtendDto;
@@ -97,17 +98,13 @@ public class SentenceController
             if (code != null){
                 dto.setCode(code.getCode());
             }
-            // 查询用户进度
-            List<UserSentenceProgress> processList = userSentenceProgressService.listTotal(user.getId());
-            Map process = Transform.getMap(processList, UserSentenceProgress.class, "chapter", "process");
-            dto.setProgress(process);
         }
 
         // 章节信息
         Setting entity = settingService.getByKey(SettingKey.SENTENCE);
         JSONObject value = entity.getValue();
-        JSONArray chapters = value.getJSONArray("chapters");
-        dto.setChapters(chapters);
+        dto.setChapters(value.getJSONArray("chapters"));
+        dto.setTrailPages(value.getInteger("trailPages"));
 
         return ResponseHelp.success(dto);
     }
@@ -138,13 +135,14 @@ public class SentenceController
             // 查询文章进度
             List<UserSentenceProgress> progressList = userSentenceProgressService.all(user.getId());
 
-            Map<String, Integer> progressMap = new HashMap<>();
+            Map<String, UserSentenceProgress> progressMap = new HashMap<>();
             for (UserSentenceProgress progress:progressList){
-                progressMap.put(String.format("%d-%d", progress.getChapter(), progress.getPart()), progress.getProgress());
+                progressMap.put(String.format("%d-%d", progress.getChapter(), progress.getPart()), progress);
             }
             for (UserSentenceArticleDto dto : pr){
-                Integer process = progressMap.get(String.format("%d-%d", dto.getChapter(), dto.getPart()));
-                dto.setProgress(process == null ? 0 : process);
+                UserSentenceProgress progress = progressMap.get(String.format("%d-%d", dto.getChapter(), dto.getPart()));
+                dto.setProgress(progress == null ? 0 : progress.getProgress());
+                dto.setTimes(progress == null ? 0:progress.getTimes());
             }
         }
         return ResponseHelp.success(pr);
@@ -163,8 +161,10 @@ public class SentenceController
         if (user != null){
             // 查询文章进度
             UserSentenceProgress progress = userSentenceProgressService.get(user.getId(), article.getChapter(), article.getPart());
-
-            dto.setProgress(progress.getProgress());
+            if (progress != null){
+                dto.setProgress(progress.getProgress());
+                dto.setTimes(progress.getTimes());
+            }
         }
 
         return ResponseHelp.success(dto);
@@ -188,10 +188,10 @@ public class SentenceController
         }
 
         // 获取本章节的最大part数
-        Integer max = sentenceArticleService.maxPart(dto.getChapter());
+//        Integer max = sentenceArticleService.maxPart(dto.getChapter());
 
         // 更新阅读进度
-        userSentenceProgressService.updateProgress(user.getId(), dto.getChapter(), dto.getPart(), dto.getProgress(), max);
+        userSentenceProgressService.updateProgress(user.getId(), dto.getChapter(), dto.getPart(), dto.getProgress());
         return ResponseHelp.success(true);
     }
 
@@ -207,6 +207,7 @@ public class SentenceController
         } else {
             p = sentencePaperService.listByLogic(SentenceLogic.TRAIL);
         }
+        List<UserSentencePaperDto> pr = Transform.convert(p, UserSentencePaperDto.class);
 
         // 获取试卷统计信息
         Map<Integer, Integer[]> questionNoIdsMap = new HashMap<>();
@@ -214,22 +215,21 @@ public class SentenceController
             questionNoIdsMap.put(paper.getId(), paper.getQuestionNoIds());
         }
         Map statMap = sentenceQuestionService.statPaperMap(questionNoIdsMap);
-        List<UserSentencePaperDto> pr = Transform.convert(p, UserSentencePaperDto.class);
         Transform.combine(pr, statMap, UserSentencePaperDto.class, "id", "stat");
 
         if (user != null){
             // 获取做题记录
             Collection ids = Transform.getIds(p, SentencePaper.class, "id");
-            List<UserPaper> paperList = userPaperService.listWithSentence(user.getId(), ids);
-            Transform.combine(pr, paperList, UserSentencePaperDto.class, "id", "paper", UserPaper.class, "originId", UserPaperBaseExtendDto.class);
+            List<UserPaper> userPaperList = userPaperService.listWithOrigin(user.getId(), PaperOrigin.SENTENCE, ids);
+            Transform.combine(pr, userPaperList, UserSentencePaperDto.class, "id", "paper", UserPaper.class, "originId", UserPaperBaseExtendDto.class);
             // 绑定userPaperId,用于关联report
-            Map userPaperMap = Transform.getMap(paperList, UserPaper.class, "originId", "id");
-            Transform.combine(pr, userPaperMap, UserSentencePaperDto.class, "id", "paperId");
+            Map userPaperMap = Transform.getMap(userPaperList, UserPaper.class, "originId", "id");
+            Transform.combine(pr, userPaperMap, UserSentencePaperDto.class, "id", "userPaperId");
 
             // 获取最后一次作业结果
-            Collection paperIds = Transform.getIds(paperList, UserPaper.class, "id");
+            Collection paperIds = Transform.getIds(userPaperList, UserPaper.class, "id");
             List<UserReport> reportList = userReportService.listWithLater(paperIds);
-            Transform.combine(pr, reportList, UserSentencePaperRelation.class, "id", "report", UserReport.class, "paperId", UserReportExtendDto.class);
+            Transform.combine(pr, reportList, UserSentencePaperDto.class, "userPaperId", "report", UserReport.class, "paperId", UserReportExtendDto.class);
         }
 
         return ResponseHelp.success(pr);

+ 39 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/admin/response/ExaminationPaperListDto.java

@@ -0,0 +1,39 @@
+package com.qxgmat.dto.admin.response;
+
+import com.nuliji.tools.annotation.Dto;
+import com.qxgmat.data.dao.entity.ExaminationPaper;
+
+
+@Dto(entity = ExaminationPaper.class)
+public class ExaminationPaperListDto {
+
+    private Integer id;
+
+    private String title;
+
+    private Integer structId;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public Integer getStructId() {
+        return structId;
+    }
+
+    public void setStructId(Integer structId) {
+        this.structId = structId;
+    }
+}

+ 10 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/admin/response/SentenceQuestionListDto.java

@@ -17,6 +17,8 @@ public class SentenceQuestionListDto {
 
     private String title;
 
+    private Integer isTrail;
+
     public Integer getId() {
         return id;
     }
@@ -40,4 +42,12 @@ public class SentenceQuestionListDto {
     public void setTitle(String title) {
         this.title = title;
     }
+
+    public Integer getIsTrail() {
+        return isTrail;
+    }
+
+    public void setIsTrail(Integer isTrail) {
+        this.isTrail = isTrail;
+    }
 }

+ 10 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/admin/response/UserAskQuestionDetailDto.java

@@ -25,6 +25,8 @@ public class UserAskQuestionDetailDto {
 
     private String target;
 
+    private String originContent;
+
     private String content;
 
     private String answer;
@@ -108,4 +110,12 @@ public class UserAskQuestionDetailDto {
     public void setTarget(String target) {
         this.target = target;
     }
+
+    public String getOriginContent() {
+        return originContent;
+    }
+
+    public void setOriginContent(String originContent) {
+        this.originContent = originContent;
+    }
 }

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


Some files were not shown because too many files changed in this diff