courseDetailed.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. <template>
  2. <view class="details">
  3. <u-navbar back-text="" title="" back-icon-color="#FFFFFF" :background="{ background: '#3D5D4C' }" :border-bottom="false"></u-navbar>
  4. <!-- 视频 -->
  5. <view class="details-video" v-if="isPlay" v-loading="videoLoading">
  6. <video
  7. class="details-video-con"
  8. id="myVideo"
  9. @timeupdate="timeUpdate"
  10. :src="videoInfo.videoUrl"
  11. controls
  12. :initial-time="initial_time"
  13. object-fit="fill"
  14. play-btn-position="center"
  15. @ended="ended"
  16. @tap="videoClick"
  17. @loadedmetadata="loadedmetadata"
  18. >
  19. <cover-view class="video-control">
  20. <cover-view class="multi rate" @tap.stop="showSwitchRate">x {{ currentRate }}</cover-view>
  21. </cover-view>
  22. <cover-view class="multi-list rate" :class="{ active: rateShow }">
  23. <cover-view
  24. v-for="(item, index) in ['0.5', '1.0', '1.5', '2.0']"
  25. :key="index"
  26. class="multi-item rate"
  27. :data-rate="item"
  28. @tap="switchRate"
  29. :class="{ active: item == currentRate }"
  30. >
  31. {{ item }}
  32. </cover-view>
  33. </cover-view>
  34. </video>
  35. </view>
  36. <!-- 介绍 -->
  37. <view class="details-content">
  38. <view class="details-content-title">{{ videoInfo.chapterName || '-' }}</view>
  39. <view class="details-content-progress"
  40. >本课程 共{{ info.amount || 0 }}课,已学完{{ info.finishCount || 0 }}课,共进度{{ info.finishPercent || 0 }}%</view
  41. >
  42. <view class="details-content-teacher">主讲老师:{{ info.presenter || '-' }}</view>
  43. <view class="details-content-info">{{ videoInfo.chapterInfo || '-' }}</view>
  44. </view>
  45. <!-- 课程章节 -->
  46. <view class="details-classes">
  47. <view class="details-classes-header">
  48. <view>精选课程</view>
  49. <view>
  50. 更多
  51. <u-icon name="arrow-right" size="22" color="#A3A3A3" />
  52. </view>
  53. </view>
  54. <view class="details-classes-list">
  55. <view
  56. class="details-classes-list-item"
  57. v-for="(item, index) in info.chapterList"
  58. :key="index"
  59. :class="{ active: index === videoIndex }"
  60. @click="classesClick(index)"
  61. >
  62. <view>{{ index + 1 }}</view>
  63. <view>{{ item.flag === 2 ? '已学' : item.finishPercent + '%' }}</view>
  64. </view>
  65. </view>
  66. </view>
  67. <view class="details-line">
  68. <view></view>
  69. </view>
  70. <!-- 评论 -->
  71. <view class="details-comment">
  72. <view class="details-comment-header">
  73. <view>课程评论</view>
  74. <view>共{{ total || 0 }}条评论</view>
  75. </view>
  76. <view class="details-comment-list">
  77. <view class="details-comment-list-item" v-for="(item, index) in commentList" :key="index">
  78. <view class="left">
  79. <u-avatar :src="item.createByAvatar" size="96" mode="square"></u-avatar>
  80. </view>
  81. <view class="right">
  82. <view>{{ item.createBy }}</view>
  83. <view>
  84. <u-rate :count="5" size="28" disabled="" active-color="#FFBC00" v-model="item.starLevel"> </u-rate>
  85. <text>{{ item.createTime }}</text>
  86. </view>
  87. <view>{{ item.content }}</view>
  88. </view>
  89. </view>
  90. </view>
  91. <view class="details-comment-page" v-if="total">
  92. <wyb-pagination :padding="0" :totalItems="total" :current="query.pageNum" @change="pageChange" />
  93. </view>
  94. <view class="details-comment-mine"><text>我的评论</text></view>
  95. <view class="details-comment-conent">
  96. <view class="details-comment-conent-star">
  97. <u-rate :count="5" size="40" active-color="#FFBC00" v-model="form.starLevel"></u-rate>
  98. </view>
  99. <view class="details-comment-content-textarea">
  100. <u-input
  101. v-model="form.content"
  102. placeholder="请输入您的评价"
  103. type="textarea"
  104. :custom-style="{ backgroundColor: '#F5F5F5', padding: '30rpx', borderRadius: '10rpx', minHeight: '280rpx' }"
  105. >
  106. </u-input>
  107. </view>
  108. <view class="details-comment-conent-button" @click="submitCommet">提交</view>
  109. </view>
  110. </view>
  111. <u-toast ref="uToast" />
  112. </view>
  113. </template>
  114. <script>
  115. export default {
  116. data() {
  117. return {
  118. info: {},
  119. videoContext: uni.createVideoContext('myVideo', this),
  120. videoInfo: {},
  121. videoIndex: 0,
  122. // 视频实时时间
  123. initial_time: 0,
  124. // 视频已经播放时间
  125. playedTime: 0,
  126. rateShow: false, // 倍速浮层
  127. currentRate: 1.0, // 默认倍速
  128. // 视频实际时间
  129. video_real_time: 0,
  130. classesId: '',
  131. isPlay: true,
  132. query: {
  133. pageNum: 1,
  134. pageSize: 5,
  135. tabId: ''
  136. },
  137. total: 0,
  138. commentList: [],
  139. form: {
  140. tabId: '',
  141. starLevel: 0,
  142. content: ''
  143. },
  144. currentDuration: 0,
  145. duration: 0,
  146. isApply: 1,
  147. // 视频加载层
  148. videoLoading: false
  149. };
  150. },
  151. onLoad(page) {
  152. if (page.id) {
  153. this.getClassesDetails(page.id);
  154. this.classesId = page.id;
  155. this.query.tabId = this.classesId;
  156. this.form.tabId = this.classesId;
  157. this.isApply = page.isApply || 1;
  158. }
  159. },
  160. beforeDestroy() {
  161. this.confirmSubmitDuration();
  162. },
  163. methods: {
  164. /**
  165. * 获取视频总时长
  166. * @param {Object} data
  167. */
  168. loadedmetadata(data) {
  169. this.duration = data.detail.duration;
  170. this.videoLoading = false;
  171. },
  172. /**
  173. * 显示倍速浮层
  174. * @param {Object} rate
  175. */
  176. showSwitchRate(rate) {
  177. let that = this;
  178. that.rateShow = true;
  179. },
  180. /**
  181. * 切换倍速
  182. * @param {Object} e
  183. */
  184. switchRate(e) {
  185. let that = this;
  186. let rate = Number(e.currentTarget.dataset.rate);
  187. that.currentRate = rate;
  188. that.rateShow = false;
  189. this.videoContext.playbackRate(rate);
  190. },
  191. /**
  192. * 视频点击
  193. * @param {Object} e
  194. */
  195. videoClick(e) {
  196. this.rateShow = false;
  197. },
  198. pause(e) {
  199. console.log(e);
  200. },
  201. /**
  202. * 获取课程详情
  203. * @param {Object} id
  204. */
  205. getClassesDetails(id) {
  206. this.videoLoading = true;
  207. this.$u.api.school
  208. .getPackageCourseDetail({
  209. id
  210. })
  211. .then((res) => {
  212. if (res.code === 200) {
  213. this.info = res.data;
  214. this.videoInfo = res.data.chapterList[this.videoIndex];
  215. this.initial_time = Number(res.data.chapterList[this.videoIndex].currentDuration);
  216. this.video_real_time = Number(res.data.chapterList[this.videoIndex].playDuration);
  217. this.playedTime = Number(res.data.chapterList[this.videoIndex].playDuration);
  218. this.query.pageNum = 1;
  219. this.isPlay = true;
  220. this.getCommentList();
  221. }
  222. });
  223. },
  224. /**
  225. * 课程章节点击
  226. * @param {Object} index
  227. */
  228. classesClick(index) {
  229. this.isPlay = false;
  230. let playDuration = this.video_real_time;
  231. if (this.videoInfo.playDuration > this.video_real_time) {
  232. this.currentDuration = this.video_real_time;
  233. playDuration = this.videoInfo.playDuration;
  234. } else {
  235. this.currentDuration = this.video_real_time;
  236. }
  237. this.submitTimeLong(
  238. {
  239. tabId: this.videoInfo.id,
  240. playDuration: playDuration,
  241. duration: this.duration,
  242. currentDuration: this.currentDuration
  243. },
  244. index
  245. );
  246. },
  247. /**
  248. * 控制视频不能快进
  249. * @param {Object} e
  250. */
  251. timeUpdate(e) {
  252. //播放的总时长
  253. let duration = e.detail.duration;
  254. //实时播放进度 秒数
  255. let jumpTime = parseInt(e.detail.currentTime);
  256. //当前视频进度
  257. if (jumpTime - this.playedTime > 3) {
  258. // 差别过大,调用seek方法跳转到实际观看时间
  259. this.videoContext.seek(this.playedTime);
  260. wx.showToast({
  261. title: '未完整看完该视频,不能快进',
  262. icon: 'none',
  263. duration: 2000
  264. });
  265. } else {
  266. this.video_real_time = parseInt(e.detail.currentTime);
  267. if (this.video_real_time > this.playedTime) {
  268. this.playedTime = this.video_real_time;
  269. }
  270. }
  271. if (parseInt(this.duration) !== 0 && parseInt(this.duration) === parseInt(this.video_real_time)) {
  272. this.videoContext.pause();
  273. this.confirmSubmitDuration();
  274. }
  275. },
  276. /**
  277. * 视频结束
  278. */
  279. ended() {
  280. // 用户把进度条拉到最后,但是实际观看时间不够,跳转回去会自动暂停。
  281. // 这里加个判断。
  282. if (this.playedTime < this.duration) {
  283. this.videoContext.play();
  284. this.confirmSubmitDuration();
  285. }
  286. },
  287. /**
  288. * 提交课程时长
  289. */
  290. submitTimeLong({ tabId, playDuration, currentDuration, duration }, index, flag) {
  291. if (tabId) {
  292. this.$u.api.training
  293. .videoTimeLongApi({
  294. tabId,
  295. playDuration,
  296. currentDuration,
  297. duration
  298. })
  299. .then((res) => {
  300. if (res.code === 200) {
  301. if (!flag) {
  302. this.$refs.uToast.show({
  303. title: '已记录章节时长!',
  304. type: 'success'
  305. });
  306. this.videoInfo = this.info.chapterList[index];
  307. this.videoIndex = index;
  308. this.getClassesDetails(this.classesId);
  309. }
  310. } else {
  311. this.$refs.uToast.show({
  312. title: res.msg,
  313. type: 'error'
  314. });
  315. }
  316. });
  317. }
  318. },
  319. /**
  320. * 获取评论列表
  321. */
  322. getCommentList() {
  323. if (this.query.tabId) {
  324. this.$u.api.training.getClassesCommentApi(this.query).then((res) => {
  325. if (res.code === 200) {
  326. this.total = Number(res.total);
  327. this.commentList = res.rows;
  328. }
  329. });
  330. }
  331. },
  332. /**
  333. * @param {Object} e 分页触发
  334. */
  335. pageChange(e) {
  336. this.query.pageNum = e.current;
  337. this.getCommentList();
  338. },
  339. /**
  340. * 提交评论
  341. */
  342. submitCommet() {
  343. if (this.form.tabId) {
  344. if (this.form.starLevel && this.form.content) {
  345. this.$u.api.training.addClassesCommentApi(this.form).then((res) => {
  346. if (res.code === 200) {
  347. this.$refs.uToast.show({
  348. title: '评论成功!',
  349. type: 'success'
  350. });
  351. this.form.content = '';
  352. this.form.starLevel = 0;
  353. this.getCommentList();
  354. } else {
  355. this.$refs.uToast.show({
  356. title: res.msg,
  357. type: 'error'
  358. });
  359. }
  360. });
  361. }
  362. if (!this.form.starLevel) {
  363. this.$refs.uToast.show({
  364. title: '请选择星级',
  365. type: 'warning'
  366. });
  367. }
  368. if (!this.form.content) {
  369. this.$refs.uToast.show({
  370. title: '请输入评论内容',
  371. type: 'warning'
  372. });
  373. }
  374. } else {
  375. this.$refs.uToast.show({
  376. title: '未找到课程章节,无法提交评论!',
  377. type: 'warning'
  378. });
  379. }
  380. },
  381. confirmSubmitDuration() {
  382. let playDuration = this.video_real_time;
  383. if (this.videoInfo.playDuration > this.video_real_time) {
  384. this.currentDuration = this.video_real_time;
  385. playDuration = this.videoInfo.playDuration;
  386. } else {
  387. this.currentDuration = this.video_real_time;
  388. }
  389. if (Number(this.isApply) === 1) {
  390. this.submitTimeLong(
  391. {
  392. tabId: this.videoInfo.id,
  393. playDuration: playDuration,
  394. currentDuration: this.currentDuration,
  395. duration: this.duration
  396. },
  397. 0,
  398. true
  399. );
  400. }
  401. }
  402. }
  403. };
  404. </script>
  405. <style lang="scss" scoped>
  406. @import './courseDetailed.scss';
  407. </style>