lsc 2 years ago
parent
commit
d587642444
86 changed files with 2403 additions and 41 deletions
  1. 1 1
      .gitignore
  2. BIN
      dist/favicon.ico
  3. 0 0
      dist/index.html
  4. 6 0
      dist/report.html
  5. 8 0
      dist/static/css/app.badbf935.css
  6. 0 0
      dist/static/css/chunk-95a5d692.567f4569.css
  7. 1 0
      dist/static/css/chunk-ddf9f6a2.a862a943.css
  8. 1 0
      dist/static/css/chunk-libs.9a4107ef.css
  9. 0 0
      dist/static/css/chunk-vantUI.59e9178d.css
  10. BIN
      dist/static/fonts/iconfont.02b03a00.ttf
  11. BIN
      dist/static/img/24game.65e7f22e.png
  12. BIN
      dist/static/img/AIprogram.eec85563.png
  13. BIN
      dist/static/img/AIprogram2.fab234d7.png
  14. BIN
      dist/static/img/GeoGebra.a1553de8.png
  15. BIN
      dist/static/img/Pythonprogram.d7a10010.png
  16. BIN
      dist/static/img/allHistory.2b427899.png
  17. BIN
      dist/static/img/answer.9f8563eb.png
  18. BIN
      dist/static/img/ask.3f6386c9.png
  19. BIN
      dist/static/img/car.9e4ef8a4.png
  20. BIN
      dist/static/img/choose.5e618a5f.png
  21. BIN
      dist/static/img/cocopi.7db1bf96.png
  22. BIN
      dist/static/img/code.afbd0bfb.png
  23. BIN
      dist/static/img/conSentences.77020d13.png
  24. BIN
      dist/static/img/courseDesign.11abbf32.png
  25. BIN
      dist/static/img/deepLearning.2bab4904.png
  26. BIN
      dist/static/img/eval.5e18f49e.png
  27. BIN
      dist/static/img/evalua.0a72e3a3.png
  28. BIN
      dist/static/img/group.73c34163.png
  29. BIN
      dist/static/img/head-back.47c17562.png
  30. BIN
      dist/static/img/isNoMessage.daf5b17d.png
  31. BIN
      dist/static/img/lineSearch.93c5ace9.png
  32. BIN
      dist/static/img/mindMapping.7870d693.png
  33. BIN
      dist/static/img/mindNetwork.f7b4fed6.png
  34. BIN
      dist/static/img/mohe.ec1fd2f9.png
  35. BIN
      dist/static/img/networkPanel.e06ca1d1.png
  36. BIN
      dist/static/img/noImg.518119e3.png
  37. BIN
      dist/static/img/noPz.94284050.png
  38. BIN
      dist/static/img/plwork.4edbb368.png
  39. BIN
      dist/static/img/program.36d098e1.png
  40. BIN
      dist/static/img/select.38960ca1.png
  41. BIN
      dist/static/img/table.2f81427c.png
  42. BIN
      dist/static/img/text.e9ab3f74.png
  43. BIN
      dist/static/img/time.bdc71f7c.png
  44. BIN
      dist/static/img/trainPlatform.5553cd42.png
  45. BIN
      dist/static/img/translation.cd5e9073.png
  46. BIN
      dist/static/img/whiteBoard.6046b5fb.png
  47. BIN
      dist/static/img/work.c1feb21d.png
  48. 0 0
      dist/static/js/app.6654119d.js
  49. 0 0
      dist/static/js/chunk-0cf437fc.ef1187f0.js
  50. 0 0
      dist/static/js/chunk-95a5d692.74349733.js
  51. 1 0
      dist/static/js/chunk-ddf9f6a2.9c0b6591.js
  52. 0 0
      dist/static/js/chunk-libs.80edb424.js
  53. 0 0
      dist/static/js/chunk-vantUI.2c46554a.js
  54. 2 0
      package.json
  55. 22 3
      src/api/course.js
  56. BIN
      src/assets/images/course/isNoMessage.png
  57. BIN
      src/assets/images/course/noPz.png
  58. BIN
      src/assets/images/works/noImg.png
  59. 1 1
      src/components/headBar.vue
  60. 0 0
      src/components/hevue-img-preview/LICENSE
  61. 133 0
      src/components/hevue-img-preview/README.md
  62. 131 0
      src/components/hevue-img-preview/css/default.css
  63. 34 0
      src/components/hevue-img-preview/css/theme-dark.css
  64. 41 0
      src/components/hevue-img-preview/css/theme-light.css
  65. 588 0
      src/components/hevue-img-preview/hevue-img-preview.vue
  66. 71 0
      src/components/hevue-img-preview/iconfont/iconfont.css
  67. BIN
      src/components/hevue-img-preview/iconfont/iconfont.ttf
  68. BIN
      src/components/hevue-img-preview/iconfont/iconfont.woff
  69. BIN
      src/components/hevue-img-preview/iconfont/iconfont.woff2
  70. 48 0
      src/components/hevue-img-preview/index.js
  71. 27 0
      src/components/hevue-img-preview/package.json
  72. 1 1
      src/const/index.js
  73. 4 1
      src/main.js
  74. 7 4
      src/plugins/vant.js
  75. 3 0
      src/utils/aws-sdk-2.235.1.min.js
  76. 5 0
      src/utils/request.js
  77. 244 0
      src/views/course/components/AudioComponent.vue
  78. 237 0
      src/views/course/components/commentBox.vue
  79. 180 0
      src/views/course/components/commentPanel.vue
  80. 7 1
      src/views/course/components/courseContentList.vue
  81. 2 1
      src/views/course/components/courseTitle.vue
  82. 355 0
      src/views/course/components/navBox.vue
  83. 119 14
      src/views/course/components/stepsBox.vue
  84. 71 0
      src/views/course/components/works.vue
  85. 44 13
      src/views/course/index.vue
  86. 8 1
      src/views/login/index.vue

+ 1 - 1
.gitignore

@@ -1,6 +1,6 @@
 .DS_Store
 node_modules
-/dist
+#/dist
 /docs
 # local env files
 .env.local

BIN
dist/favicon.ico


File diff suppressed because it is too large
+ 0 - 0
dist/index.html


File diff suppressed because it is too large
+ 6 - 0
dist/report.html


File diff suppressed because it is too large
+ 8 - 0
dist/static/css/app.badbf935.css


File diff suppressed because it is too large
+ 0 - 0
dist/static/css/chunk-95a5d692.567f4569.css


+ 1 - 0
dist/static/css/chunk-ddf9f6a2.a862a943.css

@@ -0,0 +1 @@
+.loginBox[data-v-36b7e306]{position:relative;width:100vw;height:100vh;overflow:hidden}.loginBox>iframe[data-v-36b7e306]{width:100%;height:100%;border:0}

+ 1 - 0
dist/static/css/chunk-libs.9a4107ef.css

@@ -0,0 +1 @@
+#nprogress{pointer-events:none}#nprogress .bar{background:#29d;position:fixed;z-index:1031;top:0;left:0;width:100%;height:.05333rem}#nprogress .peg{display:block;position:absolute;right:0;width:2.66667rem;height:100%;box-shadow:0 0 .26667rem #29d,0 0 .13333rem #29d;opacity:1;-webkit-transform:rotate(3deg) translateY(-.10667rem);-ms-transform:rotate(3deg) translateY(-.10667rem);transform:rotate(3deg) translateY(-.10667rem)}#nprogress .spinner{display:block;position:fixed;z-index:1031;top:.4rem;right:.4rem}#nprogress .spinner-icon{width:.48rem;height:.48rem;box-sizing:border-box;border:.05333rem solid transparent;border-top-color:#29d;border-left-color:#29d;border-radius:50%;-webkit-animation:nprogress-spinner .4s linear infinite;animation:nprogress-spinner .4s linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent #nprogress .bar,.nprogress-custom-parent #nprogress .spinner{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(1turn)}}@keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}

File diff suppressed because it is too large
+ 0 - 0
dist/static/css/chunk-vantUI.59e9178d.css


BIN
dist/static/fonts/iconfont.02b03a00.ttf


BIN
dist/static/img/24game.65e7f22e.png


BIN
dist/static/img/AIprogram.eec85563.png


BIN
dist/static/img/AIprogram2.fab234d7.png


BIN
dist/static/img/GeoGebra.a1553de8.png


BIN
dist/static/img/Pythonprogram.d7a10010.png


BIN
dist/static/img/allHistory.2b427899.png


BIN
dist/static/img/answer.9f8563eb.png


BIN
dist/static/img/ask.3f6386c9.png


BIN
dist/static/img/car.9e4ef8a4.png


BIN
dist/static/img/choose.5e618a5f.png


BIN
dist/static/img/cocopi.7db1bf96.png


BIN
dist/static/img/code.afbd0bfb.png


BIN
dist/static/img/conSentences.77020d13.png


BIN
dist/static/img/courseDesign.11abbf32.png


BIN
dist/static/img/deepLearning.2bab4904.png


BIN
dist/static/img/eval.5e18f49e.png


BIN
dist/static/img/evalua.0a72e3a3.png


BIN
dist/static/img/group.73c34163.png


BIN
dist/static/img/head-back.47c17562.png


BIN
dist/static/img/isNoMessage.daf5b17d.png


BIN
dist/static/img/lineSearch.93c5ace9.png


BIN
dist/static/img/mindMapping.7870d693.png


BIN
dist/static/img/mindNetwork.f7b4fed6.png


BIN
dist/static/img/mohe.ec1fd2f9.png


BIN
dist/static/img/networkPanel.e06ca1d1.png


BIN
dist/static/img/noImg.518119e3.png


BIN
dist/static/img/noPz.94284050.png


BIN
dist/static/img/plwork.4edbb368.png


BIN
dist/static/img/program.36d098e1.png


BIN
dist/static/img/select.38960ca1.png


BIN
dist/static/img/table.2f81427c.png


BIN
dist/static/img/text.e9ab3f74.png


BIN
dist/static/img/time.bdc71f7c.png


BIN
dist/static/img/trainPlatform.5553cd42.png


BIN
dist/static/img/translation.cd5e9073.png


BIN
dist/static/img/whiteBoard.6046b5fb.png


BIN
dist/static/img/work.c1feb21d.png


File diff suppressed because it is too large
+ 0 - 0
dist/static/js/app.6654119d.js


File diff suppressed because it is too large
+ 0 - 0
dist/static/js/chunk-0cf437fc.ef1187f0.js


File diff suppressed because it is too large
+ 0 - 0
dist/static/js/chunk-95a5d692.74349733.js


+ 1 - 0
dist/static/js/chunk-ddf9f6a2.9c0b6591.js

@@ -0,0 +1 @@
+(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-ddf9f6a2"],{"1cac":function(e,t,r){},"9ed6":function(e,t,r){"use strict";r.r(t);var n=function(){var e=this,t=e.$createElement;e._self._c;return e._m(0)},c=[function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",{staticClass:"loginBox"},[r("iframe",{attrs:{src:"https://edu.cocorobo.cn/course/login?type=2",frameborder:"0"}})])}],i={data:function(){return{redirect:void 0}},watch:{$route:{handler:function(e){this.redirect=e.query&&e.query.redirect},immediate:!0}},methods:{handleLogin:function(){this.$router.push({path:this.redirect||"/"})}}},o=i,a=(r("b64b0"),r("2877")),u=Object(a["a"])(o,n,c,!1,null,"36b7e306",null);t["default"]=u.exports},b64b0:function(e,t,r){"use strict";var n=r("1cac"),c=r.n(n);c.a}}]);

File diff suppressed because it is too large
+ 0 - 0
dist/static/js/chunk-libs.80edb424.js


File diff suppressed because it is too large
+ 0 - 0
dist/static/js/chunk-vantUI.2c46554a.js


+ 2 - 0
package.json

