hookehuyr

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

refactor(视图): 使用CheckinCard组件替换重复的帖子卡片代码
fix(播放器): 修复VideoPlayer和AudioPlayer的禁用状态和错误处理
style(样式): 调整postCountModel的底部边距
docs(类型): 更新组件类型声明文件
...@@ -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']
......
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 + try {
253 player.value.pause(); 254 player.value.pause();
254 emit('onPause', player.value); 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()) {
263 + try {
259 player.value?.play(); 264 player.value?.play();
260 emit('onPlay', player.value); 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 } 198 }
361 }); 199 });
362 - }
363 - // 更新其他帖子的播放状态
364 - checkinDataList.value.forEach(post => {
365 - if (post.id!== currentPost.id) {
366 - post.isPlaying = false;
367 - }
368 - });
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 222
421 -// 打开图片预览
422 -const openImagePreview = (index, post) => {
423 - currentPost.value = post;
424 - startPosition.value = index;
425 - showImagePreview.value = true;
426 -}
427 -
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 -
955 - .post-header {
956 - margin-bottom: 1rem;
957 - }
958 -
959 - .user-info {
960 - margin-left: 0.5rem;
961 740
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 -};
470 -
471 -/**
472 - * 处理音频播放事件
473 - * @param {Object} player - 音频播放器实例
474 - * @param {Object} post - 包含音频的帖子对象
475 - */
476 -const handleAudioPlay = (player, post) => {
477 - // 停止其他音频播放
478 - stopOtherAudio(player, post);
479 }; 313 };
480 314
481 -const stopOtherAudio = (currentPlayer, currentPost) => { 315 +// 视频播放事件处理
482 - // 确保audioPlayers.value是一个数组 316 +const handleVideoPlay = ({ post, player, videoId }) => {
483 - if (audioPlayers.value) { 317 + checkinCardRefs.value.forEach((card, id) => {
484 - // 暂停其他音频播放器 318 + if (id !== post.id) {
485 - audioPlayers.value.forEach(player => { 319 + card.stopAllMedia();
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 771
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 -
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();
719 } 661 }
720 }); 662 });
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();
748 } 692 }
749 }); 693 });
750 - }
751 694
752 - stopAllAudio(); 695 + checkinCardRefs.value.clear();
753 -
754 - // 清空引用数组
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 -/**
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 } 706 }
811 }); 707 });
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 - });
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 } 227 }
330 - }) 228 + });
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 } 229 }
340 230
341 /** 231 /**
342 * 音频播放事件:停止其他音频 232 * 音频播放事件:停止其他音频
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 */ 233 */
355 -function stopOtherAudio(currentPlayer, currentPost) { 234 +function handleAudioPlay(id) {
356 - if (audioPlayers.value) { 235 + // 暂停其他所有卡片的媒体播放
357 - audioPlayers.value.forEach(player => { 236 + checkinCardRefs.value.forEach((card, key) => {
358 - if (player.id !== currentPost.id && player.pause) { 237 + if (key !== id && card) {
359 - player.pause() 238 + card.stopAllMedia();
360 } 239 }
361 - }) 240 + });
362 - }
363 - checkinDataList.value.forEach(post => {
364 - if (post.id !== currentPost.id) {
365 - post.isPlaying = false
366 - }
367 - })
368 - stopAllVideos()
369 -}
370 -
371 -/**
372 - * 停止所有音频
373 - */
374 -function stopAllAudio() {
375 - if (!audioPlayers.value) return
376 - audioPlayers.value?.forEach(player => {
377 - if (typeof player.pause === 'function') {
378 - player?.pause()
379 - }
380 - })
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()
482 } 324 }
483 - }) 325 + });
484 - } 326 + checkinCardRefs.value.clear();
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;
......