视频详情接口

This commit is contained in:
2025-11-04 19:47:59 +08:00
parent d76c7eabff
commit f4dfeebd31

View File

@@ -9,8 +9,8 @@
<view class="video-detail-container"> <view class="video-detail-container">
<!-- 视频播放组件 --> <!-- 视频播放组件 -->
<view class="video-wrapper"> <view class="video-wrapper">
<video id="myVideo" :src="videoInfo.currentUrl" :poster="trainVideoImgUrl+ videoInfo.cover" <video id="myVideo" :src="videoInfo.currentUrl" :poster="trainVideoImgUrl+ videoInfo.cover" @seeked="onSeeked"
enable-danmu controls style="width: 100%;"></video> enable-danmu controls style="width: 100%;" @pause="onPause" @timeupdate="onTimeupdate" @ended="onEnded"></video>
</view> </view>
</view> </view>
<view class="video-info" :style="getItemBackgroundStyle('video-bj2.png')"> <view class="video-info" :style="getItemBackgroundStyle('video-bj2.png')">
@@ -21,18 +21,18 @@
<view class="info-detail"> <view class="info-detail">
<view class="info-left"> <view class="info-left">
<view class="info-item"> <view class="info-item">
<image class="icon-img" src="../../../static/images/train/zs.png" mode=""></image> <image class="icon-img" :src="baseUrl+'/train/zs.png'" mode=""></image>
<view class="info-label"> <view class="info-label">
分类 分类
</view> </view>
<view class="info-value"> <view class="info-value" :data-content="getCategoryLabelByValue(videoInfo.category)">
{{getCategoryLabelByValue(videoInfo.category)}} {{getCategoryLabelByValue(videoInfo.category)}}
</view> </view>
</view> </view>
</view> </view>
<view class="info-right"> <view class="info-right">
<view class="info-item"> <view class="info-item">
<image class="icon-img" src="../../../static/images/train/zs.png" mode=""></image> <image class="icon-img" :src="baseUrl+'/train/zs.png'" mode=""></image>
<view class="info-label"> <view class="info-label">
等级 等级
</view> </view>
@@ -41,7 +41,7 @@
</view> </view>
</view> </view>
<view class="info-item"> <view class="info-item">
<image class="icon-img" src="../../../static/images/train/zs.png" mode=""></image> <image class="icon-img" :src="baseUrl+'/train/zs.png'" mode=""></image>
<view class="info-label"> <view class="info-label">
讲师 讲师
</view> </view>
@@ -54,7 +54,7 @@
<view class="info-detail"> <view class="info-detail">
<view class="info-left"> <view class="info-left">
<view class="info-item"> <view class="info-item">
<image class="icon-img" src="../../../static/images/train/zs.png" mode=""></image> <image class="icon-img" :src="baseUrl+'/train/zs.png'" mode=""></image>
<view class="info-label"> <view class="info-label">
时长 时长
</view> </view>
@@ -65,7 +65,7 @@
</view> </view>
<view class="info-right"> <view class="info-right">
<view class="info-item"> <view class="info-item">
<image class="icon-img" src="../../../static/images/train/zs.png" mode=""></image> <image class="icon-img" :src="baseUrl+'/train/zs.png'" mode=""></image>
<view class="info-label"> <view class="info-label">
发布时间 发布时间
</view> </view>
@@ -77,14 +77,14 @@
</view> </view>
<view class="video-intro" :style="videoIntroBackgroundStyle('video-bj.png')"> <view class="video-intro" :style="videoIntroBackgroundStyle('video-bj.png')">
<view class="intro-title"> <view class="intro-title">
<image class="intro-img1" src="../../../static/images/train/video-kc.png" mode=""></image> <image class="intro-img1" :src="baseUrl+'/train/video-kc.png'" mode=""></image>
<view class="title1"> <view class="title1">
课程 课程
</view> </view>
<view class="title2"> <view class="title2">
简介 简介
</view> </view>
<image class="intro-img2" src="../../../static/images/train/video-sc.png" mode=""></image> <image class="intro-img2" :src="baseUrl+'/train/video-sc.png'" mode=""></image>
</view> </view>
<view class="intro-content"> <view class="intro-content">
{{videoInfo.introduce}} {{videoInfo.introduce}}
@@ -105,6 +105,25 @@
</view> </view>
</view> </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>
</view> </view>
@@ -114,7 +133,7 @@
<script setup> <script setup>
import { inject, reactive,ref, onMounted, onUnmounted, nextTick } from 'vue'; import { inject, reactive,ref, onMounted, onUnmounted, nextTick } from 'vue';
import { onLoad } from '@dcloudio/uni-app'; import { onLoad,onHide,onUnload } from '@dcloudio/uni-app';
const { $api, navTo, navBack } = inject('globalFunction'); const { $api, navTo, navBack } = inject('globalFunction');
import config from "@/config.js" import config from "@/config.js"
@@ -126,11 +145,11 @@ const videoInfo=ref({})
const trainVideoImgUrl=config.trainVideoImgUrl const trainVideoImgUrl=config.trainVideoImgUrl
const categories=ref([]) const categories=ref([])
const levalLabels=ref([]) const levalLabels=ref([])
const videoContext = ref(null); const latestTime = ref(0)
const videoUrl = ref(''); const totalTime=ref(0)
const posterUrl = ref('');
const videoTitle = ref('');
const baseUrl = config.imgBaseUrl const baseUrl = config.imgBaseUrl
const pageEnterTime = ref(0)
const currentChapter = ref(0)
const getItemBackgroundStyle = (imageName) => ({ const getItemBackgroundStyle = (imageName) => ({
backgroundImage: `url(${baseUrl}/train/${imageName})`, backgroundImage: `url(${baseUrl}/train/${imageName})`,
backgroundSize: '100% 100%', // 覆盖整个容器 backgroundSize: '100% 100%', // 覆盖整个容器
@@ -144,23 +163,23 @@ const videoIntroBackgroundStyle = (imageName) => ({
backgroundRepeat: 'no-repeat' backgroundRepeat: 'no-repeat'
}); });
const params = reactive({ const params = reactive({
videoId: '', videoId: '',
userId: '' userId: ''
}) })
onLoad((options) => { onLoad((options) => {
console.log("options",options) getHeart()
pageEnterTime.value = Date.now() // 记录毫秒时间戳
videoId.value=options.id videoId.value=options.id
userId.value=uni.getStorageSync('userInfo').userId
getDictionary() getDictionary()
getData()
});
onMounted(() => {
// 初始化视频上下文
// nextTick(() => {
// videoContext.value = uni.createVideoContext('videoPlayer');
// });
}); });
onHide(() => {
updateVideoInfo() // 用缓存值,不要调 getCurrentTime
reportPageDuration()
})
onUnload(() => {
updateVideoInfo()
reportPageDuration()
})
function getData() { function getData() {
params.videoId=videoId.value params.videoId=videoId.value
params.userId=userId.value params.userId=userId.value
@@ -172,16 +191,35 @@ function getData() {
'Content-Type': "application/x-www-form-urlencoded" 'Content-Type': "application/x-www-form-urlencoded"
} }
$api.myRequest('/train/public/trainVideo/model', params,'post',9100,header).then((resData) => { $api.myRequest('/train/public/trainVideo/model', params,'post',9100,header).then((resData) => {
console.log("resData",resData)
videoInfo.value=resData videoInfo.value=resData
videoInfo.value.currentUrl=trainVideoImgUrl+videoInfo.value.trainClassList[0].url videoInfo.value.currentUrl=trainVideoImgUrl+videoInfo.value.trainClassList[0].url
console.log("videoInfo.value.currentUrl",videoInfo.value.currentUrl) videoInfo.value.percentage=((videoInfo.value.process/(videoInfo.value.hour*60))*100).toFixed(2)
videoInfo.value.percentage=(videoInfo.value.process/(videoInfo.value.hour*60))*100 videoInfo.value.uploadTime=videoInfo.value.uploadTime.split(' ')[0]
console.log("videoInfo",videoInfo.value) });
}
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(){ function getDictionary(){
$api.myRequest('/system/public/dict/data/type/train_category', {},'get',9100).then((resData) => { $api.myRequest('/system/public/dict/data/type/question_classification', {},'get',9100).then((resData) => {
categories.value=resData.data categories.value=resData.data
}); });
$api.myRequest('/system/public/dict/data/type/train_level', {},'get',9100).then((resData) => { $api.myRequest('/system/public/dict/data/type/train_level', {},'get',9100).then((resData) => {
@@ -204,13 +242,76 @@ function getLevelLabelByValue(value) {
const item = levalLabels.value.find(item => item.dictValue === String(value)) const item = levalLabels.value.find(item => item.dictValue === String(value))
return item ? item.dictLabel : '暂无等级' 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(() => { onUnmounted(() => {
// 组件卸载时停止播放并退出全屏
if (videoContext.value) {
videoContext.value.pause();
videoContext.value.exitFullScreen();
}
}); });
@@ -277,15 +378,20 @@ onUnmounted(() => {
.icon-img{ .icon-img{
width: 24rpx; width: 24rpx;
height: 28rpx; height: 28rpx;
margin-right: 4rpx;
} }
.info-label{ .info-label{
font-weight: bold; font-weight: bold;
font-size: 28rpx; font-size: 28rpx;
color: #0068C8; color: #0068C8;
min-width: 86rpx;
} }
.info-value{ .info-value{
font-size: 28rpx; font-size: 28rpx;
color: #404040; color: #404040;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.video-intro{ .video-intro{
padding: 20rpx; padding: 20rpx;
@@ -326,6 +432,7 @@ onUnmounted(() => {
box-shadow: 0px 0px 10px 0px rgba(0,48,107,0.1); box-shadow: 0px 0px 10px 0px rgba(0,48,107,0.1);
border-radius: 16rpx; border-radius: 16rpx;
padding: 40rpx 30rpx; padding: 40rpx 30rpx;
margin-bottom: 30rpx;
} }
.progress-info{ .progress-info{
display: flex; display: flex;
@@ -335,4 +442,51 @@ onUnmounted(() => {
color: #333333; color: #333333;
margin-top: 20rpx; 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> </style>