hookehuyr

feat(组件): 新增CheckinCard组件并重构多处使用

refactor(视图): 使用CheckinCard组件替换重复的帖子卡片代码
fix(播放器): 修复VideoPlayer和AudioPlayer的禁用状态和错误处理
style(样式): 调整postCountModel的底部边距
docs(类型): 更新组件类型声明文件
......@@ -13,6 +13,7 @@ declare module 'vue' {
AppLayout: typeof import('./components/layout/AppLayout.vue')['default']
AudioPlayer: typeof import('./components/ui/AudioPlayer.vue')['default']
BottomNav: typeof import('./components/layout/BottomNav.vue')['default']
CheckinCard: typeof import('./components/checkin/CheckinCard.vue')['default']
CheckInDialog: typeof import('./components/ui/CheckInDialog.vue')['default']
CheckInList: typeof import('./components/ui/CheckInList.vue')['default']
CollapsibleCalendar: typeof import('./components/ui/CollapsibleCalendar.vue')['default']
......
<template>
<div class="post-card shadow-md">
<!-- Header -->
<div class="post-header">
<van-row>
<van-col span="4">
<van-image round width="2.5rem" height="2.5rem"
:src="getOptimizedUrl(post.user.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg')"
fit="cover" />
</van-col>
<van-col span="17">
<div class="user-info">
<div class="username">
{{ post.user.name }}
<!-- Makeup Tag -->
<span v-if="post.user.is_makeup"
class="MakeupTag inline-flex items-center ml-2 px-2 py-0.5 rounded-full text-xs font-medium bg-green-50 text-green-600 border border-green-300">补打卡</span>
<slot name="header-tags"></slot>
</div>
<div class="post-time">{{ post.user.time }}</div>
</div>
</van-col>
<van-col span="3">
<div v-if="post.is_my" class="post-menu">
<slot name="menu">
<!-- Default menu items if needed, or left empty for parent to fill -->
<van-icon name="edit" @click="emit('edit', post)" class="mr-2" />
<van-icon name="delete-o" @click="emit('delete', post)" />
</slot>
</div>
<slot name="header-right" v-else></slot>
</van-col>
</van-row>
</div>
<!-- Content -->
<div class="post-content">
<slot name="content-top"></slot>
<PostCountModel :post-data="post" />
<div class="post-text">{{ post.content }}</div>
<!-- Media -->
<div class="post-media">
<!-- Images -->
<div v-if="post.images.length" class="post-images">
<van-image width="30%" fit="cover" v-for="(image, index) in post.images" :key="index"
:src="getOptimizedUrl(image)" radius="5" @click="openImagePreview(index)" />
</div>
<van-image-preview v-if="post.images.length" v-model:show="showLocalImagePreview" :images="post.images"
:start-position="localStartPosition" :show-index="true" />
<!-- Videos -->
<div v-for="(v, idx) in post.videoList" :key="idx">
<!-- Cover -->
<div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden"
style="aspect-ratio: 16/9; margin-bottom: 1rem;">
<img :src="getOptimizedUrl(v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png')"
:alt="post.content" class="w-full h-full object-cover" />
<div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20"
@click="startPlay(v)">
<div
class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors">
<van-icon name="play-circle-o" class="text-white" size="40" />
</div>
</div>
</div>
<!-- Video Player -->
<VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video"
:video-id="v.id || `video-${post.id}-${idx}`" class="post-video rounded-lg overflow-hidden"
:ref="(el) => setVideoRef(el, v.id)" @onPlay="(player) => handleVideoPlay(player, v)"
@onPause="handleVideoPause" />
</div>
<!-- Audio -->
<AudioPlayer v-if="post.audio && post.audio.length" :songs="post.audio" class="post-audio" :id="post.id"
:ref="(el) => setAudioRef(el, post.id)" @play="handleAudioPlay" />
</div>
</div>
<!-- Footer -->
<div class="post-footer flex items-center justify-between">
<!-- Left: Like -->
<div class="flex items-center">
<van-icon @click="emit('like', post)" name="good-job" class="like-icon"
:color="post.is_liked ? 'red' : ''" />
<span class="like-count ml-1">{{ post.likes }}</span>
</div>
<!-- Right: Custom Actions -->
<div class="flex items-center">
<slot name="footer-right"></slot>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import PostCountModel from "@/components/count/postCountModel.vue";
import VideoPlayer from "@/components/ui/VideoPlayer.vue";
import AudioPlayer from "@/components/ui/AudioPlayer.vue";
const props = defineProps({
post: { type: Object, required: true },
useCdnOptimization: { type: Boolean, default: false }
})
const emit = defineEmits(['like', 'edit', 'delete', 'video-play', 'audio-play'])
// Image Preview State
const showLocalImagePreview = ref(false)
const localStartPosition = ref(0)
const openImagePreview = (index) => {
localStartPosition.value = index
showLocalImagePreview.value = true
}
// Media Refs
const videoRefs = ref(new Map())
const audioRefs = ref(new Map())
const setVideoRef = (el, id) => {
if (el) {
videoRefs.value.set(id, el)
} else {
videoRefs.value.delete(id)
}
}
const setAudioRef = (el, id) => {
if (el) {
audioRefs.value.set(id, el)
} else {
audioRefs.value.delete(id)
}
}
// Optimization
const getOptimizedUrl = (url) => {
if (!props.useCdnOptimization) return url
if (!url || !url.includes('cdn.ipadbiz.cn')) return url
if (url.includes('?')) return url
return `${url}?imageMogr2/thumbnail/200x/strip/quality/70`
}
// Video Logic
const startPlay = (v) => {
// Pause other videos in this card
props.post.videoList.forEach(item => {
if (item.id !== v.id) item.isPlaying = false
})
v.isPlaying = true
}
const handleVideoPlay = (player, v) => {
// Stop local audio
stopLocalAudio()
// Emit to parent to stop other cards
emit('video-play', { post: props.post, player, videoId: v.id })
}
const handleVideoPause = () => {
// do nothing
}
// Audio Logic
const handleAudioPlay = (player) => {
// Stop local videos
stopLocalVideos()
// Emit to parent
emit('audio-play', { post: props.post, player })
}
const stopLocalVideos = () => {
videoRefs.value.forEach(player => {
if (player && typeof player.pause === 'function') {
player.pause()
}
})
props.post.videoList.forEach(v => v.isPlaying = false)
}
const stopLocalAudio = () => {
audioRefs.value.forEach(player => {
if (player && typeof player.pause === 'function') {
player.pause()
}
})
// Also update isPlaying state if AudioPlayer doesn't handle it fully self-contained
// But AudioPlayer usually manages its own state or we don't track audio isPlaying on post object for audio lists?
// Looking at IndexCheckInPage: post.audio is array. AudioPlayer takes songs.
// So we just pause the player component.
}
// Expose methods for parent
defineExpose({
stopAllMedia: () => {
stopLocalVideos()
stopLocalAudio()
},
// Also expose post id for convenience
id: props.post.id
})
</script>
<style lang="less" scoped>
.post-card {
background: #fff;
border-radius: 10px;
padding: 1rem;
margin-bottom: 1rem;
.post-header {
margin-bottom: 0.5rem;
.user-info {
display: flex;
flex-direction: column;
justify-content: center;
height: 2.5rem;
margin-left: 0.5rem;
.username {
font-weight: bold;
font-size: 0.95rem;
display: flex;
align-items: center;
}
.post-time {
font-size: 0.75rem;
color: #999;
}
}
.post-menu {
display: flex;
justify-content: flex-end;
font-size: 1.2rem;
color: #999;
}
}
.post-content {
.post-text {
color: #666;
margin-bottom: 1rem;
white-space: pre-wrap;
word-wrap: break-word;
}
.post-media {
.post-images {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.post-video {
margin: 1rem 0;
width: 100%;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.post-audio {
margin: 1rem 0;
}
}
}
.post-footer {
margin-top: 1rem;
color: #666;
.like-icon {
margin-right: 0.25rem;
}
.like-count {
font-size: 0.9rem;
}
}
}
</style>
<!--
* @Date: 2025-12-11 17:26:25
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-12-11 20:19:04
* @LastEditTime: 2025-12-11 20:49:09
* @FilePath: /mlaj/src/components/count/postCountModel.vue
* @Description: 发布作业统计模型
-->
......@@ -45,5 +45,6 @@ const countData = ref({
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
border-radius: 10px;
font-size: 0.85rem;
margin-bottom: 1rem;
}
</style>
......
......@@ -48,6 +48,8 @@
<div class="flex items-center space-x-12 mt-4" style="justify-content: space-evenly;">
<button
@click="prevSong"
:disabled="songs.length <= 1"
:class="{'opacity-50 cursor-not-allowed': songs.length <= 1}"
class="w-10 h-10 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
>
<font-awesome-icon icon="backward-step" class="text-xl text-gray-600" />
......@@ -66,6 +68,8 @@
</button>
<button
@click="nextSong"
:disabled="songs.length <= 1"
:class="{'opacity-50 cursor-not-allowed': songs.length <= 1}"
class="w-10 h-10 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
>
<font-awesome-icon icon="forward-step" class="text-xl text-gray-600" />
......
......@@ -249,15 +249,23 @@ onBeforeUnmount(() => {
defineExpose({
pause() {
if (player.value && typeof player.value.pause === 'function') {
if (player.value && !player.value.isDisposed && typeof player.value.isDisposed === 'function' && !player.value.isDisposed() && typeof player.value.pause === 'function') {
try {
player.value.pause();
emit('onPause', player.value);
} catch (e) {
console.warn('Video pause error:', e);
}
}
},
play() {
if (player.value) {
if (player.value && !player.value.isDisposed && typeof player.value.isDisposed === 'function' && !player.value.isDisposed()) {
try {
player.value?.play();
emit('onPlay', player.value);
} catch (e) {
console.warn('Video play error:', e);
}
}
},
getPlayer() {
......
<!--
* @Date: 2025-05-29 15:34:17
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-12-11 17:28:28
* @LastEditTime: 2025-12-11 20:58:24
* @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue
* @Description: 文件描述
-->
......@@ -71,90 +71,22 @@
@load="onLoad"
class="py-3 space-y-4"
>
<div class="post-card" v-for="post in checkinDataList" :key="post.id">
<div class="post-header">
<van-row>
<van-col span="3">
<van-image round width="2.5rem" height="2.5rem" :src="post.user.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg?imageMogr2/thumbnail/200x/strip/quality/70'" fit="cover" />
</van-col>
<van-col span="18">
<div class="user-info">
<div class="username">
{{ post.user.name }}
<!-- 补打卡标识标签 -->
<span
v-if="post.user.is_makeup"
class="MakeupTag inline-flex items-center ml-2 px-2 py-0.5 rounded-full text-xs font-medium bg-green-50 text-green-600 border border-green-300"
>补打卡</span>
</div>
<div class="post-time">{{ post.user.time }}</div>
</div>
</van-col>
<van-col span="3">
<div v-if="post.is_my" class="post-menu">
<van-icon name="edit" @click="editCheckin(post)" />
<van-icon name="delete-o" @click="delCheckin(post)" />
</div>
</van-col>
</van-row>
</div>
<div class="post-content">
<PostCountModel :post-data="post" />
<div class="post-text">{{ post.content }}</div>
<div class="post-media">
<div v-if="post.images.length" class="post-images">
<van-image width="30%" fit="cover" v-for="(image, index) in post.images" :key="index" :src="image" radius="5"
@click="openImagePreview(index, post)" />
</div>
<van-image-preview v-if="currentPost" v-model:show="showImagePreview" :images="currentPost.images" :start-position="startPosition" :show-index="true" @change="onChange" />
<div v-for="(v, idx) in post.videoList" :key="idx">
<!-- 视频封面和播放按钮 -->
<div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden" style="aspect-ratio: 16/9; margin-bottom: 1rem;">
<img :src="v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png?imageMogr2/thumbnail/200x/strip/quality/70'"
:alt="v.content" class="w-full h-full object-cover" />
<div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20"
@click="startPlay(v)">
<div
class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors">
<van-icon name="play-circle-o" class="text-white" size="40" />
</div>
</div>
</div>
<!-- 视频播放器 -->
<VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" class="post-video rounded-lg overflow-hidden"
:ref="el => {
if(el) {
// 确保不重复添加
if (!videoPlayers?.includes(el)) {
videoPlayers?.push(el);
}
}
}"
@onPlay="handleVideoPlay(player, post)"
@onPause="handleVideoPause(post)" />
</div>
<AudioPlayer
v-if="post.audio.length"
:songs="post.audio"
class="post-audio"
:id="post.id"
:ref="el => {
if(el) {
// 确保不重复添加
if (!audioPlayers?.includes(el)) {
audioPlayers?.push(el);
}
}
}"
@play="(player) => handleAudioPlay(player, post)"
/>
</div>
</div>
<div class="post-footer">
<van-icon @click="handLike(post)"name="good-job" class="like-icon" :color="post.is_liked ? 'red' : ''" />
<span class="like-count">{{ post.likes }}</span>
</div>
</div>
<CheckinCard
v-for="post in checkinDataList"
:key="post.id"
:post="post"
:use-cdn-optimization="true"
:ref="(el) => setCheckinCardRef(el, post.id)"
@like="handLike"
@edit="editCheckin"
@delete="delCheckin"
@video-play="handleVideoPlay"
@audio-play="handleAudioPlay"
>
<template #content-top>
<div class="text-gray-500 font-bold text-sm mb-4">阅读与写作</div>
</template>
</CheckinCard>
</van-list>
<van-empty v-else description="暂无数据" />
</div>
......@@ -188,10 +120,9 @@ import { useRoute, useRouter } from 'vue-router'
import { showConfirmDialog, showSuccessToast, showFailToast, showLoadingToast } from 'vant';
import AppLayout from "@/components/layout/AppLayout.vue";
import FrostedGlass from "@/components/ui/FrostedGlass.vue";
import VideoPlayer from "@/components/ui/VideoPlayer.vue";
import AudioPlayer from "@/components/ui/AudioPlayer.vue";
import CollapsibleCalendar from "@/components/ui/CollapsibleCalendar.vue";
import PostCountModel from "@/components/count/postCountModel.vue";
import CheckinCard from "@/components/checkin/CheckinCard.vue";
import { useTitle } from '@vueuse/core';
import dayjs from 'dayjs';
......@@ -246,162 +177,37 @@ onMounted(() => {
updateCalendarHeight();
});
// 存储所有视频播放器的引用
const videoPlayers = ref([]);
// 存储所有音频播放器的引用
const audioPlayers = ref([]);
// CheckinCard refs
const checkinCardRefs = ref(new Map());
const setCheckinCardRef = (el, id) => {
if (el) checkinCardRefs.value.set(id, el);
};
// 组件卸载前清理播放器引用和事件监听器
onBeforeUnmount(() => {
// 停止所有视频和音频播放
if (videoPlayers.value) {
videoPlayers.value.forEach(player => {
if (player && typeof player?.pause === 'function') {
player?.pause();
}
});
}
stopAllAudio();
// 清空引用数组
if (videoPlayers.value) videoPlayers.value = [];
if (audioPlayers.value) audioPlayers.value = [];
// 清理事件监听器
window.removeEventListener('resize', handleResize);
window.removeEventListener('orientationchange', handleResize);
});
/**
* 开始播放指定帖子的视频
* @param {Object} post - 要播放视频的帖子对象
*/
const startPlay = (post) => {
// 确保checkinDataList.value是一个数组
if (checkinDataList.value) {
// 先暂停所有其他视频
checkinDataList.value.forEach(p => {
p.videoList.forEach(v => {
if (v.id !== post.id) {
v.isPlaying = false;
}
});
});
}
// 设置当前视频为播放状态
post.isPlaying = true;
};
/**
* 处理视频播放事件
* @param {Object} player - 视频播放器实例
* @param {Object} post - 包含视频的帖子对象
*/
const handleVideoPlay = (player, post) => {
stopAllAudio();
};
/**
* 处理视频暂停事件
* @param {Object} post - 包含视频的帖子对象
*/
const handleVideoPause = (post) => {
// 视频暂停时不改变isPlaying状态,保持播放器可见
// 这样用户可以继续从暂停处播放
};
/**
* 停止除当前播放器外的所有其他视频
* @param {Object} currentPlayer - 当前播放器的视频播放器实例
* @param {Object} currentPost - 当前播放的帖子对象
*/
const stopOtherVideos = (currentPlayer, currentPost) => {
// 确保videoPlayers.value是一个数组
if (videoPlayers.value) {
// 暂停其他视频播放器
videoPlayers.value.forEach(player => {
if (player !== currentPlayer && player.pause) {
player.pause();
}
});
}
// 更新其他帖子的播放状态
checkinDataList.value.forEach(p => {
p.videoList.forEach(v => {
if (v.id !== currentPost.id) {
v.isPlaying = false;
}
});
});
};
/**
* 处理音频播放事件
* @param {Object} player - 音频播放器实例
* @param {Object} post - 包含音频的帖子对象
*/
const handleAudioPlay = (player, post) => {
// 停止其他音频播放
stopOtherAudio(player, post);
};
const stopOtherAudio = (currentPlayer, currentPost) => {
// 确保audioPlayers.value是一个数组
if (audioPlayers.value) {
// 暂停其他音频播放器
audioPlayers.value.forEach(player => {
if (player.id!== currentPost.id && player.pause) {
player.pause();
// 视频播放事件处理
const handleVideoPlay = ({ post, player, videoId }) => {
checkinCardRefs.value.forEach((card, id) => {
if (id !== post.id) {
card.stopAllMedia();
}
});
}
// 更新其他帖子的播放状态
checkinDataList.value.forEach(post => {
if (post.id!== currentPost.id) {
post.isPlaying = false;
}
});
// 停止所有视频播放
stopAllVideos();
}
const stopAllAudio = () => {
// 确保audioPlayers.value是一个数组
if (!audioPlayers.value) return;
audioPlayers.value?.forEach(player => {
// 使用组件暴露的pause方法
if (typeof player.pause === 'function') {
player.pause();
}
});
// 更新所有帖子的播放状态
checkinDataList.value.forEach(post => {
if (post.audio.length) {
post.isPlaying = false;
// 音频播放事件处理
const handleAudioPlay = ({ post, player }) => {
checkinCardRefs.value.forEach((card, id) => {
if (id !== post.id) {
card.stopAllMedia();
}
});
}
/**
* 停止所有视频播放
*/
const stopAllVideos = () => {
// 确保videoPlayers.value是一个数组
if (!videoPlayers.value) return;
// 更新所有帖子的播放状态
checkinDataList.value.forEach(p => {
p.videoList.forEach(v => {
v.isPlaying = false;
});
});
};
const themeVars = {
calendarSelectedDayBackground: '#4caf50',
calendarHeaderShadow: 'rgba(0, 0, 0, 0.1)',
......@@ -413,22 +219,7 @@ const progress1 = ref(0);
const teamAvatars = ref([])
// 图片预览相关
const showImagePreview = ref(false);
const startPosition = ref(0);
const currentPost = ref(null);
// 打开图片预览
const openImagePreview = (index, post) => {
currentPost.value = post;
startPosition.value = index;
showImagePreview.value = true;
}
// 图片切换事件处理
const onChange = (index) => {
startPosition.value = index;
}
/**
* 日历日期格式化函数
* @param {Object} day - 日期对象
......@@ -946,78 +737,7 @@ const formatData = (data) => {
}
}
.post-card {
margin: 1rem 0;
padding: 1rem;
background-color: #FFF;
border-radius: 5px;
.post-header {
margin-bottom: 1rem;
}
.user-info {
margin-left: 0.5rem;
.username {
font-weight: 500;
}
.post-time {
color: gray;
font-size: 0.8rem;
}
}
.post-menu {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.post-content {
.post-text {
color: #666;
margin-bottom: 1rem;
white-space: pre-wrap;
word-wrap: break-word;
}
.post-media {
.post-images {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.post-video {
margin: 1rem 0;
width: 100%;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.post-audio {
margin: 1rem 0;
}
}
}
.post-footer {
margin-top: 1rem;
color: #666;
.like-icon {
margin-right: 0.25rem;
}
.like-count {
font-size: 0.9rem;
}
}
}
</style>
<style scoped>
......
<!--
* @Date: 2025-05-29 15:34:17
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-12-11 13:31:17
* @LastEditTime: 2025-12-11 20:54:50
* @FilePath: /mlaj/src/views/teacher/checkinPage.vue
* @Description: 文件描述
-->
......@@ -72,7 +72,7 @@
</div>-->
<div class="text-wrapper mt-4" style="padding: 0 1rem; color: #4caf50;">
<div class="text-header">学生动态</div>
<div class="text-header mb-4">学生动态</div>
<van-list
v-if="checkinDataList.length"
v-model:loading="loading"
......@@ -81,87 +81,20 @@
@load="onLoad"
class="space-y-4"
>
<div class="post-card" v-for="post in checkinDataList" :key="post.id">
<div class="post-header">
<van-row>
<van-col span="4">
<van-image round width="2.5rem" height="2.5rem" :src="post.user.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" fit="cover" />
</van-col>
<van-col span="17">
<div class="user-info">
<div class="username">{{ post.user.name }}</div>
<div class="post-time">{{ post.user.time }}</div>
</div>
</van-col>
<!-- <van-col span="3">
<div v-if="post.is_my" class="post-menu">
<van-icon name="edit" @click="editCheckin(post)" />
<van-icon name="delete-o" @click="delCheckin(post)" />
</div>
</van-col> -->
</van-row>
</div>
<div class="post-content">
<PostCountModel :post-data="post" />
<div class="post-text">{{ post.content }}</div>
<div class="post-media">
<div v-if="post.images.length" class="post-images">
<van-image width="30%" fit="cover" v-for="(image, index) in post.images" :key="index" :src="image" radius="5"
@click="openImagePreview(index, post)" />
</div>
<van-image-preview v-if="currentPost" v-model:show="showImagePreview" :images="currentPost.images" :start-position="startPosition" :show-index="true" @change="onChange" />
<div v-for="(v, idx) in post.videoList" :key="idx">
<!-- 视频封面和播放按钮 -->
<div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden" style="aspect-ratio: 16/9; margin-bottom: 1rem;">
<img :src="v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png'"
:alt="v.content" class="w-full h-full object-cover" />
<div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20"
@click="startPlay(v)">
<div
class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors">
<van-icon name="play-circle-o" class="text-white" size="40" />
</div>
</div>
</div>
<!-- 视频播放器 -->
<VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" class="post-video rounded-lg overflow-hidden"
:ref="el => {
if(el) {
// 确保不重复添加
if (!videoPlayers?.includes(el)) {
videoPlayers?.push(el);
}
}
}"
@onPlay="handleVideoPlay(player, post)"
@onPause="handleVideoPause(post)" />
</div>
<AudioPlayer
v-if="post.audio.length"
:songs="post.audio"
class="post-audio"
:id="post.id"
:ref="el => {
if(el) {
// 确保不重复添加
if (!audioPlayers?.includes(el)) {
audioPlayers?.push(el);
}
}
}"
@play="(player) => handleAudioPlay(player, post)"
/>
</div>
</div>
<div class="post-footer flex items-center justify-between">
<!-- 左侧:点赞 -->
<div class="flex items-center">
<van-icon @click="handLike(post)" name="good-job" class="like-icon" :color="post.is_liked ? 'red' : ''" />
<span class="like-count ml-1">{{ post.likes }}</span>
</div>
<!-- 右侧:审核操作 -->
<!-- TODO: 审核操作没有什么实质作用, 未通过用户看不到提示, 用户修改之后老师也接不到提示, 唯一作用就是让其他学员看不到 -->
<CheckinCard
v-for="post in checkinDataList"
:key="post.id"
:post="post"
:use-cdn-optimization="true"
:ref="(el) => setCheckinCardRef(el, post.id)"
@like="handLike"
@video-play="handleVideoPlay"
@audio-play="handleAudioPlay"
>
<template #content-top>
<div class="text-gray-500 font-bold text-sm mb-4">阅读与写作</div>
</template>
<template #footer-right>
<div class="flex items-center cursor-pointer" @click="openAuditDialog(post)">
<van-icon
:name="post.is_audit ? 'passed' : 'close'"
......@@ -173,8 +106,8 @@
{{ post.is_audit ? '已通过' : '未通过' }}
</span>
</div>
</div>
</div>
</template>
</CheckinCard>
</van-list>
<van-empty v-else description="暂无数据" />
</div>
......@@ -193,8 +126,7 @@ import { useRoute, useRouter } from 'vue-router'
import { showConfirmDialog, showSuccessToast, showFailToast, showLoadingToast, showToast } from 'vant';
import AppLayout from "@/components/layout/AppLayout.vue";
import FrostedGlass from "@/components/ui/FrostedGlass.vue";
import VideoPlayer from "@/components/ui/VideoPlayer.vue";
import AudioPlayer from "@/components/ui/AudioPlayer.vue";
import CheckinCard from "@/components/checkin/CheckinCard.vue";
import CourseGroupCascader from '@/components/ui/CourseGroupCascader.vue'
import PostCountModel from '@/components/count/postCountModel.vue'
import { useTitle } from '@vueuse/core';
......@@ -374,162 +306,30 @@ onMounted(() => {
});
});
// 存储所有视频播放器的引用
const videoPlayers = ref([]);
// 存储所有音频播放器的引用
const audioPlayers = ref([]);
// 组件卸载前清理播放器引用和事件监听器
onBeforeUnmount(() => {
// 停止所有视频和音频播放
if (videoPlayers.value) {
videoPlayers.value.forEach(player => {
if (player && typeof player?.pause === 'function') {
player?.pause();
}
});
}
stopAllAudio();
// 清空引用数组
if (videoPlayers.value) videoPlayers.value = [];
if (audioPlayers.value) audioPlayers.value = [];
// 清理事件监听器
window.removeEventListener('resize', handleResize);
window.removeEventListener('orientationchange', handleResize);
});
/**
* 开始播放指定帖子的视频
* @param {Object} post - 要播放视频的帖子对象
*/
const startPlay = (post) => {
// 确保checkinDataList.value是一个数组
if (checkinDataList.value) {
// 先暂停所有其他视频
checkinDataList.value.forEach(p => {
p.videoList.forEach(v => {
if (v.id !== post.id) {
v.isPlaying = false;
}
});
});
}
// 设置当前视频为播放状态
post.isPlaying = true;
};
/**
* 处理视频播放事件
* @param {Object} player - 视频播放器实例
* @param {Object} post - 包含视频的帖子对象
*/
const handleVideoPlay = (player, post) => {
stopAllAudio();
};
/**
* 处理视频暂停事件
* @param {Object} post - 包含视频的帖子对象
*/
const handleVideoPause = (post) => {
// 视频暂停时不改变isPlaying状态,保持播放器可见
// 这样用户可以继续从暂停处播放
};
/**
* 停止除当前播放器外的所有其他视频
* @param {Object} currentPlayer - 当前播放的视频播放器实例
* @param {Object} currentPost - 当前播放的帖子对象
*/
const stopOtherVideos = (currentPlayer, currentPost) => {
// 确保videoPlayers.value是一个数组
if (videoPlayers.value) {
// 暂停其他视频播放器
videoPlayers.value.forEach(player => {
if (player !== currentPlayer && player.pause) {
player.pause();
}
});
}
// 更新其他帖子的播放状态
checkinDataList.value.forEach(p => {
p.videoList.forEach(v => {
if (v.id !== currentPost.id) {
v.isPlaying = false;
}
});
});
};
/**
* 处理音频播放事件
* @param {Object} player - 音频播放器实例
* @param {Object} post - 包含音频的帖子对象
*/
const handleAudioPlay = (player, post) => {
// 停止其他音频播放
stopOtherAudio(player, post);
// CheckinCard refs
const checkinCardRefs = ref(new Map());
const setCheckinCardRef = (el, id) => {
if (el) checkinCardRefs.value.set(id, el);
};
const stopOtherAudio = (currentPlayer, currentPost) => {
// 确保audioPlayers.value是一个数组
if (audioPlayers.value) {
// 暂停其他音频播放器
audioPlayers.value.forEach(player => {
if (player.id!== currentPost.id && player.pause) {
player.pause();
}
});
}
// 更新其他帖子的播放状态
checkinDataList.value.forEach(post => {
if (post.id!== currentPost.id) {
post.isPlaying = false;
// 视频播放事件处理
const handleVideoPlay = ({ post, player, videoId }) => {
checkinCardRefs.value.forEach((card, id) => {
if (id !== post.id) {
card.stopAllMedia();
}
});
// 停止所有视频播放
stopAllVideos();
}
const stopAllAudio = () => {
// 确保audioPlayers.value是一个数组
if (!audioPlayers.value) return;
audioPlayers.value?.forEach(player => {
// 使用组件暴露的pause方法
if (typeof player.pause === 'function') {
player.pause();
}
});
// 更新所有帖子的播放状态
checkinDataList.value.forEach(post => {
if (post.audio.length) {
post.isPlaying = false;
// 音频播放事件处理
const handleAudioPlay = ({ post, player }) => {
checkinCardRefs.value.forEach((card, id) => {
if (id !== post.id) {
card.stopAllMedia();
}
});
}
/**
* 停止所有视频播放
*/
const stopAllVideos = () => {
// 确保videoPlayers.value是一个数组
if (!videoPlayers.value) return;
// 更新所有帖子的播放状态
checkinDataList.value.forEach(p => {
p.videoList.forEach(v => {
v.isPlaying = false;
});
});
};
const themeVars = reactive({
calendarSelectedDayBackground: '#4caf50',
calendarHeaderShadow: 'rgba(0, 0, 0, 0.1)',
......@@ -542,22 +342,7 @@ const progress1 = ref(0);
const teamAvatars = ref([])
// 图片预览相关
const showImagePreview = ref(false);
const startPosition = ref(0);
const currentPost = ref(null);
// 打开图片预览
const openImagePreview = (index, post) => {
currentPost.value = post;
startPosition.value = index;
showImagePreview.value = true;
}
// 图片切换事件处理
const onChange = (index) => {
startPosition.value = index;
}
const formatter = (day) => {
const year = day.date.getFullYear();
const month = day.date.getMonth() + 1;
......@@ -983,76 +768,5 @@ const handleAdd = (type) => {
}
}
.post-card {
margin: 1rem 0;
padding: 1rem;
background-color: #FFF;
border-radius: 5px;
.post-header {
margin-bottom: 1rem;
}
.user-info {
margin-left: 0.5rem;
.username {
font-weight: 500;
}
.post-time {
color: gray;
font-size: 0.8rem;
}
}
.post-menu {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.post-content {
.post-text {
color: #666;
margin-bottom: 1rem;
white-space: pre-wrap;
word-wrap: break-word;
}
.post-media {
.post-images {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.post-video {
margin: 1rem 0;
width: 100%;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.post-audio {
margin: 1rem 0;
}
}
}
.post-footer {
margin-top: 1rem;
color: #666;
.like-icon {
margin-right: 0.25rem;
}
.like-count {
font-size: 0.9rem;
}
}
}
</style>
......
......@@ -2,7 +2,7 @@
* @Author: hookehuyr hookehuyr@gmail.com
* @Date: 2025-06-19 17:12:19
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-12-11 13:33:01
* @LastEditTime: 2025-12-11 20:55:35
* @FilePath: /mlaj/src/views/teacher/studentPage.vue
* @Description: 学生详情页面
-->
......@@ -187,76 +187,20 @@
<!--作业记录 -->
<van-list v-show="activeTab === 'homework' && checkinDataList.length" v-model:loading="loading"
:finished="finished" finished-text="没有更多了" @load="onLoad" class="space-y-4 px-4">
<div class="post-card shadow-md" v-for="post in checkinDataList" :key="post.id">
<div class="post-header">
<van-row>
<van-col span="4">
<van-image round width="2.5rem" height="2.5rem"
:src="post.user.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" fit="cover" />
</van-col>
<van-col span="17">
<div class="user-info">
<div class="username">{{ post.user.name }}</div>
<div class="post-time">{{ post.user.time }}</div>
</div>
</van-col>
<van-col span="3">
</van-col>
</van-row>
</div>
<div class="post-content">
<PostCountModel :post-data="post" />
<div class="post-text">{{ post.content }}</div>
<div class="post-media">
<div v-if="post.images.length" class="post-images">
<van-image width="30%" fit="cover" v-for="(image, index) in post.images" :key="index" :src="image"
radius="5" @click="openImagePreview(index, post)" />
</div>
<van-image-preview v-if="currentPost" v-model:show="showImagePreview" :images="currentPost.images"
:start-position="startPosition" :show-index="true" @change="onChange" />
<div v-for="(v, idx) in post.videoList" :key="idx">
<!-- 视频封面和播放按钮 -->
<div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden"
style="aspect-ratio: 16/9; margin-bottom: 1rem;">
<img :src="v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png'" :alt="v.content"
class="w-full h-full object-cover" />
<div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20"
@click="startPlay(v)">
<div
class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors">
<van-icon name="play-circle-o" class="text-white" size="40" />
</div>
</div>
</div>
<!-- 视频播放器 -->
<VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video"
class="post-video rounded-lg overflow-hidden" :ref="el => {
if (el) {
// 确保不重复添加
if (!videoPlayers?.includes(el)) {
videoPlayers?.push(el);
}
}
}" @onPlay="handleVideoPlay(player, post)" @onPause="handleVideoPause(post)" />
</div>
<AudioPlayer v-if="post.audio.length" :songs="post.audio" class="post-audio" :id="post.id" :ref="el => {
if (el) {
// 确保不重复添加
if (!audioPlayers?.includes(el)) {
audioPlayers?.push(el);
}
}
}" @play="(player) => handleAudioPlay(player, post)" />
</div>
</div>
<div class="post-footer flex items-center justify-between">
<!-- 左侧:点赞 -->
<div class="flex items-center">
<van-icon @click="handLike(post)" name="good-job" class="like-icon" :color="post.is_liked ? 'red' : ''" />
<span class="like-count ml-1">{{ post.likes }}</span>
</div>
<!-- 右侧:点评 -->
<CheckinCard
v-for="post in checkinDataList"
:key="post.id"
:post="post"
:use-cdn-optimization="true"
:ref="(el) => setCheckinCardRef(el, post.id)"
@like="handLike(post)"
@video-play="handleVideoPlay"
@audio-play="handleAudioPlay"
>
<template #content-top>
<div class="text-gray-500 font-bold text-sm mb-4">阅读与写作</div>
</template>
<template #footer-right>
<div class="flex items-center cursor-pointer" @click="openCommentPopup(post)">
<van-icon
name="comment-o"
......@@ -269,8 +213,8 @@
{{ post?.is_feedback ? '已点评' : '待点评' }}
</span>
</div>
</div>
</div>
</template>
</CheckinCard>
</van-list>
<van-empty v-show="activeTab === 'homework' && !checkinDataList.length" description="暂无数据" />
<div style="height: 5rem;"></div>
......@@ -376,11 +320,10 @@
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { ref, computed, onMounted, onBeforeUnmount, nextTick, reactive } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { showConfirmDialog, showSuccessToast, showFailToast, showLoadingToast } from 'vant';
import VideoPlayer from "@/components/ui/VideoPlayer.vue";
import AudioPlayer from "@/components/ui/AudioPlayer.vue";
import CheckinCard from "@/components/checkin/CheckinCard.vue";
import PostCountModel from '@/components/count/postCountModel.vue'
import { useTitle } from '@vueuse/core';
import dayjs from 'dayjs';
......@@ -712,15 +655,11 @@ const handleTabChange = (name) => {
nextTick(() => {
// 停止所有视频和音频播放
if (videoPlayers.value) {
videoPlayers.value.forEach(player => {
if (player && typeof player?.pause === 'function') {
player?.pause();
checkinCardRefs.value.forEach(card => {
if (card) {
card.stopAllMedia();
}
});
}
stopAllAudio();
})
if (name === 'homework') {
......@@ -732,174 +671,54 @@ const handleTabChange = (name) => {
}
};
// 存储所有视频播放器的引用
const videoPlayers = ref([]);
// 存储 CheckinCard 组件引用的 Map
const checkinCardRefs = ref(new Map());
// 存储所有音频播放器的引用
const audioPlayers = ref([]);
// 设置 CheckinCard 引用
const setCheckinCardRef = (el, id) => {
if (el) {
checkinCardRefs.value.set(id, el);
} else {
checkinCardRefs.value.delete(id);
}
};
// 组件卸载前清理播放器引用和事件监听器
onBeforeUnmount(() => {
// 停止所有视频和音频播放
if (videoPlayers.value) {
videoPlayers.value.forEach(player => {
if (player && typeof player?.pause === 'function') {
player?.pause();
checkinCardRefs.value.forEach(card => {
if (card) {
card.stopAllMedia();
}
});
}
stopAllAudio();
// 清空引用数组
if (videoPlayers.value) videoPlayers.value = [];
if (audioPlayers.value) audioPlayers.value = [];
checkinCardRefs.value.clear();
});
/**
* 开始播放指定帖子的视频
* @param {Object} post - 要播放视频的帖子对象
*/
const startPlay = (post) => {
// 确保checkinDataList.value是一个数组
if (checkinDataList.value) {
// 先暂停所有其他视频
checkinDataList.value.forEach(p => {
p.videoList.forEach(v => {
if (v.id !== post.id) {
v.isPlaying = false;
}
});
});
}
// 设置当前视频为播放状态
post.isPlaying = true;
};
/**
* 处理视频播放事件
* @param {Object} player - 视频播放器实例
* @param {Object} post - 包含视频的帖子对象
*/
const handleVideoPlay = (player, post) => {
stopAllAudio();
};
/**
* 处理视频暂停事件
* @param {Object} post - 包含视频的帖子对象
*/
const handleVideoPause = (post) => {
// 视频暂停时不改变isPlaying状态,保持播放器可见
// 这样用户可以继续从暂停处播放
};
/**
* 停止除当前播放器外的所有其他视频
* @param {Object} currentPlayer - 当前播放的视频播放器实例
* @param {Object} currentPost - 当前播放的帖子对象
*/
const stopOtherVideos = (currentPlayer, currentPost) => {
// 确保videoPlayers.value是一个数组
if (videoPlayers.value) {
// 暂停其他视频播放器
videoPlayers.value.forEach(player => {
if (player !== currentPlayer && player.pause) {
player.pause();
const handleVideoPlay = (id) => {
// 暂停其他所有卡片的媒体播放
checkinCardRefs.value.forEach((card, key) => {
if (key !== id && card) {
card.stopAllMedia();
}
});
}
// 更新其他帖子的播放状态
checkinDataList.value.forEach(p => {
p.videoList.forEach(v => {
if (v.id !== currentPost.id) {
v.isPlaying = false;
}
});
});
};
/**
* 处理音频播放事件
* @param {Object} player - 音频播放器实例
* @param {Object} post - 包含音频的帖子对象
*/
const handleAudioPlay = (player, post) => {
// 停止其他音频播放
stopOtherAudio(player, post);
};
const stopOtherAudio = (currentPlayer, currentPost) => {
// 确保audioPlayers.value是一个数组
if (audioPlayers.value) {
// 暂停其他音频播放器
audioPlayers.value.forEach(player => {
if (player.id !== currentPost.id && player.pause) {
player.pause();
}
});
}
// 更新其他帖子的播放状态
checkinDataList.value.forEach(post => {
if (post.id !== currentPost.id) {
post.isPlaying = false;
const handleAudioPlay = (id) => {
// 暂停其他所有卡片的媒体播放
checkinCardRefs.value.forEach((card, key) => {
if (key !== id && card) {
card.stopAllMedia();
}
});
// 停止所有视频播放
stopAllVideos();
}
const stopAllAudio = () => {
// 确保audioPlayers.value是一个数组
if (!audioPlayers.value) return;
audioPlayers.value?.forEach(player => {
// 使用组件暴露的pause方法
if (typeof player.pause === 'function') {
player?.pause();
}
});
// 更新所有帖子的播放状态
checkinDataList.value.forEach(post => {
if (post.audio.length) {
post.isPlaying = false;
}
});
}
/**
* 停止所有视频播放
*/
const stopAllVideos = () => {
// 确保videoPlayers.value是一个数组
if (!videoPlayers.value) return;
// 更新所有帖子的播放状态
checkinDataList.value.forEach(p => {
p.videoList.forEach(v => {
v.isPlaying = false;
});
});
};
// 图片预览相关
const showImagePreview = ref(false);
const startPosition = ref(0);
const currentPost = ref(null);
// 打开图片预览
const openImagePreview = (index, post) => {
currentPost.value = post;
startPosition.value = index;
showImagePreview.value = true;
}
// 图片切换事件处理
const onChange = (index) => {
startPosition.value = index;
}
const handLike = async (post) => {
if (!post.is_liked) {
const { code, data } = await likeUploadTaskInfoAPI({ checkin_id: post.id, })
......@@ -1078,79 +897,6 @@ const getStatList = async () => {
border-bottom-width: 2px;
}
.post-card {
// margin: 1rem 0;
padding: 1rem;
background-color: #FFF;
border-radius: 5px;
.post-header {
margin-bottom: 1rem;
}
.user-info {
margin-left: 0.5rem;
.username {
font-weight: 500;
}
.post-time {
color: gray;
font-size: 0.8rem;
}
}
.post-menu {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.post-content {
.post-text {
color: #666;
margin-bottom: 1rem;
white-space: pre-wrap;
word-wrap: break-word;
}
.post-media {
.post-images {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.post-video {
margin: 1rem 0;
width: 100%;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.post-audio {
margin: 1rem 0;
}
}
}
.post-footer {
margin-top: 1rem;
color: #666;
.like-icon {
margin-right: 0.25rem;
}
.like-count {
font-size: 0.9rem;
}
}
}
/* 点评弹窗样式 */
.comment-popup {
.van-popup {
......
<!--
* @Date: 2025-11-19 22:05:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-12-11 17:30:04
* @LastEditTime: 2025-12-11 20:56:02
* @FilePath: /mlaj/src/views/teacher/studentRecordPage.vue
* @Description: 学生作业记录页面(仅作业记录与点评功能),固定 user_id 与 group_id
-->
......@@ -9,77 +9,28 @@
<div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen">
<!-- 作业记录列表 -->
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad" class="space-y-4 p-4">
<div class="post-card shadow-md" v-for="post in checkinDataList" :key="post.id">
<div class="post-header">
<van-row>
<van-col span="4">
<van-image round width="2.5rem" height="2.5rem"
:src="optimizeCdn(post.user.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg')" fit="cover" />
</van-col>
<van-col span="17">
<div class="user-info">
<div class="username">{{ post.user.name }}</div>
<div class="post-time">{{ post.user.time }}</div>
</div>
</van-col>
<van-col span="3">
</van-col>
</van-row>
</div>
<div class="post-content">
<PostCountModel :post-data="post" />
<div class="post-text">{{ post.content }}</div>
<div class="post-media">
<div v-if="post.images.length" class="post-images">
<van-image width="30%" fit="cover" v-for="(image, index) in post.images" :key="index" :src="optimizeCdn(image)"
radius="5" @click="openImagePreview(index, post)" />
</div>
<van-image-preview v-if="currentPost" v-model:show="showImagePreview" :images="currentPost.images"
:start-position="startPosition" :show-index="true" @change="onChange" />
<div v-for="(v, idx) in post.videoList" :key="idx">
<!-- 视频封面和播放按钮 -->
<div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden" style="aspect-ratio: 16/9; margin-bottom: 1rem;">
<img :src="optimizeCdn(v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png')" :alt="post.content" class="w-full h-full object-cover" />
<div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20" @click="startPlay(v)">
<div class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors">
<van-icon name="play-circle-o" class="text-white" size="40" />
</div>
</div>
</div>
<!-- 视频播放器 -->
<VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" class="post-video rounded-lg overflow-hidden" :ref="el => {
if (el) {
if (!videoPlayers?.includes(el)) {
videoPlayers?.push(el);
}
}
}" @onPlay="handleVideoPlay(player, post)" @onPause="handleVideoPause(post)" />
</div>
<AudioPlayer v-if="post.audio.length" :songs="post.audio" class="post-audio" :id="post.id" :ref="el => {
if (el) {
if (!audioPlayers?.includes(el)) {
audioPlayers?.push(el);
}
}
}" @play="(player) => handleAudioPlay(player, post)" />
</div>
</div>
<div class="post-footer flex items-center justify-between">
<!-- 左侧:点赞 -->
<div class="flex items-center">
<van-icon @click="handLike(post)" name="good-job" class="like-icon" :color="post.is_liked ? 'red' : ''" />
<span class="like-count ml-1">{{ post.likes }}</span>
</div>
<!-- 右侧:点评 -->
<CheckinCard
v-for="post in checkinDataList"
:key="post.id"
:post="post"
:use-cdn-optimization="true"
:ref="(el) => setCheckinCardRef(el, post.id)"
@like="handLike(post)"
@video-play="handleVideoPlay"
@audio-play="handleAudioPlay"
>
<template #content-top>
<div class="text-gray-500 font-bold text-sm mb-4">阅读与写作</div>
</template>
<template #footer-right>
<div class="flex items-center cursor-pointer" @click="openCommentPopup(post)">
<van-icon name="comment-o" :color="post?.is_feedback ? '#10b981' : '#999'" size="19" class="mr-1" style="margin-top: 0.2rem;" />
<span class="text-sm" :class="post?.is_feedback ? 'text-green-600' : 'text-gray-500'">
{{ post?.is_feedback ? '已点评' : '待点评' }}
</span>
</div>
</div>
</div>
</template>
</CheckinCard>
</van-list>
<van-empty v-show="!checkinDataList.length" description="暂无数据" />
......@@ -119,8 +70,7 @@ import { ref, onMounted, onBeforeUnmount } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTitle } from '@vueuse/core'
import { showSuccessToast, showFailToast, showLoadingToast } from 'vant'
import VideoPlayer from '@/components/ui/VideoPlayer.vue'
import AudioPlayer from '@/components/ui/AudioPlayer.vue'
import CheckinCard from '@/components/checkin/CheckinCard.vue'
import PostCountModel from '@/components/count/postCountModel.vue'
import { addCheckinFeedbackAPI } from '@/api/teacher'
import { likeUploadTaskInfoAPI, dislikeUploadTaskInfoAPI, getCheckinTeacherListAPI } from '@/api/checkin'
......@@ -140,14 +90,17 @@ const finished = ref(false)
const limit = ref(10)
const page = ref(0)
// 图片预览相关
const showImagePreview = ref(false)
const startPosition = ref(0)
const currentPost = ref(null)
// 存储 CheckinCard 组件引用的 Map
const checkinCardRefs = ref(new Map());
// 播放器引用
const videoPlayers = ref([])
const audioPlayers = ref([])
// 设置 CheckinCard 引用
const setCheckinCardRef = (el, id) => {
if (el) {
checkinCardRefs.value.set(id, el);
} else {
checkinCardRefs.value.delete(id);
}
};
// 点评相关状态
const showCommentPopup = ref(false)
......@@ -159,45 +112,6 @@ const commentForm = ref({
})
/**
* CDN图片优化:为 cdn.ipadbiz.cn 域名添加压缩参数
* @param {string} url - 图片地址
* @returns {string} 处理后的图片地址
*/
function optimizeCdn(url) {
if (!url) return url
try {
const u = String(url)
if (u.includes('cdn.ipadbiz.cn')) {
const hasQuery = u.includes('?')
const param = 'imageMogr2/thumbnail/200x/strip/quality/70'
return hasQuery ? `${u}&${param}` : `${u}?${param}`
}
return u
} catch (e) {
return url
}
}
/**
* 打开图片预览
* @param {number} index - 起始索引
* @param {Object} post - 当前帖子
*/
function openImagePreview(index, post) {
currentPost.value = post
startPosition.value = index
showImagePreview.value = true
}
/**
* 图片切换事件
* @param {number} index - 当前索引
*/
function onChange(index) {
startPosition.value = index
}
/**
* 点赞/取消点赞
* @param {Object} post - 帖子对象
* @returns {Promise<void>}
......@@ -305,96 +219,25 @@ function startPlay(post) {
/**
* 视频播放事件:同时停止音频
*/
function handleVideoPlay() {
stopAllAudio()
}
/**
* 视频暂停事件:保持播放器可见
*/
function handleVideoPause() {
// 暂不处理,保持播放器状态
}
/**
* 停止其他视频与音频
* @param {Object} currentPlayer - 当前播放器
* @param {Object} currentPost - 当前帖子
*/
function stopOtherVideos(currentPlayer, currentPost) {
if (videoPlayers.value) {
videoPlayers.value.forEach(player => {
if (player !== currentPlayer && player.pause) {
player.pause()
function handleVideoPlay(id) {
// 暂停其他所有卡片的媒体播放
checkinCardRefs.value.forEach((card, key) => {
if (key !== id && card) {
card.stopAllMedia();
}
})
}
checkinDataList.value.forEach(p => {
p.videoList.forEach(v => {
if (v.id !== currentPost.id) {
v.isPlaying = false
}
})
})
});
}
/**
* 音频播放事件:停止其他音频
* @param {Object} player - 音频播放器
* @param {Object} post - 当前帖子
*/
function handleAudioPlay(player, post) {
stopOtherAudio(player, post)
}
/**
* 停止其他音频
* @param {Object} currentPlayer - 当前播放器
* @param {Object} currentPost - 当前帖子
*/
function stopOtherAudio(currentPlayer, currentPost) {
if (audioPlayers.value) {
audioPlayers.value.forEach(player => {
if (player.id !== currentPost.id && player.pause) {
player.pause()
function handleAudioPlay(id) {
// 暂停其他所有卡片的媒体播放
checkinCardRefs.value.forEach((card, key) => {
if (key !== id && card) {
card.stopAllMedia();
}
})
}
checkinDataList.value.forEach(post => {
if (post.id !== currentPost.id) {
post.isPlaying = false
}
})
stopAllVideos()
}
/**
* 停止所有音频
*/
function stopAllAudio() {
if (!audioPlayers.value) return
audioPlayers.value?.forEach(player => {
if (typeof player.pause === 'function') {
player?.pause()
}
})
checkinDataList.value.forEach(post => {
if (post.audio.length) {
post.isPlaying = false
}
})
}
/**
* 停止所有视频
*/
function stopAllVideos() {
if (!videoPlayers.value) return
checkinDataList.value.forEach(p => {
p.videoList.forEach(v => {
v.isPlaying = false
})
})
});
}
/**
......@@ -475,85 +318,16 @@ function formatData(data) {
// 生命周期:卸载时清理播放器引用
onMounted(() => {})
onBeforeUnmount(() => {
if (videoPlayers.value) {
videoPlayers.value.forEach(player => {
if (player && typeof player?.pause === 'function') {
player?.pause()
checkinCardRefs.value.forEach(card => {
if (card) {
card.stopAllMedia();
}
})
}
stopAllAudio()
if (videoPlayers.value) videoPlayers.value = []
if (audioPlayers.value) audioPlayers.value = []
});
checkinCardRefs.value.clear();
})
</script>
<style lang="less">
.post-card {
padding: 1rem;
background-color: #FFF;
border-radius: 5px;
.post-header {
margin-bottom: 1rem;
}
.user-info {
margin-left: 0.5rem;
.username {
font-weight: 500;
}
.post-time {
color: gray;
font-size: 0.8rem;
}
}
.post-content {
.post-text {
color: #666;
margin-bottom: 1rem;
white-space: pre-wrap;
word-wrap: break-word;
}
.post-media {
.post-images {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.post-video {
margin: 1rem 0;
width: 100%;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.post-audio {
margin: 1rem 0;
}
}
}
.post-footer {
margin-top: 1rem;
color: #666;
.like-icon {
margin-right: 0.25rem;
}
.like-count {
font-size: 0.9rem;
}
}
}
.comment-popup {
.van-popup {
max-width: 90vw;
......