Files
ks-app-employment-service/packageB/train/video/videoDetail.vue
2025-11-06 16:51:31 +08:00

493 lines
13 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 id="myVideo" :src="videoInfo.currentUrl" :poster="trainVideoImgUrl+ videoInfo.cover" @seeked="onSeeked"
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 } 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 totalTime=ref(0)
const baseUrl = config.imgBaseUrl
const pageEnterTime = ref(0)
const currentChapter = ref(0)
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: ''
})
onLoad((options) => {
getHeart()
pageEnterTime.value = Date.now() // 记录毫秒时间戳
videoId.value=options.id
getDictionary()
});
onHide(() => {
updateVideoInfo() // 用缓存值,不要调 getCurrentTime
reportPageDuration()
})
onUnload(() => {
updateVideoInfo()
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]
});
}
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();
} else {
navTo('/packageB/login')
}
});
}
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()
}
function onEnded(e){
updateVideoInfo()
}
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"
}
if(videoInfo.value.isCollect===null && videoInfo.value.process ===null){
$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){
currentChapter.value=index
videoInfo.value.currentUrl=trainVideoImgUrl+video.url
}
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>