@@ -17,7 +17,9 @@
   "dependencies": {
     "axios": "^0.19.2",
     "core-js": "^3.6.4",
+    "js-audio-recorder": "^1.0.7",
     "js-cookie": "^3.0.1",
+    "lamejs": "^1.2.1",
     "lib-flexible": "^0.3.2",
     "lodash": "^4.17.15",
     "nprogress": "^0.2.0",

+ 22 - 3
src/api/course.js

@@ -7,7 +7,7 @@ export function getCourseInfo(params) {
     url: '/selectCourseDetail3',
     method: 'get',
     params,
-    hideloading: true
+    hideloading: false
   })
 }
 // 获取所有作业
@@ -16,7 +16,7 @@ export function getCourseWorks(params) {
     url: '/selectSWorks',
     method: 'get',
     params,
-    hideloading: true
+    hideloading: false
   })
 }
 
@@ -26,6 +26,25 @@ export function getCourseWorksStudentJuri(params) {
     url: '/selectWorksStudent',
     method: 'get',
     params,
-    hideloading: true
+    hideloading: false
+  })
+}
+// 查看批注
+export function getCoursePz(params) {
+  return request({
+    url: '/selectPzList',
+    method: 'get',
+    params,
+    hideloading: false
+  })
+}
+
+// 添加批注
+export function addPz2(data) {
+  return request({
+    url: '/addPz2',
+    method: 'post',
+    data,
+    hideloading: false
   })
 }

BIN
src/assets/images/course/isNoMessage.png


BIN
src/assets/images/course/noPz.png


BIN
src/assets/images/works/noImg.png


+ 1 - 1
src/components/headBar.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="head-container">
     <div class="back" v-if="isBack"></div>
-    <div class="head-box">
+    <div class="head-box" :style="{ padding: isBack ? '0 1.5rem' : '0 .5rem' }">
       <slot name="title"></slot>
     </div>
   </div>

+ 0 - 0
src/components/hevue-img-preview/LICENSE


+ 133 - 0
src/components/hevue-img-preview/README.md

