Files
ks-app-employment-service/packageB/train/video/videoDetail.vue

589 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<AppLayout :title="title" :show-bg-image="false">
<!-- <template #headerleft>
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template> -->
<view class="video-box">
<view class="video-detail-container">
<!-- 视频播放组件 -->
<view class="video-wrapper">
<video
v-if="videoInfo && videoInfo.cover && isVideoReady"
id="myVideo"
:src="videoInfo.currentUrl"
:poster="trainVideoImgUrl+ videoInfo.cover"
:initial-time="initialTime"
:autoplay="shouldAutoPlay"
enable-danmu
controls
style="width: 100%;"
@pause="onPause"
@timeupdate="onTimeupdate"
@ended="onEnded">
</video>
</view>
</view>
<view class="video-info" :style="getItemBackgroundStyle('video-bj2.png')">
<view class="video-title">
<text>视频详情</text>
<view class="title-line"></view>
</view>
<view class="info-detail">
<view class="info-left">
<view class="info-item">
<image class="icon-img" :src="baseUrl+'/train/zs.png'" mode=""></image>
<view class="info-label">
分类
</view>
<view class="info-value" :data-content="getCategoryLabelByValue(videoInfo.category)">
{{getCategoryLabelByValue(videoInfo.category)}}
</view>
</view>
</view>
<view class="info-right">
<view class="info-item">
<image class="icon-img" :src="baseUrl+'/train/zs.png'" mode=""></image>
<view class="info-label">
等级
</view>
<view class="info-value">
{{getLevelLabelByValue(videoInfo.level)}}
</view>
</view>
<view class="info-item">
<image class="icon-img" :src="baseUrl+'/train/zs.png'" mode=""></image>
<view class="info-label">
讲师
</view>
<view class="info-value">
{{videoInfo.teacherName}}
</view>
</view>
</view>
</view>
<view class="info-detail">
<view class="info-left">
<view class="info-item">
<image class="icon-img" :src="baseUrl+'/train/zs.png'" mode=""></image>
<view class="info-label">
时长
</view>
<view class="info-value">
{{videoInfo.hour}}分钟
</view>
</view>
</view>
<view class="info-right">
<view class="info-item">
<image class="icon-img" :src="baseUrl+'/train/zs.png'" mode=""></image>
<view class="info-label">
发布时间
</view>
<view class="info-value">
{{videoInfo.uploadTime}}
</view>
</view>
</view>
</view>
<view class="video-intro" :style="videoIntroBackgroundStyle('video-bj.png')">
<view class="intro-title">
<image class="intro-img1" :src="baseUrl+'/train/video-kc.png'" mode=""></image>
<view class="title1">
课程
</view>
<view class="title2">
简介
</view>
<image class="intro-img2" :src="baseUrl+'/train/video-sc.png'" mode=""></image>
</view>
<view class="intro-content">
{{videoInfo.introduce}}
</view>
</view>
<view class="video-title">
<text>学习进度</text>
<view class="title-line"></view>
</view>
<view class="progress-box">
<progress :percent="videoInfo.percentage" activeColor="#30A0FF" backgroundColor="#B0DBFF" stroke-width="6" border-radius="10" />
<view class="progress-info">
<view class="progress-left">
已观看
</view>
<view class="progress-right">
{{videoInfo.percentage}}%
</view>
</view>
</view>
<view class="video-title" v-if="videoInfo.trainClassList && videoInfo.trainClassList.length>0">
<text>课程章节</text>
<view class="title-line"></view>
</view>
<view class="chapter-box" v-if="videoInfo.trainClassList && videoInfo.trainClassList.length>0">
<view class="chapter-item" :class="{ active: currentChapter === index}" @click="chapterChange(item,index)" v-for="(item ,index) in videoInfo.trainClassList" :key="index">
<view class="chapter-left">
<view class="chapter-number">
{{ index + 1 }}
</view>
<view class="chapter-info">
{{item.className}}
</view>
</view>
<view class="chapter-icon" v-if="currentChapter === index">
<uni-icons type="videocam" size="24"></uni-icons>
</view>
</view>
</view>
</view>
</view>
</AppLayout>
</template>
<script setup>
import { inject, reactive,ref, onMounted, onUnmounted, nextTick ,watch} from 'vue';
import { onLoad,onHide,onUnload } from '@dcloudio/uni-app';
const { $api, navTo, navBack } = inject('globalFunction');
import config from "@/config.js"
// state
const title = ref('');
const videoId=ref('')
const userId=ref('')
const videoInfo=ref({})
const trainVideoImgUrl=config.trainVideoImgUrl
const categories=ref([])
const levalLabels=ref([])
const latestTime = ref(0)
const initialTime = ref(0)
const totalTime=ref(0)
const baseUrl = config.imgBaseUrl
const pageEnterTime = ref(0)
const currentChapter = ref(0)
const isVideoReady = ref(false)
const shouldAutoPlay = ref(false)
const getItemBackgroundStyle = (imageName) => ({
backgroundImage: `url(${baseUrl}/train/${imageName})`,
backgroundSize: '100% 100%', // 覆盖整个容器
backgroundPosition: 'center', // 居中
backgroundRepeat: 'no-repeat'
});
const videoIntroBackgroundStyle = (imageName) => ({
backgroundImage: `url(${baseUrl}/train/${imageName})`,
backgroundSize: '100% 100%', // 覆盖整个容器
backgroundPosition: 'center', // 居中
backgroundRepeat: 'no-repeat'
});
const params = reactive({
videoId: '',
userId: ''
})
// 监听 initialTime 变化,确保设置完成后再渲染
watch(initialTime, (newVal) => {
if (newVal >= 0 && videoInfo.value && videoInfo.value.cover) {
// 延迟渲染,确保初始时间设置生效
nextTick(() => {
isVideoReady.value = true
shouldAutoPlay.value = newVal > 0
})
}
})
// 监听 videoInfo 变化
watch([videoInfo, initialTime], ([newVideoInfo, newInitialTime]) => {
if (newVideoInfo && newVideoInfo.cover && newInitialTime >= 0) {
nextTick(() => {
isVideoReady.value = true
shouldAutoPlay.value = newInitialTime > 0
})
}
}, { deep: true })
onLoad((options) => {
getHeart()
pageEnterTime.value = Date.now() // 记录毫秒时间戳
videoId.value=options.id
getDictionary()
});
// onHide(() => {
// updateVideoInfo() // 用缓存值,不要调 getCurrentTime
// reportPageDuration()
// })
onUnload(() => {
//updateVideoInfo()
saveCurrentChapterProgress()
reportPageDuration()
})
function getData() {
params.videoId=videoId.value
params.userId=userId.value
$api.myRequest('/train/public/trainVideo/updateWatchCount',{videoId:videoId.value}).then((resData) => {
console.log("视频更新次数成功")
});
let header={
'Authorization':uni.getStorageSync('Padmin-Token'),
'Content-Type': "application/x-www-form-urlencoded"
}
$api.myRequest('/train/public/trainVideo/model', params,'post',9100,header).then((resData) => {
videoInfo.value=resData.data
videoInfo.value.currentUrl=trainVideoImgUrl+videoInfo.value.trainClassList[0].url
videoInfo.value.percentage=((videoInfo.value.process/(videoInfo.value.hour*60))*100).toFixed(2)
videoInfo.value.uploadTime=videoInfo.value.uploadTime.split(' ')[0]
if(videoInfo.value.process !=null){
queryModelUserPlay(videoInfo.value.trainClassList[0].classId)
} else {
// 如果没有历史进度,直接准备渲染
initialTime.value = 0
}
updateVideoInfo()
});
}
function getHeart() {
const raw = uni.getStorageSync("Padmin-Token");
const token = typeof raw === "string" ? raw.trim() : "";
const headers = token ? { Authorization: raw.startsWith("Bearer ") ? raw : `Bearer ${token}` }: {};
$api.myRequest("/dashboard/auth/heart", {}, "POST", 10100, headers).then((resData) => {
if (resData.code == 200) {
getUserInfo();
}
});
}
function getUserInfo(){
let header={
'Authorization':uni.getStorageSync('Padmin-Token')
}
$api.myRequest('/system/user/login/user/info', {},'get',10100,header).then((resData) => {
userId.value=resData.info.userId
getData()
});
}
function getDictionary(){
$api.myRequest('/system/public/dict/data/type/question_classification', {},'get',9100).then((resData) => {
categories.value=resData.data
});
$api.myRequest('/system/public/dict/data/type/train_level', {},'get',9100).then((resData) => {
levalLabels.value=resData.data
});
}
function getCategoryLabelByValue(value) {
if (!Array.isArray(categories.value)) {
console.warn('categories 不是数组:', categories.value)
return ''
}
const item = categories.value.find(item => item.dictValue === String(value))
return item ? item.dictLabel : '暂无分类'
}
function getLevelLabelByValue(value) {
if (!Array.isArray(levalLabels.value)) {
console.warn('levalLabels 不是数组:', levalLabels.value)
return ''
}
const item = levalLabels.value.find(item => item.dictValue === String(value))
return item ? item.dictLabel : '暂无等级'
}
function onPause(e){
updateVideoInfo()
saveCurrentChapterProgress()
}
function onEnded(e){
updateVideoInfo()
saveCurrentChapterProgress()
}
function onTimeupdate(e){
latestTime.value = e.detail.currentTime
}
// function onSeeked(){
// updateVideoInfo()
// }
// 更新播放时长
function updateVideoInfo(){
totalTime.value=0
if(currentChapter.value>0){
videoInfo.value.trainClassList.forEach((item,index)=>{
if(index<currentChapter.value){
totalTime.value+=Number(item.hour)
}
})
}
totalTime.value+=Number(latestTime.value)
let paramsData={
userId:userId.value,
videoId:videoId.value,
collect:'',
process:Math.floor(Number(totalTime.value))
}
let header={
'Authorization':uni.getStorageSync('Padmin-Token'),
'Content-Type': "application/x-www-form-urlencoded"
}
$api.myRequest("/dashboard/auth/heart", {}, "POST", 10100, header).then((resData) => {
if (resData.code == 200) {
if(videoInfo.value.isCollect===null && videoInfo.value.process ===null && paramsData.process ==0 ){
$api.myRequest('/train/public/videoUser/add', paramsData,'post',9100,header).then((resData) => {
console.log("视频播放时长更新成功")
});
}else{
$api.myRequest('/train/public/videoUser/update', paramsData,'post',9100,header).then((resData) => {
console.log("视频播放时长更新成功")
});
}
}
})
}
// 计算并上报停留时长
function reportPageDuration() {
const duration = Date.now() - pageEnterTime.value // 毫秒
const durationSeconds = Math.floor(duration / 1000) // 转为秒
if (durationSeconds > 0) {
let paramsData={
type:'video',
hour:durationSeconds,
videoId:videoId.value,
userId:userId.value,
title:videoInfo.value.videoTitle
}
let header={
'Authorization':uni.getStorageSync('Padmin-Token'),
'Content-Type': "application/x-www-form-urlencoded"
}
$api.myRequest('/train/public/userHour/add', paramsData,'post',9100,header).then((resData) => {
console.log("学习时长更新成功")
});
}
}
function chapterChange(video,index){
saveCurrentChapterProgress()
currentChapter.value=index
videoInfo.value.currentUrl=trainVideoImgUrl+video.url
isVideoReady.value = false
shouldAutoPlay.value = false
queryModelUserPlay(video.classId)
}
const updateUserPlayClassHousr = (classId,hour) => {
let paramsData={
userId:userId.value,
classId:classId,
hour:hour,
dqHour:latestTime.value
}
let header={
'Authorization':uni.getStorageSync('Padmin-Token'),
'Content-Type': "application/x-www-form-urlencoded"
}
$api.myRequest('/train/public/videoUser/updateUserPlayClassHousr', paramsData,'post',9100,header).then((resData) => {
if(resData.code == 200){
console.log("学习时长更新成功")
}
});
}
const queryModelUserPlay = (classId) => {
let queryParams = {
classId: classId,
userId: userId.value,
};
let header = {
Authorization: uni.getStorageSync("Padmin-Token"),
"Content-Type": "application/x-www-form-urlencoded",
};
$api.myRequest("/train/public/videoUser/modelUserPlay", queryParams, "post", 9100, header).then((resData) => {
if(resData.code == 200){
initialTime.value=Number(resData?.data?.dqHour ?? 0)
}else {
initialTime.value = 0
}
}).catch(() => {
initialTime.value = 0
});
}
// 保存当前章节进度
function saveCurrentChapterProgress() {
if (videoInfo.value.trainClassList && videoInfo.value.trainClassList.length > 0) {
const currentVideo = videoInfo.value.trainClassList[currentChapter.value];
if (currentVideo) {
updateUserPlayClassHousr(currentVideo.classId, currentVideo.hour);
}
}
}
onUnmounted(() => {
});
</script>
<style lang="stylus" scoped>
.video-box{
padding: 10rpx 20rpx;
}
.video-detail-container{
width: 100%;
background-color: #ffffff;
}
.video-wrapper{
position: relative;
width: 100%;
background-color: #000000;
height: auto;
}
.video-info{
width:100%;
margin-top:30rpx;
padding: 20rpx 30rpx;
box-sizing: border-box;
}
.video-title{
font-size: 32rpx;
color: #404040;
font-weight: bold;
position: relative;
margin-bottom: 40rpx;
}
.title-line{
position: absolute;
bottom: -10rpx;
left: 36rpx;
width: 60rpx;
height: 8rpx;
background: linear-gradient(90deg, #FFAD58 0%, #FF7A5B 100%);
border-radius: 4rpx;
}
.info-detail{
display: flex;
flex-wrap: nowrap;
align-items: center;
margin-bottom: 30rpx;
}
.info-left{
width: 35%;
}
.info-right{
width: 65%;
display: flex;
align-items: center;
justify-content: space-between;
}
.info-item{
display: flex;
align-items: center;
}
.icon-img{
width: 24rpx;
height: 28rpx;
margin-right: 4rpx;
}
.info-label{
font-weight: bold;
font-size: 28rpx;
color: #0068C8;
min-width: 86rpx;
}
.info-value{
font-size: 28rpx;
color: #404040;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.video-intro{
padding: 20rpx;
margin-bottom: 30rpx;
}
.intro-title{
display: flex;
align-items: center;
margin-bottom: 10rpx;
}
.intro-img1{
width: 36rpx;
height: 30rpx;
margin-right: 10rpx;
}
.intro-img2{
width: 30rpx;
height: 30rpx;
margin-left: 10rpx;
}
.title1{
font-size: 26rpx;
font-weight: bold;
color: #333333;
}
.title2{
color: #077DF5;
font-size: 26rpx;
font-weight: bold;
}
.intro-content{
font-size: 24rpx;
color: #333333;
line-height: 1.5;
}
.progress-box{
background: linear-gradient(0deg, #DFEDFF 0%, #F8FCFF 100%);
box-shadow: 0px 0px 10px 0px rgba(0,48,107,0.1);
border-radius: 16rpx;
padding: 40rpx 30rpx;
margin-bottom: 30rpx;
}
.progress-info{
display: flex;
align-items: center;
justify-content: space-between;
font-size: 24rpx;
color: #333333;
margin-top: 20rpx;
}
.chapter-box{
background: linear-gradient(0deg, #DFEDFF 0%, #F8FCFF 100%);
box-shadow: 0px 0px 10px 0px rgba(0,48,107,0.1);
border-radius: 16rpx;
padding: 40rpx 30rpx;
}
.chapter-item{
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 24rpx;
border-radius: 12rpx;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 16rpx;
border: 2rpx solid #F0F0F0;
background: #F9F9F9;
}
.chapter-item.active {
background-color: #e6f7ff;
color: #409EFF;
}
.chapter-left{
display: flex;
align-items: center;
}
.chapter-number{
width: 50rpx;
height: 50rpx;
background: #cccccc;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
font-weight: 500;
margin-right: 16rpx;
color: #fff;
}
.chapter-item.active .chapter-number {
background: #409EFF;
color: #fff;
}
.chapter-info {
font-size: 28rpx;
color: #303133;
}
</style>