videoDetails.vue 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. <template>
  2. <view class="page_body">
  3. <!-- 头部 -->
  4. <nav-bar height="96" title="视频详情" :onlyTitle="true" :showHeaderborder="true" @GoBack="GoBack"></nav-bar>
  5. <view class="page_content">
  6. <view class="video_content">
  7. <!-- duration:指定视频时长,单位为秒(s) initial-time:指定视频初始播放位置,单位为秒(s) -->
  8. <video
  9. ref="myVideoRef"
  10. id="myVideoRef"
  11. :title="state?.videoInfo?.courseName || ''"
  12. :initial-time="state?.videoInfo?.initialTime"
  13. :duration="state?.videoInfo?.duration"
  14. :show-mute-btn="true"
  15. :poster="state?.videoInfo?.videoCoverUrl"
  16. :src="state?.videoInfo?.attachmentUrl"
  17. style="width: 100%; height: 100%; object-fit: contain;"
  18. @timeupdate="videoTimeupdate"
  19. >
  20. </video>
  21. </view>
  22. <view class="video_title">{{state?.videoInfo?.courseName}}</view>
  23. <view class="video_progress">
  24. <view class="video_progress_box">
  25. <text class="video_progress_text" v-if="state?.videoInfo?.courseType===0">必修任务:{{state.videoInfo.courseStatus===0?`剩余${state.videoInfo.remainingTime}`:'已结束'}}</text>
  26. <text class="video_progress_text">观看进度:{{state?.videoInfo?.progress}}</text>
  27. </view>
  28. <view class="task_collect">
  29. <view class="preview">
  30. <image class="img" src="@/static/image/icon/preview.png"></image>
  31. <text class="text">{{state?.videoInfo?.watchCount || 0}}</text>
  32. </view>
  33. <view class="collect" v-if="state?.videoInfo?.courseType===1" @click="ChangeFollowStatus()">
  34. <uni-icons v-if="state?.videoInfo?.followStatus==1" class="img filled" type="heart-filled"></uni-icons>
  35. <uni-icons v-else class="img" type="heart"></uni-icons>
  36. <text :class="['text',{'filled':state?.videoInfo?.followStatus==1}]">{{state?.videoInfo?.followCount || 0}}</text>
  37. </view>
  38. </view>
  39. </view>
  40. <view class="video_class_name">
  41. <text class="class_name_item">{{state?.videoInfo?.teachCourseTypeName}}</text>
  42. <text class="class_name_item">{{state?.videoInfo?.teachCourseTypeClassName}}</text>
  43. <text class="class_name_item">{{state?.videoInfo?.createdBy}}</text>
  44. </view>
  45. <view class="video_time">
  46. <text class="time_item">{{state?.videoInfo?.courseType===0 ?'开始':'上传'}}时间:{{state?.videoInfo?.courseStartDatetime}}</text>
  47. <text class="time_item" v-if="state?.videoInfo?.courseType===0">结束时间:{{state.videoInfo.courseEndDatetime}}</text>
  48. </view>
  49. <view class="video_desc">
  50. 课程简介:{{state?.videoInfo?.courseContent}}
  51. </view>
  52. </view>
  53. </view>
  54. <!-- 提示框 -->
  55. <custom-popup-dialog ref="popupDialogRef" title="试题弹出" dialogWidth="702" :showCloseBtn="false" @DialogConfirm="DialogConfirm">
  56. <view class="dialog_questions_input">
  57. <view class="questions_title">{{state?.question?.codeName}}:{{state?.question?.questionsName}}</view>
  58. <uni-easyinput type="textarea" v-model="state.questionsAnswer" :style="state.easyinputStyle" placeholder="请输入你的回答" placeholderStyle="font-weight: 400;font-size: 28rpx;color: #999999;"></uni-easyinput>
  59. </view>
  60. </custom-popup-dialog>
  61. </template>
  62. <script setup>
  63. import uniEasyinput from '@/uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput.vue';
  64. import uniIcons from '@/uni_modules/uni-icons/components/uni-icons/uni-icons.vue';
  65. import navBar from '@/components/nav-bar.vue';
  66. import customPopupDialog from '@/components/custom-popup-dialog.vue';
  67. import { selectCourseDetail,addQuestionAnswerData,updateFollowCount } from '@/reqApi/teacherStudy.js';
  68. import {
  69. onLoad,
  70. onUnload
  71. } from '@dcloudio/uni-app';
  72. import {
  73. reactive,
  74. ref,
  75. getCurrentInstance
  76. } from 'vue';
  77. const state = reactive({
  78. id: '', //视频id
  79. videoInfo:{},
  80. currentTime:'',//播放进度时分秒
  81. question:null,//试题弹出信息
  82. easyinputStyle:{
  83. color: '#303133',
  84. borderColor: '#E9E9E9'
  85. },
  86. questionsAnswer:''
  87. });
  88. //提示框
  89. const popupDialogRef = ref(null);
  90. const myVideoRef = ref(null);
  91. const videoCtx = ref(null);//播放器
  92. onLoad((option) => {
  93. state.id = option.id;
  94. const instance = getCurrentInstance();
  95. videoCtx.value = uni.createVideoContext('myVideoRef', instance.proxy);
  96. GetCourseDetail();
  97. })
  98. //视频详情
  99. const GetCourseDetail = () => {
  100. selectCourseDetail(state.id).then(res => {
  101. if(res.code == 200){
  102. state.videoInfo = res.data;
  103. state.videoInfo.initialTime = timeToSecond(state.videoInfo.courseLearnTime || '');
  104. state.videoInfo.duration = timeToSecond(state.videoInfo.videoTime || '')
  105. }
  106. })
  107. }
  108. //计算秒数
  109. const timeToSecond = (timeStr) => {
  110. const timeArr = timeStr.split(':');
  111. const [h, m, s] = timeArr.map(item => Number(item));
  112. return h * 3600 + m * 60 + s;
  113. }
  114. //播放进度变化时触发,event.detail = {currentTime, duration} 。触发频率 250ms 一次
  115. const videoTimeupdate = (event) => {
  116. const currentTime = event.detail.currentTime;
  117. state.currentTime = formatSecondsToHms(currentTime);
  118. const questionsList = state?.videoInfo?.questionsList || [];
  119. const question = questionsList.find(item=>item.showTime == state.currentTime);//视频显示的时间
  120. state.question = question;//试题弹框信息
  121. if(question){
  122. videoCtx?.value?.pause();//暂停播放
  123. state.questionsAnswer = '';
  124. popupDialogRef.value.OpenDialog();
  125. }
  126. }
  127. //提交试题
  128. const DialogConfirm = () => {
  129. if(state.questionsAnswer == ''){
  130. uni.showToast({
  131. title: '请输入答案!',
  132. icon: 'none'
  133. })
  134. return false
  135. }
  136. addQuestionAnswerData({
  137. teachCourseId: state.id,
  138. teachQuestionsId: state?.question?.id || '',
  139. teachQuestionsAnswer: state.questionsAnswer
  140. }).then(res=>{
  141. if(res.code == 200){
  142. popupDialogRef.value.CloseDialog();//关闭弹框
  143. console.log(videoCtx?.value)
  144. videoCtx?.value?.play();//播放
  145. }else{
  146. uni.showToast({
  147. title: res.msg || '请求异常',
  148. icon: 'none'
  149. })
  150. }
  151. })
  152. }
  153. // 数字补零工具
  154. const addZero = (num) => {
  155. return num < 10 ? '0' + num : String(num);
  156. }
  157. //转换时分秒
  158. const formatSecondsToHms = (seconds) => {
  159. let total = Math.floor(seconds);
  160. const h = Math.floor(total / 3600);
  161. const m = Math.floor((total % 3600) / 60);
  162. const s = total % 60;
  163. const hh = addZero(h);
  164. const mm = addZero(m);
  165. const ss = addZero(s);
  166. return `${hh}:${mm}:${ss}`;
  167. }
  168. //收藏
  169. const ChangeFollowStatus = () => {
  170. updateFollowCount(state.id).then(res=>{
  171. if(res.code == 200){
  172. state.videoInfo.followStatus = state.videoInfo.followStatus===1?0:1;
  173. state.videoInfo.followCount = res.data;
  174. }
  175. })
  176. }
  177. //页面销毁时触发
  178. onUnload(()=>{
  179. })
  180. //返回
  181. const GoBack = () => {
  182. uni.navigateBack({
  183. delta: 1
  184. });
  185. }
  186. </script>
  187. <style scoped lang="scss">
  188. .video_content {
  189. width: 100%;
  190. border-radius: 8rpx;
  191. margin-top: 24rpx;
  192. height: 398rpx;
  193. overflow: hidden;
  194. }
  195. .video_title{
  196. width: 100%;
  197. margin-top: 32rpx;
  198. font-weight: 500;
  199. font-size: 36rpx;
  200. color: #333333;
  201. display: -webkit-box;
  202. -webkit-line-clamp: 2;
  203. -webkit-box-orient: vertical;
  204. overflow: hidden;
  205. text-overflow: ellipsis;
  206. }
  207. .video_progress{
  208. display: flex;
  209. justify-content: space-between;
  210. width: 100%;
  211. margin-top: 16rpx;
  212. align-items: flex-start;
  213. .video_progress_box{
  214. display: inline-flex;
  215. flex-wrap: wrap;
  216. gap: 20rpx;
  217. .video_progress_text{
  218. font-weight: 400;
  219. font-size: 24rpx;
  220. color: #2E64FA;
  221. }
  222. }
  223. .task_collect {
  224. display: inline-flex;
  225. gap: 20rpx;
  226. .preview,
  227. .collect {
  228. display: inline-flex;
  229. align-items: center;
  230. }
  231. .img {
  232. width: 24rpx;
  233. height: 24rpx;
  234. margin-right: 12rpx;
  235. font-size: 24rpx !important;
  236. color: #999999 !important;
  237. &.filled{
  238. color: #2e64fa !important;
  239. }
  240. }
  241. .text {
  242. font-weight: 400;
  243. font-size: 24rpx;
  244. color: #999999;
  245. &.filled{
  246. color: #2e64fa;
  247. }
  248. }
  249. }
  250. }
  251. .video_class_name{
  252. display: flex;
  253. width: 100%;
  254. margin-top: 16rpx;
  255. gap: 16rpx 24rpx;
  256. flex-wrap: wrap;
  257. .class_name_item{
  258. font-weight: 400;
  259. font-size: 24rpx;
  260. color: #666666;
  261. }
  262. }
  263. .video_time{
  264. display: flex;
  265. width: 100%;
  266. margin-top: 16rpx;
  267. gap: 16rpx;
  268. flex-wrap: wrap;
  269. .time_item{
  270. font-weight: 400;
  271. font-size: 24rpx;
  272. color: #666666;
  273. }
  274. }
  275. .video_desc{
  276. display: flex;
  277. width: 100%;
  278. margin-top: 32rpx;
  279. flex-wrap: wrap;
  280. word-break: break-all;
  281. font-weight: 400;
  282. font-size: 24rpx;
  283. color: #999999;
  284. line-height: 44rpx;
  285. }
  286. .dialog_questions_input{
  287. display: flex;
  288. width: 100%;
  289. flex-direction: column;
  290. .questions_title{
  291. display: flex;
  292. width: 100%;
  293. font-weight: 400;
  294. font-size: 32rpx;
  295. color: #303133;
  296. text-align: left;
  297. word-break: break-all;
  298. }
  299. :deep(.uni-easyinput){
  300. margin-top: 20rpx;
  301. font-size: 28rpx;
  302. .is-input-border{
  303. border-radius: 8rpx;
  304. border-color: #E9E9E9 !important;
  305. }
  306. .uni-easyinput__content-textarea{
  307. height: 440rpx;
  308. min-height: 440rpx;
  309. font-size: 28rpx;
  310. margin:20rpx 24rpx 20rpx 14rpx;
  311. }
  312. .input-padding{
  313. padding-left: 10rpx;
  314. }
  315. }
  316. }
  317. </style>