feat(组件): 新增CheckinCard组件并重构多处使用
refactor(视图): 使用CheckinCard组件替换重复的帖子卡片代码 fix(播放器): 修复VideoPlayer和AudioPlayer的禁用状态和错误处理 style(样式): 调整postCountModel的底部边距 docs(类型): 更新组件类型声明文件
Showing
9 changed files
with
470 additions
and
1217 deletions
| ... | @@ -13,6 +13,7 @@ declare module 'vue' { | ... | @@ -13,6 +13,7 @@ declare module 'vue' { |
| 13 | AppLayout: typeof import('./components/layout/AppLayout.vue')['default'] | 13 | AppLayout: typeof import('./components/layout/AppLayout.vue')['default'] |
| 14 | AudioPlayer: typeof import('./components/ui/AudioPlayer.vue')['default'] | 14 | AudioPlayer: typeof import('./components/ui/AudioPlayer.vue')['default'] |
| 15 | BottomNav: typeof import('./components/layout/BottomNav.vue')['default'] | 15 | BottomNav: typeof import('./components/layout/BottomNav.vue')['default'] |
| 16 | + CheckinCard: typeof import('./components/checkin/CheckinCard.vue')['default'] | ||
| 16 | CheckInDialog: typeof import('./components/ui/CheckInDialog.vue')['default'] | 17 | CheckInDialog: typeof import('./components/ui/CheckInDialog.vue')['default'] |
| 17 | CheckInList: typeof import('./components/ui/CheckInList.vue')['default'] | 18 | CheckInList: typeof import('./components/ui/CheckInList.vue')['default'] |
| 18 | CollapsibleCalendar: typeof import('./components/ui/CollapsibleCalendar.vue')['default'] | 19 | CollapsibleCalendar: typeof import('./components/ui/CollapsibleCalendar.vue')['default'] | ... | ... |
src/components/checkin/CheckinCard.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="post-card shadow-md"> | ||
| 3 | + <!-- Header --> | ||
| 4 | + <div class="post-header"> | ||
| 5 | + <van-row> | ||
| 6 | + <van-col span="4"> | ||
| 7 | + <van-image round width="2.5rem" height="2.5rem" | ||
| 8 | + :src="getOptimizedUrl(post.user.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg')" | ||
| 9 | + fit="cover" /> | ||
| 10 | + </van-col> | ||
| 11 | + <van-col span="17"> | ||
| 12 | + <div class="user-info"> | ||
| 13 | + <div class="username"> | ||
| 14 | + {{ post.user.name }} | ||
| 15 | + <!-- Makeup Tag --> | ||
| 16 | + <span v-if="post.user.is_makeup" | ||
| 17 | + 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> | ||
| 18 | + <slot name="header-tags"></slot> | ||
| 19 | + </div> | ||
| 20 | + <div class="post-time">{{ post.user.time }}</div> | ||
| 21 | + </div> | ||
| 22 | + </van-col> | ||
| 23 | + <van-col span="3"> | ||
| 24 | + <div v-if="post.is_my" class="post-menu"> | ||
| 25 | + <slot name="menu"> | ||
| 26 | + <!-- Default menu items if needed, or left empty for parent to fill --> | ||
| 27 | + <van-icon name="edit" @click="emit('edit', post)" class="mr-2" /> | ||
| 28 | + <van-icon name="delete-o" @click="emit('delete', post)" /> | ||
| 29 | + </slot> | ||
| 30 | + </div> | ||
| 31 | + <slot name="header-right" v-else></slot> | ||
| 32 | + </van-col> | ||
| 33 | + </van-row> | ||
| 34 | + </div> | ||
| 35 | + | ||
| 36 | + <!-- Content --> | ||
| 37 | + <div class="post-content"> | ||
| 38 | + <slot name="content-top"></slot> | ||
| 39 | + <PostCountModel :post-data="post" /> | ||
| 40 | + <div class="post-text">{{ post.content }}</div> | ||
| 41 | + | ||
| 42 | + <!-- Media --> | ||
| 43 | + <div class="post-media"> | ||
| 44 | + <!-- Images --> | ||
| 45 | + <div v-if="post.images.length" class="post-images"> | ||
| 46 | + <van-image width="30%" fit="cover" v-for="(image, index) in post.images" :key="index" | ||
| 47 | + :src="getOptimizedUrl(image)" radius="5" @click="openImagePreview(index)" /> | ||
| 48 | + </div> | ||
| 49 | + <van-image-preview v-if="post.images.length" v-model:show="showLocalImagePreview" :images="post.images" | ||
| 50 | + :start-position="localStartPosition" :show-index="true" /> | ||
| 51 | + | ||
| 52 | + <!-- Videos --> | ||
| 53 | + <div v-for="(v, idx) in post.videoList" :key="idx"> | ||
| 54 | + <!-- Cover --> | ||
| 55 | + <div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden" | ||
| 56 | + style="aspect-ratio: 16/9; margin-bottom: 1rem;"> | ||
| 57 | + <img :src="getOptimizedUrl(v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png')" | ||
| 58 | + :alt="post.content" class="w-full h-full object-cover" /> | ||
| 59 | + <div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20" | ||
| 60 | + @click="startPlay(v)"> | ||
| 61 | + <div | ||
| 62 | + class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors"> | ||
| 63 | + <van-icon name="play-circle-o" class="text-white" size="40" /> | ||
| 64 | + </div> | ||
| 65 | + </div> | ||
| 66 | + </div> | ||
| 67 | + <!-- Video Player --> | ||
| 68 | + <VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" | ||
| 69 | + :video-id="v.id || `video-${post.id}-${idx}`" class="post-video rounded-lg overflow-hidden" | ||
| 70 | + :ref="(el) => setVideoRef(el, v.id)" @onPlay="(player) => handleVideoPlay(player, v)" | ||
| 71 | + @onPause="handleVideoPause" /> | ||
| 72 | + </div> | ||
| 73 | + | ||
| 74 | + <!-- Audio --> | ||
| 75 | + <AudioPlayer v-if="post.audio && post.audio.length" :songs="post.audio" class="post-audio" :id="post.id" | ||
| 76 | + :ref="(el) => setAudioRef(el, post.id)" @play="handleAudioPlay" /> | ||
| 77 | + </div> | ||
| 78 | + </div> | ||
| 79 | + | ||
| 80 | + <!-- Footer --> | ||
| 81 | + <div class="post-footer flex items-center justify-between"> | ||
| 82 | + <!-- Left: Like --> | ||
| 83 | + <div class="flex items-center"> | ||
| 84 | + <van-icon @click="emit('like', post)" name="good-job" class="like-icon" | ||
| 85 | + :color="post.is_liked ? 'red' : ''" /> | ||
| 86 | + <span class="like-count ml-1">{{ post.likes }}</span> | ||
| 87 | + </div> | ||
| 88 | + <!-- Right: Custom Actions --> | ||
| 89 | + <div class="flex items-center"> | ||
| 90 | + <slot name="footer-right"></slot> | ||
| 91 | + </div> | ||
| 92 | + </div> | ||
| 93 | + </div> | ||
| 94 | +</template> | ||
| 95 | + | ||
| 96 | +<script setup> | ||
| 97 | +import { ref } from 'vue' | ||
| 98 | +import PostCountModel from "@/components/count/postCountModel.vue"; | ||
| 99 | +import VideoPlayer from "@/components/ui/VideoPlayer.vue"; | ||
| 100 | +import AudioPlayer from "@/components/ui/AudioPlayer.vue"; | ||
| 101 | + | ||
| 102 | +const props = defineProps({ | ||
| 103 | + post: { type: Object, required: true }, | ||
| 104 | + useCdnOptimization: { type: Boolean, default: false } | ||
| 105 | +}) | ||
| 106 | + | ||
| 107 | +const emit = defineEmits(['like', 'edit', 'delete', 'video-play', 'audio-play']) | ||
| 108 | + | ||
| 109 | +// Image Preview State | ||
| 110 | +const showLocalImagePreview = ref(false) | ||
| 111 | +const localStartPosition = ref(0) | ||
| 112 | + | ||
| 113 | +const openImagePreview = (index) => { | ||
| 114 | + localStartPosition.value = index | ||
| 115 | + showLocalImagePreview.value = true | ||
| 116 | +} | ||
| 117 | + | ||
| 118 | +// Media Refs | ||
| 119 | +const videoRefs = ref(new Map()) | ||
| 120 | +const audioRefs = ref(new Map()) | ||
| 121 | + | ||
| 122 | +const setVideoRef = (el, id) => { | ||
| 123 | + if (el) { | ||
| 124 | + videoRefs.value.set(id, el) | ||
| 125 | + } else { | ||
| 126 | + videoRefs.value.delete(id) | ||
| 127 | + } | ||
| 128 | +} | ||
| 129 | +const setAudioRef = (el, id) => { | ||
| 130 | + if (el) { | ||
| 131 | + audioRefs.value.set(id, el) | ||
| 132 | + } else { | ||
| 133 | + audioRefs.value.delete(id) | ||
| 134 | + } | ||
| 135 | +} | ||
| 136 | + | ||
| 137 | +// Optimization | ||
| 138 | +const getOptimizedUrl = (url) => { | ||
| 139 | + if (!props.useCdnOptimization) return url | ||
| 140 | + if (!url || !url.includes('cdn.ipadbiz.cn')) return url | ||
| 141 | + if (url.includes('?')) return url | ||
| 142 | + return `${url}?imageMogr2/thumbnail/200x/strip/quality/70` | ||
| 143 | +} | ||
| 144 | + | ||
| 145 | +// Video Logic | ||
| 146 | +const startPlay = (v) => { | ||
| 147 | + // Pause other videos in this card | ||
| 148 | + props.post.videoList.forEach(item => { | ||
| 149 | + if (item.id !== v.id) item.isPlaying = false | ||
| 150 | + }) | ||
| 151 | + v.isPlaying = true | ||
| 152 | +} | ||
| 153 | + | ||
| 154 | +const handleVideoPlay = (player, v) => { | ||
| 155 | + // Stop local audio | ||
| 156 | + stopLocalAudio() | ||
| 157 | + // Emit to parent to stop other cards | ||
| 158 | + emit('video-play', { post: props.post, player, videoId: v.id }) | ||
| 159 | +} | ||
| 160 | + | ||
| 161 | +const handleVideoPause = () => { | ||
| 162 | + // do nothing | ||
| 163 | +} | ||
| 164 | + | ||
| 165 | +// Audio Logic | ||
| 166 | +const handleAudioPlay = (player) => { | ||
| 167 | + // Stop local videos | ||
| 168 | + stopLocalVideos() | ||
| 169 | + // Emit to parent | ||
| 170 | + emit('audio-play', { post: props.post, player }) | ||
| 171 | +} | ||
| 172 | + | ||
| 173 | +const stopLocalVideos = () => { | ||
| 174 | + videoRefs.value.forEach(player => { | ||
| 175 | + if (player && typeof player.pause === 'function') { | ||
| 176 | + player.pause() | ||
| 177 | + } | ||
| 178 | + }) | ||
| 179 | + props.post.videoList.forEach(v => v.isPlaying = false) | ||
| 180 | +} | ||
| 181 | + | ||
| 182 | +const stopLocalAudio = () => { | ||
| 183 | + audioRefs.value.forEach(player => { | ||
| 184 | + if (player && typeof player.pause === 'function') { | ||
| 185 | + player.pause() | ||
| 186 | + } | ||
| 187 | + }) | ||
| 188 | + // Also update isPlaying state if AudioPlayer doesn't handle it fully self-contained | ||
| 189 | + // But AudioPlayer usually manages its own state or we don't track audio isPlaying on post object for audio lists? | ||
| 190 | + // Looking at IndexCheckInPage: post.audio is array. AudioPlayer takes songs. | ||
| 191 | + // So we just pause the player component. | ||
| 192 | +} | ||
| 193 | + | ||
| 194 | +// Expose methods for parent | ||
| 195 | +defineExpose({ | ||
| 196 | + stopAllMedia: () => { | ||
| 197 | + stopLocalVideos() | ||
| 198 | + stopLocalAudio() | ||
| 199 | + }, | ||
| 200 | + // Also expose post id for convenience | ||
| 201 | + id: props.post.id | ||
| 202 | +}) | ||
| 203 | +</script> | ||
| 204 | + | ||
| 205 | +<style lang="less" scoped> | ||
| 206 | +.post-card { | ||
| 207 | + background: #fff; | ||
| 208 | + border-radius: 10px; | ||
| 209 | + padding: 1rem; | ||
| 210 | + margin-bottom: 1rem; | ||
| 211 | + | ||
| 212 | + .post-header { | ||
| 213 | + margin-bottom: 0.5rem; | ||
| 214 | + | ||
| 215 | + .user-info { | ||
| 216 | + display: flex; | ||
| 217 | + flex-direction: column; | ||
| 218 | + justify-content: center; | ||
| 219 | + height: 2.5rem; | ||
| 220 | + margin-left: 0.5rem; | ||
| 221 | + | ||
| 222 | + .username { | ||
| 223 | + font-weight: bold; | ||
| 224 | + font-size: 0.95rem; | ||
| 225 | + display: flex; | ||
| 226 | + align-items: center; | ||
| 227 | + } | ||
| 228 | + | ||
| 229 | + .post-time { | ||
| 230 | + font-size: 0.75rem; | ||
| 231 | + color: #999; | ||
| 232 | + } | ||
| 233 | + } | ||
| 234 | + | ||
| 235 | + .post-menu { | ||
| 236 | + display: flex; | ||
| 237 | + justify-content: flex-end; | ||
| 238 | + font-size: 1.2rem; | ||
| 239 | + color: #999; | ||
| 240 | + } | ||
| 241 | + } | ||
| 242 | + | ||
| 243 | + .post-content { | ||
| 244 | + .post-text { | ||
| 245 | + color: #666; | ||
| 246 | + margin-bottom: 1rem; | ||
| 247 | + white-space: pre-wrap; | ||
| 248 | + word-wrap: break-word; | ||
| 249 | + } | ||
| 250 | + | ||
| 251 | + .post-media { | ||
| 252 | + .post-images { | ||
| 253 | + display: flex; | ||
| 254 | + flex-wrap: wrap; | ||
| 255 | + gap: 0.5rem; | ||
| 256 | + } | ||
| 257 | + | ||
| 258 | + .post-video { | ||
| 259 | + margin: 1rem 0; | ||
| 260 | + width: 100%; | ||
| 261 | + border-radius: 8px; | ||
| 262 | + overflow: hidden; | ||
| 263 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 264 | + } | ||
| 265 | + | ||
| 266 | + .post-audio { | ||
| 267 | + margin: 1rem 0; | ||
| 268 | + } | ||
| 269 | + } | ||
| 270 | + } | ||
| 271 | + | ||
| 272 | + .post-footer { | ||
| 273 | + margin-top: 1rem; | ||
| 274 | + color: #666; | ||
| 275 | + | ||
| 276 | + .like-icon { | ||
| 277 | + margin-right: 0.25rem; | ||
| 278 | + } | ||
| 279 | + | ||
| 280 | + .like-count { | ||
| 281 | + font-size: 0.9rem; | ||
| 282 | + } | ||
| 283 | + } | ||
| 284 | +} | ||
| 285 | +</style> |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-12-11 17:26:25 | 2 | * @Date: 2025-12-11 17:26:25 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-12-11 20:19:04 | 4 | + * @LastEditTime: 2025-12-11 20:49:09 |
| 5 | * @FilePath: /mlaj/src/components/count/postCountModel.vue | 5 | * @FilePath: /mlaj/src/components/count/postCountModel.vue |
| 6 | * @Description: 发布作业统计模型 | 6 | * @Description: 发布作业统计模型 |
| 7 | --> | 7 | --> |
| ... | @@ -45,5 +45,6 @@ const countData = ref({ | ... | @@ -45,5 +45,6 @@ const countData = ref({ |
| 45 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); | 45 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); |
| 46 | border-radius: 10px; | 46 | border-radius: 10px; |
| 47 | font-size: 0.85rem; | 47 | font-size: 0.85rem; |
| 48 | + margin-bottom: 1rem; | ||
| 48 | } | 49 | } |
| 49 | </style> | 50 | </style> | ... | ... |
| ... | @@ -48,6 +48,8 @@ | ... | @@ -48,6 +48,8 @@ |
| 48 | <div class="flex items-center space-x-12 mt-4" style="justify-content: space-evenly;"> | 48 | <div class="flex items-center space-x-12 mt-4" style="justify-content: space-evenly;"> |
| 49 | <button | 49 | <button |
| 50 | @click="prevSong" | 50 | @click="prevSong" |
| 51 | + :disabled="songs.length <= 1" | ||
| 52 | + :class="{'opacity-50 cursor-not-allowed': songs.length <= 1}" | ||
| 51 | class="w-10 h-10 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 transition-colors" | 53 | class="w-10 h-10 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 transition-colors" |
| 52 | > | 54 | > |
| 53 | <font-awesome-icon icon="backward-step" class="text-xl text-gray-600" /> | 55 | <font-awesome-icon icon="backward-step" class="text-xl text-gray-600" /> |
| ... | @@ -66,6 +68,8 @@ | ... | @@ -66,6 +68,8 @@ |
| 66 | </button> | 68 | </button> |
| 67 | <button | 69 | <button |
| 68 | @click="nextSong" | 70 | @click="nextSong" |
| 71 | + :disabled="songs.length <= 1" | ||
| 72 | + :class="{'opacity-50 cursor-not-allowed': songs.length <= 1}" | ||
| 69 | class="w-10 h-10 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 transition-colors" | 73 | class="w-10 h-10 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 transition-colors" |
| 70 | > | 74 | > |
| 71 | <font-awesome-icon icon="forward-step" class="text-xl text-gray-600" /> | 75 | <font-awesome-icon icon="forward-step" class="text-xl text-gray-600" /> | ... | ... |
| ... | @@ -249,15 +249,23 @@ onBeforeUnmount(() => { | ... | @@ -249,15 +249,23 @@ onBeforeUnmount(() => { |
| 249 | 249 | ||
| 250 | defineExpose({ | 250 | defineExpose({ |
| 251 | pause() { | 251 | pause() { |
| 252 | - if (player.value && typeof player.value.pause === 'function') { | 252 | + if (player.value && !player.value.isDisposed && typeof player.value.isDisposed === 'function' && !player.value.isDisposed() && typeof player.value.pause === 'function') { |
| 253 | - player.value.pause(); | 253 | + try { |
| 254 | - emit('onPause', player.value); | 254 | + player.value.pause(); |
| 255 | + emit('onPause', player.value); | ||
| 256 | + } catch (e) { | ||
| 257 | + console.warn('Video pause error:', e); | ||
| 258 | + } | ||
| 255 | } | 259 | } |
| 256 | }, | 260 | }, |
| 257 | play() { | 261 | play() { |
| 258 | - if (player.value) { | 262 | + if (player.value && !player.value.isDisposed && typeof player.value.isDisposed === 'function' && !player.value.isDisposed()) { |
| 259 | - player.value?.play(); | 263 | + try { |
| 260 | - emit('onPlay', player.value); | 264 | + player.value?.play(); |
| 265 | + emit('onPlay', player.value); | ||
| 266 | + } catch (e) { | ||
| 267 | + console.warn('Video play error:', e); | ||
| 268 | + } | ||
| 261 | } | 269 | } |
| 262 | }, | 270 | }, |
| 263 | getPlayer() { | 271 | getPlayer() { | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-05-29 15:34:17 | 2 | * @Date: 2025-05-29 15:34:17 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-12-11 17:28:28 | 4 | + * @LastEditTime: 2025-12-11 20:58:24 |
| 5 | * @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue | 5 | * @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | --> | 7 | --> |
| ... | @@ -71,90 +71,22 @@ | ... | @@ -71,90 +71,22 @@ |
| 71 | @load="onLoad" | 71 | @load="onLoad" |
| 72 | class="py-3 space-y-4" | 72 | class="py-3 space-y-4" |
| 73 | > | 73 | > |
| 74 | - <div class="post-card" v-for="post in checkinDataList" :key="post.id"> | 74 | + <CheckinCard |
| 75 | - <div class="post-header"> | 75 | + v-for="post in checkinDataList" |
| 76 | - <van-row> | 76 | + :key="post.id" |
| 77 | - <van-col span="3"> | 77 | + :post="post" |
| 78 | - <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" /> | 78 | + :use-cdn-optimization="true" |
| 79 | - </van-col> | 79 | + :ref="(el) => setCheckinCardRef(el, post.id)" |
| 80 | - <van-col span="18"> | 80 | + @like="handLike" |
| 81 | - <div class="user-info"> | 81 | + @edit="editCheckin" |
| 82 | - <div class="username"> | 82 | + @delete="delCheckin" |
| 83 | - {{ post.user.name }} | 83 | + @video-play="handleVideoPlay" |
| 84 | - <!-- 补打卡标识标签 --> | 84 | + @audio-play="handleAudioPlay" |
| 85 | - <span | 85 | + > |
| 86 | - v-if="post.user.is_makeup" | 86 | + <template #content-top> |
| 87 | - 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" | 87 | + <div class="text-gray-500 font-bold text-sm mb-4">阅读与写作</div> |
| 88 | - >补打卡</span> | 88 | + </template> |
| 89 | - </div> | 89 | + </CheckinCard> |
| 90 | - <div class="post-time">{{ post.user.time }}</div> | ||
| 91 | - </div> | ||
| 92 | - </van-col> | ||
| 93 | - <van-col span="3"> | ||
| 94 | - <div v-if="post.is_my" class="post-menu"> | ||
| 95 | - <van-icon name="edit" @click="editCheckin(post)" /> | ||
| 96 | - <van-icon name="delete-o" @click="delCheckin(post)" /> | ||
| 97 | - </div> | ||
| 98 | - </van-col> | ||
| 99 | - </van-row> | ||
| 100 | - </div> | ||
| 101 | - <div class="post-content"> | ||
| 102 | - <PostCountModel :post-data="post" /> | ||
| 103 | - <div class="post-text">{{ post.content }}</div> | ||
| 104 | - <div class="post-media"> | ||
| 105 | - <div v-if="post.images.length" class="post-images"> | ||
| 106 | - <van-image width="30%" fit="cover" v-for="(image, index) in post.images" :key="index" :src="image" radius="5" | ||
| 107 | - @click="openImagePreview(index, post)" /> | ||
| 108 | - </div> | ||
| 109 | - <van-image-preview v-if="currentPost" v-model:show="showImagePreview" :images="currentPost.images" :start-position="startPosition" :show-index="true" @change="onChange" /> | ||
| 110 | - <div v-for="(v, idx) in post.videoList" :key="idx"> | ||
| 111 | - <!-- 视频封面和播放按钮 --> | ||
| 112 | - <div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden" style="aspect-ratio: 16/9; margin-bottom: 1rem;"> | ||
| 113 | - <img :src="v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png?imageMogr2/thumbnail/200x/strip/quality/70'" | ||
| 114 | - :alt="v.content" class="w-full h-full object-cover" /> | ||
| 115 | - <div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20" | ||
| 116 | - @click="startPlay(v)"> | ||
| 117 | - <div | ||
| 118 | - class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors"> | ||
| 119 | - <van-icon name="play-circle-o" class="text-white" size="40" /> | ||
| 120 | - </div> | ||
| 121 | - </div> | ||
| 122 | - </div> | ||
| 123 | - <!-- 视频播放器 --> | ||
| 124 | - <VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" class="post-video rounded-lg overflow-hidden" | ||
| 125 | - :ref="el => { | ||
| 126 | - if(el) { | ||
| 127 | - // 确保不重复添加 | ||
| 128 | - if (!videoPlayers?.includes(el)) { | ||
| 129 | - videoPlayers?.push(el); | ||
| 130 | - } | ||
| 131 | - } | ||
| 132 | - }" | ||
| 133 | - @onPlay="handleVideoPlay(player, post)" | ||
| 134 | - @onPause="handleVideoPause(post)" /> | ||
| 135 | - </div> | ||
| 136 | - <AudioPlayer | ||
| 137 | - v-if="post.audio.length" | ||
| 138 | - :songs="post.audio" | ||
| 139 | - class="post-audio" | ||
| 140 | - :id="post.id" | ||
| 141 | - :ref="el => { | ||
| 142 | - if(el) { | ||
| 143 | - // 确保不重复添加 | ||
| 144 | - if (!audioPlayers?.includes(el)) { | ||
| 145 | - audioPlayers?.push(el); | ||
| 146 | - } | ||
| 147 | - } | ||
| 148 | - }" | ||
| 149 | - @play="(player) => handleAudioPlay(player, post)" | ||
| 150 | - /> | ||
| 151 | - </div> | ||
| 152 | - </div> | ||
| 153 | - <div class="post-footer"> | ||
| 154 | - <van-icon @click="handLike(post)"name="good-job" class="like-icon" :color="post.is_liked ? 'red' : ''" /> | ||
| 155 | - <span class="like-count">{{ post.likes }}</span> | ||
| 156 | - </div> | ||
| 157 | - </div> | ||
| 158 | </van-list> | 90 | </van-list> |
| 159 | <van-empty v-else description="暂无数据" /> | 91 | <van-empty v-else description="暂无数据" /> |
| 160 | </div> | 92 | </div> |
| ... | @@ -188,10 +120,9 @@ import { useRoute, useRouter } from 'vue-router' | ... | @@ -188,10 +120,9 @@ import { useRoute, useRouter } from 'vue-router' |
| 188 | import { showConfirmDialog, showSuccessToast, showFailToast, showLoadingToast } from 'vant'; | 120 | import { showConfirmDialog, showSuccessToast, showFailToast, showLoadingToast } from 'vant'; |
| 189 | import AppLayout from "@/components/layout/AppLayout.vue"; | 121 | import AppLayout from "@/components/layout/AppLayout.vue"; |
| 190 | import FrostedGlass from "@/components/ui/FrostedGlass.vue"; | 122 | import FrostedGlass from "@/components/ui/FrostedGlass.vue"; |
| 191 | -import VideoPlayer from "@/components/ui/VideoPlayer.vue"; | ||
| 192 | -import AudioPlayer from "@/components/ui/AudioPlayer.vue"; | ||
| 193 | import CollapsibleCalendar from "@/components/ui/CollapsibleCalendar.vue"; | 123 | import CollapsibleCalendar from "@/components/ui/CollapsibleCalendar.vue"; |
| 194 | import PostCountModel from "@/components/count/postCountModel.vue"; | 124 | import PostCountModel from "@/components/count/postCountModel.vue"; |
| 125 | +import CheckinCard from "@/components/checkin/CheckinCard.vue"; | ||
| 195 | import { useTitle } from '@vueuse/core'; | 126 | import { useTitle } from '@vueuse/core'; |
| 196 | import dayjs from 'dayjs'; | 127 | import dayjs from 'dayjs'; |
| 197 | 128 | ||
| ... | @@ -246,162 +177,37 @@ onMounted(() => { | ... | @@ -246,162 +177,37 @@ onMounted(() => { |
| 246 | updateCalendarHeight(); | 177 | updateCalendarHeight(); |
| 247 | }); | 178 | }); |
| 248 | 179 | ||
| 249 | -// 存储所有视频播放器的引用 | 180 | +// CheckinCard refs |
| 250 | -const videoPlayers = ref([]); | 181 | +const checkinCardRefs = ref(new Map()); |
| 251 | - | 182 | +const setCheckinCardRef = (el, id) => { |
| 252 | -// 存储所有音频播放器的引用 | 183 | + if (el) checkinCardRefs.value.set(id, el); |
| 253 | -const audioPlayers = ref([]); | 184 | +}; |
| 254 | 185 | ||
| 255 | // 组件卸载前清理播放器引用和事件监听器 | 186 | // 组件卸载前清理播放器引用和事件监听器 |
| 256 | onBeforeUnmount(() => { | 187 | onBeforeUnmount(() => { |
| 257 | - // 停止所有视频和音频播放 | ||
| 258 | - if (videoPlayers.value) { | ||
| 259 | - videoPlayers.value.forEach(player => { | ||
| 260 | - if (player && typeof player?.pause === 'function') { | ||
| 261 | - player?.pause(); | ||
| 262 | - } | ||
| 263 | - }); | ||
| 264 | - } | ||
| 265 | - | ||
| 266 | - stopAllAudio(); | ||
| 267 | - | ||
| 268 | - // 清空引用数组 | ||
| 269 | - if (videoPlayers.value) videoPlayers.value = []; | ||
| 270 | - if (audioPlayers.value) audioPlayers.value = []; | ||
| 271 | - | ||
| 272 | // 清理事件监听器 | 188 | // 清理事件监听器 |
| 273 | window.removeEventListener('resize', handleResize); | 189 | window.removeEventListener('resize', handleResize); |
| 274 | window.removeEventListener('orientationchange', handleResize); | 190 | window.removeEventListener('orientationchange', handleResize); |
| 275 | }); | 191 | }); |
| 276 | 192 | ||
| 277 | - | 193 | +// 视频播放事件处理 |
| 278 | -/** | 194 | +const handleVideoPlay = ({ post, player, videoId }) => { |
| 279 | - * 开始播放指定帖子的视频 | 195 | + checkinCardRefs.value.forEach((card, id) => { |
| 280 | - * @param {Object} post - 要播放视频的帖子对象 | 196 | + if (id !== post.id) { |
| 281 | - */ | 197 | + card.stopAllMedia(); |
| 282 | -const startPlay = (post) => { | ||
| 283 | - // 确保checkinDataList.value是一个数组 | ||
| 284 | - if (checkinDataList.value) { | ||
| 285 | - // 先暂停所有其他视频 | ||
| 286 | - checkinDataList.value.forEach(p => { | ||
| 287 | - p.videoList.forEach(v => { | ||
| 288 | - if (v.id !== post.id) { | ||
| 289 | - v.isPlaying = false; | ||
| 290 | - } | ||
| 291 | - }); | ||
| 292 | - }); | ||
| 293 | - } | ||
| 294 | - | ||
| 295 | - // 设置当前视频为播放状态 | ||
| 296 | - post.isPlaying = true; | ||
| 297 | -}; | ||
| 298 | - | ||
| 299 | -/** | ||
| 300 | - * 处理视频播放事件 | ||
| 301 | - * @param {Object} player - 视频播放器实例 | ||
| 302 | - * @param {Object} post - 包含视频的帖子对象 | ||
| 303 | - */ | ||
| 304 | -const handleVideoPlay = (player, post) => { | ||
| 305 | - stopAllAudio(); | ||
| 306 | -}; | ||
| 307 | - | ||
| 308 | -/** | ||
| 309 | - * 处理视频暂停事件 | ||
| 310 | - * @param {Object} post - 包含视频的帖子对象 | ||
| 311 | - */ | ||
| 312 | -const handleVideoPause = (post) => { | ||
| 313 | - // 视频暂停时不改变isPlaying状态,保持播放器可见 | ||
| 314 | - // 这样用户可以继续从暂停处播放 | ||
| 315 | -}; | ||
| 316 | - | ||
| 317 | -/** | ||
| 318 | - * 停止除当前播放器外的所有其他视频 | ||
| 319 | - * @param {Object} currentPlayer - 当前播放器的视频播放器实例 | ||
| 320 | - * @param {Object} currentPost - 当前播放的帖子对象 | ||
| 321 | - */ | ||
| 322 | -const stopOtherVideos = (currentPlayer, currentPost) => { | ||
| 323 | - // 确保videoPlayers.value是一个数组 | ||
| 324 | - if (videoPlayers.value) { | ||
| 325 | - // 暂停其他视频播放器 | ||
| 326 | - videoPlayers.value.forEach(player => { | ||
| 327 | - if (player !== currentPlayer && player.pause) { | ||
| 328 | - player.pause(); | ||
| 329 | - } | ||
| 330 | - }); | ||
| 331 | - } | ||
| 332 | - | ||
| 333 | - // 更新其他帖子的播放状态 | ||
| 334 | - checkinDataList.value.forEach(p => { | ||
| 335 | - p.videoList.forEach(v => { | ||
| 336 | - if (v.id !== currentPost.id) { | ||
| 337 | - v.isPlaying = false; | ||
| 338 | - } | ||
| 339 | - }); | ||
| 340 | - }); | ||
| 341 | -}; | ||
| 342 | - | ||
| 343 | -/** | ||
| 344 | - * 处理音频播放事件 | ||
| 345 | - * @param {Object} player - 音频播放器实例 | ||
| 346 | - * @param {Object} post - 包含音频的帖子对象 | ||
| 347 | - */ | ||
| 348 | -const handleAudioPlay = (player, post) => { | ||
| 349 | - // 停止其他音频播放 | ||
| 350 | - stopOtherAudio(player, post); | ||
| 351 | -}; | ||
| 352 | - | ||
| 353 | -const stopOtherAudio = (currentPlayer, currentPost) => { | ||
| 354 | - // 确保audioPlayers.value是一个数组 | ||
| 355 | - if (audioPlayers.value) { | ||
| 356 | - // 暂停其他音频播放器 | ||
| 357 | - audioPlayers.value.forEach(player => { | ||
| 358 | - if (player.id!== currentPost.id && player.pause) { | ||
| 359 | - player.pause(); | ||
| 360 | - } | ||
| 361 | - }); | ||
| 362 | - } | ||
| 363 | - // 更新其他帖子的播放状态 | ||
| 364 | - checkinDataList.value.forEach(post => { | ||
| 365 | - if (post.id!== currentPost.id) { | ||
| 366 | - post.isPlaying = false; | ||
| 367 | } | 198 | } |
| 368 | }); | 199 | }); |
| 369 | - // 停止所有视频播放 | ||
| 370 | - stopAllVideos(); | ||
| 371 | } | 200 | } |
| 372 | 201 | ||
| 373 | -const stopAllAudio = () => { | 202 | +// 音频播放事件处理 |
| 374 | - // 确保audioPlayers.value是一个数组 | 203 | +const handleAudioPlay = ({ post, player }) => { |
| 375 | - if (!audioPlayers.value) return; | 204 | + checkinCardRefs.value.forEach((card, id) => { |
| 376 | - audioPlayers.value?.forEach(player => { | 205 | + if (id !== post.id) { |
| 377 | - // 使用组件暴露的pause方法 | 206 | + card.stopAllMedia(); |
| 378 | - if (typeof player.pause === 'function') { | ||
| 379 | - player.pause(); | ||
| 380 | - } | ||
| 381 | - }); | ||
| 382 | - // 更新所有帖子的播放状态 | ||
| 383 | - checkinDataList.value.forEach(post => { | ||
| 384 | - if (post.audio.length) { | ||
| 385 | - post.isPlaying = false; | ||
| 386 | } | 207 | } |
| 387 | }); | 208 | }); |
| 388 | } | 209 | } |
| 389 | 210 | ||
| 390 | -/** | ||
| 391 | - * 停止所有视频播放 | ||
| 392 | - */ | ||
| 393 | -const stopAllVideos = () => { | ||
| 394 | - // 确保videoPlayers.value是一个数组 | ||
| 395 | - if (!videoPlayers.value) return; | ||
| 396 | - | ||
| 397 | - // 更新所有帖子的播放状态 | ||
| 398 | - checkinDataList.value.forEach(p => { | ||
| 399 | - p.videoList.forEach(v => { | ||
| 400 | - v.isPlaying = false; | ||
| 401 | - }); | ||
| 402 | - }); | ||
| 403 | -}; | ||
| 404 | - | ||
| 405 | const themeVars = { | 211 | const themeVars = { |
| 406 | calendarSelectedDayBackground: '#4caf50', | 212 | calendarSelectedDayBackground: '#4caf50', |
| 407 | calendarHeaderShadow: 'rgba(0, 0, 0, 0.1)', | 213 | calendarHeaderShadow: 'rgba(0, 0, 0, 0.1)', |
| ... | @@ -413,22 +219,7 @@ const progress1 = ref(0); | ... | @@ -413,22 +219,7 @@ const progress1 = ref(0); |
| 413 | 219 | ||
| 414 | const teamAvatars = ref([]) | 220 | const teamAvatars = ref([]) |
| 415 | 221 | ||
| 416 | -// 图片预览相关 | ||
| 417 | -const showImagePreview = ref(false); | ||
| 418 | -const startPosition = ref(0); | ||
| 419 | -const currentPost = ref(null); | ||
| 420 | - | ||
| 421 | -// 打开图片预览 | ||
| 422 | -const openImagePreview = (index, post) => { | ||
| 423 | - currentPost.value = post; | ||
| 424 | - startPosition.value = index; | ||
| 425 | - showImagePreview.value = true; | ||
| 426 | -} | ||
| 427 | 222 | ||
| 428 | -// 图片切换事件处理 | ||
| 429 | -const onChange = (index) => { | ||
| 430 | - startPosition.value = index; | ||
| 431 | -} | ||
| 432 | /** | 223 | /** |
| 433 | * 日历日期格式化函数 | 224 | * 日历日期格式化函数 |
| 434 | * @param {Object} day - 日期对象 | 225 | * @param {Object} day - 日期对象 |
| ... | @@ -946,78 +737,7 @@ const formatData = (data) => { | ... | @@ -946,78 +737,7 @@ const formatData = (data) => { |
| 946 | } | 737 | } |
| 947 | } | 738 | } |
| 948 | 739 | ||
| 949 | -.post-card { | ||
| 950 | - margin: 1rem 0; | ||
| 951 | - padding: 1rem; | ||
| 952 | - background-color: #FFF; | ||
| 953 | - border-radius: 5px; | ||
| 954 | 740 | ||
| 955 | - .post-header { | ||
| 956 | - margin-bottom: 1rem; | ||
| 957 | - } | ||
| 958 | - | ||
| 959 | - .user-info { | ||
| 960 | - margin-left: 0.5rem; | ||
| 961 | - | ||
| 962 | - .username { | ||
| 963 | - font-weight: 500; | ||
| 964 | - } | ||
| 965 | - | ||
| 966 | - .post-time { | ||
| 967 | - color: gray; | ||
| 968 | - font-size: 0.8rem; | ||
| 969 | - } | ||
| 970 | - } | ||
| 971 | - | ||
| 972 | - .post-menu { | ||
| 973 | - display: flex; | ||
| 974 | - justify-content: space-between; | ||
| 975 | - align-items: center; | ||
| 976 | - margin-bottom: 1rem; | ||
| 977 | - } | ||
| 978 | - | ||
| 979 | - .post-content { | ||
| 980 | - .post-text { | ||
| 981 | - color: #666; | ||
| 982 | - margin-bottom: 1rem; | ||
| 983 | - white-space: pre-wrap; | ||
| 984 | - word-wrap: break-word; | ||
| 985 | - } | ||
| 986 | - | ||
| 987 | - .post-media { | ||
| 988 | - .post-images { | ||
| 989 | - display: flex; | ||
| 990 | - flex-wrap: wrap; | ||
| 991 | - gap: 0.5rem; | ||
| 992 | - } | ||
| 993 | - | ||
| 994 | - .post-video { | ||
| 995 | - margin: 1rem 0; | ||
| 996 | - width: 100%; | ||
| 997 | - border-radius: 8px; | ||
| 998 | - overflow: hidden; | ||
| 999 | - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 1000 | - } | ||
| 1001 | - | ||
| 1002 | - .post-audio { | ||
| 1003 | - margin: 1rem 0; | ||
| 1004 | - } | ||
| 1005 | - } | ||
| 1006 | - } | ||
| 1007 | - | ||
| 1008 | - .post-footer { | ||
| 1009 | - margin-top: 1rem; | ||
| 1010 | - color: #666; | ||
| 1011 | - | ||
| 1012 | - .like-icon { | ||
| 1013 | - margin-right: 0.25rem; | ||
| 1014 | - } | ||
| 1015 | - | ||
| 1016 | - .like-count { | ||
| 1017 | - font-size: 0.9rem; | ||
| 1018 | - } | ||
| 1019 | - } | ||
| 1020 | -} | ||
| 1021 | </style> | 741 | </style> |
| 1022 | 742 | ||
| 1023 | <style scoped> | 743 | <style scoped> | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-05-29 15:34:17 | 2 | * @Date: 2025-05-29 15:34:17 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-12-11 13:31:17 | 4 | + * @LastEditTime: 2025-12-11 20:54:50 |
| 5 | * @FilePath: /mlaj/src/views/teacher/checkinPage.vue | 5 | * @FilePath: /mlaj/src/views/teacher/checkinPage.vue |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | --> | 7 | --> |
| ... | @@ -72,7 +72,7 @@ | ... | @@ -72,7 +72,7 @@ |
| 72 | </div>--> | 72 | </div>--> |
| 73 | 73 | ||
| 74 | <div class="text-wrapper mt-4" style="padding: 0 1rem; color: #4caf50;"> | 74 | <div class="text-wrapper mt-4" style="padding: 0 1rem; color: #4caf50;"> |
| 75 | - <div class="text-header">学生动态</div> | 75 | + <div class="text-header mb-4">学生动态</div> |
| 76 | <van-list | 76 | <van-list |
| 77 | v-if="checkinDataList.length" | 77 | v-if="checkinDataList.length" |
| 78 | v-model:loading="loading" | 78 | v-model:loading="loading" |
| ... | @@ -81,87 +81,20 @@ | ... | @@ -81,87 +81,20 @@ |
| 81 | @load="onLoad" | 81 | @load="onLoad" |
| 82 | class="space-y-4" | 82 | class="space-y-4" |
| 83 | > | 83 | > |
| 84 | - <div class="post-card" v-for="post in checkinDataList" :key="post.id"> | 84 | + <CheckinCard |
| 85 | - <div class="post-header"> | 85 | + v-for="post in checkinDataList" |
| 86 | - <van-row> | 86 | + :key="post.id" |
| 87 | - <van-col span="4"> | 87 | + :post="post" |
| 88 | - <van-image round width="2.5rem" height="2.5rem" :src="post.user.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" fit="cover" /> | 88 | + :use-cdn-optimization="true" |
| 89 | - </van-col> | 89 | + :ref="(el) => setCheckinCardRef(el, post.id)" |
| 90 | - <van-col span="17"> | 90 | + @like="handLike" |
| 91 | - <div class="user-info"> | 91 | + @video-play="handleVideoPlay" |
| 92 | - <div class="username">{{ post.user.name }}</div> | 92 | + @audio-play="handleAudioPlay" |
| 93 | - <div class="post-time">{{ post.user.time }}</div> | 93 | + > |
| 94 | - </div> | 94 | + <template #content-top> |
| 95 | - </van-col> | 95 | + <div class="text-gray-500 font-bold text-sm mb-4">阅读与写作</div> |
| 96 | - <!-- <van-col span="3"> | 96 | + </template> |
| 97 | - <div v-if="post.is_my" class="post-menu"> | 97 | + <template #footer-right> |
| 98 | - <van-icon name="edit" @click="editCheckin(post)" /> | ||
| 99 | - <van-icon name="delete-o" @click="delCheckin(post)" /> | ||
| 100 | - </div> | ||
| 101 | - </van-col> --> | ||
| 102 | - </van-row> | ||
| 103 | - </div> | ||
| 104 | - <div class="post-content"> | ||
| 105 | - <PostCountModel :post-data="post" /> | ||
| 106 | - <div class="post-text">{{ post.content }}</div> | ||
| 107 | - <div class="post-media"> | ||
| 108 | - <div v-if="post.images.length" class="post-images"> | ||
| 109 | - <van-image width="30%" fit="cover" v-for="(image, index) in post.images" :key="index" :src="image" radius="5" | ||
| 110 | - @click="openImagePreview(index, post)" /> | ||
| 111 | - </div> | ||
| 112 | - <van-image-preview v-if="currentPost" v-model:show="showImagePreview" :images="currentPost.images" :start-position="startPosition" :show-index="true" @change="onChange" /> | ||
| 113 | - <div v-for="(v, idx) in post.videoList" :key="idx"> | ||
| 114 | - <!-- 视频封面和播放按钮 --> | ||
| 115 | - <div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden" style="aspect-ratio: 16/9; margin-bottom: 1rem;"> | ||
| 116 | - <img :src="v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png'" | ||
| 117 | - :alt="v.content" class="w-full h-full object-cover" /> | ||
| 118 | - <div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20" | ||
| 119 | - @click="startPlay(v)"> | ||
| 120 | - <div | ||
| 121 | - class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors"> | ||
| 122 | - <van-icon name="play-circle-o" class="text-white" size="40" /> | ||
| 123 | - </div> | ||
| 124 | - </div> | ||
| 125 | - </div> | ||
| 126 | - <!-- 视频播放器 --> | ||
| 127 | - <VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" class="post-video rounded-lg overflow-hidden" | ||
| 128 | - :ref="el => { | ||
| 129 | - if(el) { | ||
| 130 | - // 确保不重复添加 | ||
| 131 | - if (!videoPlayers?.includes(el)) { | ||
| 132 | - videoPlayers?.push(el); | ||
| 133 | - } | ||
| 134 | - } | ||
| 135 | - }" | ||
| 136 | - @onPlay="handleVideoPlay(player, post)" | ||
| 137 | - @onPause="handleVideoPause(post)" /> | ||
| 138 | - </div> | ||
| 139 | - <AudioPlayer | ||
| 140 | - v-if="post.audio.length" | ||
| 141 | - :songs="post.audio" | ||
| 142 | - class="post-audio" | ||
| 143 | - :id="post.id" | ||
| 144 | - :ref="el => { | ||
| 145 | - if(el) { | ||
| 146 | - // 确保不重复添加 | ||
| 147 | - if (!audioPlayers?.includes(el)) { | ||
| 148 | - audioPlayers?.push(el); | ||
| 149 | - } | ||
| 150 | - } | ||
| 151 | - }" | ||
| 152 | - @play="(player) => handleAudioPlay(player, post)" | ||
| 153 | - /> | ||
| 154 | - </div> | ||
| 155 | - </div> | ||
| 156 | - <div class="post-footer flex items-center justify-between"> | ||
| 157 | - <!-- 左侧:点赞 --> | ||
| 158 | - <div class="flex items-center"> | ||
| 159 | - <van-icon @click="handLike(post)" name="good-job" class="like-icon" :color="post.is_liked ? 'red' : ''" /> | ||
| 160 | - <span class="like-count ml-1">{{ post.likes }}</span> | ||
| 161 | - </div> | ||
| 162 | - | ||
| 163 | - <!-- 右侧:审核操作 --> | ||
| 164 | - <!-- TODO: 审核操作没有什么实质作用, 未通过用户看不到提示, 用户修改之后老师也接不到提示, 唯一作用就是让其他学员看不到 --> | ||
| 165 | <div class="flex items-center cursor-pointer" @click="openAuditDialog(post)"> | 98 | <div class="flex items-center cursor-pointer" @click="openAuditDialog(post)"> |
| 166 | <van-icon | 99 | <van-icon |
| 167 | :name="post.is_audit ? 'passed' : 'close'" | 100 | :name="post.is_audit ? 'passed' : 'close'" |
| ... | @@ -173,8 +106,8 @@ | ... | @@ -173,8 +106,8 @@ |
| 173 | {{ post.is_audit ? '已通过' : '未通过' }} | 106 | {{ post.is_audit ? '已通过' : '未通过' }} |
| 174 | </span> | 107 | </span> |
| 175 | </div> | 108 | </div> |
| 176 | - </div> | 109 | + </template> |
| 177 | - </div> | 110 | + </CheckinCard> |
| 178 | </van-list> | 111 | </van-list> |
| 179 | <van-empty v-else description="暂无数据" /> | 112 | <van-empty v-else description="暂无数据" /> |
| 180 | </div> | 113 | </div> |
| ... | @@ -193,8 +126,7 @@ import { useRoute, useRouter } from 'vue-router' | ... | @@ -193,8 +126,7 @@ import { useRoute, useRouter } from 'vue-router' |
| 193 | import { showConfirmDialog, showSuccessToast, showFailToast, showLoadingToast, showToast } from 'vant'; | 126 | import { showConfirmDialog, showSuccessToast, showFailToast, showLoadingToast, showToast } from 'vant'; |
| 194 | import AppLayout from "@/components/layout/AppLayout.vue"; | 127 | import AppLayout from "@/components/layout/AppLayout.vue"; |
| 195 | import FrostedGlass from "@/components/ui/FrostedGlass.vue"; | 128 | import FrostedGlass from "@/components/ui/FrostedGlass.vue"; |
| 196 | -import VideoPlayer from "@/components/ui/VideoPlayer.vue"; | 129 | +import CheckinCard from "@/components/checkin/CheckinCard.vue"; |
| 197 | -import AudioPlayer from "@/components/ui/AudioPlayer.vue"; | ||
| 198 | import CourseGroupCascader from '@/components/ui/CourseGroupCascader.vue' | 130 | import CourseGroupCascader from '@/components/ui/CourseGroupCascader.vue' |
| 199 | import PostCountModel from '@/components/count/postCountModel.vue' | 131 | import PostCountModel from '@/components/count/postCountModel.vue' |
| 200 | import { useTitle } from '@vueuse/core'; | 132 | import { useTitle } from '@vueuse/core'; |
| ... | @@ -374,162 +306,30 @@ onMounted(() => { | ... | @@ -374,162 +306,30 @@ onMounted(() => { |
| 374 | }); | 306 | }); |
| 375 | }); | 307 | }); |
| 376 | 308 | ||
| 377 | -// 存储所有视频播放器的引用 | 309 | +// CheckinCard refs |
| 378 | -const videoPlayers = ref([]); | 310 | +const checkinCardRefs = ref(new Map()); |
| 379 | - | 311 | +const setCheckinCardRef = (el, id) => { |
| 380 | -// 存储所有音频播放器的引用 | 312 | + if (el) checkinCardRefs.value.set(id, el); |
| 381 | -const audioPlayers = ref([]); | ||
| 382 | - | ||
| 383 | -// 组件卸载前清理播放器引用和事件监听器 | ||
| 384 | -onBeforeUnmount(() => { | ||
| 385 | - // 停止所有视频和音频播放 | ||
| 386 | - if (videoPlayers.value) { | ||
| 387 | - videoPlayers.value.forEach(player => { | ||
| 388 | - if (player && typeof player?.pause === 'function') { | ||
| 389 | - player?.pause(); | ||
| 390 | - } | ||
| 391 | - }); | ||
| 392 | - } | ||
| 393 | - | ||
| 394 | - stopAllAudio(); | ||
| 395 | - | ||
| 396 | - // 清空引用数组 | ||
| 397 | - if (videoPlayers.value) videoPlayers.value = []; | ||
| 398 | - if (audioPlayers.value) audioPlayers.value = []; | ||
| 399 | - | ||
| 400 | - // 清理事件监听器 | ||
| 401 | - window.removeEventListener('resize', handleResize); | ||
| 402 | - window.removeEventListener('orientationchange', handleResize); | ||
| 403 | -}); | ||
| 404 | - | ||
| 405 | - | ||
| 406 | -/** | ||
| 407 | - * 开始播放指定帖子的视频 | ||
| 408 | - * @param {Object} post - 要播放视频的帖子对象 | ||
| 409 | - */ | ||
| 410 | -const startPlay = (post) => { | ||
| 411 | - // 确保checkinDataList.value是一个数组 | ||
| 412 | - if (checkinDataList.value) { | ||
| 413 | - // 先暂停所有其他视频 | ||
| 414 | - checkinDataList.value.forEach(p => { | ||
| 415 | - p.videoList.forEach(v => { | ||
| 416 | - if (v.id !== post.id) { | ||
| 417 | - v.isPlaying = false; | ||
| 418 | - } | ||
| 419 | - }); | ||
| 420 | - }); | ||
| 421 | - } | ||
| 422 | - | ||
| 423 | - // 设置当前视频为播放状态 | ||
| 424 | - post.isPlaying = true; | ||
| 425 | -}; | ||
| 426 | - | ||
| 427 | -/** | ||
| 428 | - * 处理视频播放事件 | ||
| 429 | - * @param {Object} player - 视频播放器实例 | ||
| 430 | - * @param {Object} post - 包含视频的帖子对象 | ||
| 431 | - */ | ||
| 432 | -const handleVideoPlay = (player, post) => { | ||
| 433 | - stopAllAudio(); | ||
| 434 | -}; | ||
| 435 | - | ||
| 436 | -/** | ||
| 437 | - * 处理视频暂停事件 | ||
| 438 | - * @param {Object} post - 包含视频的帖子对象 | ||
| 439 | - */ | ||
| 440 | -const handleVideoPause = (post) => { | ||
| 441 | - // 视频暂停时不改变isPlaying状态,保持播放器可见 | ||
| 442 | - // 这样用户可以继续从暂停处播放 | ||
| 443 | -}; | ||
| 444 | - | ||
| 445 | -/** | ||
| 446 | - * 停止除当前播放器外的所有其他视频 | ||
| 447 | - * @param {Object} currentPlayer - 当前播放的视频播放器实例 | ||
| 448 | - * @param {Object} currentPost - 当前播放的帖子对象 | ||
| 449 | - */ | ||
| 450 | -const stopOtherVideos = (currentPlayer, currentPost) => { | ||
| 451 | - // 确保videoPlayers.value是一个数组 | ||
| 452 | - if (videoPlayers.value) { | ||
| 453 | - // 暂停其他视频播放器 | ||
| 454 | - videoPlayers.value.forEach(player => { | ||
| 455 | - if (player !== currentPlayer && player.pause) { | ||
| 456 | - player.pause(); | ||
| 457 | - } | ||
| 458 | - }); | ||
| 459 | - } | ||
| 460 | - | ||
| 461 | - // 更新其他帖子的播放状态 | ||
| 462 | - checkinDataList.value.forEach(p => { | ||
| 463 | - p.videoList.forEach(v => { | ||
| 464 | - if (v.id !== currentPost.id) { | ||
| 465 | - v.isPlaying = false; | ||
| 466 | - } | ||
| 467 | - }); | ||
| 468 | - }); | ||
| 469 | }; | 313 | }; |
| 470 | 314 | ||
| 471 | -/** | 315 | +// 视频播放事件处理 |
| 472 | - * 处理音频播放事件 | 316 | +const handleVideoPlay = ({ post, player, videoId }) => { |
| 473 | - * @param {Object} player - 音频播放器实例 | 317 | + checkinCardRefs.value.forEach((card, id) => { |
| 474 | - * @param {Object} post - 包含音频的帖子对象 | 318 | + if (id !== post.id) { |
| 475 | - */ | 319 | + card.stopAllMedia(); |
| 476 | -const handleAudioPlay = (player, post) => { | ||
| 477 | - // 停止其他音频播放 | ||
| 478 | - stopOtherAudio(player, post); | ||
| 479 | -}; | ||
| 480 | - | ||
| 481 | -const stopOtherAudio = (currentPlayer, currentPost) => { | ||
| 482 | - // 确保audioPlayers.value是一个数组 | ||
| 483 | - if (audioPlayers.value) { | ||
| 484 | - // 暂停其他音频播放器 | ||
| 485 | - audioPlayers.value.forEach(player => { | ||
| 486 | - if (player.id!== currentPost.id && player.pause) { | ||
| 487 | - player.pause(); | ||
| 488 | - } | ||
| 489 | - }); | ||
| 490 | - } | ||
| 491 | - // 更新其他帖子的播放状态 | ||
| 492 | - checkinDataList.value.forEach(post => { | ||
| 493 | - if (post.id!== currentPost.id) { | ||
| 494 | - post.isPlaying = false; | ||
| 495 | } | 320 | } |
| 496 | }); | 321 | }); |
| 497 | - // 停止所有视频播放 | ||
| 498 | - stopAllVideos(); | ||
| 499 | } | 322 | } |
| 500 | 323 | ||
| 501 | -const stopAllAudio = () => { | 324 | +// 音频播放事件处理 |
| 502 | - // 确保audioPlayers.value是一个数组 | 325 | +const handleAudioPlay = ({ post, player }) => { |
| 503 | - if (!audioPlayers.value) return; | 326 | + checkinCardRefs.value.forEach((card, id) => { |
| 504 | - audioPlayers.value?.forEach(player => { | 327 | + if (id !== post.id) { |
| 505 | - // 使用组件暴露的pause方法 | 328 | + card.stopAllMedia(); |
| 506 | - if (typeof player.pause === 'function') { | ||
| 507 | - player.pause(); | ||
| 508 | - } | ||
| 509 | - }); | ||
| 510 | - // 更新所有帖子的播放状态 | ||
| 511 | - checkinDataList.value.forEach(post => { | ||
| 512 | - if (post.audio.length) { | ||
| 513 | - post.isPlaying = false; | ||
| 514 | } | 329 | } |
| 515 | }); | 330 | }); |
| 516 | } | 331 | } |
| 517 | 332 | ||
| 518 | -/** | ||
| 519 | - * 停止所有视频播放 | ||
| 520 | - */ | ||
| 521 | -const stopAllVideos = () => { | ||
| 522 | - // 确保videoPlayers.value是一个数组 | ||
| 523 | - if (!videoPlayers.value) return; | ||
| 524 | - | ||
| 525 | - // 更新所有帖子的播放状态 | ||
| 526 | - checkinDataList.value.forEach(p => { | ||
| 527 | - p.videoList.forEach(v => { | ||
| 528 | - v.isPlaying = false; | ||
| 529 | - }); | ||
| 530 | - }); | ||
| 531 | -}; | ||
| 532 | - | ||
| 533 | const themeVars = reactive({ | 333 | const themeVars = reactive({ |
| 534 | calendarSelectedDayBackground: '#4caf50', | 334 | calendarSelectedDayBackground: '#4caf50', |
| 535 | calendarHeaderShadow: 'rgba(0, 0, 0, 0.1)', | 335 | calendarHeaderShadow: 'rgba(0, 0, 0, 0.1)', |
| ... | @@ -542,22 +342,7 @@ const progress1 = ref(0); | ... | @@ -542,22 +342,7 @@ const progress1 = ref(0); |
| 542 | 342 | ||
| 543 | const teamAvatars = ref([]) | 343 | const teamAvatars = ref([]) |
| 544 | 344 | ||
| 545 | -// 图片预览相关 | ||
| 546 | -const showImagePreview = ref(false); | ||
| 547 | -const startPosition = ref(0); | ||
| 548 | -const currentPost = ref(null); | ||
| 549 | - | ||
| 550 | -// 打开图片预览 | ||
| 551 | -const openImagePreview = (index, post) => { | ||
| 552 | - currentPost.value = post; | ||
| 553 | - startPosition.value = index; | ||
| 554 | - showImagePreview.value = true; | ||
| 555 | -} | ||
| 556 | 345 | ||
| 557 | -// 图片切换事件处理 | ||
| 558 | -const onChange = (index) => { | ||
| 559 | - startPosition.value = index; | ||
| 560 | -} | ||
| 561 | const formatter = (day) => { | 346 | const formatter = (day) => { |
| 562 | const year = day.date.getFullYear(); | 347 | const year = day.date.getFullYear(); |
| 563 | const month = day.date.getMonth() + 1; | 348 | const month = day.date.getMonth() + 1; |
| ... | @@ -983,76 +768,5 @@ const handleAdd = (type) => { | ... | @@ -983,76 +768,5 @@ const handleAdd = (type) => { |
| 983 | } | 768 | } |
| 984 | } | 769 | } |
| 985 | 770 | ||
| 986 | -.post-card { | ||
| 987 | - margin: 1rem 0; | ||
| 988 | - padding: 1rem; | ||
| 989 | - background-color: #FFF; | ||
| 990 | - border-radius: 5px; | ||
| 991 | - | ||
| 992 | - .post-header { | ||
| 993 | - margin-bottom: 1rem; | ||
| 994 | - } | ||
| 995 | - | ||
| 996 | - .user-info { | ||
| 997 | - margin-left: 0.5rem; | ||
| 998 | - | ||
| 999 | - .username { | ||
| 1000 | - font-weight: 500; | ||
| 1001 | - } | ||
| 1002 | - | ||
| 1003 | - .post-time { | ||
| 1004 | - color: gray; | ||
| 1005 | - font-size: 0.8rem; | ||
| 1006 | - } | ||
| 1007 | - } | ||
| 1008 | - | ||
| 1009 | - .post-menu { | ||
| 1010 | - display: flex; | ||
| 1011 | - justify-content: space-between; | ||
| 1012 | - align-items: center; | ||
| 1013 | - margin-bottom: 1rem; | ||
| 1014 | - } | ||
| 1015 | - | ||
| 1016 | - .post-content { | ||
| 1017 | - .post-text { | ||
| 1018 | - color: #666; | ||
| 1019 | - margin-bottom: 1rem; | ||
| 1020 | - white-space: pre-wrap; | ||
| 1021 | - word-wrap: break-word; | ||
| 1022 | - } | ||
| 1023 | - | ||
| 1024 | - .post-media { | ||
| 1025 | - .post-images { | ||
| 1026 | - display: flex; | ||
| 1027 | - flex-wrap: wrap; | ||
| 1028 | - gap: 0.5rem; | ||
| 1029 | - } | ||
| 1030 | - | ||
| 1031 | - .post-video { | ||
| 1032 | - margin: 1rem 0; | ||
| 1033 | - width: 100%; | ||
| 1034 | - border-radius: 8px; | ||
| 1035 | - overflow: hidden; | ||
| 1036 | - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 1037 | - } | ||
| 1038 | - | ||
| 1039 | - .post-audio { | ||
| 1040 | - margin: 1rem 0; | ||
| 1041 | - } | ||
| 1042 | - } | ||
| 1043 | - } | ||
| 1044 | - | ||
| 1045 | - .post-footer { | ||
| 1046 | - margin-top: 1rem; | ||
| 1047 | - color: #666; | ||
| 1048 | 771 | ||
| 1049 | - .like-icon { | ||
| 1050 | - margin-right: 0.25rem; | ||
| 1051 | - } | ||
| 1052 | - | ||
| 1053 | - .like-count { | ||
| 1054 | - font-size: 0.9rem; | ||
| 1055 | - } | ||
| 1056 | - } | ||
| 1057 | -} | ||
| 1058 | </style> | 772 | </style> | ... | ... |
| ... | @@ -2,7 +2,7 @@ | ... | @@ -2,7 +2,7 @@ |
| 2 | * @Author: hookehuyr hookehuyr@gmail.com | 2 | * @Author: hookehuyr hookehuyr@gmail.com |
| 3 | * @Date: 2025-06-19 17:12:19 | 3 | * @Date: 2025-06-19 17:12:19 |
| 4 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 4 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 5 | - * @LastEditTime: 2025-12-11 13:33:01 | 5 | + * @LastEditTime: 2025-12-11 20:55:35 |
| 6 | * @FilePath: /mlaj/src/views/teacher/studentPage.vue | 6 | * @FilePath: /mlaj/src/views/teacher/studentPage.vue |
| 7 | * @Description: 学生详情页面 | 7 | * @Description: 学生详情页面 |
| 8 | --> | 8 | --> |
| ... | @@ -187,76 +187,20 @@ | ... | @@ -187,76 +187,20 @@ |
| 187 | <!--作业记录 --> | 187 | <!--作业记录 --> |
| 188 | <van-list v-show="activeTab === 'homework' && checkinDataList.length" v-model:loading="loading" | 188 | <van-list v-show="activeTab === 'homework' && checkinDataList.length" v-model:loading="loading" |
| 189 | :finished="finished" finished-text="没有更多了" @load="onLoad" class="space-y-4 px-4"> | 189 | :finished="finished" finished-text="没有更多了" @load="onLoad" class="space-y-4 px-4"> |
| 190 | - <div class="post-card shadow-md" v-for="post in checkinDataList" :key="post.id"> | 190 | + <CheckinCard |
| 191 | - <div class="post-header"> | 191 | + v-for="post in checkinDataList" |
| 192 | - <van-row> | 192 | + :key="post.id" |
| 193 | - <van-col span="4"> | 193 | + :post="post" |
| 194 | - <van-image round width="2.5rem" height="2.5rem" | 194 | + :use-cdn-optimization="true" |
| 195 | - :src="post.user.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" fit="cover" /> | 195 | + :ref="(el) => setCheckinCardRef(el, post.id)" |
| 196 | - </van-col> | 196 | + @like="handLike(post)" |
| 197 | - <van-col span="17"> | 197 | + @video-play="handleVideoPlay" |
| 198 | - <div class="user-info"> | 198 | + @audio-play="handleAudioPlay" |
| 199 | - <div class="username">{{ post.user.name }}</div> | 199 | + > |
| 200 | - <div class="post-time">{{ post.user.time }}</div> | 200 | + <template #content-top> |
| 201 | - </div> | 201 | + <div class="text-gray-500 font-bold text-sm mb-4">阅读与写作</div> |
| 202 | - </van-col> | 202 | + </template> |
| 203 | - <van-col span="3"> | 203 | + <template #footer-right> |
| 204 | - </van-col> | ||
| 205 | - </van-row> | ||
| 206 | - </div> | ||
| 207 | - <div class="post-content"> | ||
| 208 | - <PostCountModel :post-data="post" /> | ||
| 209 | - <div class="post-text">{{ post.content }}</div> | ||
| 210 | - <div class="post-media"> | ||
| 211 | - <div v-if="post.images.length" class="post-images"> | ||
| 212 | - <van-image width="30%" fit="cover" v-for="(image, index) in post.images" :key="index" :src="image" | ||
| 213 | - radius="5" @click="openImagePreview(index, post)" /> | ||
| 214 | - </div> | ||
| 215 | - <van-image-preview v-if="currentPost" v-model:show="showImagePreview" :images="currentPost.images" | ||
| 216 | - :start-position="startPosition" :show-index="true" @change="onChange" /> | ||
| 217 | - <div v-for="(v, idx) in post.videoList" :key="idx"> | ||
| 218 | - <!-- 视频封面和播放按钮 --> | ||
| 219 | - <div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden" | ||
| 220 | - style="aspect-ratio: 16/9; margin-bottom: 1rem;"> | ||
| 221 | - <img :src="v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png'" :alt="v.content" | ||
| 222 | - class="w-full h-full object-cover" /> | ||
| 223 | - <div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20" | ||
| 224 | - @click="startPlay(v)"> | ||
| 225 | - <div | ||
| 226 | - class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors"> | ||
| 227 | - <van-icon name="play-circle-o" class="text-white" size="40" /> | ||
| 228 | - </div> | ||
| 229 | - </div> | ||
| 230 | - </div> | ||
| 231 | - <!-- 视频播放器 --> | ||
| 232 | - <VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" | ||
| 233 | - class="post-video rounded-lg overflow-hidden" :ref="el => { | ||
| 234 | - if (el) { | ||
| 235 | - // 确保不重复添加 | ||
| 236 | - if (!videoPlayers?.includes(el)) { | ||
| 237 | - videoPlayers?.push(el); | ||
| 238 | - } | ||
| 239 | - } | ||
| 240 | - }" @onPlay="handleVideoPlay(player, post)" @onPause="handleVideoPause(post)" /> | ||
| 241 | - </div> | ||
| 242 | - <AudioPlayer v-if="post.audio.length" :songs="post.audio" class="post-audio" :id="post.id" :ref="el => { | ||
| 243 | - if (el) { | ||
| 244 | - // 确保不重复添加 | ||
| 245 | - if (!audioPlayers?.includes(el)) { | ||
| 246 | - audioPlayers?.push(el); | ||
| 247 | - } | ||
| 248 | - } | ||
| 249 | - }" @play="(player) => handleAudioPlay(player, post)" /> | ||
| 250 | - </div> | ||
| 251 | - </div> | ||
| 252 | - <div class="post-footer flex items-center justify-between"> | ||
| 253 | - <!-- 左侧:点赞 --> | ||
| 254 | - <div class="flex items-center"> | ||
| 255 | - <van-icon @click="handLike(post)" name="good-job" class="like-icon" :color="post.is_liked ? 'red' : ''" /> | ||
| 256 | - <span class="like-count ml-1">{{ post.likes }}</span> | ||
| 257 | - </div> | ||
| 258 | - | ||
| 259 | - <!-- 右侧:点评 --> | ||
| 260 | <div class="flex items-center cursor-pointer" @click="openCommentPopup(post)"> | 204 | <div class="flex items-center cursor-pointer" @click="openCommentPopup(post)"> |
| 261 | <van-icon | 205 | <van-icon |
| 262 | name="comment-o" | 206 | name="comment-o" |
| ... | @@ -269,8 +213,8 @@ | ... | @@ -269,8 +213,8 @@ |
| 269 | {{ post?.is_feedback ? '已点评' : '待点评' }} | 213 | {{ post?.is_feedback ? '已点评' : '待点评' }} |
| 270 | </span> | 214 | </span> |
| 271 | </div> | 215 | </div> |
| 272 | - </div> | 216 | + </template> |
| 273 | - </div> | 217 | + </CheckinCard> |
| 274 | </van-list> | 218 | </van-list> |
| 275 | <van-empty v-show="activeTab === 'homework' && !checkinDataList.length" description="暂无数据" /> | 219 | <van-empty v-show="activeTab === 'homework' && !checkinDataList.length" description="暂无数据" /> |
| 276 | <div style="height: 5rem;"></div> | 220 | <div style="height: 5rem;"></div> |
| ... | @@ -376,11 +320,10 @@ | ... | @@ -376,11 +320,10 @@ |
| 376 | </template> | 320 | </template> |
| 377 | 321 | ||
| 378 | <script setup> | 322 | <script setup> |
| 379 | -import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue' | 323 | +import { ref, computed, onMounted, onBeforeUnmount, nextTick, reactive } from 'vue' |
| 380 | import { useRouter, useRoute } from 'vue-router' | 324 | import { useRouter, useRoute } from 'vue-router' |
| 381 | import { showConfirmDialog, showSuccessToast, showFailToast, showLoadingToast } from 'vant'; | 325 | import { showConfirmDialog, showSuccessToast, showFailToast, showLoadingToast } from 'vant'; |
| 382 | -import VideoPlayer from "@/components/ui/VideoPlayer.vue"; | 326 | +import CheckinCard from "@/components/checkin/CheckinCard.vue"; |
| 383 | -import AudioPlayer from "@/components/ui/AudioPlayer.vue"; | ||
| 384 | import PostCountModel from '@/components/count/postCountModel.vue' | 327 | import PostCountModel from '@/components/count/postCountModel.vue' |
| 385 | import { useTitle } from '@vueuse/core'; | 328 | import { useTitle } from '@vueuse/core'; |
| 386 | import dayjs from 'dayjs'; | 329 | import dayjs from 'dayjs'; |
| ... | @@ -712,15 +655,11 @@ const handleTabChange = (name) => { | ... | @@ -712,15 +655,11 @@ const handleTabChange = (name) => { |
| 712 | 655 | ||
| 713 | nextTick(() => { | 656 | nextTick(() => { |
| 714 | // 停止所有视频和音频播放 | 657 | // 停止所有视频和音频播放 |
| 715 | - if (videoPlayers.value) { | 658 | + checkinCardRefs.value.forEach(card => { |
| 716 | - videoPlayers.value.forEach(player => { | 659 | + if (card) { |
| 717 | - if (player && typeof player?.pause === 'function') { | 660 | + card.stopAllMedia(); |
| 718 | - player?.pause(); | 661 | + } |
| 719 | - } | 662 | + }); |
| 720 | - }); | ||
| 721 | - } | ||
| 722 | - | ||
| 723 | - stopAllAudio(); | ||
| 724 | }) | 663 | }) |
| 725 | 664 | ||
| 726 | if (name === 'homework') { | 665 | if (name === 'homework') { |
| ... | @@ -732,174 +671,54 @@ const handleTabChange = (name) => { | ... | @@ -732,174 +671,54 @@ const handleTabChange = (name) => { |
| 732 | } | 671 | } |
| 733 | }; | 672 | }; |
| 734 | 673 | ||
| 735 | -// 存储所有视频播放器的引用 | 674 | +// 存储 CheckinCard 组件引用的 Map |
| 736 | -const videoPlayers = ref([]); | 675 | +const checkinCardRefs = ref(new Map()); |
| 737 | 676 | ||
| 738 | -// 存储所有音频播放器的引用 | 677 | +// 设置 CheckinCard 引用 |
| 739 | -const audioPlayers = ref([]); | 678 | +const setCheckinCardRef = (el, id) => { |
| 679 | + if (el) { | ||
| 680 | + checkinCardRefs.value.set(id, el); | ||
| 681 | + } else { | ||
| 682 | + checkinCardRefs.value.delete(id); | ||
| 683 | + } | ||
| 684 | +}; | ||
| 740 | 685 | ||
| 741 | // 组件卸载前清理播放器引用和事件监听器 | 686 | // 组件卸载前清理播放器引用和事件监听器 |
| 742 | onBeforeUnmount(() => { | 687 | onBeforeUnmount(() => { |
| 743 | // 停止所有视频和音频播放 | 688 | // 停止所有视频和音频播放 |
| 744 | - if (videoPlayers.value) { | 689 | + checkinCardRefs.value.forEach(card => { |
| 745 | - videoPlayers.value.forEach(player => { | 690 | + if (card) { |
| 746 | - if (player && typeof player?.pause === 'function') { | 691 | + card.stopAllMedia(); |
| 747 | - player?.pause(); | 692 | + } |
| 748 | - } | 693 | + }); |
| 749 | - }); | ||
| 750 | - } | ||
| 751 | - | ||
| 752 | - stopAllAudio(); | ||
| 753 | 694 | ||
| 754 | - // 清空引用数组 | 695 | + checkinCardRefs.value.clear(); |
| 755 | - if (videoPlayers.value) videoPlayers.value = []; | ||
| 756 | - if (audioPlayers.value) audioPlayers.value = []; | ||
| 757 | }); | 696 | }); |
| 758 | 697 | ||
| 759 | /** | 698 | /** |
| 760 | - * 开始播放指定帖子的视频 | ||
| 761 | - * @param {Object} post - 要播放视频的帖子对象 | ||
| 762 | - */ | ||
| 763 | -const startPlay = (post) => { | ||
| 764 | - // 确保checkinDataList.value是一个数组 | ||
| 765 | - if (checkinDataList.value) { | ||
| 766 | - // 先暂停所有其他视频 | ||
| 767 | - checkinDataList.value.forEach(p => { | ||
| 768 | - p.videoList.forEach(v => { | ||
| 769 | - if (v.id !== post.id) { | ||
| 770 | - v.isPlaying = false; | ||
| 771 | - } | ||
| 772 | - }); | ||
| 773 | - }); | ||
| 774 | - } | ||
| 775 | - | ||
| 776 | - // 设置当前视频为播放状态 | ||
| 777 | - post.isPlaying = true; | ||
| 778 | -}; | ||
| 779 | - | ||
| 780 | -/** | ||
| 781 | * 处理视频播放事件 | 699 | * 处理视频播放事件 |
| 782 | - * @param {Object} player - 视频播放器实例 | ||
| 783 | - * @param {Object} post - 包含视频的帖子对象 | ||
| 784 | - */ | ||
| 785 | -const handleVideoPlay = (player, post) => { | ||
| 786 | - stopAllAudio(); | ||
| 787 | -}; | ||
| 788 | - | ||
| 789 | -/** | ||
| 790 | - * 处理视频暂停事件 | ||
| 791 | - * @param {Object} post - 包含视频的帖子对象 | ||
| 792 | */ | 700 | */ |
| 793 | -const handleVideoPause = (post) => { | 701 | +const handleVideoPlay = (id) => { |
| 794 | - // 视频暂停时不改变isPlaying状态,保持播放器可见 | 702 | + // 暂停其他所有卡片的媒体播放 |
| 795 | - // 这样用户可以继续从暂停处播放 | 703 | + checkinCardRefs.value.forEach((card, key) => { |
| 796 | -}; | 704 | + if (key !== id && card) { |
| 797 | - | 705 | + card.stopAllMedia(); |
| 798 | -/** | 706 | + } |
| 799 | - * 停止除当前播放器外的所有其他视频 | ||
| 800 | - * @param {Object} currentPlayer - 当前播放的视频播放器实例 | ||
| 801 | - * @param {Object} currentPost - 当前播放的帖子对象 | ||
| 802 | - */ | ||
| 803 | -const stopOtherVideos = (currentPlayer, currentPost) => { | ||
| 804 | - // 确保videoPlayers.value是一个数组 | ||
| 805 | - if (videoPlayers.value) { | ||
| 806 | - // 暂停其他视频播放器 | ||
| 807 | - videoPlayers.value.forEach(player => { | ||
| 808 | - if (player !== currentPlayer && player.pause) { | ||
| 809 | - player.pause(); | ||
| 810 | - } | ||
| 811 | - }); | ||
| 812 | - } | ||
| 813 | - | ||
| 814 | - // 更新其他帖子的播放状态 | ||
| 815 | - checkinDataList.value.forEach(p => { | ||
| 816 | - p.videoList.forEach(v => { | ||
| 817 | - if (v.id !== currentPost.id) { | ||
| 818 | - v.isPlaying = false; | ||
| 819 | - } | ||
| 820 | - }); | ||
| 821 | }); | 707 | }); |
| 822 | }; | 708 | }; |
| 823 | 709 | ||
| 824 | /** | 710 | /** |
| 825 | * 处理音频播放事件 | 711 | * 处理音频播放事件 |
| 826 | - * @param {Object} player - 音频播放器实例 | ||
| 827 | - * @param {Object} post - 包含音频的帖子对象 | ||
| 828 | */ | 712 | */ |
| 829 | -const handleAudioPlay = (player, post) => { | 713 | +const handleAudioPlay = (id) => { |
| 830 | - // 停止其他音频播放 | 714 | + // 暂停其他所有卡片的媒体播放 |
| 831 | - stopOtherAudio(player, post); | 715 | + checkinCardRefs.value.forEach((card, key) => { |
| 832 | -}; | 716 | + if (key !== id && card) { |
| 833 | - | 717 | + card.stopAllMedia(); |
| 834 | -const stopOtherAudio = (currentPlayer, currentPost) => { | ||
| 835 | - // 确保audioPlayers.value是一个数组 | ||
| 836 | - if (audioPlayers.value) { | ||
| 837 | - // 暂停其他音频播放器 | ||
| 838 | - audioPlayers.value.forEach(player => { | ||
| 839 | - if (player.id !== currentPost.id && player.pause) { | ||
| 840 | - player.pause(); | ||
| 841 | - } | ||
| 842 | - }); | ||
| 843 | - } | ||
| 844 | - // 更新其他帖子的播放状态 | ||
| 845 | - checkinDataList.value.forEach(post => { | ||
| 846 | - if (post.id !== currentPost.id) { | ||
| 847 | - post.isPlaying = false; | ||
| 848 | } | 718 | } |
| 849 | }); | 719 | }); |
| 850 | - // 停止所有视频播放 | ||
| 851 | - stopAllVideos(); | ||
| 852 | -} | ||
| 853 | - | ||
| 854 | -const stopAllAudio = () => { | ||
| 855 | - // 确保audioPlayers.value是一个数组 | ||
| 856 | - if (!audioPlayers.value) return; | ||
| 857 | - audioPlayers.value?.forEach(player => { | ||
| 858 | - // 使用组件暴露的pause方法 | ||
| 859 | - if (typeof player.pause === 'function') { | ||
| 860 | - player?.pause(); | ||
| 861 | - } | ||
| 862 | - }); | ||
| 863 | - // 更新所有帖子的播放状态 | ||
| 864 | - checkinDataList.value.forEach(post => { | ||
| 865 | - if (post.audio.length) { | ||
| 866 | - post.isPlaying = false; | ||
| 867 | - } | ||
| 868 | - }); | ||
| 869 | -} | ||
| 870 | - | ||
| 871 | -/** | ||
| 872 | - * 停止所有视频播放 | ||
| 873 | - */ | ||
| 874 | -const stopAllVideos = () => { | ||
| 875 | - // 确保videoPlayers.value是一个数组 | ||
| 876 | - if (!videoPlayers.value) return; | ||
| 877 | - | ||
| 878 | - // 更新所有帖子的播放状态 | ||
| 879 | - checkinDataList.value.forEach(p => { | ||
| 880 | - p.videoList.forEach(v => { | ||
| 881 | - v.isPlaying = false; | ||
| 882 | - }); | ||
| 883 | - }); | ||
| 884 | }; | 720 | }; |
| 885 | 721 | ||
| 886 | -// 图片预览相关 | ||
| 887 | -const showImagePreview = ref(false); | ||
| 888 | -const startPosition = ref(0); | ||
| 889 | -const currentPost = ref(null); | ||
| 890 | - | ||
| 891 | -// 打开图片预览 | ||
| 892 | -const openImagePreview = (index, post) => { | ||
| 893 | - currentPost.value = post; | ||
| 894 | - startPosition.value = index; | ||
| 895 | - showImagePreview.value = true; | ||
| 896 | -} | ||
| 897 | - | ||
| 898 | -// 图片切换事件处理 | ||
| 899 | -const onChange = (index) => { | ||
| 900 | - startPosition.value = index; | ||
| 901 | -} | ||
| 902 | - | ||
| 903 | const handLike = async (post) => { | 722 | const handLike = async (post) => { |
| 904 | if (!post.is_liked) { | 723 | if (!post.is_liked) { |
| 905 | const { code, data } = await likeUploadTaskInfoAPI({ checkin_id: post.id, }) | 724 | const { code, data } = await likeUploadTaskInfoAPI({ checkin_id: post.id, }) |
| ... | @@ -1078,79 +897,6 @@ const getStatList = async () => { | ... | @@ -1078,79 +897,6 @@ const getStatList = async () => { |
| 1078 | border-bottom-width: 2px; | 897 | border-bottom-width: 2px; |
| 1079 | } | 898 | } |
| 1080 | 899 | ||
| 1081 | -.post-card { | ||
| 1082 | - // margin: 1rem 0; | ||
| 1083 | - padding: 1rem; | ||
| 1084 | - background-color: #FFF; | ||
| 1085 | - border-radius: 5px; | ||
| 1086 | - | ||
| 1087 | - .post-header { | ||
| 1088 | - margin-bottom: 1rem; | ||
| 1089 | - } | ||
| 1090 | - | ||
| 1091 | - .user-info { | ||
| 1092 | - margin-left: 0.5rem; | ||
| 1093 | - | ||
| 1094 | - .username { | ||
| 1095 | - font-weight: 500; | ||
| 1096 | - } | ||
| 1097 | - | ||
| 1098 | - .post-time { | ||
| 1099 | - color: gray; | ||
| 1100 | - font-size: 0.8rem; | ||
| 1101 | - } | ||
| 1102 | - } | ||
| 1103 | - | ||
| 1104 | - .post-menu { | ||
| 1105 | - display: flex; | ||
| 1106 | - justify-content: space-between; | ||
| 1107 | - align-items: center; | ||
| 1108 | - margin-bottom: 1rem; | ||
| 1109 | - } | ||
| 1110 | - | ||
| 1111 | - .post-content { | ||
| 1112 | - .post-text { | ||
| 1113 | - color: #666; | ||
| 1114 | - margin-bottom: 1rem; | ||
| 1115 | - white-space: pre-wrap; | ||
| 1116 | - word-wrap: break-word; | ||
| 1117 | - } | ||
| 1118 | - | ||
| 1119 | - .post-media { | ||
| 1120 | - .post-images { | ||
| 1121 | - display: flex; | ||
| 1122 | - flex-wrap: wrap; | ||
| 1123 | - gap: 0.5rem; | ||
| 1124 | - } | ||
| 1125 | - | ||
| 1126 | - .post-video { | ||
| 1127 | - margin: 1rem 0; | ||
| 1128 | - width: 100%; | ||
| 1129 | - border-radius: 8px; | ||
| 1130 | - overflow: hidden; | ||
| 1131 | - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 1132 | - } | ||
| 1133 | - | ||
| 1134 | - .post-audio { | ||
| 1135 | - margin: 1rem 0; | ||
| 1136 | - } | ||
| 1137 | - } | ||
| 1138 | - } | ||
| 1139 | - | ||
| 1140 | - .post-footer { | ||
| 1141 | - margin-top: 1rem; | ||
| 1142 | - color: #666; | ||
| 1143 | - | ||
| 1144 | - .like-icon { | ||
| 1145 | - margin-right: 0.25rem; | ||
| 1146 | - } | ||
| 1147 | - | ||
| 1148 | - .like-count { | ||
| 1149 | - font-size: 0.9rem; | ||
| 1150 | - } | ||
| 1151 | - } | ||
| 1152 | -} | ||
| 1153 | - | ||
| 1154 | /* 点评弹窗样式 */ | 900 | /* 点评弹窗样式 */ |
| 1155 | .comment-popup { | 901 | .comment-popup { |
| 1156 | .van-popup { | 902 | .van-popup { | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-11-19 22:05:00 | 2 | * @Date: 2025-11-19 22:05:00 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-12-11 17:30:04 | 4 | + * @LastEditTime: 2025-12-11 20:56:02 |
| 5 | * @FilePath: /mlaj/src/views/teacher/studentRecordPage.vue | 5 | * @FilePath: /mlaj/src/views/teacher/studentRecordPage.vue |
| 6 | * @Description: 学生作业记录页面(仅作业记录与点评功能),固定 user_id 与 group_id | 6 | * @Description: 学生作业记录页面(仅作业记录与点评功能),固定 user_id 与 group_id |
| 7 | --> | 7 | --> |
| ... | @@ -9,77 +9,28 @@ | ... | @@ -9,77 +9,28 @@ |
| 9 | <div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen"> | 9 | <div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen"> |
| 10 | <!-- 作业记录列表 --> | 10 | <!-- 作业记录列表 --> |
| 11 | <van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad" class="space-y-4 p-4"> | 11 | <van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad" class="space-y-4 p-4"> |
| 12 | - <div class="post-card shadow-md" v-for="post in checkinDataList" :key="post.id"> | 12 | + <CheckinCard |
| 13 | - <div class="post-header"> | 13 | + v-for="post in checkinDataList" |
| 14 | - <van-row> | 14 | + :key="post.id" |
| 15 | - <van-col span="4"> | 15 | + :post="post" |
| 16 | - <van-image round width="2.5rem" height="2.5rem" | 16 | + :use-cdn-optimization="true" |
| 17 | - :src="optimizeCdn(post.user.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg')" fit="cover" /> | 17 | + :ref="(el) => setCheckinCardRef(el, post.id)" |
| 18 | - </van-col> | 18 | + @like="handLike(post)" |
| 19 | - <van-col span="17"> | 19 | + @video-play="handleVideoPlay" |
| 20 | - <div class="user-info"> | 20 | + @audio-play="handleAudioPlay" |
| 21 | - <div class="username">{{ post.user.name }}</div> | 21 | + > |
| 22 | - <div class="post-time">{{ post.user.time }}</div> | 22 | + <template #content-top> |
| 23 | - </div> | 23 | + <div class="text-gray-500 font-bold text-sm mb-4">阅读与写作</div> |
| 24 | - </van-col> | 24 | + </template> |
| 25 | - <van-col span="3"> | 25 | + <template #footer-right> |
| 26 | - </van-col> | ||
| 27 | - </van-row> | ||
| 28 | - </div> | ||
| 29 | - <div class="post-content"> | ||
| 30 | - <PostCountModel :post-data="post" /> | ||
| 31 | - <div class="post-text">{{ post.content }}</div> | ||
| 32 | - <div class="post-media"> | ||
| 33 | - <div v-if="post.images.length" class="post-images"> | ||
| 34 | - <van-image width="30%" fit="cover" v-for="(image, index) in post.images" :key="index" :src="optimizeCdn(image)" | ||
| 35 | - radius="5" @click="openImagePreview(index, post)" /> | ||
| 36 | - </div> | ||
| 37 | - <van-image-preview v-if="currentPost" v-model:show="showImagePreview" :images="currentPost.images" | ||
| 38 | - :start-position="startPosition" :show-index="true" @change="onChange" /> | ||
| 39 | - <div v-for="(v, idx) in post.videoList" :key="idx"> | ||
| 40 | - <!-- 视频封面和播放按钮 --> | ||
| 41 | - <div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden" style="aspect-ratio: 16/9; margin-bottom: 1rem;"> | ||
| 42 | - <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" /> | ||
| 43 | - <div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20" @click="startPlay(v)"> | ||
| 44 | - <div class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors"> | ||
| 45 | - <van-icon name="play-circle-o" class="text-white" size="40" /> | ||
| 46 | - </div> | ||
| 47 | - </div> | ||
| 48 | - </div> | ||
| 49 | - <!-- 视频播放器 --> | ||
| 50 | - <VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" class="post-video rounded-lg overflow-hidden" :ref="el => { | ||
| 51 | - if (el) { | ||
| 52 | - if (!videoPlayers?.includes(el)) { | ||
| 53 | - videoPlayers?.push(el); | ||
| 54 | - } | ||
| 55 | - } | ||
| 56 | - }" @onPlay="handleVideoPlay(player, post)" @onPause="handleVideoPause(post)" /> | ||
| 57 | - </div> | ||
| 58 | - <AudioPlayer v-if="post.audio.length" :songs="post.audio" class="post-audio" :id="post.id" :ref="el => { | ||
| 59 | - if (el) { | ||
| 60 | - if (!audioPlayers?.includes(el)) { | ||
| 61 | - audioPlayers?.push(el); | ||
| 62 | - } | ||
| 63 | - } | ||
| 64 | - }" @play="(player) => handleAudioPlay(player, post)" /> | ||
| 65 | - </div> | ||
| 66 | - </div> | ||
| 67 | - <div class="post-footer flex items-center justify-between"> | ||
| 68 | - <!-- 左侧:点赞 --> | ||
| 69 | - <div class="flex items-center"> | ||
| 70 | - <van-icon @click="handLike(post)" name="good-job" class="like-icon" :color="post.is_liked ? 'red' : ''" /> | ||
| 71 | - <span class="like-count ml-1">{{ post.likes }}</span> | ||
| 72 | - </div> | ||
| 73 | - | ||
| 74 | - <!-- 右侧:点评 --> | ||
| 75 | <div class="flex items-center cursor-pointer" @click="openCommentPopup(post)"> | 26 | <div class="flex items-center cursor-pointer" @click="openCommentPopup(post)"> |
| 76 | <van-icon name="comment-o" :color="post?.is_feedback ? '#10b981' : '#999'" size="19" class="mr-1" style="margin-top: 0.2rem;" /> | 27 | <van-icon name="comment-o" :color="post?.is_feedback ? '#10b981' : '#999'" size="19" class="mr-1" style="margin-top: 0.2rem;" /> |
| 77 | <span class="text-sm" :class="post?.is_feedback ? 'text-green-600' : 'text-gray-500'"> | 28 | <span class="text-sm" :class="post?.is_feedback ? 'text-green-600' : 'text-gray-500'"> |
| 78 | {{ post?.is_feedback ? '已点评' : '待点评' }} | 29 | {{ post?.is_feedback ? '已点评' : '待点评' }} |
| 79 | </span> | 30 | </span> |
| 80 | </div> | 31 | </div> |
| 81 | - </div> | 32 | + </template> |
| 82 | - </div> | 33 | + </CheckinCard> |
| 83 | </van-list> | 34 | </van-list> |
| 84 | <van-empty v-show="!checkinDataList.length" description="暂无数据" /> | 35 | <van-empty v-show="!checkinDataList.length" description="暂无数据" /> |
| 85 | 36 | ||
| ... | @@ -119,8 +70,7 @@ import { ref, onMounted, onBeforeUnmount } from 'vue' | ... | @@ -119,8 +70,7 @@ import { ref, onMounted, onBeforeUnmount } from 'vue' |
| 119 | import { useRoute, useRouter } from 'vue-router' | 70 | import { useRoute, useRouter } from 'vue-router' |
| 120 | import { useTitle } from '@vueuse/core' | 71 | import { useTitle } from '@vueuse/core' |
| 121 | import { showSuccessToast, showFailToast, showLoadingToast } from 'vant' | 72 | import { showSuccessToast, showFailToast, showLoadingToast } from 'vant' |
| 122 | -import VideoPlayer from '@/components/ui/VideoPlayer.vue' | 73 | +import CheckinCard from '@/components/checkin/CheckinCard.vue' |
| 123 | -import AudioPlayer from '@/components/ui/AudioPlayer.vue' | ||
| 124 | import PostCountModel from '@/components/count/postCountModel.vue' | 74 | import PostCountModel from '@/components/count/postCountModel.vue' |
| 125 | import { addCheckinFeedbackAPI } from '@/api/teacher' | 75 | import { addCheckinFeedbackAPI } from '@/api/teacher' |
| 126 | import { likeUploadTaskInfoAPI, dislikeUploadTaskInfoAPI, getCheckinTeacherListAPI } from '@/api/checkin' | 76 | import { likeUploadTaskInfoAPI, dislikeUploadTaskInfoAPI, getCheckinTeacherListAPI } from '@/api/checkin' |
| ... | @@ -140,14 +90,17 @@ const finished = ref(false) | ... | @@ -140,14 +90,17 @@ const finished = ref(false) |
| 140 | const limit = ref(10) | 90 | const limit = ref(10) |
| 141 | const page = ref(0) | 91 | const page = ref(0) |
| 142 | 92 | ||
| 143 | -// 图片预览相关 | 93 | +// 存储 CheckinCard 组件引用的 Map |
| 144 | -const showImagePreview = ref(false) | 94 | +const checkinCardRefs = ref(new Map()); |
| 145 | -const startPosition = ref(0) | ||
| 146 | -const currentPost = ref(null) | ||
| 147 | 95 | ||
| 148 | -// 播放器引用 | 96 | +// 设置 CheckinCard 引用 |
| 149 | -const videoPlayers = ref([]) | 97 | +const setCheckinCardRef = (el, id) => { |
| 150 | -const audioPlayers = ref([]) | 98 | + if (el) { |
| 99 | + checkinCardRefs.value.set(id, el); | ||
| 100 | + } else { | ||
| 101 | + checkinCardRefs.value.delete(id); | ||
| 102 | + } | ||
| 103 | +}; | ||
| 151 | 104 | ||
| 152 | // 点评相关状态 | 105 | // 点评相关状态 |
| 153 | const showCommentPopup = ref(false) | 106 | const showCommentPopup = ref(false) |
| ... | @@ -159,45 +112,6 @@ const commentForm = ref({ | ... | @@ -159,45 +112,6 @@ const commentForm = ref({ |
| 159 | }) | 112 | }) |
| 160 | 113 | ||
| 161 | /** | 114 | /** |
| 162 | - * CDN图片优化:为 cdn.ipadbiz.cn 域名添加压缩参数 | ||
| 163 | - * @param {string} url - 图片地址 | ||
| 164 | - * @returns {string} 处理后的图片地址 | ||
| 165 | - */ | ||
| 166 | -function optimizeCdn(url) { | ||
| 167 | - if (!url) return url | ||
| 168 | - try { | ||
| 169 | - const u = String(url) | ||
| 170 | - if (u.includes('cdn.ipadbiz.cn')) { | ||
| 171 | - const hasQuery = u.includes('?') | ||
| 172 | - const param = 'imageMogr2/thumbnail/200x/strip/quality/70' | ||
| 173 | - return hasQuery ? `${u}&${param}` : `${u}?${param}` | ||
| 174 | - } | ||
| 175 | - return u | ||
| 176 | - } catch (e) { | ||
| 177 | - return url | ||
| 178 | - } | ||
| 179 | -} | ||
| 180 | - | ||
| 181 | -/** | ||
| 182 | - * 打开图片预览 | ||
| 183 | - * @param {number} index - 起始索引 | ||
| 184 | - * @param {Object} post - 当前帖子 | ||
| 185 | - */ | ||
| 186 | -function openImagePreview(index, post) { | ||
| 187 | - currentPost.value = post | ||
| 188 | - startPosition.value = index | ||
| 189 | - showImagePreview.value = true | ||
| 190 | -} | ||
| 191 | - | ||
| 192 | -/** | ||
| 193 | - * 图片切换事件 | ||
| 194 | - * @param {number} index - 当前索引 | ||
| 195 | - */ | ||
| 196 | -function onChange(index) { | ||
| 197 | - startPosition.value = index | ||
| 198 | -} | ||
| 199 | - | ||
| 200 | -/** | ||
| 201 | * 点赞/取消点赞 | 115 | * 点赞/取消点赞 |
| 202 | * @param {Object} post - 帖子对象 | 116 | * @param {Object} post - 帖子对象 |
| 203 | * @returns {Promise<void>} | 117 | * @returns {Promise<void>} |
| ... | @@ -305,96 +219,25 @@ function startPlay(post) { | ... | @@ -305,96 +219,25 @@ function startPlay(post) { |
| 305 | /** | 219 | /** |
| 306 | * 视频播放事件:同时停止音频 | 220 | * 视频播放事件:同时停止音频 |
| 307 | */ | 221 | */ |
| 308 | -function handleVideoPlay() { | 222 | +function handleVideoPlay(id) { |
| 309 | - stopAllAudio() | 223 | + // 暂停其他所有卡片的媒体播放 |
| 310 | -} | 224 | + checkinCardRefs.value.forEach((card, key) => { |
| 311 | - | 225 | + if (key !== id && card) { |
| 312 | -/** | 226 | + card.stopAllMedia(); |
| 313 | - * 视频暂停事件:保持播放器可见 | ||
| 314 | - */ | ||
| 315 | -function handleVideoPause() { | ||
| 316 | - // 暂不处理,保持播放器状态 | ||
| 317 | -} | ||
| 318 | - | ||
| 319 | -/** | ||
| 320 | - * 停止其他视频与音频 | ||
| 321 | - * @param {Object} currentPlayer - 当前播放器 | ||
| 322 | - * @param {Object} currentPost - 当前帖子 | ||
| 323 | - */ | ||
| 324 | -function stopOtherVideos(currentPlayer, currentPost) { | ||
| 325 | - if (videoPlayers.value) { | ||
| 326 | - videoPlayers.value.forEach(player => { | ||
| 327 | - if (player !== currentPlayer && player.pause) { | ||
| 328 | - player.pause() | ||
| 329 | - } | ||
| 330 | - }) | ||
| 331 | - } | ||
| 332 | - checkinDataList.value.forEach(p => { | ||
| 333 | - p.videoList.forEach(v => { | ||
| 334 | - if (v.id !== currentPost.id) { | ||
| 335 | - v.isPlaying = false | ||
| 336 | - } | ||
| 337 | - }) | ||
| 338 | - }) | ||
| 339 | -} | ||
| 340 | - | ||
| 341 | -/** | ||
| 342 | - * 音频播放事件:停止其他音频 | ||
| 343 | - * @param {Object} player - 音频播放器 | ||
| 344 | - * @param {Object} post - 当前帖子 | ||
| 345 | - */ | ||
| 346 | -function handleAudioPlay(player, post) { | ||
| 347 | - stopOtherAudio(player, post) | ||
| 348 | -} | ||
| 349 | - | ||
| 350 | -/** | ||
| 351 | - * 停止其他音频 | ||
| 352 | - * @param {Object} currentPlayer - 当前播放器 | ||
| 353 | - * @param {Object} currentPost - 当前帖子 | ||
| 354 | - */ | ||
| 355 | -function stopOtherAudio(currentPlayer, currentPost) { | ||
| 356 | - if (audioPlayers.value) { | ||
| 357 | - audioPlayers.value.forEach(player => { | ||
| 358 | - if (player.id !== currentPost.id && player.pause) { | ||
| 359 | - player.pause() | ||
| 360 | - } | ||
| 361 | - }) | ||
| 362 | - } | ||
| 363 | - checkinDataList.value.forEach(post => { | ||
| 364 | - if (post.id !== currentPost.id) { | ||
| 365 | - post.isPlaying = false | ||
| 366 | } | 227 | } |
| 367 | - }) | 228 | + }); |
| 368 | - stopAllVideos() | ||
| 369 | } | 229 | } |
| 370 | 230 | ||
| 371 | /** | 231 | /** |
| 372 | - * 停止所有音频 | 232 | + * 音频播放事件:停止其他音频 |
| 373 | */ | 233 | */ |
| 374 | -function stopAllAudio() { | 234 | +function handleAudioPlay(id) { |
| 375 | - if (!audioPlayers.value) return | 235 | + // 暂停其他所有卡片的媒体播放 |
| 376 | - audioPlayers.value?.forEach(player => { | 236 | + checkinCardRefs.value.forEach((card, key) => { |
| 377 | - if (typeof player.pause === 'function') { | 237 | + if (key !== id && card) { |
| 378 | - player?.pause() | 238 | + card.stopAllMedia(); |
| 379 | } | 239 | } |
| 380 | - }) | 240 | + }); |
| 381 | - checkinDataList.value.forEach(post => { | ||
| 382 | - if (post.audio.length) { | ||
| 383 | - post.isPlaying = false | ||
| 384 | - } | ||
| 385 | - }) | ||
| 386 | -} | ||
| 387 | - | ||
| 388 | -/** | ||
| 389 | - * 停止所有视频 | ||
| 390 | - */ | ||
| 391 | -function stopAllVideos() { | ||
| 392 | - if (!videoPlayers.value) return | ||
| 393 | - checkinDataList.value.forEach(p => { | ||
| 394 | - p.videoList.forEach(v => { | ||
| 395 | - v.isPlaying = false | ||
| 396 | - }) | ||
| 397 | - }) | ||
| 398 | } | 241 | } |
| 399 | 242 | ||
| 400 | /** | 243 | /** |
| ... | @@ -475,85 +318,16 @@ function formatData(data) { | ... | @@ -475,85 +318,16 @@ function formatData(data) { |
| 475 | // 生命周期:卸载时清理播放器引用 | 318 | // 生命周期:卸载时清理播放器引用 |
| 476 | onMounted(() => {}) | 319 | onMounted(() => {}) |
| 477 | onBeforeUnmount(() => { | 320 | onBeforeUnmount(() => { |
| 478 | - if (videoPlayers.value) { | 321 | + checkinCardRefs.value.forEach(card => { |
| 479 | - videoPlayers.value.forEach(player => { | 322 | + if (card) { |
| 480 | - if (player && typeof player?.pause === 'function') { | 323 | + card.stopAllMedia(); |
| 481 | - player?.pause() | 324 | + } |
| 482 | - } | 325 | + }); |
| 483 | - }) | 326 | + checkinCardRefs.value.clear(); |
| 484 | - } | ||
| 485 | - stopAllAudio() | ||
| 486 | - if (videoPlayers.value) videoPlayers.value = [] | ||
| 487 | - if (audioPlayers.value) audioPlayers.value = [] | ||
| 488 | }) | 327 | }) |
| 489 | </script> | 328 | </script> |
| 490 | 329 | ||
| 491 | <style lang="less"> | 330 | <style lang="less"> |
| 492 | -.post-card { | ||
| 493 | - padding: 1rem; | ||
| 494 | - background-color: #FFF; | ||
| 495 | - border-radius: 5px; | ||
| 496 | - | ||
| 497 | - .post-header { | ||
| 498 | - margin-bottom: 1rem; | ||
| 499 | - } | ||
| 500 | - | ||
| 501 | - .user-info { | ||
| 502 | - margin-left: 0.5rem; | ||
| 503 | - | ||
| 504 | - .username { | ||
| 505 | - font-weight: 500; | ||
| 506 | - } | ||
| 507 | - | ||
| 508 | - .post-time { | ||
| 509 | - color: gray; | ||
| 510 | - font-size: 0.8rem; | ||
| 511 | - } | ||
| 512 | - } | ||
| 513 | - | ||
| 514 | - .post-content { | ||
| 515 | - .post-text { | ||
| 516 | - color: #666; | ||
| 517 | - margin-bottom: 1rem; | ||
| 518 | - white-space: pre-wrap; | ||
| 519 | - word-wrap: break-word; | ||
| 520 | - } | ||
| 521 | - | ||
| 522 | - .post-media { | ||
| 523 | - .post-images { | ||
| 524 | - display: flex; | ||
| 525 | - flex-wrap: wrap; | ||
| 526 | - gap: 0.5rem; | ||
| 527 | - } | ||
| 528 | - | ||
| 529 | - .post-video { | ||
| 530 | - margin: 1rem 0; | ||
| 531 | - width: 100%; | ||
| 532 | - border-radius: 8px; | ||
| 533 | - overflow: hidden; | ||
| 534 | - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 535 | - } | ||
| 536 | - | ||
| 537 | - .post-audio { | ||
| 538 | - margin: 1rem 0; | ||
| 539 | - } | ||
| 540 | - } | ||
| 541 | - } | ||
| 542 | - | ||
| 543 | - .post-footer { | ||
| 544 | - margin-top: 1rem; | ||
| 545 | - color: #666; | ||
| 546 | - | ||
| 547 | - .like-icon { | ||
| 548 | - margin-right: 0.25rem; | ||
| 549 | - } | ||
| 550 | - | ||
| 551 | - .like-count { | ||
| 552 | - font-size: 0.9rem; | ||
| 553 | - } | ||
| 554 | - } | ||
| 555 | -} | ||
| 556 | - | ||
| 557 | .comment-popup { | 331 | .comment-popup { |
| 558 | .van-popup { | 332 | .van-popup { |
| 559 | max-width: 90vw; | 333 | max-width: 90vw; | ... | ... |
-
Please register or login to post a comment