@@ -0,0 +1,133 @@
+## hevue-img-preview 简介
+
+
+[![](https://img.shields.io/npm/dm/hevue-img-preview.svg)](https://www.npmjs.com/package/hevue-img-preview)
+[![](https://img.shields.io/github/v/release/heyongsheng/hevue-img-preview)](https://www.npmjs.com/package/hevue-img-preview)
+[![](https://img.shields.io/badge/License-MIT-green)](https://www.npmjs.com/package/hevue-img-preview)
+
+> 本组件是一个基于 vue 编写的 vue 图片预览组件,支持 pc 和手机端,支持单图和多图预览,仅传入一个图片地址,即可实现图片预览效果。手机端支持单指拖拽和双指缩放。如果能帮上你,希望可以移步 [GitHub](https://github.com/heyongsheng/hevue-img-preview) ,或者[码云](https://gitee.com/ihope_top/hevue-img-preview) 给个小星星,如果有任何使用意见或建议,也欢迎回复或者提交 issue
+
+## 官方文档
+
+> 官方使用文档请访问 [https://heyongsheng.github.io/#/](https://heyongsheng.github.io/#/)
+
+## 安装
+
+使用npm进行安装
+``` bash
+npm install hevue-img-preview
+```
+
+在main.js进行全局引入
+```javascript
+import hevueImgPreview from 'hevue-img-preview'
+Vue.use(hevueImgPreview)
+```
+
+在组件中进行使用
+```html
+<img :src="url" @click="previewImg(url)">
+```
+```javascript
+methods: {
+	previewImg (url) {
+		this.$hevueImgPreview(url)
+	}
+}
+```
+
+## 使用
+
+> this.\$hevueImgPreview() 方法可以接收一个字符串类型的 url,或者对象类型的配置,具体使用方法如下
+
+- 接收一个地址字符串`this.$hevueImgPreview(url)`
+
+```Javascript
+this.$hevueImgPreview('https://fuss10.elemecdn.com/8/27/f01c15bb73e1ef3793e64e6b7bbccjpeg.jpeg') // 线上地址
+this.$hevueImgPreview('./img/logo.jpeg') // 本地地址
+```
+
+- 接收一个对象`this.$hevueImgPreview(options)`
+
+```Javascript
+# 单图预览
+this.$hevueImgPreview({
+    url: 'https://fuss10.elemecdn.com/8/27/f01c15bb73e1ef3793e64e6b7bbccjpeg.jpeg',
+})
+
+# 多图预览
+this.$hevueImgPreview({
+    multiple: true, // 开启多图预览模式
+    nowImgIndex: 1, // 多图预览,默认展示第二张图片
+    imgList: ['1.png', '2.png', '3.png'], // 需要预览的多图数组
+})
+```
+
+### 配置项
+
+| 字段              | 类型    | 默认值               | 备注                           |
+| ----------------- | ------- | -------------------- | ------------------------------ |
+| url               | String  | 无                   | 预览的图片地址,多图预览时省略 |
+| multiple          | Boolean | false                | 是否多图预览                   |
+| nowImgIndex       | Number  | 0                    | 多图预览时默认显示的图片下标   |
+| imgList           | Array   | 无                   | 多图预览时传入的图片数组       |
+| keyboard          | Boolean | false                | 是否开启键盘控制               |
+| clickMaskCLose    | Boolean | false                | 是否可以点击遮罩层关闭         |
+| controlBar        | Boolean | true                 | 是否显示控制条及页码         |
+| closeBtn          | Boolean | true                 | 是否显示关闭按钮         |
+| arrowBtn          | Boolean | true                | 是否显示左右翻页按钮         |
+
+#### 全局配置
+
+对于某些配置,例如开启键盘事件,点击遮罩层关闭,我们可能要全局保持统一,但却需要在每一次调用时重复配置,为了解决这个问题,我们提供了全局配置项,您可以在引入插件的时候,将全局配置项作为第二个参数传入即可,以免再每次调用的时候重复配置。
+
+```javascript
+// main.js
+import hevueImgPreview from 'hevueImgPreview'
+Vue.use(hevueImgPreview, {
+  keyboard: true,
+  clickMaskCLose: true
+  ...
+})
+```
+
+
+如开启键盘控制事件后,相对应功能控制按键如下
+
+| 按键 | 功能
+| ----------------- | ----------------------------------------
+| w | 放大
+| s | 缩小
+| a | 上一张
+| d | 下一张
+| q | 逆时针旋转
+| e | 顺时针旋转
+| r | 图片复位
+| esc | 关闭图片预览
+
+*如不考虑兼容性问题,上述的背景颜色均可接收渐变色*
+
+## 更新日志
+
+### 5.0.2
+
+组件支持实例化,调用组件将返回组件实例,支持直接通过实例关闭弹窗
+```javascript
+const hevueImgPreviewEl = this.$hevueImgPreview(...)
+hevueImgPreviewEl.close()
+```
+
+## 作者注
+
+> 本人前端小白一枚,工作经验较少,所写东西尽量保证没 bug,但性能界面什么的可能没办法做到最优,如果您有什么使用中的建议或意见,欢迎反馈给我,可以加联系方式,也可以直接回复,或者到`GitHub`提个`issue`[建议此方法],如果对您有所帮助,万分期待您能给个赞并且到`GitHub`给个小星星
+
+> GitHub 链接:[https://github.com/heyongsheng/hevue-img-preview](https://github.com/heyongsheng/hevue-img-preview)
+
+> 码云链接:[https://gitee.com/ihope_top/hevue-img-preview](https://gitee.com/ihope_top/hevue-img-preview)
+
+作者 QQ:1378431028
+
+QQ 群:595472617
+
+作者微信:heyongsheng1996
+![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/57478efc7ace4a8c9e27081a26f2c8cf~tplv-k3u1fbpfcp-zoom-1.image)

+ 131 - 0
src/components/hevue-img-preview/css/default.css

@@ -0,0 +1,131 @@
+.hevue-imgpreview-wrap {
+  position: fixed;
+  top: 0;
+  bottom: 0;
+  right: 0;
+  left: 0;
+  user-select: none;
+  -moz-user-select: none;
+  -webkit-user-select: none;
+  -ms-user-select: none;
+  z-index: 9999;
+  color: rgba(255,255,255,.6);
+}
+.hevue-imgpreview-wrap .he-img-wrap {
+  width: 100%;
+  height: 100%;
+  text-align: center;
+  vertical-align: middle;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  background: rgba(0, 0, 0, .3);
+}
+.hevue-imgpreview-wrap .he-img-view {
+  /* transition: transform 0.3s; */
+}
+.hevue-imgpreview-wrap .arrow {
+  width: 42px;
+  height: 42px;
+  text-align: center;
+  line-height: 42px;
+  position: absolute;
+  top: 50%;
+  border-radius: 50%;
+  transform: translateY(-50%);
+  -ms-transform: translateY(-50%);
+  font-size: 24px;
+  cursor: pointer;
+  transition: all 0.2s;
+  background: rgba(0,0,0,.3);
+}
+.hevue-imgpreview-wrap .arrow:hover {
+  opacity: 0.8;
+  transform: translateY(-50%) scale(1.2);
+}
+.hevue-imgpreview-wrap .arrow-left {
+  left: 50px;
+}
+.hevue-imgpreview-wrap .arrow-right {
+  right: 50px;
+}
+/* 关闭按钮 */
+.hevue-imgpreview-wrap .he-close-icon {
+  position: absolute;
+  right: 50px;
+  top: 50px;
+  width: 36px;
+  height: 36px;
+  font-size: 22px;
+  line-height: 36px;
+  text-align: center;
+  border-radius: 50%;
+  cursor: pointer;
+  transition: all 0.2s;
+  background: rgba(0,0,0,.3);
+}
+.hevue-imgpreview-wrap .he-close-icon:hover {
+  transform: rotate(90deg);
+}
+.hevue-imgpreview-wrap .he-control-bar-wrap {
+  display: flex;
+  position: absolute;
+  width: 100%;
+  bottom: 10%;
+  left: 0;
+}
+.hevue-imgpreview-wrap .he-control-bar {
+  height: 44px;
+  bottom: 10%;
+  padding: 0 22px;
+  display: flex;
+  border-radius: 22px;
+  margin: 0 auto;
+  background: rgba(0,0,0,.3);
+}
+.hevue-imgpreview-wrap .he-control-num {
+  position: absolute;
+  bottom: 5%;
+  left: 50%;
+  transform: translateX(-50%);
+  -ms-transform: translateX(-50%);
+  padding: 0 22px;
+  font-size: 16px;
+  border-radius: 15px;
+  background: rgba(0,0,0,.3);
+}
+.hevue-imgpreview-wrap .he-control-btn {
+  line-height: 44px;
+  font-size: 24px;
+  cursor: pointer;
+  padding: 0 9px;
+  /* display: inline-block; */
+  transition: all 0.2s;
+}
+.hevue-imgpreview-wrap .he-control-btn:hover {
+  transform: scale(1.2);
+}
+
+.hevue-imgpreview-wrap .fade-enter-active,
+.hevue-imgpreview-wrap .fade-leave-active {
+  transition: opacity 0.3s;
+}
+.hevue-imgpreview-wrap .fade-enter, .hevue-imgpreview-wrap .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
+  opacity: 0;
+}
+.hevue-imgpreview-wrap .hevue-img-status-icon {
+  font-size: 56px;
+}
+
+.hevue-imgpreview-wrap .rotate-animation {
+  animation: rotate 1.5s linear infinite;
+}
+@keyframes rotate {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}

+ 34 - 0
src/components/hevue-img-preview/css/theme-dark.css

@@ -0,0 +1,34 @@
+
+.hevue-imgpreview-wrap {
+  color: rgba(255,255,255,.6);
+}
+
+/* 遮罩层 */
+.hevue-imgpreview-wrap .he-img-wrap {
+  background: rgba(0, 0, 0, .8);
+  backdrop-filter: blur(8px);
+}
+
+/* 控制条 */
+.hevue-imgpreview-wrap .he-control-bar {
+  background: rgba(0,0,0,.3);
+  backdrop-filter: blur(5px);
+}
+
+/* 左右箭头 */
+.hevue-imgpreview-wrap .arrow {
+  background: rgba(0,0,0,.3);
+  backdrop-filter: blur(5px);
+}
+
+/* 关闭按钮 */
+.he-close-icon {
+  background: rgba(0,0,0,.3);
+  backdrop-filter: blur(5px);
+}
+
+/* 页码指示器 */
+.he-control-num {
+  background: rgba(0,0,0,.3);
+  backdrop-filter: blur(5px);
+}

+ 41 - 0
src/components/hevue-img-preview/css/theme-light.css

@@ -0,0 +1,41 @@
+.hevue-imgpreview-wrap {
+  color: rgba(52, 52, 52, 0.6);
+}
+
+/* 遮罩层 */
+.hevue-imgpreview-wrap .he-img-wrap {
+  background: rgba(70, 70, 70, 0.3);
+  backdrop-filter: blur(8px);
+}
+
+/* 控制条 */
+.hevue-imgpreview-wrap .he-control-bar {
+  background: rgba(255, 255, 255, .8);
+  backdrop-filter: blur(5px);
+  /* box-shadow: 0 0 5px 1px rgb(0 0 0 / 10%); */
+  /* border: 1px solid rgba(160, 160, 160, .15); */
+}
+
+/* 左右箭头 */
+.hevue-imgpreview-wrap .arrow {
+  background: rgba(255, 255, 255, .8);
+  backdrop-filter: blur(5px);
+  /* box-shadow: 0 0 5px 1px rgb(0 0 0 / 10%); */
+  /* border: 1px solid rgba(160, 160, 160, .15); */
+}
+
+/* 关闭按钮 */
+.hevue-imgpreview-wrap .he-close-icon {
+  background: rgba(255, 255, 255, .8);
+  backdrop-filter: blur(5px);
+  /* box-shadow: 0 0 5px 1px rgb(0 0 0 / 10%); */
+  /* border: 1px solid rgba(160, 160, 160, .15); */
+}
+
+/* 页码指示器 */
+.hevue-imgpreview-wrap .he-control-num {
+  background: rgba(255, 255, 255, .8);
+  backdrop-filter: blur(5px);
+  /* box-shadow: 0 0 5px 1px rgb(0 0 0 / 10%); */
+  /* border: 1px solid rgba(160, 160, 160, .15); */
+}

+ 588 - 0
src/components/hevue-img-preview/hevue-img-preview.vue

@@ -0,0 +1,588 @@
+<!--
+ * @Author: 贺永胜
+ * @Date: 2021-04-19 16:39:30
+ * @email: 1378431028@qq.com
+ * @LastEditors: 贺永胜
+ * @LastEditTime: 2021-10-28 11:15:41
+ * @Description: file content
+-->
+
+<!--
+ *                                                     __----~~~~~~~~~~~------___
+ *                                    .  .   ~~//====......          __--~ ~~
+ *                    -.            \_|//     |||\\  ~~~~~~::::... /~
+ *                 ___-==_       _-~o~  \/    |||  \\            _/~~-
+ *         __---~~~.==~||\=_    -_--~/_-~|-   |\\   \\        _/~
+ *     _-~~     .=~    |  \\-_    '-~7  /-   /  ||    \      /
+ *   .~       .~       |   \\ -_    /  /-   /   ||      \   /
+ *  /  ____  /         |     \\ ~-_/  /|- _/   .||       \ /
+ *  |~~    ~~|--~~~~--_ \     ~==-/   | \~--===~~        .\
+ *           '         ~-|      /|    |-~\~~       __--~~
+ *                       |-~~-_/ |    |   ~\_   _-~            /\
+ *                            /  \     \__   \/~                \__
+ *                        _--~ _/ | .-~~____--~-/                  ~~==.
+ *                       ((->/~   '.|||' -_|    ~~-/ ,              . _||
+ *                                  -_     ~\      ~~---l__i__i__i--~~_/
+ *                                  _-~-__   ~)  \--______________--~~
+ *                                //.-~~~-~_--~- |-------~~~~~~~~
+ *                                       //.-~~~--\
+ *                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ * 
+ *                               神兽保佑            永无BUG
+ -->
+
+<template>
+  <transition name="fade">
+    <div
+      class="hevue-imgpreview-wrap"
+      id="hevue-imgpreview-wrap"
+      v-if="show"
+      ref="heImg"
+      @mouseup="removeMove('pc')"
+      @touchend="removeMove('mobile')"
+      @click.stop="clickMask"
+    >
+      <div class="he-img-wrap">
+        <div
+          class="heimgfont hevue-img-status-icon rotate-animation"
+          v-show="imgState === 1"
+        >
+          &#xe6b1;
+        </div>
+        <!-- <div class="heimgfont loading">&#xe6b1;</div> -->
+        <img
+          :src="imgurl"
+          ref="heImView"
+          @click.stop=""
+          v-show="imgState === 2"
+          class="he-img-view"
+          :style="
+            'transform: scale(' +
+            imgScale +
+            ') rotate(' +
+            imgRotate +
+            'deg);margin-top:' +
+            imgTop +
+            'px;margin-left:' +
+            imgLeft +
+            'px;' +
+            maxWH
+          "
+          @mousedown="addMove"
+          @touchstart="addMoveMobile"
+        />
+        <!-- 图片加载失败 -->
+        <div class="heimgfont hevue-img-status-icon" v-show="imgState === 3">
+          &#xec0d;
+        </div>
+        <!-- 关闭按钮 -->
+        <div
+          class="heimgfont he-close-icon"
+          @click.stop="close"
+          v-if="closeBtn"
+        >
+          &#xe608;
+        </div>
+        <!-- 左箭头 -->
+        <div
+          class="arrow arrow-left heimgfont"
+          @click.stop="toogleImg(false)"
+          v-if="arrowBtn && multiple"
+        >
+          &#xe620;
+        </div>
+        <!-- 右箭头 -->
+        <div
+          class="arrow arrow-right heimgfont"
+          @click.stop="toogleImg(true)"
+          v-if="arrowBtn && multiple"
+        >
+          &#xe60d;
+        </div>
+        <!-- 控制条 -->
+        <div class="he-control-bar-wrap" v-if="controlBar">
+          <div class="he-control-bar" @click.stop>
+            <!-- 缩小 -->
+            <div
+              class="he-control-btn heimgfont"
+              @click.stop="scaleFunc(-0.15)"
+            >
+              &#xe65e;
+            </div>
+            <!-- 放大 -->
+            <div class="he-control-btn heimgfont" @click.stop="scaleFunc(0.15)">
+              &#xe65d;
+            </div>
+            <!-- 复位 -->
+            <div
+              class="he-control-btn heimgfont"
+              v-show="isFull"
+              @click.stop="imgToggle"
+            >
+              &#xe698;
+            </div>
+            <!-- 复位 -->
+            <div
+              class="he-control-btn heimgfont"
+              v-show="!isFull"
+              @click.stop="imgToggle"
+            >
+              &#xe86b;
+            </div>
+            <!-- 左转 -->
+            <div class="he-control-btn heimgfont" @click.stop="rotateFunc(-90)">
+              &#xe670;
+            </div>
+            <!-- 右转 -->
+            <div class="he-control-btn heimgfont" @click.stop="rotateFunc(90)">
+              &#xe66f;
+            </div>
+            <!-- 下载 -->
+            <!-- <div class="he-control-btn heimgfont" @click.stop="downloadIamge">
+              &#xe694;
+            </div> -->
+          </div>
+        </div>
+        <!-- 页码指示器 -->
+        <div class="he-control-num" v-if="controlBar && multiple">
+          {{ imgIndex + 1 }} / {{ imgList.length }}
+        </div>
+      </div>
+    </div>
+  </transition>
+</template>
+
+<script>
+export default {
+  name: "hevue-img-preview",
+  data() {
+    return {
+      // imgWidth: 0,
+      // imgHeight: 0,
+      imgScale: 1,
+      imgTop: 0,
+      imgLeft: 0,
+      imgRotate: 0,
+      isFull: false,
+      maxWH: "max-width:100%;max-height:100%;",
+      clientX: 0,
+      clientY: 0,
+      imgIndex: 0,
+      canRun: true,
+      imgurl: "",
+      imgState: 1,
+      start: [{}, {}],
+      mobileScale: 0, // 手指离开时图片的缩放比例
+      // 以下内容为用户传入配置
+      show: false, // 插件显示,默认为false
+      url: "", // 预览图片的地址
+      nowImgIndex: 0,
+      multiple: false,
+      imgList: [],
+      // 以下为可全局配置
+      controlBar: true,
+      closeBtn: true,
+      arrowBtn: true,
+      keyboard: false,
+      clickMaskCLose: false, // 是否点击遮罩关闭,默认false
+    };
+  },
+  mounted() {
+    this.initImg();
+  },
+  watch: {
+    url() {
+      this.initImg();
+    },
+    show: {
+      handler(newV) {
+        if (newV) {
+          this.$nextTick(() => {
+            let _dom = document.getElementById("hevue-imgpreview-wrap");
+            // _dom.onmousewheel = this.scrollFunc//滚轮缩放
+            // _dom.onmousewheel = this.scrollFunc2;
+            // 火狐浏览器没有onmousewheel事件,用DOMMouseScroll代替(滚轮事件)
+            // document.body.addEventListener('DOMMouseScroll', this.scrollFunc)
+            // document.body.addEventListener("DOMMouseScroll", this.scrollFunc2);
+            // 禁止火狐浏览器下拖拽图片的默认事件
+            document.ondragstart = function () {
+              return false;
+            };
+            // 判断是否多图
+            if (this.multiple) {
+              if (Array.isArray(this.imgList) && this.imgList.length > 0) {
+                this.imgIndex = Number(this.nowImgIndex) || 0;
+                // this.url = this.imgList[this.imgIndex]
+                this.changeUrl(this.imgList[this.imgIndex], this.imgIndex);
+              } else {
+                // console.error("imgList 为空或格式不正确");
+              }
+            } else {
+              this.changeUrl(this.url);
+            }
+            // 判断是否开启键盘事件
+            if (this.keyboard) {
+              document.addEventListener("keydown", this.keyHandleDebounce);
+            } else {
+              document.addEventListener("keydown", (e) => {
+                e = window.event || e;
+                var key = e.keyCode || e.which || e.charCode;
+                switch (key) {
+                  case 17:
+                    _dom.onmousewheel = this.scrollFunc; //滚轮缩放
+                    document.body.addEventListener(
+                      "DOMMouseScroll",
+                      this.scrollFunc
+                    );
+                    break;
+                }
+              });
+              document.addEventListener("keyup", (e) => {
+                e = window.event || e;
+                var key = e.keyCode || e.which || e.charCode;
+                switch (key) {
+                  case 17:
+                    _dom.onmousewheel = this.scrollFunc2;
+                    document.body.addEventListener(
+                      "DOMMouseScroll",
+                      this.scrollFunc2
+                    );
+                    break;
+                }
+              });
+            }
+          });
+        }
+      },
+      immediate: true,
+    },
+  },
+  methods: {
+    close() {
+      // this.initImg();
+      // this.maxWH = "max-width:100%;max-height:100%;";
+      // this.isFull = false;
+      // 移除火狐浏览器下的鼠标滚动事件
+      document.body.removeEventListener("DOMMouseScroll", this.scrollFunc);
+      //恢复火狐及Safari浏览器下的图片拖拽
+      document.ondragstart = null;
+      // 移除键盘事件
+      if (this.keyboard) {
+        document.removeEventListener("keydown", this.keyHandleDebounce);
+      } else {
+        document.removeEventListener("keydown", (e) => {
+          e = window.event || e;
+          var key = e.keyCode || e.which || e.charCode;
+          switch (key) {
+            case 17:
+              _dom.onmousewheel = this.scrollFunc; //滚轮缩放
+              document.body.addEventListener("DOMMouseScroll", this.scrollFunc);
+              break;
+          }
+        });
+        document.removeEventListener("keyup", (e) => {
+          e = window.event || e;
+          var key = e.keyCode || e.which || e.charCode;
+          switch (key) {
+            case 17:
+              _dom.onmousewheel = this.scrollFunc2;
+              document.body.addEventListener(
+                "DOMMouseScroll",
+                this.scrollFunc2
+              );
+              break;
+          }
+        });
+      }
+      this.show = false;
+    },
+    initImg() {
+      this.mobileScale = 1;
+      this.imgScale = 1;
+      this.imgRotate = 0;
+      this.imgTop = 0;
+      this.imgLeft = 0;
+    },
+    /**
+     * 切换图片
+     * true 下一张
+     * false 上一张
+     */
+    toogleImg(bool) {
+      if (bool) {
+        this.imgIndex++;
+        if (this.imgIndex > this.imgList.length - 1) {
+          this.imgIndex = 0;
+        }
+      } else {
+        this.imgIndex--;
+        if (this.imgIndex < 0) {
+          this.imgIndex = this.imgList.length - 1;
+        }
+      }
+      // this.url = this.imgList[this.imgIndex]
+      this.changeUrl(this.imgList[this.imgIndex], this.imgIndex);
+    },
+    // 改变图片地址
+    /**
+     * @description:
+     * @param {String} url 要显示的图片的url
+     * @param {Number} index 当前显示当图片下标,防止用户点击切换图片过快
+     * @return {*}
+     */
+    changeUrl(url, index) {
+      this.imgState = 1;
+      let img = new Image();
+      img.src = url;
+      img.onload = () => {
+        // 如果加载出来图片当下标不是当前显示图片当下标,则不予显示(用户点击过快当时候,会出现用户点到第三张了,此时第一张图片才加载完当情况)
+        if (index != undefined && index == this.imgIndex) {
+          this.imgState = 2;
+          this.imgurl = url;
+        } else if (index == undefined) {
+          this.imgState = 2;
+          this.imgurl = url;
+        }
+      };
+      img.onerror = () => {
+        if (index != undefined && index == this.imgIndex) {
+          this.imgState = 3;
+        } else if (index == undefined) {
+          this.imgState = 3;
+        }
+      };
+    },
+    // 旋转图片
+    rotateFunc(deg) {
+      this.imgRotate += deg;
+    },
+    // 图片缩放
+    scaleFunc(num, bool) {
+      if (this.imgScale <= 0.2 && num < 0) return;
+      if (bool) {
+        this.imgScale = num;
+      } else {
+        this.imgScale += num;
+      }
+    },
+    // 图片原尺寸切换
+    imgToggle() {
+      this.initImg();
+      if (this.isFull) {
+        this.maxWH = "max-width:100%;max-height:100%;";
+      } else {
+        this.maxWH = "width:100%;height:auto;";
+      }
+      this.isFull = !this.isFull;
+    },
+    // 鼠标滚轮缩放
+    scrollFunc(e) {
+      e = e || window.event;
+      // e.returnValue = false // ie
+      // 火狐下没有wheelDelta,用detail代替,由于detail值的正负和wheelDelta相反,所以取反
+      e.delta = e.wheelDelta || -e.detail;
+
+      e.preventDefault();
+      if (e.delta > 0) {
+        //当滑轮向上滚动时
+        this.scaleFunc(0.05);
+      }
+      if (e.delta < 0) {
+        //当滑轮向下滚动时
+        this.scaleFunc(-0.05);
+      }
+    },
+    // 鼠标滚轮缩放
+    scrollFunc2(e) {
+      e = e || window.event;
+      // e.returnValue = false // ie
+      // 火狐下没有wheelDelta,用detail代替,由于detail值的正负和wheelDelta相反,所以取反
+      e.delta = e.wheelDelta || -e.detail;
+
+      e.preventDefault();
+      if (e.delta > 0) {
+        //当滑轮向上滚动时
+        this.imgTop += 50;
+      }
+      if (e.delta < 0) {
+        //当滑轮向下滚动时
+        this.imgTop -= 50;
+      }
+    },
+    // 鼠标按下
+    addMove(e) {
+      e = e || window.event;
+      this.clientX = e.clientX;
+      this.clientY = e.clientY;
+      this.$refs.heImg.onmousemove = this.moveFunc;
+    },
+    // 手指按下事件
+    addMoveMobile(e) {
+      e.preventDefault();
+      e = e || window.event;
+      if (e.touches.length > 1) {
+        this.start = e.touches;
+      } else {
+        this.clientX = e.touches[0].pageX;
+        this.clientY = e.touches[0].pageY;
+      }
+      // 添加手指拖动事件
+      this.$refs.heImg.ontouchmove = this.moveFuncMobile;
+    },
+    // 鼠标拖动
+    moveFunc(e) {
+      e = e || window.event;
+      e.preventDefault();
+      let movementX = e.clientX - this.clientX;
+      let movementY = e.clientY - this.clientY;
+      // event.clientY;
+      this.imgLeft += movementX * 2;
+      this.imgTop += movementY * 2;
+      this.clientX = e.clientX;
+      this.clientY = e.clientY;
+    },
+    // 手指拖动
+    moveFuncMobile(e) {
+      e = e || window.event;
+      // console.log(e);
+      if (e.touches.length > 1) {
+        var now = e.touches;
+        var scale =
+          this.getDistance(now[0], now[1]) /
+          this.getDistance(this.start[0], this.start[1]);
+        // 判断是否手指缩放过,如果缩放过,要在上次缩放的比例基础上进行缩放
+        if (this.mobileScale) {
+          if (scale > 1) {
+            // 放大
+            this.scaleFunc(scale + this.mobileScale - 1, true);
+          } else {
+            // 缩小
+            this.scaleFunc(scale * this.mobileScale, true);
+          }
+        } else {
+          this.scaleFunc(scale, true);
+        }
+      } else {
+        let touch = e.touches[0];
+        e.preventDefault();
+        let movementX = touch.pageX - this.clientX;
+        let movementY = touch.pageY - this.clientY;
+        // event.clientY;
+        this.imgLeft += movementX * 2;
+        this.imgTop += movementY * 2;
+        this.clientX = touch.pageX;
+        this.clientY = touch.pageY;
+      }
+    },
+    // 移除拖动事件
+    removeMove(type) {
+      if (type === "pc") {
+        this.$refs.heImg.onmousemove = null;
+      } else {
+        this.mobileScale = this.imgScale;
+        this.$refs.heImg.ontouchmove = null;
+      }
+    },
+    keyHandleDebounce(e) {
+      if (this.canRun) {
+        // 如果this.canRun为true证明当前可以执行函数
+        this.keyHandle(e);
+        this.canRun = false; // 执行函数后一段时间内不可再次执行
+        setTimeout(() => {
+          this.canRun = true; // 等到了我们设定的时间之后,把this.canRun改为true,可以再次执行函数
+        }, 300);
+      }
+    },
+    // 键盘事件
+    keyHandle(e) {
+      e = window.event || e;
+      var key = e.keyCode || e.which || e.charCode;
+      switch (key) {
+        case 27: // esc
+          this.close();
+          break;
+        case 65: // a键-上一张
+          if (this.multiple) {
+            this.toogleImg(false);
+          }
+          break;
+        case 68: // d键-下一张
+          if (this.multiple) {
+            this.toogleImg(true);
+          }
+          break;
+        case 87: // w键-放大
+          this.scaleFunc(0.15);
+          break;
+        case 83: // s键-缩小
+          this.scaleFunc(-0.15);
+          break;
+        case 81: // q键-逆时针旋转
+          this.rotateFunc(-90);
+          break;
+        case 69: // e键-顺时针旋转
+          this.rotateFunc(90);
+          break;
+        case 82: // r键-复位键
+          this.initImg();
+          break;
+
+        default:
+          break;
+      }
+    },
+    // 点击遮罩层
+    clickMask() {
+      // console.log("hello");
+      if (this.clickMaskCLose) {
+        this.close();
+      }
+    },
+    //缩放 勾股定理方法-求两点之间的距离
+    getDistance(p1, p2) {
+      var x = p2.pageX - p1.pageX,
+        y = p2.pageY - p1.pageY;
+      return Math.sqrt(x * x + y * y);
+    },
+    /**
+     * @description:
+     * @param {String} imgsrc
+     * @param {*} name
+     * @return {*}
+     */
+    downloadIamge() {
+      //下载图片地址和图片名
+      let image = new Image();
+      // 解决跨域 Canvas 污染问题
+      image.setAttribute("crossOrigin", "anonymous");
+      image.onload = function () {
+        let canvas = document.createElement("canvas");
+        canvas.width = image.width;
+        canvas.height = image.height;
+        let context = canvas.getContext("2d");
+        context.drawImage(image, 0, 0, image.width, image.height);
+        let url = canvas.toDataURL("image/png"); //得到图片的base64编码数据
+        let a = document.createElement("a"); // 生成一个a元素
+        let event = new MouseEvent("click"); // 创建一个单击事件
+        a.download = "photo" + +new Date(); // 设置图片名称
+        a.href = url; // 将生成的URL设置为a.href属性
+        a.dispatchEvent(event); // 触发a的单击事件
+      };
+      image.onerror = function (err) {
+        console.log("图片信息不正确或图片服务器禁止访问");
+        console.log(err);
+      };
+      if (this.multiple) {
+        image.src = this.imgList[this.imgIndex];
+      } else {
+        image.src = this.url;
+      }
+    },
+  },
+};
+</script>
+
+<style scoped>
+@import "./iconfont/iconfont.css";
+@import "./css/default.css";
+</style>

+ 71 - 0
src/components/hevue-img-preview/iconfont/iconfont.css

@@ -0,0 +1,71 @@
+@font-face {
+  font-family: "heimgfont"; /* Project id 1776686 */
+  src: url('iconfont.woff2?t=1635390861127') format('woff2'),
+       url('iconfont.woff?t=1635390861127') format('woff'),
+       url('iconfont.ttf?t=1635390861127') format('truetype');
+}
+
+.heimgfont {
+  font-family: "heimgfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.heimg-iconguanbi1:before {
+  content: "\e608";
+}
+
+.heimg-iconiconfontzhizuobiaozhun023146:before {
+  content: "\e694";
+}
+
+.heimg-iconhelp:before {
+  content: "\e626";
+}
+
+.heimg-iconjiazaizhong:before {
+  content: "\e6b1";
+}
+
+.heimg-icontupianjiazaishibai:before {
+  content: "\ec0d";
+}
+
+.heimg-iconicon_arrow_right:before {
+  content: "\e60d";
+}
+
+.heimg-iconjiantouzuo:before {
+  content: "\e620";
+}
+
+.heimg-iconxuanzhuan:before {
+  content: "\e66f";
+}
+
+.heimg-iconxuanzhuan1:before {
+  content: "\e670";
+}
+
+.heimg-iconyuanshibili:before {
+  content: "\e86b";
+}
+
+.heimg-iconfangda:before {
+  content: "\e65d";
+}
+
+.heimg-iconsuoxiao:before {
+  content: "\e65e";
+}
+
+.heimg-iconquanping:before {
+  content: "\e698";
+}
+
+.heimg-iconguanbi:before {
+  content: "\e764";
+}
+

BIN
src/components/hevue-img-preview/iconfont/iconfont.ttf


BIN
src/components/hevue-img-preview/iconfont/iconfont.woff


BIN
src/components/hevue-img-preview/iconfont/iconfont.woff2


+ 48 - 0
src/components/hevue-img-preview/index.js

@@ -0,0 +1,48 @@
+/*
+ * @Author: heyongsheng
+ * @Date: 2020-04-22 15:40:42
+ * @Last Modified by: heyongsheng
+ * @Last Modified time: 2020-07-08 22:49:13
+ */
+import Vue from "vue";
+import VueToast from "./hevue-img-preview.vue";
+
+const ToastConstructor = Vue.extend(VueToast);
+
+let instance
+let hevueImgPreviewConfig
+
+const ImgPreview = (options = {}) => {
+  if (typeof options === 'string') {
+    options = {
+      url: options
+    };
+  }
+  options.show = true
+  // 优先采取局部配置,其次采取全局配置
+  Object.keys(hevueImgPreviewConfig).map(name => {
+    if ( options[name] == undefined) {
+      options[name] = hevueImgPreviewConfig[name]
+    }
+  })
+
+  instance = new ToastConstructor({
+    data: options
+  })
+  instance.$mount()
+  let dom = instance.$el
+  document.body.appendChild(dom)
+  return instance
+};
+
+const install = (Vue, opts = {}) => {
+  hevueImgPreviewConfig = opts
+  Vue.prototype.$hevueImgPreview = ImgPreview;
+};
+
+if (typeof window !== "undefined" && window.Vue) {
+  // window.Vue.use(install);
+  install(window.Vue)
+}
+
+export default install;

+ 27 - 0
src/components/hevue-img-preview/package.json

@@ -0,0 +1,27 @@
+{
+  "name": "hevue-img-preview",
+  "description": "> 本组件是一个基于 vue 编写的 vue 图片预览组件,支持 pc 和手机端,支持单图和多图预览,仅传入一个图片地址,即可实现图片预览效果。手机端支持单指拖拽和双指缩放。页面各组件颜色均可可自定义,实现个性化设计,如果能帮上你,希望可以移步 [GitHub](https://github.com/heyongsheng/hevue-img-preview) ,或者[码云](https://gitee.com/ihope_top/hevue-img-preview) 给个小星星,如果有任何使用意见或建议,也欢迎回复或者提交 issue",
+  "version": "5.0.3",
+  "author": "贺永胜 <1378431028@qq.com>",
+  "license": "MIT",
+  "private": false,
+  "main": "./index.js",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/heyongsheng/hevue-img-preview"
+  },
+  "keywords": [
+    "hevue-img-preview",
+    "vue-img-preview",
+    "vue-img",
+    "vue-img-view",
+    "img-view",
+    "view-preview",
+    "hevue"
+  ],
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not ie <= 10"
+  ]
+}

+ 1 - 1
src/const/index.js

@@ -45,7 +45,7 @@ export const tools = {
   },
   40: {
     name: '个人评价',
-    img: require('@/assets/images/tool/thirdToolList/evalua.png')
+    img: require('@/assets/images/tool/thirdToolList/eval.png')
   },
   41: {
     name: '选择填空',

+ 4 - 1
src/main.js

@@ -28,12 +28,15 @@ import WechatPlugin from '@/utils/wechatPlugin'
 Vue.use(WechatPlugin)
 // 引入本地存储
 import { storage, sessionStorage } from '@/utils/storage'
+import hevueImgPreview from '@/components/hevue-img-preview'
 
 import '@/permission' // permission control
 Vue.prototype.$storage = storage
 Vue.prototype.$sessionStorage = sessionStorage
 // 动态设置title
-Vue.use(require('vue-wechat-title')).use(Calendar)
+Vue.use(require('vue-wechat-title'))
+  .use(Calendar)
+  .use(hevueImgPreview)
 
 Vue.config.productionTip = false
 

+ 7 - 4
src/plugins/vant.js

@@ -1,9 +1,12 @@
 // 按需全局引入 vant组件
 import Vue from 'vue'
-import { Button, List, Cell, Tabbar, TabbarItem, Notify } from 'vant'
+import { Button, List, Cell, Tabbar, TabbarItem, Notify, Field, CellGroup, Toast } from 'vant'
 Vue.use(Button)
-Vue.use(Cell)
-Vue.use(List)
-Vue.use(Tabbar)
+  .use(Cell)
+  .use(List)
+  .use(Tabbar)
   .use(TabbarItem)
   .use(Notify)
+  .use(Field)
+  .use(CellGroup)
+  .use(Toast)

File diff suppressed because it is too large
+ 3 - 0
src/utils/aws-sdk-2.235.1.min.js


+ 5 - 0
src/utils/request.js

@@ -1,8 +1,10 @@
 import axios from 'axios'
 import store from '@/store'
+import qs from 'qs'
 import { Toast } from 'vant'
 // 根据环境不同引入不同api地址
 import { baseApi } from '@/config'
+
 // create an axios instance
 const service = axios.create({
   baseURL: baseApi, // url = base api url + request url
@@ -20,6 +22,9 @@ service.interceptors.request.use(
         forbidClick: true
       })
     }
+    if (config.method === 'post') {
+      config.data = qs.stringify(config.data) // 序列化post 参数
+    }
     if (store.getters.token) {
       config.headers['X-Token'] = ''
     }

+ 244 - 0
src/views/course/components/AudioComponent.vue

@@ -0,0 +1,244 @@
+<template>
+  <div class="audio-container">
+    <div class="audio-box">
+      <div
+        type="primary"
+        :style="{ background: isRecord ? '#ee5255' : '', borderColor: isRecord ? '#ee5255' : '' }"
+        @click="startRecorder()"
+      >
+        {{ !isRecord ? '开始录音' : '结束录音' }}
+      </div>
+      <div
+        type="primary"
+        :style="{ background: isPlayerRecord ? '#ee5255' : '', borderColor: isPlayerRecord ? '#ee5255' : '' }"
+        @click="playRecorder()"
+      >
+        {{ !isPlayerRecord ? '录音播放' : '停止播放' }}
+      </div>
+      <div type="primary" @click="getMp3Data()">上传录音</div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Recorder from 'js-audio-recorder'
+const lamejs = require('lamejs')
+
+const recorder = new Recorder({
+  sampleBits: 16, // 采样位数,支持 8 或 16,默认是16
+  sampleRate: 48000, // 采样率,支持 11025、16000、22050、24000、44100、48000,根据浏览器默认值,我的chrome是48000
+  numChannels: 1 // 声道,支持 1 或 2, 默认是1
+  // compiling: false,(0.x版本中生效,1.x增加中) // 是否边录边转换,默认是false
+})
+
+// 绑定事件-打印的是当前录音数据
+recorder.onprogress = function(params) {
+  // console.log('--------------START---------------')
+  // console.log('录音时长(秒)', params.duration);
+  // console.log('录音大小(字节)', params.fileSize);
+  // console.log('录音音量百分比(%)', params.vol);
+  // console.log('当前录音的总数据([DataView, DataView...])', params.data);
+  // console.log('--------------END---------------')
+}
+export default {
+  props: [],
+  data() {
+    return {
+      LuAudioUrl: '',
+      isRecord: false,
+      isPlayerRecord: false
+    }
+  },
+  methods: {
+    // 开始录音
+    startRecorder() {
+      const _this = this
+      if (!_this.isRecord) {
+        recorder.destroy() // 销毁录音
+        _this.isRecord = true
+        recorder.start().then(
+          () => {},
+          error => {
+            _this.isRecord = false
+            // _this.$message.error(`${error.name} : ${error.message}`);
+            _this.$message.error(`没有找到可使用的麦克风,或者您没有允许此网页使用麦克风`)
+            // 出错了
+            console.log(`${error.name} : ${error.message}`)
+          }
+        )
+      } else {
+        _this.isRecord = false
+        recorder.stop() // 结束录音
+      }
+    },
+
+    // 录音播放
+    playRecorder() {
+      if (!recorder.fileSize) {
+        return
+      }
+      if (!this.isPlayerRecord) {
+        this.isPlayerRecord = true
+        recorder.play()
+      } else {
+        this.isPlayerRecord = false
+        recorder.stopPlay() // 停止录音播放
+      }
+      recorder.onplayend = () => {
+        this.isPlayerRecord = false
+        console.log('onplayend')
+      }
+    },
+
+    /**
+     * 文件格式转换 wav-map3
+     * */
+    getMp3Data() {
+      if (!recorder.fileSize) {
+        this.$message.error('请录音后在上传语音')
+        return
+      }
+      const mp3Blob = this.convertToMp3(recorder.getWAV())
+      const audioFile = this.dataURLtoAudio(mp3Blob, '音频')
+      console.log(audioFile)
+      this.beforeUpload1(audioFile, 3)
+      // recorder.download(mp3Blob, "recorder", "mp3");
+    },
+    convertToMp3(wavDataView) {
+      // 获取wav头信息
+      const wav = lamejs.WavHeader.readHeader(wavDataView) // 此处其实可以不用去读wav头信息,毕竟有对应的config配置
+      const { channels, sampleRate } = wav
+      const mp3enc = new lamejs.Mp3Encoder(channels, sampleRate, 128)
+      // 获取左右通道数据
+      const result = recorder.getChannelData()
+      const buffer = []
+      const leftData = result.left && new Int16Array(result.left.buffer, 0, result.left.byteLength / 2)
+      const rightData = result.right && new Int16Array(result.right.buffer, 0, result.right.byteLength / 2)
+      const remaining = leftData.length + (rightData ? rightData.length : 0)
+      const maxSamples = 1152
+      for (let i = 0; i < remaining; i += maxSamples) {
+        const left = leftData.subarray(i, i + maxSamples)
+        let right = null
+        let mp3buf = null
+        if (channels === 2) {
+          right = rightData.subarray(i, i + maxSamples)
+          mp3buf = mp3enc.encodeBuffer(left, right)
+        } else {
+          mp3buf = mp3enc.encodeBuffer(left)
+        }
+        if (mp3buf.length > 0) {
+          buffer.push(mp3buf)
+        }
+      }
+
+      const enc = mp3enc.flush()
+      if (enc.length > 0) {
+        buffer.push(enc)
+      }
+      return new Blob(buffer, { type: 'audio/mp3' })
+    },
+    dataURLtoAudio(blob, filename) {
+      return new File([blob], filename, { type: 'audio/mp3' })
+    },
+    beforeUpload1(event, type) {
+      var file
+      if (type === 3) {
+        file = event
+      } else {
+        file = event.target.files[0]
+      }
+      var credentials = {
+        accessKeyId: 'AKIATLPEDU37QV5CHLMH',
+        secretAccessKey: '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'
+      } // 秘钥形式的登录上传
+      window.AWS.config.update(credentials)
+      window.AWS.config.region = 'cn-northwest-1' // 设置区域
+
+      // eslint-disable-next-line prettier/prettier
+      var bucket = new window.AWS.S3({ params: { Bucket: 'ccrb' }}) // 选择桶
+      var _this = this
+      const loading = _this.$toast.loading({
+        forbidClick: true,
+        duration: 0
+      })
+
+      if (file) {
+        var params = {
+          Key:
+            file.name.split('.')[0] +
+            new Date().getTime() +
+            '.' +
+            file.name.split('.')[file.name.split('.').length - 1],
+          ContentType: file.type,
+          Body: file,
+          'Access-Control-Allow-Credentials': '*',
+          ACL: 'public-read'
+        } // key可以设置为桶的相抵路径,Body为文件, ACL最好要设置
+        var options = {
+          partSize: 2048 * 1024 * 1024,
+          queueSize: 2,
+          leavePartsOnError: true
+        }
+        bucket
+          .upload(params, options)
+          .on('httpUploadProgress', function(evt) {
+            // 这里可以写进度条
+            // console.log("Uploaded : " + parseInt((evt.loaded * 80) / evt.total) + '%');
+            // _this.progress = parseInt((evt.loaded * 80) / evt.total);
+          })
+          .send(function(err, data) {
+            // _this.progress = 100;
+            setTimeout(() => {
+              loading.clear()
+            }, 1000)
+            if (err) {
+              var a = _this.$refs.upload1.uploadFiles
+              a.splice(a.length - 1, a.length)
+              _this.$message.error('上传失败')
+            } else {
+              if (type === 3) {
+                _this.LuAudioUrl = data.Location
+                // _this.addWork(8);
+                _this.addPz(data.Location)
+              }
+              console.log(data.Location)
+            }
+          })
+      }
+    },
+    addPz(audio) {
+      this.$emit('addPz', '2', audio)
+    }
+  },
+  mounted() {}
+}
+</script>
+
+<style lang="scss" scoped>
+.audio-container {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  .audio-box {
+    height: 100%;
+    width: 100%;
+    display: flex;
+    // flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    > div {
+      font-size: 14px;
+      padding: 10px 20px;
+      background: #409eff;
+      color: #fff;
+      border-radius: 5px;
+      margin-top: 20px;
+      & + div {
+        margin-left: 10px;
+      }
+    }
+  }
+}
+</style>

+ 237 - 0
src/views/course/components/commentBox.vue

@@ -0,0 +1,237 @@
+<template>
+  <div class="cb-container">
+    <div class="title" v-if="pzList.length">评课笔记</div>
+    <div class="pzListBox" v-if="pzList && pzList.length">
+      <div class="pzList" v-for="(pz, pzIndex) in pzList" :key="pzIndex">
+        <div class="pzNavTop">
+          <div>批</div>
+          <div>
+            {{ pz.username && pz.username.length > 8 ? pz.username.substring(0, 8) + '...' : pz.username }}的批注
+          </div>
+          <div class="pzDelete" v-if="pz.userid == userid" @click="deletePz(pz.id)">
+            删除
+          </div>
+        </div>
+        <div class="pzContent cont" v-html="pz.content" v-if="pz.type == '1'"></div>
+        <div class="pzContent" v-if="pz.type == '2'">
+          <audio :src="pz.content" controls="controls" ref="audio">
+            Your browser does not support the audio element.
+          </audio>
+        </div>
+        <div class="pzContent" v-if="pz.type == '3'">
+          <img :src="pz.content" style="width: 90%; margin: 0 auto; display: block" @click="previewImg(pz.content)" />
+        </div>
+        <div class="time">
+          {{ pz.time }}
+        </div>
+      </div>
+    </div>
+    <div class="noPz" v-else>
+      <img src="@/assets/images/course/noPz.png" alt="" />
+    </div>
+
+    <div class="commentInput" @click="panelVisible = true">
+      <div class="input">点击发一条评课内容</div>
+    </div>
+    <commentPanel
+      :panelVisible.sync="panelVisible"
+      :courseid="courseid"
+      :courseType="courseType"
+      :taskCount="taskCount"
+    ></commentPanel>
+  </div>
+</template>
+
+<script>
+import commentPanel from './commentPanel.vue'
+import { getCoursePz } from '@/api/course'
+export default {
+  props: {
+    courseid: {
+      type: String,
+      default: ''
+    },
+    courseType: {
+      type: Number,
+      default: 0
+    },
+    taskCount: {
+      type: Number,
+      default: 0
+    }
+  },
+  components: {
+    commentPanel
+  },
+  data() {
+    return {
+      pzList: [],
+      panelVisible: false,
+      userid: ''
+    }
+  },
+  watch: {
+    courseType(newValue, oldValue) {
+      this.getData()
+    },
+    taskCount(newValue, oldValue) {
+      this.getData()
+    },
+    panelVisible(newValue, oldValue) {
+      this.getData()
+    }
+  },
+  methods: {
+    getData() {
+      const params = {
+        cid: this.courseid,
+        s: this.courseType,
+        t: this.taskCount
+      }
+      getCoursePz(params)
+        .then(res => {
+          this.pzList = res[0]
+        })
+        .catch(err => {
+          console.log(err)
+        })
+    }
+  },
+  mounted() {
+    this.getData()
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.cb-container {
+  width: 100%;
+  padding-bottom: 1.5rem;
+  .commentInput {
+    background: #fff;
+    box-shadow: 0 -1px 3px 1px #dedede82;
+    height: 1.5rem;
+    width: 100%;
+    position: fixed;
+    bottom: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    .input {
+      width: 95%;
+      height: 70%;
+      border-radius: 5px;
+      background: rgb(243, 243, 243);
+      display: flex;
+      align-items: center;
+      box-sizing: border-box;
+      padding: 0 0.4rem;
+      font-size: 14px;
+      color: #585858;
+    }
+  }
+  .title {
+    font-size: 14px;
+    width: 95%;
+    margin: 10px auto 0;
+  }
+  .pzBox,
+  .noPzBox {
+    height: calc(100% - 60px);
+    /* background: #ededed; */
+    background: #fff;
+    /* border-radius: 0 0 15px 15px; */
+  }
+
+  .noPzBox {
+    display: flex;
+    flex-direction: column;
+    flex-wrap: nowrap;
+    justify-content: center;
+    align-items: center;
+  }
+
+  .pzList {
+    background: #f7f7f7;
+    width: 95%;
+    margin: 0 auto 10px;
+    border-radius: 5px;
+  }
+
+  .pzNavTop {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: nowrap;
+    padding: 10px 10px 0 10px;
+    align-items: center;
+  }
+
+  .pzDelete {
+    cursor: pointer;
+    margin-left: auto;
+    font-size: 14px;
+    color: rgb(248, 248, 248);
+  }
+
+  .pzNavTop > div:nth-child(1) {
+    background: #3760af;
+    width: 30px;
+    height: 30px;
+    color: #fff;
+    text-align: center;
+    line-height: 30px;
+    border-radius: 50%;
+    font-size: 14px;
+  }
+
+  .pzNavTop > div:nth-child(2) {
+    font-size: 16px;
+    color: #959595;
+    margin-left: 5px;
+  }
+
+  /deep/ .pzContent {
+    padding: 10px;
+    word-break: break-word;
+    font-size: 14px;
+    > p {
+      margin: 0;
+    }
+  }
+
+  .pzContent audio {
+    width: 100%;
+    height: 1.2rem;
+  }
+
+  .pzContent audio::-webkit-media-controls-panel {
+    background: #fff;
+  }
+
+  .pzListBox {
+    padding-top: 15px;
+    height: calc(100% - 60px);
+    overflow: auto;
+  }
+
+  .noPz {
+    width: 100%;
+    height: 5rem;
+    margin: 0 auto 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    > img {
+      width: 3rem;
+    }
+  }
+  .pzList .time {
+    text-align: right;
+    box-sizing: border-box;
+    padding: 0 10px 10px 0px;
+    color: #949494;
+    font-size: 14px;
+  }
+}
+</style>

+ 180 - 0
src/views/course/components/commentPanel.vue

@@ -0,0 +1,180 @@
+<template>
+  <div class="cp-container" v-if="panelVisible">
+    <div class="cp-title">
+      <span class="back" @click="back"></span>
+      <span class="title">评课</span>
+      <span class="btn" v-if="type == 1" @click="addPz(type)">提交</span>
+    </div>
+    <div class="cp-box">
+      <div class="type-nav">
+        <div class="type-nav-box" :class="{ active: type == 1 }" @click="setType(1)">
+          <span>文本</span>
+        </div>
+        <div class="type-nav-box" :class="{ active: type == 2 }" @click="setType(2)">
+          <span>音频</span>
+        </div>
+      </div>
+      <div class="cp-conent" v-if="type == 1">
+        <van-field v-model="message" rows="20" autosize type="textarea" placeholder="请输入..." />
+      </div>
+      <div class="cp-audio" v-if="type == 2">
+        <audio-component @addPz="addPz"></audio-component>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+
+import AudioComponent from './AudioComponent.vue'
+import { addPz2 } from '@/api/course'
+
+export default {
+  props: {
+    panelVisible: {
+      type: Boolean
+    },
+    courseid: {
+      type: String,
+      default: ''
+    },
+    courseType: {
+      type: Number,
+      default: 0
+    },
+    taskCount: {
+      type: Number,
+      default: 0
+    }
+  },
+  components: {
+    AudioComponent
+  },
+  data() {
+    return {
+      type: 1,
+      message: ''
+    }
+  },
+  methods: {
+    back() {
+      this.$emit('update:panelVisible', false)
+    },
+    setType(type) {
+      this.type = type
+    },
+    addPz(type, content) {
+      if (type === 1 && this.message === '') {
+        this.$toast({ message: '批注不能为空!', type: 'fail' })
+        return
+      }
+      const params = [
+        {
+          cid: this.courseid,
+          uid: '',
+          s: this.courseType,
+          t: this.taskCount,
+          c: type === 1 ? this.message.replaceAll(/%/g, '%25') : content,
+          type: type
+        }
+      ]
+
+      addPz2(params)
+        .then(res => {
+          this.$toast({
+            message: '添加成功',
+            type: 'success'
+          })
+          this.$emit('update:panelVisible', false)
+        })
+        .catch(err => {
+          console.error(err)
+        })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.cp-container {
+  width: 100vw;
+  height: 100vh;
+  position: fixed;
+  top: 0;
+  background: #fff;
+
+  .cp-title {
+    height: 1.5rem;
+    width: 100%;
+    border-bottom: 1px solid #cecece;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    box-sizing: border-box;
+    position: relative;
+    .back {
+      width: 0.3rem;
+      height: 0.3rem;
+      border-top: 2px solid #000000;
+      border-left: 2px solid #000000;
+      position: absolute;
+      transform: rotate(-45deg) translateY(-50%);
+      top: 47%;
+      left: 0.8rem;
+      cursor: pointer;
+    }
+    .title {
+      font-size: 0.5rem;
+    }
+    .btn {
+      font-size: 0.4rem;
+      transform: translateY(-50%);
+      top: 50%;
+      right: 0.8rem;
+      position: absolute;
+      color: #2274ff;
+      cursor: pointer;
+    }
+  }
+  .cp-box {
+    width: 100%;
+    height: calc(100% - 1.5rem);
+    .type-nav {
+      height: 1rem;
+      width: 100%;
+      display: flex;
+      align-items: center;
+      padding: 0 0.5rem;
+      box-sizing: border-box;
+
+      .type-nav-box {
+        height: 100%;
+        display: flex;
+        font-size: 0.4rem;
+        align-items: center;
+        box-sizing: border-box;
+        padding-bottom: 2px;
+
+        + .type-nav-box {
+          margin-left: 1rem;
+        }
+
+        &.active {
+          padding-bottom: 0;
+          color: rgb(73, 161, 241);
+          border-bottom: 2px solid rgb(73, 161, 241);
+        }
+        > img {
+          width: 0.5rem;
+          height: 0.5rem;
+          object-fit: cover;
+        }
+      }
+    }
+    .cp-conent {
+      height: calc(100% - 1rem);
+      overflow: auto;
+    }
+  }
+}
+</style>

+ 7 - 1
src/views/course/components/courseContentList.vue

@@ -3,7 +3,13 @@
 </template>
 
 <script>
-export default {}
+export default {
+  props: {},
+  data() {
+    return {}
+  },
+  methods: {}
+}
 </script>
 
 <style lang="scss" scoped></style>

+ 2 - 1
src/views/course/components/courseTitle.vue

@@ -67,12 +67,13 @@ export default {
   }
   /deep/ .ct-detail {
     width: 100%;
-    padding: 0.1rem;
+    padding: 10px;
     line-height: 25px;
     box-sizing: border-box;
     background: rgb(241, 241, 241);
     word-break: break-all;
     margin: 0.3rem 0 0;
+    font-size: 14px;
     p {
       margin: 0;
     }

+ 355 - 0
src/views/course/components/navBox.vue

@@ -0,0 +1,355 @@
+<template>
+  <div class="nav-container" v-if="navVisible" @click="back">
+    <div
+      class="nav-box"
+      @click.stop="
+        () => {
+          return
+        }
+      "
+    >
+      <div class="title">
+        <span class="name">目录</span>
+        <span class="close" @click="back">×</span>
+      </div>
+      <div class="navBox">
+        <div v-for="(item, stageIndex) in navList" :key="stageIndex">
+          <div class="blue_box_one" @click="get(stageIndex)">
+            <div>第{{ stageIndex + 1 }}阶段</div>
+            <div>{{ item.dyName }}</div>
+          </div>
+          <div class="twoChild" :class="{ navActive: item.isOpen }">
+            <div class="navChild" v-for="(nav, navIndex) in item.task" :key="navIndex">
+              <div
+                class="navTask"
+                @click="openTask(stageIndex, navIndex, nav.id)"
+                :class="{
+                  openTaskActive: navIndex == taskCount && nav.id == navId && stageIndex == courseType
+                }"
+              >
+                <div
+                  class="vedioNav"
+                  :class="{
+                    isClick: navIndex == taskCount && nav.id == navId && stageIndex == courseType
+                  }"
+                  style="margin: 0"
+                >
+                  任务{{ navIndex + 1 }}
+                </div>
+                <div
+                  class="navTaskname"
+                  :style="{
+                    width: IsLookOpen && !nav.isLook ? 'calc(100% - 75px)' : 'auto'
+                  }"
+                >
+                  {{ nav.taskName }}
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    navVisible: {
+      type: Boolean
+    },
+    chapList: {
+      type: Array,
+      default: () => []
+    },
+    courseType: {
+      type: Number,
+      default: 0
+    },
+    taskCount: {
+      type: Number,
+      default: 0
+    }
+  },
+  data() {
+    return {
+      navList: [],
+      navId: [],
+      IsLookOpen: false
+    }
+  },
+  watch: {
+    chapList(newValue, oldValue) {
+      this.setNavList()
+    }
+  },
+  methods: {
+    back() {
+      this.$emit('update:navVisible', false)
+    },
+    setNavList() {
+      if (this.navList.length === 0 && this.chapList.length) {
+        const navList = []
+        for (var l = 0; l < this.chapList.length; l++) {
+          var q = this.chapList[l].dyName
+          var w = this.chapList[l].chapterInfo[0].taskJson
+          var e
+          navList.push({
+            dyName: q,
+            isOpen: l === 0,
+            task: []
+          })
+          for (var r = 0; r < w.length; r++) {
+            e = w[r].task
+            if (this.chapList[l].easy === 1) {
+              navList[l].task.push({
+                taskName: q,
+                id: l + '-' + r,
+                isLook: w[r].isLook
+              })
+            } else {
+              navList[l].task.push({
+                taskName: e,
+                id: l + '-' + r,
+                isLook: w[r].isLook
+              })
+            }
+          }
+        }
+        navList[0].isOpen = false
+        navList[this.courseType].isOpen = true
+        this.navList = navList
+        this.navId = navList[this.courseType].task[this.taskCount].id
+      }
+    },
+    nextOrpreSteps(t) {
+      var b = this.chapList.length - 1
+      if (t === 0) {
+        if (this.courseType === 0) {
+          if (this.taskCount === 0) {
+            this.navList[this.courseType].isOpen = false
+            this.$emit('update:courseType', b)
+            this.$emit('update:taskCount', this.chapList[b].chapterInfo[0].taskJson.length - 1)
+            this.navList[this.courseType].isOpen = true
+          } else {
+            this.$emit('update:taskCount', this.taskCount - 1)
+          }
+        } else {
+          if (this.taskCount === 0) {
+            this.navList[this.courseType].isOpen = false
+            this.$emit('update:courseType', this.courseType - 1)
+            this.$emit('update:taskCount', this.chapList[this.courseType - 1].chapterInfo[0].taskJson.length - 1)
+            this.navList[this.courseType].isOpen = true
+          } else {
+            this.$emit('update:taskCount', this.taskCount - 1)
+          }
+        }
+      } else {
+        if (this.courseType === b) {
+          if (this.taskCount === this.chapList[this.courseType].chapterInfo[0].taskJson.length - 1) {
+            this.navList[this.courseType].isOpen = false
+            this.$emit('update:courseType', 0)
+            this.$emit('update:taskCount', 0)
+            this.navList[this.courseType].isOpen = true
+          }
+        } else {
+          if (this.taskCount === this.chapList[this.courseType].chapterInfo[0].taskJson.length - 1) {
+            this.navList[this.courseType].isOpen = false
+            this.$emit('update:courseType', this.courseType + 1)
+            this.$emit('update:taskCount', 0)
+            this.navList[this.courseType].isOpen = true
+          } else {
+            this.$emit('update:taskCount', this.taskCount + 1)
+          }
+        }
+      }
+      document.scrollingElement.scrollTop = 0
+      this.$emit('getCourse')
+      this.$forceUpdate()
+    },
+    get(i) {
+      this.navList[i].isOpen = !this.navList[i].isOpen
+    },
+    openTask(s, n, i) {
+      this.navId = i
+      document.scrollingElement.scrollTop = 0
+      this.$emit('update:courseType', s)
+      this.$emit('update:taskCount', n)
+      this.$emit('update:navVisible', false)
+      this.$emit('getCourse')
+    }
+  },
+  mounted() {
+    this.setNavList()
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.nav-container {
+  width: 100vw;
+  height: 100vh;
+  background-color: rgba(0, 0, 0, 0.5);
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 99;
+  .nav-box {
+    background: #fff;
+    height: 90%;
+    width: 95%;
+    border-radius: 10px;
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    .title {
+      position: relative;
+      display: flex;
+      align-items: center;
+      height: 1.2rem;
+      justify-content: center;
+      font-size: 16px;
+      font-weight: bold;
+      border-bottom: 1px solid #d8d8d8;
+
+      .close {
+        position: absolute;
+        right: 0.5rem;
+        width: 0.6rem;
+        height: 0.6rem;
+        border-radius: 100%;
+        background: rgb(231, 235, 236);
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        font-size: 18px;
+        font-weight: 400;
+        color: rgb(128, 131, 139);
+      }
+    }
+
+    .navBox {
+      height: calc(100% - 1.8rem);
+      overflow: auto;
+      width: 100%;
+      .blue_box_one {
+        text-align: center;
+        color: #fff;
+        background-image: linear-gradient(90deg, #477cd7, #65b9fc);
+        border-radius: 7px;
+        margin: 10px;
+        cursor: pointer;
+        width: 95%;
+        height: 45px;
+        display: flex;
+        flex-direction: row;
+        justify-content: flex-start;
+        align-items: center;
+        font-size: 16px;
+      }
+
+      .blue_box_one > div:nth-child(1) {
+        line-height: 30px;
+        margin: 0 5px 0 10px;
+        width: 30%;
+        min-width: 70px;
+        border-radius: 4px;
+      }
+
+      .blue_box_one > div:nth-child(2) {
+        @include textoverflow();
+        width: 70%;
+        text-align: left;
+        margin-right: 10px;
+        max-width: calc(100% - 85px);
+      }
+
+      .blue_box_one > div:nth-child(2):hover {
+        @include textoverflow();
+        cursor: pointer;
+      }
+      .twoChild {
+        width: 95%;
+        margin: 10px;
+        border-radius: 5px;
+        background: #f2f2f2;
+        display: flex;
+        flex-direction: column;
+        flex-wrap: nowrap;
+        justify-content: flex-start;
+        align-items: flex-start;
+        transition: all 0.5s;
+        overflow: hidden;
+        height: 0;
+        background: #e7f3ff;
+      }
+
+      .twoChild > div:nth-child(1) {
+        margin-top: 5px;
+      }
+
+      .navChild {
+        width: 100%;
+        cursor: pointer;
+        margin-bottom: 10px;
+        position: relative;
+      }
+
+      .navChild img {
+        position: absolute;
+        right: 11px;
+        width: 15px;
+        top: 50%;
+        transform: translateY(-50%);
+      }
+
+      .navActive {
+        height: auto;
+      }
+
+      .navTask {
+        display: flex;
+        flex-direction: row;
+        flex-wrap: nowrap;
+        align-items: center;
+        align-content: flex-start;
+        height: 40px;
+        justify-content: flex-start;
+        padding: 0 10px;
+        width: 100%;
+        box-sizing: border-box;
+      }
+
+      .navTaskname {
+        @include textoverflow();
+        padding-left: 5px;
+        font-size: 16px;
+      }
+
+      .openTaskActive {
+        color: #4386e6;
+      }
+      .isClick {
+        background: #4d9def !important;
+      }
+
+      .vedioNav {
+        margin: 10px 0 0 15px;
+        border-bottom: 1px solid #d7d7d7;
+        padding-bottom: 5px;
+        background: #96d1ff;
+        width: 55px;
+        min-width: 55px;
+        border-radius: 5px;
+        color: #fff;
+        text-align: center;
+        height: 20px;
+        line-height: 26px;
+        font-size: 14px;
+      }
+    }
+  }
+}
+</style>

+ 119 - 14
src/views/course/components/stepsBox.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="sb-container" v-if="chapInfo && chapInfo.chapterInfo">
-    <div class="steps-box" v-for="(step, stepI) in chapInfo.chapterInfo[0].taskJson[taskCount].toolChoose" :key="stepI">
+    <div class="steps-box" v-for="(step, stepI) in toolChoose()" :key="stepI">
       <div class="step-title">
         <div class="step">
           <span>步骤{{ stepI + 1 }}</span>
@@ -16,18 +16,33 @@
         v-if="isWorkTool.indexOf(step.tool[0]) !== -1 && worksStudent[stepI] && worksStudent[stepI].length"
       >
         <div class="title">作业预览</div>
+        <div class="works">
+          <Works class="work" v-for="(work, workI) in worksStudent[stepI]" :key="stepI + '-' + workI" :work="work">
+          </Works>
+        </div>
       </div>
-      <div class="no-works-box" v-if="isWorkTool.indexOf(step.tool[0]) !== -1">
+      <div
+        class="no-works-box"
+        v-if="isWorkTool.indexOf(step.tool[0]) !== -1 && noWorksS[stepI] && noWorksS[stepI].length"
+      >
         <div class="title">未提交</div>
-        <div v-if="noWorksS"></div>
+        <div class="no-works-box-students">
+          <div v-for="(student, noI) in noWorksS[stepI]" :key="stepI + '-' + noI" class="student">
+            {{ student.student }}
+          </div>
+        </div>
       </div>
     </div>
+    <div v-if="!toolChoose().length" class="noMssage">
+      <img src="@/assets/images/course/isNoMessage.png" />
+    </div>
   </div>
 </template>
 
 <script>
 import { getCourseWorks, getCourseWorksStudentJuri } from '@/api/course'
 import { tools } from '@/const/index'
+import Works from './works.vue'
 export default {
   props: {
     chapInfo: {
@@ -53,21 +68,45 @@ export default {
     tcid: {
       type: String,
       default: ''
+    },
+    oid: {
+      type: String,
+      default: ''
     }
   },
+  components: {
+    Works
+  },
   data() {
     return {
       worksStudent: [],
       isWorksS: [],
       isCloseList: [],
       noWorksS: [],
-      isWorkTool: [16, 32, 57, 4, 45, 15, 1, 3, 6, 7, 26, 41, 47, 48, 52, 50, 40, 49],
+      isWorkTool: [16, 32, 57, 4, 45, 15, 1, 3, 6, 7, 26, 41, 47, 48, 52, 50, 40], //, 49
       tools: tools
     }
   },
   watch: {
     courseid(newValue, oldValue) {
       this.getWorks()
+    },
+    courseType(newValue, oldValue) {
+      this.getWorks()
+    },
+    taskCount(newValue, oldValue) {
+      this.getWorks()
+    }
+  },
+  computed: {
+    toolChoose() {
+      return () => {
+        let toolchoose = this.chapInfo.chapterInfo[0].taskJson[this.taskCount].toolChoose
+        toolchoose = toolchoose.filter(el => {
+          return el.tool.length
+        })
+        return toolchoose
+      }
     }
   },
   methods: {
@@ -202,7 +241,7 @@ export default {
                       works[j].content.split('.')[works[j].content.split('.').length - 1].toLocaleUpperCase()
                     ) !== -1 &&
                     steps[step].tool[0] === 16 &&
-                    works[j].atool !== 50
+                    (works[j].atool ? parseInt(works[j].atool) : '') !== 50
                   ) {
                     const _works = {
                       userid: works[j].userid,
@@ -226,7 +265,7 @@ export default {
                       works[j].content.split('.')[works[j].content.split('.').length - 1].toLocaleUpperCase()
                     ) !== -1 &&
                     steps[step].tool[0] === 16 &&
-                    works[j].atool !== 50
+                    (works[j].atool ? parseInt(works[j].atool) : '') !== 50
                   ) {
                     const _works = {
                       userid: works[j].userid,
@@ -246,7 +285,7 @@ export default {
 
                     worksStudent[step].push(_works)
                     isWorksS[step].push({ uid: works[j].userid, sName: works[j].name })
-                  } else if (works[j].type === 6 && works[j].atool !== 50) {
+                  } else if (works[j].type === 6 && (works[j].atool ? parseInt(works[j].atool) : '') !== 50) {
                     const _works = {
                       userid: works[j].userid,
                       ateacher: works[j].ateacher,
@@ -265,7 +304,7 @@ export default {
 
                     worksStudent[step].push(_works)
                     isWorksS[step].push({ uid: works[j].userid, sName: works[j].name })
-                  } else if (works[j].type === 7 && works[j].atool !== 50) {
+                  } else if (works[j].type === 7 && (works[j].atool ? parseInt(works[j].atool) : '') !== 50) {
                     const _works = {
                       userid: works[j].userid,
                       ateacher: works[j].ateacher,
@@ -284,7 +323,10 @@ export default {
 
                     worksStudent[step].push(_works)
                     isWorksS[step].push({ uid: works[j].userid, sName: works[j].name })
-                  } else if (works[j].type === 12 && steps[step].tool[0] === works[j].atool) {
+                  } else if (
+                    works[j].type === 12 &&
+                    steps[step].tool[0] === (works[j].atool ? parseInt(works[j].atool) : '')
+                  ) {
                     const _works = {
                       userid: works[j].userid,
                       ateacher: works[j].ateacher,
@@ -303,7 +345,10 @@ export default {
 
                     worksStudent[step].push(_works)
                     isWorksS[step].push({ uid: works[j].userid, sName: works[j].name })
-                  } else if (works[j].type === 1 && steps[step].tool[0] === works[j].atool) {
+                  } else if (
+                    works[j].type === 1 &&
+                    steps[step].tool[0] === (works[j].atool ? parseInt(works[j].atool) : '')
+                  ) {
                     const _works = {
                       userid: works[j].userid,
                       ateacher: works[j].ateacher,
@@ -322,7 +367,7 @@ export default {
 
                     worksStudent[step].push(_works)
                     isWorksS[step].push({ uid: works[j].userid, sName: works[j].name })
-                  } else if (works[j].type === 1 && !parseInt(works[j].atool)) {
+                  } else if (works[j].type === 1 && !parseInt(works[j].atool ? parseInt(works[j].atool) : '')) {
                     const _works = {
                       userid: works[j].userid,
                       ateacher: works[j].ateacher,
@@ -481,7 +526,7 @@ export default {
                     })
                   }
                   isWorksS[step].push({ uid: works[j].userid, sName: works[j].name })
-                } else if (steps[step].tool[0] === 50 && works[j].atool === 50) {
+                } else if (steps[step].tool[0] === 50 && (works[j].atool ? parseInt(works[j].atool) : '') === 50) {
                   const _works = {
                     userid: works[j].userid,
                     ateacher: works[j].ateacher,
@@ -500,7 +545,7 @@ export default {
 
                   worksStudent[step].push(_works)
                   isWorksS[step].push({ uid: works[j].userid, sName: works[j].name })
-                } else if (steps[step].tool[0] === 32 && works[j].atool === 32) {
+                } else if (steps[step].tool[0] === 32 && (works[j].atool ? parseInt(works[j].atool) : '') === 32) {
                   const _works = {
                     userid: works[j].userid,
                     ateacher: works[j].ateacher,
@@ -519,7 +564,7 @@ export default {
 
                   worksStudent[step].push(_works)
                   isWorksS[step].push({ uid: works[j].userid, sName: works[j].name })
-                } else if (steps[step].tool[0] === 57 && works[j].atool === 57) {
+                } else if (steps[step].tool[0] === 57 && (works[j].atool ? parseInt(works[j].atool) : '') === 57) {
                   const _works = {
                     userid: works[j].userid,
                     ateacher: works[j].ateacher,
@@ -631,17 +676,77 @@ export default {
       }
     }
   }
+  .tool-box {
+    margin: 10px 0;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    width: 1.5rem;
+    .tool-img {
+      width: 1.5rem;
+      border-radius: 15px;
+      box-shadow: 0 1px 8px 0 #14141424;
+      > img {
+        width: 100%;
+        height: auto;
+        object-fit: cover;
+      }
+    }
+    .tool-name {
+      margin-top: 5px;
+    }
+  }
+
   .works-box {
     .title {
       font-size: 0.35rem;
       color: #858585;
     }
+    .works {
+      margin: 10px 0 0;
+      display: flex;
+      flex-wrap: wrap;
+      .work {
+      }
+    }
   }
   .no-works-box {
+    margin-top: 10px;
     .title {
       font-size: 0.35rem;
       color: #858585;
     }
+    .no-works-box-students {
+      width: 100%;
+      display: flex;
+      flex-wrap: wrap;
+      margin: 10px 0 0;
+      .student {
+        background: #7cbcf1;
+        color: #fff;
+        width: 70px;
+        height: 20px;
+        text-align: center;
+        line-height: 20px;
+        border-radius: 5px;
+        margin: 0 15px 10px 0;
+        padding: 5px;
+        cursor: pointer;
+        @include textoverflow();
+      }
+    }
+  }
+
+  .noMssage {
+    width: 100%;
+    height: 5rem;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    > img {
+      width: 3rem;
+    }
   }
 }
 </style>

+ 71 - 0
src/views/course/components/works.vue

@@ -0,0 +1,71 @@
+<template>
+  <div class="work-container">
+    <!-- v-if="work.type == " -->
+    <div class="w-work">
+      <div class="work" v-if="work.type == 0" @click="imageView(work.works)"><img :src="work.works" alt="" /></div>
+      <div class="work" v-else><img src="@/assets/images/works/noImg.png" alt="" /></div>
+    </div>
+    <div class="s-name">
+      <span>{{ work.sName }}</span>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    work: {
+      type: Object,
+      default: () => {}
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {
+    imageView(src) {
+      // ImagePreview([src]);
+      this.$hevueImgPreview({ url: src, clickMaskCLose: true })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.work-container {
+  max-width: 4.5rem;
+  width: calc((100% - 20px) / 2);
+  overflow: hidden;
+  height: 3rem;
+  border-radius: 10px;
+  box-shadow: rgb(223, 218, 218) 0px 0px 6px 1px;
+  margin: 0 10px 15px 0;
+
+  .w-work {
+    width: 100%;
+    height: calc(100% - 0.7rem);
+    .work {
+      width: 100%;
+      height: 100%;
+      > img {
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+      }
+    }
+  }
+  .s-name {
+    width: 100%;
+    height: 0.6rem;
+    font-size: 14px;
+    display: flex;
+    align-items: center;
+    padding: 0 10px;
+    box-sizing: border-box;
+    > span {
+      width: 100%;
+      @include textoverflow();
+    }
+  }
+}
+</style>

+ 44 - 13
src/views/course/index.vue

@@ -3,16 +3,16 @@
     <head-bar :isBack="false">
       <template #title>
         <div class="course-nav-box">
-          <span class="list"></span>
-          <span class="last">上一步</span>
-          <span class="next">下一步</span>
-          <span class="comment">评课</span>
+          <span class="list" @click="navCheck"></span>
+          <span class="last" @click="nextOrpreSteps(0)">上一步</span>
+          <span class="next" @click="nextOrpreSteps(1)">下一步</span>
+          <span class="comment" @click="setType(2)">评课</span>
         </div>
       </template>
     </head-bar>
     <div class="course-box">
       <course-title :chapInfo="chapInfo" :courseType="courseType" :taskCount="taskCount"></course-title>
-      <course-content-list></course-content-list>
+      <course-content-list v-if="false"></course-content-list>
       <div class="course-type-box">
         <div class="type-nav">
           <div class="type-nav-box" :class="{ active: type == 1 }" @click="setType(1)">
@@ -34,38 +34,59 @@
           :taskCount="taskCount"
           :courseid="courseid"
           :tcid="tcid"
+          :oid="oid"
         ></steps-box>
+        <comment-box
+          v-if="type == 2"
+          :courseid="courseid"
+          :courseType="courseType"
+          :taskCount="taskCount"
+        ></comment-box>
       </div>
     </div>
+    <nav-box
+      ref="navbox"
+      :navVisible.sync="navVisible"
+      :chapList="chapList"
+      :courseType.sync="courseType"
+      :taskCount.sync="taskCount"
+      @getCourse="getCourse"
+    ></nav-box>
   </div>
 </template>
 
 <script>
 import headBar from '@/components/headBar.vue'
-import courseTitle from './components/courseTitle'
-import courseContentList from './components/courseContentList'
-import CourseContentList from './components/courseContentList.vue'
+import courseTitle from './components/courseTitle.vue'
+import courseContentList from './components/courseContentList.vue'
+import commentBox from './components/commentBox.vue'
 import stepsBox from './components/stepsBox.vue'
+import navBox from './components/navBox.vue'
 import { getCourseInfo } from '@/api/course'
+import '@/utils/aws-sdk-2.235.1.min.js'
 
 export default {
   components: {
     headBar,
     courseTitle,
     courseContentList,
-    CourseContentList,
-    stepsBox
+    stepsBox,
+    navBox,
+    commentBox
   },
   data() {
     return {
       courseid: this.$route.query.courseid,
+      oid: this.$route.query.oid,
       courseDetail: {},
       chapInfo: {},
+      chapList: [],
       courseType: 0,
       taskCount: 0,
       type: 1,
       tcid: '',
-      tcid2: this.$route.query.tcid
+      tcid2: this.$route.query.tcid,
+      navVisible: false
     }
   },
   methods: {
@@ -92,6 +113,7 @@ export default {
         .then(res => {
           this.courseDetail = res[0][0]
           this.chapInfo = JSON.parse(this.courseDetail.chapters)[this.courseType]
+          this.chapList = JSON.parse(this.courseDetail.chapters)
           this.tcid = this.arrayToArray(
             this.courseDetail.juri ? this.courseDetail.juri.split(',') : [],
             this.tcid2 ? this.tcid2.split(',') : []
@@ -104,6 +126,12 @@ export default {
     },
     setType(type) {
       this.type = type
+    },
+    navCheck() {
+      this.navVisible = true
+    },
+    nextOrpreSteps(t) {
+      this.$refs.navbox.nextOrpreSteps(t)
     }
   },
   created() {
@@ -123,7 +151,7 @@ export default {
     color: #fff;
     position: relative;
     box-sizing: border-box;
-    padding-left: 0.8rem;
+    // padding-left: 0.8rem;
     display: flex;
     align-items: center;
     justify-content: center;
@@ -141,16 +169,19 @@ export default {
       left: 0;
     }
     .next {
-      margin: 0 0.6rem;
+      margin: 0 1rem;
     }
   }
   .course-box {
     margin-top: 1.5rem;
     width: 100%;
+    height: calc(100% - 1.5rem);
+    overflow: auto;
   }
   .course-type-box {
     background: #fff;
     width: 100%;
+    margin-top: 10px;
     .type-nav {
       height: 1rem;
       width: 100%;

+ 8 - 1
src/views/login/index.vue

@@ -1,6 +1,7 @@
 <template>
   <div class="loginBox">
-    <button @click="handleLogin">登录</button>
+    <iframe src="https://edu.cocorobo.cn/course/login?type=2" frameborder="0"></iframe>
+    <!-- <button @click="handleLogin">登录</button> -->
   </div>
 </template>
 
@@ -33,5 +34,11 @@ export default {
   width: 100vw;
   height: 100vh;
   overflow: hidden;
+
+  > iframe {
+    width: 100%;
+    height: 100%;
+    border: 0;
+  }
 }
 </style>

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