hookehuyr

feat(study): 添加学习记录功能并更新播放器组件

实现学习记录功能,包括添加记录API接口和前端交互逻辑
为VideoPlayer和AudioPlayer组件添加获取ID的方法
更新LearningRecordsPage使用真实API获取数据
在StudyDetailPage中实现播放时持续记录功能
/*
* @Date: 2025-06-11 13:24:46
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-11 13:49:42
* @FilePath: /mlaj/src/api/record.js
* @Description: 学习记录相关接口
*/
import { fn, fetch } from './fn'
const Api = {
STUDY_RECORD_LIST: '/srv/?a=study_record&t=list',
STUDY_RECORD_ADD: '/srv/?a=study_record&t=add',
}
/**
* @description: 获取学习记录列表
* @param: page 页码
* @param: limit 每页数量
* @param: keyword 搜索
* @return: data: { id: 课程id, title: 课程名称, subtitle: 课程副标题, cover: 封面图, study_duration: 学习时长, recent_study_time: 最近学习时间, study_progress: 学习进度(小数) }
*/
export const getStudyRecordListAPI = (params) => fn(fetch.get(Api.STUDY_RECORD_LIST, params))
/**
* @description: 添加记录
* @param: schedule_id 课程章节ID
* @param: meta_id 课程章节的视频、音频的ID
* @param: media_duration 视频、音频的时长
* @param: playback_position 视频、音频当前播放位置
* @param: playback_id 某一轮播放的ID,需要区分不同轮次播放的开始和结束,最终用来统计播放时长
* @return: data: { }
*/
export const addStudyRecordAPI = (params) => fn(fetch.post(Api.STUDY_RECORD_ADD, params))
<!--
* @Date: 2025-04-07 12:35:35
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-05-30 18:03:29
* @LastEditTime: 2025-06-11 11:58:22
* @FilePath: /mlaj/src/components/ui/AudioPlayer.vue
* @Description: 音频播放器组件,支持播放控制、进度条调节、音量控制、播放列表等功能
-->
......@@ -480,6 +480,10 @@ defineExpose({
},
isPlaying: () => isPlaying.value,
id: props.id,
getPlayer: () => audio.value,
getId() {
return currentSong.value.meta_id || 'meta_id'
}
})
</script>
......
......@@ -30,6 +30,10 @@ const props = defineProps({
type: String,
required: true,
},
videoId: {
type: String,
required: true,
},
autoplay: {
type: Boolean,
required: false,
......@@ -140,7 +144,10 @@ defineExpose({
},
getPlayer() {
return player.value;
}
},
getId() {
return props.videoId || "meta_id";
},
});
</script>
......
<!--
* @Date: 2025-05-29 15:34:17
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-10 16:13:24
* @LastEditTime: 2025-06-11 13:38:38
* @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue
* @Description: 文件描述
-->
......
......@@ -4,7 +4,7 @@
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
:finished-text="finishText"
@load="onLoad"
class="px-4 py-3 space-y-4"
>
......@@ -18,8 +18,8 @@
class="w-20 h-20 rounded-lg overflow-hidden flex-shrink-0 mr-3"
>
<van-image
:src="record.course.coverImage"
:alt="record.course.title"
:src="record.cover"
:alt="record.title"
class="w-full h-full"
fit="cover"
error-icon="photo-fail"
......@@ -30,7 +30,7 @@
</div>
<div class="flex-1">
<h3 class="text-base font-medium mb-2 line-clamp-1">
{{ record.course.title }}
{{ record.title }}
</h3>
<div class="flex items-center text-sm text-gray-500 mb-2">
<svg
......@@ -47,7 +47,7 @@
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>学习时长:{{ formatDuration(record.duration) }}</span>
<span>学习时长:{{ formatDuration(record.study_duration) }}</span>
</div>
<div class="flex items-center text-sm text-gray-500 mb-3">
<svg
......@@ -64,12 +64,12 @@
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
/>
</svg>
<span>最近学习:{{ formatDate(record.lastStudyTime) }}</span>
<span>最近学习:{{ formatDate(record.recent_study_time) }}</span>
</div>
<div class="flex items-center">
<div class="flex-1">
<van-progress
:percentage="record.progress"
:percentage="record.study_progress"
:stroke-width="4"
color="#10B981"
/>
......@@ -112,6 +112,9 @@ import { useTitle } from '@vueuse/core';
import FrostedGlass from '@/components/ui/FrostedGlass.vue';
import { formatDate, formatDuration } from '@/utils/tools';
// 导入接口
import { getStudyRecordListAPI } from "@/api/record";
const $route = useRoute();
useTitle($route.meta.title);
......@@ -119,50 +122,28 @@ const records = ref([]);
const loading = ref(false);
const finished = ref(false);
const page = ref(1);
const pageSize = 10;
// 模拟学习记录数据
const mockRecords = [
{
id: 1,
course: {
title: '亲子教育必修课:如何培养孩子的学习兴趣',
coverImage: 'https://cdn.ipadbiz.cn/mlaj/images/jbwr0qZvpD4.jpg'
},
duration: 3600, // 秒
lastStudyTime: '2024-01-15T10:30:00',
progress: 75
},
{
id: 2,
course: {
title: '儿童心理发展指南:0-6岁关键期教育方法',
coverImage: 'https://cdn.ipadbiz.cn/mlaj/images/27kCu7bXGEI.jpg'
},
duration: 7200,
lastStudyTime: '2024-01-14T15:20:00',
progress: 45
}
];
const limit = ref(10);
const finishText = ref('没有更多了');
// 加载数据
const onLoad = () => {
const onLoad = async () => {
loading.value = true;
// 模拟异步加载
setTimeout(() => {
const start = (page.value - 1) * pageSize;
const end = start + pageSize;
const newRecords = mockRecords.slice(start, end);
records.value.push(...newRecords);
const nextPage = page.value;
const { code, data } = await getStudyRecordListAPI({
limit: limit.value,
page: nextPage,
});
if (code) {
records.value.push(...data);
finished.value = data.length < limit.value;
page.value = nextPage + 1;
loading.value = false;
if (newRecords.length < pageSize) {
finished.value = true;
} else {
page.value += 1;
}
}, 1000);
} else {
finished.value = true;
finishText.value = '';
loading.value = false;
}
};
// 处理图片加载错误
......
......@@ -24,6 +24,7 @@
<!-- 视频播放器 -->
<VideoPlayer v-show="isPlaying" ref="videoPlayerRef"
:video-url="courseFile?.list?.length ? courseFile['list'][0]['url'] : ''" :autoplay="false"
:video-id="courseFile?.list?.length ? courseFile['list'][0]['meta_id'] : ''"
@onPlay="handleVideoPlay" @onPause="handleVideoPause" />
</div>
<div v-if="course.course_type === 'audio'" class="w-full relative"
......@@ -256,6 +257,7 @@ import PdfPreview from '@/components/ui/PdfPreview.vue';
// 导入接口
import { getScheduleCourseAPI, getGroupCommentListAPI, addGroupCommentAPI, addGroupCommentLikeAPI, delGroupCommentLikeAPI, getCourseDetailAPI } from '@/api/course';
import { addStudyRecordAPI } from "@/api/record";
const route = useRoute();
const router = useRouter();
......@@ -402,10 +404,16 @@ const pdfShow = ref(false);
const pdfTitle = ref('');
const pdfUrl = ref('');
const showPdf = ({ title, url }) => {
const showPdf = ({ title, url, meta_id }) => {
pdfTitle.value = title;
pdfUrl.value = url;
pdfShow.value = true;
// 新增记录
let paramsObj = {
schedule_id: courseId.value,
meta_id
}
addRecord(paramsObj);
};
const courseId = computed(() => {
......@@ -709,12 +717,16 @@ const onAudioPause = (audio) => {
const videoDuration = ref(0);
const currentPosition = ref(0);
// 记录音频时长和当前播放位置的变量
const audioDuration = ref(0);
const audioPosition = ref(0);
/**
* 开始操作
* @param action
* @param item
*/
const startAction = (action, item) => {
const startAction = (item) => {
// 先清除可能存在的定时器
if (window.actionTimer) {
clearInterval(window.actionTimer);
......@@ -725,6 +737,11 @@ const startAction = (action, item) => {
videoDuration.value = videoPlayerRef.value.getPlayer().duration();
}
// 获取音频总时长(如果是音频播放)
if (audioPlayerRef.value && audioPlayerRef.value.getPlayer()) {
audioDuration.value = audioPlayerRef.value.getPlayer().duration;
}
// 生成唯一标识符
let uuid = uuidv4();
console.warn('开始操作', uuid);
......@@ -733,12 +750,37 @@ const startAction = (action, item) => {
window.actionTimer = setInterval(() => {
console.warn('持续操作中', uuid);
let paramsObj = {}
// 更新当前播放位置(如果是视频播放)
if (videoPlayerRef.value && videoPlayerRef.value.getPlayer()) {
currentPosition.value = videoPlayerRef.value.getPlayer().currentTime();
console.log('视频总时长:', videoDuration.value, '当前位置:', currentPosition.value);
console.log('视频总时长:', videoDuration.value, '当前位置:', currentPosition.value, 'id:', videoPlayerRef.value.getId());
paramsObj = {
schedule_id: courseId.value,
meta_id: videoPlayerRef.value.getId(),
media_duration: videoDuration.value,
playback_position: currentPosition.value,
playback_id: uuid,
}
}
// 更新当前播放位置(如果是音频播放)
if (audioPlayerRef.value && audioPlayerRef.value.getPlayer()) {
audioPosition.value = audioPlayerRef.value.getPlayer().currentTime;
console.log('音频总时长:', audioDuration.value, '当前位置:', audioPosition.value, 'id:', audioPlayerRef.value.getId());
paramsObj = {
schedule_id: courseId.value,
meta_id: audioPlayerRef.value.getId(),
media_duration: audioDuration.value,
playback_position: audioPosition.value,
playback_id: uuid,
}
}
// 新增记录
addRecord(paramsObj);
// 这里可以添加需要持续执行的具体操作
}, 1000); // 每秒执行一次,可以根据需求调整时间间隔
}
......@@ -748,11 +790,18 @@ const startAction = (action, item) => {
* @param action
* @param item
*/
const endAction = (action, item) => {
const endAction = (item) => {
// 在结束前记录最后的播放位置
if (videoPlayerRef.value && videoPlayerRef.value.player) {
window.currentPosition = videoPlayerRef.value.player.currentTime();
console.log('结束时 - 视频总时长:', window.videoDuration, '最终位置:', window.currentPosition);
currentPosition.value = videoPlayerRef.value.player.currentTime();
console.log('结束时 - 视频总时长:', videoDuration.value, '最终位置:', currentPosition.value);
}
// 在结束前记录最后的音频播放位置
if (course.value?.course_type === 'audio' && document.querySelector('audio')) {
const audioElement = document.querySelector('audio');
audioPosition.value = audioElement.currentTime || 0;
console.log('结束时 - 音频总时长:', audioDuration.value, '最终位置:', audioPosition.value);
}
// 清除定时器,停止执行startAction
......@@ -762,6 +811,14 @@ const endAction = (action, item) => {
console.warn('操作已停止');
}
}
/**
* 新增记录
* @param paramsObj
*/
const addRecord = async (paramsObj) => {
await addStudyRecordAPI(paramsObj);
}
</script>
<style lang="less" scoped>
......