feat(教师端): 新增学员作业记录页面及跳转功能
- 添加学员作业记录页面路由及视图组件 - 在作业主页学生卡片添加点击跳转功能 - 实现作业记录列表展示、媒体播放及点评功能 - 更新README文档说明新增功能
Showing
4 changed files
with
597 additions
and
3 deletions
| ... | @@ -12,3 +12,7 @@ https://oa-dev.onwall.cn/f/mlaj | ... | @@ -12,3 +12,7 @@ https://oa-dev.onwall.cn/f/mlaj |
| 12 | - 统计:出勤率与任务完成率(参考 `myClassPage.vue` 统计样式,数据Mock)。 | 12 | - 统计:出勤率与任务完成率(参考 `myClassPage.vue` 统计样式,数据Mock)。 |
| 13 | - 日历:使用 `van-calendar` 单选模式,选择日期后展示当日学生完成情况。 | 13 | - 日历:使用 `van-calendar` 单选模式,选择日期后展示当日学生完成情况。 |
| 14 | - 学生完成情况:参考图片2样式,勾选代表已完成,未勾选代表未完成(数据Mock)。 | 14 | - 学生完成情况:参考图片2样式,勾选代表已完成,未勾选代表未完成(数据Mock)。 |
| 15 | + - 教师端新增学员作业记录页面:路径 `/teacher/student-record`,标题“学员作业记录”。 | ||
| 16 | + - 在作业主页的学生列表点击卡片可跳转至该页面(当前版本为固定示例页面)。 | ||
| 17 | + - 列表展示:作业帖子、图片/视频/音频、点赞与点评弹窗(与 `studentPage.vue` 的作业记录样式一致)。 | ||
| 18 | + - 接口参数固定:`user_id=817017`,`group_id=816653`(后续可替换为动态参数)。 | ... | ... |
| ... | @@ -60,4 +60,13 @@ export default [ | ... | @@ -60,4 +60,13 @@ export default [ |
| 60 | requiresAuth: true | 60 | requiresAuth: true |
| 61 | }, | 61 | }, |
| 62 | }, | 62 | }, |
| 63 | + { | ||
| 64 | + path: '/teacher/student-record', | ||
| 65 | + name: 'StudentRecord', | ||
| 66 | + component: () => import('../views/teacher/studentRecordPage.vue'), | ||
| 67 | + meta: { | ||
| 68 | + title: '学员作业记录', | ||
| 69 | + requiresAuth: true | ||
| 70 | + }, | ||
| 71 | + }, | ||
| 63 | ] | 72 | ] | ... | ... |
src/views/teacher/studentRecordPage.vue
0 → 100644
| 1 | +<!-- | ||
| 2 | + * @Date: 2025-11-19 22:05:00 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-11-19 22:12:24 | ||
| 5 | + * @FilePath: /mlaj/src/views/teacher/studentRecordPage.vue | ||
| 6 | + * @Description: 学生作业记录页面(仅作业记录与点评功能),固定 user_id 与 group_id | ||
| 7 | +--> | ||
| 8 | +<template> | ||
| 9 | + <div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen"> | ||
| 10 | + <!-- 作业记录列表 --> | ||
| 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"> | ||
| 13 | + <div class="post-header"> | ||
| 14 | + <van-row> | ||
| 15 | + <van-col span="4"> | ||
| 16 | + <van-image round width="2.5rem" height="2.5rem" | ||
| 17 | + :src="optimizeCdn(post.user.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg')" fit="cover" /> | ||
| 18 | + </van-col> | ||
| 19 | + <van-col span="17"> | ||
| 20 | + <div class="user-info"> | ||
| 21 | + <div class="username">{{ post.user.name }}</div> | ||
| 22 | + <div class="post-time">{{ post.user.time }}</div> | ||
| 23 | + </div> | ||
| 24 | + </van-col> | ||
| 25 | + <van-col span="3"> | ||
| 26 | + </van-col> | ||
| 27 | + </van-row> | ||
| 28 | + </div> | ||
| 29 | + <div class="post-content"> | ||
| 30 | + <div class="post-text">{{ post.content }}</div> | ||
| 31 | + <div class="post-media"> | ||
| 32 | + <div v-if="post.images.length" class="post-images"> | ||
| 33 | + <van-image width="30%" fit="cover" v-for="(image, index) in post.images" :key="index" :src="optimizeCdn(image)" | ||
| 34 | + radius="5" @click="openImagePreview(index, post)" /> | ||
| 35 | + </div> | ||
| 36 | + <van-image-preview v-if="currentPost" v-model:show="showImagePreview" :images="currentPost.images" | ||
| 37 | + :start-position="startPosition" :show-index="true" @change="onChange" /> | ||
| 38 | + <div v-for="(v, idx) in post.videoList" :key="idx"> | ||
| 39 | + <!-- 视频封面和播放按钮 --> | ||
| 40 | + <div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden" style="aspect-ratio: 16/9; margin-bottom: 1rem;"> | ||
| 41 | + <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" /> | ||
| 42 | + <div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20" @click="startPlay(v)"> | ||
| 43 | + <div class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors"> | ||
| 44 | + <van-icon name="play-circle-o" class="text-white" size="40" /> | ||
| 45 | + </div> | ||
| 46 | + </div> | ||
| 47 | + </div> | ||
| 48 | + <!-- 视频播放器 --> | ||
| 49 | + <VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" class="post-video rounded-lg overflow-hidden" :ref="el => { | ||
| 50 | + if (el) { | ||
| 51 | + if (!videoPlayers?.includes(el)) { | ||
| 52 | + videoPlayers?.push(el); | ||
| 53 | + } | ||
| 54 | + } | ||
| 55 | + }" @onPlay="handleVideoPlay(player, post)" @onPause="handleVideoPause(post)" /> | ||
| 56 | + </div> | ||
| 57 | + <AudioPlayer v-if="post.audio.length" :songs="post.audio" class="post-audio" :id="post.id" :ref="el => { | ||
| 58 | + if (el) { | ||
| 59 | + if (!audioPlayers?.includes(el)) { | ||
| 60 | + audioPlayers?.push(el); | ||
| 61 | + } | ||
| 62 | + } | ||
| 63 | + }" @play="(player) => handleAudioPlay(player, post)" /> | ||
| 64 | + </div> | ||
| 65 | + </div> | ||
| 66 | + <div class="post-footer flex items-center justify-between"> | ||
| 67 | + <!-- 左侧:点赞 --> | ||
| 68 | + <div class="flex items-center"> | ||
| 69 | + <van-icon @click="handLike(post)" name="good-job" class="like-icon" :color="post.is_liked ? 'red' : ''" /> | ||
| 70 | + <span class="like-count ml-1">{{ post.likes }}</span> | ||
| 71 | + </div> | ||
| 72 | + | ||
| 73 | + <!-- 右侧:点评 --> | ||
| 74 | + <div class="flex items-center cursor-pointer" @click="openCommentPopup(post)"> | ||
| 75 | + <van-icon name="comment-o" :color="post?.is_feedback ? '#10b981' : '#999'" size="19" class="mr-1" style="margin-top: 0.2rem;" /> | ||
| 76 | + <span class="text-sm" :class="post?.is_feedback ? 'text-green-600' : 'text-gray-500'"> | ||
| 77 | + {{ post?.is_feedback ? '已点评' : '待点评' }} | ||
| 78 | + </span> | ||
| 79 | + </div> | ||
| 80 | + </div> | ||
| 81 | + </div> | ||
| 82 | + </van-list> | ||
| 83 | + <van-empty v-show="!checkinDataList.length" description="暂无数据" /> | ||
| 84 | + | ||
| 85 | + <!-- 点评弹窗 --> | ||
| 86 | + <van-popup v-model:show="showCommentPopup" position="bottom" round class="comment-popup"> | ||
| 87 | + <div class="p-6 w-100"> | ||
| 88 | + <div class="text-center text-lg font-bold mb-4">作业点评</div> | ||
| 89 | + | ||
| 90 | + <!-- 评分 --> | ||
| 91 | + <div class="mb-4"> | ||
| 92 | + <div class="text-sm text-gray-600 mb-2">评分</div> | ||
| 93 | + <van-rate v-model="commentForm.score" :size="24" color="#ffd21e" void-color="#eee" :readonly="currentCommentPost?.is_feedback" /> | ||
| 94 | + </div> | ||
| 95 | + | ||
| 96 | + <!-- 点评内容 --> | ||
| 97 | + <div class="mb-6"> | ||
| 98 | + <div class="text-sm text-gray-600 mb-2">点评内容</div> | ||
| 99 | + <van-field v-model="commentForm.note" type="textarea" placeholder="请输入点评内容..." rows="4" maxlength="200" show-word-limit :border="false" class="bg-gray-50 rounded-lg" :readonly="currentCommentPost?.is_feedback" /> | ||
| 100 | + </div> | ||
| 101 | + | ||
| 102 | + <!-- 操作按钮 --> | ||
| 103 | + <div v-if="!currentCommentPost?.is_feedback" class="flex gap-3"> | ||
| 104 | + <van-button block type="default" @click="closeCommentPopup" class="flex-1">取消</van-button> | ||
| 105 | + <van-button block type="primary" @click="submitComment" class="flex-1">提交</van-button> | ||
| 106 | + </div> | ||
| 107 | + <div v-else class="flex gap-3"> | ||
| 108 | + <van-button block type="default" @click="closeCommentPopup" class="flex-1">关闭</van-button> | ||
| 109 | + </div> | ||
| 110 | + </div> | ||
| 111 | + </van-popup> | ||
| 112 | + </div> | ||
| 113 | + <van-back-top right="5vw" bottom="25vh" offset="600" /> | ||
| 114 | +</template> | ||
| 115 | + | ||
| 116 | +<script setup> | ||
| 117 | +import { ref, onMounted, onBeforeUnmount } from 'vue' | ||
| 118 | +import { showSuccessToast, showFailToast, showLoadingToast } from 'vant' | ||
| 119 | +import VideoPlayer from '@/components/ui/VideoPlayer.vue' | ||
| 120 | +import AudioPlayer from '@/components/ui/AudioPlayer.vue' | ||
| 121 | +import { getStudentUploadListAPI, addCheckinFeedbackAPI } from '@/api/teacher' | ||
| 122 | +import { likeUploadTaskInfoAPI, dislikeUploadTaskInfoAPI } from '@/api/checkin' | ||
| 123 | + | ||
| 124 | +// 固定的用户与班级ID | ||
| 125 | +const fixedUserId = 817017 | ||
| 126 | +const fixedGroupId = 816653 | ||
| 127 | + | ||
| 128 | +// 列表相关状态 | ||
| 129 | +const checkinDataList = ref([]) | ||
| 130 | +const loading = ref(false) | ||
| 131 | +const finished = ref(false) | ||
| 132 | +const limit = ref(10) | ||
| 133 | +const page = ref(0) | ||
| 134 | + | ||
| 135 | +// 图片预览相关 | ||
| 136 | +const showImagePreview = ref(false) | ||
| 137 | +const startPosition = ref(0) | ||
| 138 | +const currentPost = ref(null) | ||
| 139 | + | ||
| 140 | +// 播放器引用 | ||
| 141 | +const videoPlayers = ref([]) | ||
| 142 | +const audioPlayers = ref([]) | ||
| 143 | + | ||
| 144 | +// 点评相关状态 | ||
| 145 | +const showCommentPopup = ref(false) | ||
| 146 | +const currentCommentPost = ref(null) | ||
| 147 | +const commentForm = ref({ | ||
| 148 | + checkin_id: '', | ||
| 149 | + score: 0, | ||
| 150 | + note: '' | ||
| 151 | +}) | ||
| 152 | + | ||
| 153 | +/** | ||
| 154 | + * CDN图片优化:为 cdn.ipadbiz.cn 域名添加压缩参数 | ||
| 155 | + * @param {string} url - 图片地址 | ||
| 156 | + * @returns {string} 处理后的图片地址 | ||
| 157 | + */ | ||
| 158 | +function optimizeCdn(url) { | ||
| 159 | + if (!url) return url | ||
| 160 | + try { | ||
| 161 | + const u = String(url) | ||
| 162 | + if (u.includes('cdn.ipadbiz.cn')) { | ||
| 163 | + const hasQuery = u.includes('?') | ||
| 164 | + const param = 'imageMogr2/thumbnail/200x/strip/quality/70' | ||
| 165 | + return hasQuery ? `${u}&${param}` : `${u}?${param}` | ||
| 166 | + } | ||
| 167 | + return u | ||
| 168 | + } catch (e) { | ||
| 169 | + return url | ||
| 170 | + } | ||
| 171 | +} | ||
| 172 | + | ||
| 173 | +/** | ||
| 174 | + * 打开图片预览 | ||
| 175 | + * @param {number} index - 起始索引 | ||
| 176 | + * @param {Object} post - 当前帖子 | ||
| 177 | + */ | ||
| 178 | +function openImagePreview(index, post) { | ||
| 179 | + currentPost.value = post | ||
| 180 | + startPosition.value = index | ||
| 181 | + showImagePreview.value = true | ||
| 182 | +} | ||
| 183 | + | ||
| 184 | +/** | ||
| 185 | + * 图片切换事件 | ||
| 186 | + * @param {number} index - 当前索引 | ||
| 187 | + */ | ||
| 188 | +function onChange(index) { | ||
| 189 | + startPosition.value = index | ||
| 190 | +} | ||
| 191 | + | ||
| 192 | +/** | ||
| 193 | + * 点赞/取消点赞 | ||
| 194 | + * @param {Object} post - 帖子对象 | ||
| 195 | + * @returns {Promise<void>} | ||
| 196 | + */ | ||
| 197 | +async function handLike(post) { | ||
| 198 | + if (!post.is_liked) { | ||
| 199 | + const { code } = await likeUploadTaskInfoAPI({ checkin_id: post.id }) | ||
| 200 | + if (code) { | ||
| 201 | + showSuccessToast('点赞成功') | ||
| 202 | + post.likes++ | ||
| 203 | + post.is_liked = true | ||
| 204 | + } | ||
| 205 | + } else { | ||
| 206 | + const { code } = await dislikeUploadTaskInfoAPI({ checkin_id: post.id }) | ||
| 207 | + if (code) { | ||
| 208 | + showSuccessToast('取消点赞成功') | ||
| 209 | + post.likes-- | ||
| 210 | + post.is_liked = false | ||
| 211 | + } | ||
| 212 | + } | ||
| 213 | +} | ||
| 214 | + | ||
| 215 | +/** | ||
| 216 | + * 打开点评弹窗 | ||
| 217 | + * @param {Object} post - 帖子对象 | ||
| 218 | + */ | ||
| 219 | +function openCommentPopup(post) { | ||
| 220 | + currentCommentPost.value = post | ||
| 221 | + commentForm.value.checkin_id = post.id | ||
| 222 | + if (post.feedback_id) { | ||
| 223 | + commentForm.value.score = post.feedback_score || 0 | ||
| 224 | + commentForm.value.note = post.feedback || '' | ||
| 225 | + } else { | ||
| 226 | + commentForm.value.score = 0 | ||
| 227 | + commentForm.value.note = '' | ||
| 228 | + } | ||
| 229 | + showCommentPopup.value = true | ||
| 230 | +} | ||
| 231 | + | ||
| 232 | +/** | ||
| 233 | + * 关闭点评弹窗 | ||
| 234 | + */ | ||
| 235 | +function closeCommentPopup() { | ||
| 236 | + if (currentCommentPost.value && currentCommentPost.value.is_feedback) { | ||
| 237 | + showCommentPopup.value = false | ||
| 238 | + return | ||
| 239 | + } | ||
| 240 | + showCommentPopup.value = false | ||
| 241 | + currentCommentPost.value = null | ||
| 242 | + commentForm.value.score = 0 | ||
| 243 | + commentForm.value.note = '' | ||
| 244 | +} | ||
| 245 | + | ||
| 246 | +/** | ||
| 247 | + * 提交点评 | ||
| 248 | + * @returns {Promise<void>} | ||
| 249 | + */ | ||
| 250 | +async function submitComment() { | ||
| 251 | + if (!commentForm.value.note.trim()) { | ||
| 252 | + showFailToast('请输入点评内容') | ||
| 253 | + return | ||
| 254 | + } | ||
| 255 | + if (commentForm.value.score === 0) { | ||
| 256 | + showFailToast('请选择评分') | ||
| 257 | + return | ||
| 258 | + } | ||
| 259 | + try { | ||
| 260 | + showLoadingToast('提交中...') | ||
| 261 | + const { code, data } = await addCheckinFeedbackAPI(commentForm.value) | ||
| 262 | + if (code) { | ||
| 263 | + commentForm.value.feedback_id = data.id | ||
| 264 | + currentCommentPost.value.is_feedback = true | ||
| 265 | + checkinDataList.value.forEach(item => { | ||
| 266 | + if (item.id === currentCommentPost.value.id) { | ||
| 267 | + item.feedback_id = commentForm.value.feedback_id | ||
| 268 | + item.feedback_score = commentForm.value.score | ||
| 269 | + item.feedback = commentForm.value.note | ||
| 270 | + } | ||
| 271 | + }) | ||
| 272 | + showSuccessToast('点评提交成功') | ||
| 273 | + closeCommentPopup() | ||
| 274 | + } | ||
| 275 | + } catch (err) { | ||
| 276 | + showFailToast('提交失败,请重试') | ||
| 277 | + } | ||
| 278 | +} | ||
| 279 | + | ||
| 280 | +/** | ||
| 281 | + * 开始播放视频 | ||
| 282 | + * @param {Object} post - 视频条目(含video、isPlaying) | ||
| 283 | + */ | ||
| 284 | +function startPlay(post) { | ||
| 285 | + if (checkinDataList.value) { | ||
| 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 | + post.isPlaying = true | ||
| 295 | +} | ||
| 296 | + | ||
| 297 | +/** | ||
| 298 | + * 视频播放事件:同时停止音频 | ||
| 299 | + */ | ||
| 300 | +function handleVideoPlay() { | ||
| 301 | + stopAllAudio() | ||
| 302 | +} | ||
| 303 | + | ||
| 304 | +/** | ||
| 305 | + * 视频暂停事件:保持播放器可见 | ||
| 306 | + */ | ||
| 307 | +function handleVideoPause() { | ||
| 308 | + // 暂不处理,保持播放器状态 | ||
| 309 | +} | ||
| 310 | + | ||
| 311 | +/** | ||
| 312 | + * 停止其他视频与音频 | ||
| 313 | + * @param {Object} currentPlayer - 当前播放器 | ||
| 314 | + * @param {Object} currentPost - 当前帖子 | ||
| 315 | + */ | ||
| 316 | +function stopOtherVideos(currentPlayer, currentPost) { | ||
| 317 | + if (videoPlayers.value) { | ||
| 318 | + videoPlayers.value.forEach(player => { | ||
| 319 | + if (player !== currentPlayer && player.pause) { | ||
| 320 | + player.pause() | ||
| 321 | + } | ||
| 322 | + }) | ||
| 323 | + } | ||
| 324 | + checkinDataList.value.forEach(p => { | ||
| 325 | + p.videoList.forEach(v => { | ||
| 326 | + if (v.id !== currentPost.id) { | ||
| 327 | + v.isPlaying = false | ||
| 328 | + } | ||
| 329 | + }) | ||
| 330 | + }) | ||
| 331 | +} | ||
| 332 | + | ||
| 333 | +/** | ||
| 334 | + * 音频播放事件:停止其他音频 | ||
| 335 | + * @param {Object} player - 音频播放器 | ||
| 336 | + * @param {Object} post - 当前帖子 | ||
| 337 | + */ | ||
| 338 | +function handleAudioPlay(player, post) { | ||
| 339 | + stopOtherAudio(player, post) | ||
| 340 | +} | ||
| 341 | + | ||
| 342 | +/** | ||
| 343 | + * 停止其他音频 | ||
| 344 | + * @param {Object} currentPlayer - 当前播放器 | ||
| 345 | + * @param {Object} currentPost - 当前帖子 | ||
| 346 | + */ | ||
| 347 | +function stopOtherAudio(currentPlayer, currentPost) { | ||
| 348 | + if (audioPlayers.value) { | ||
| 349 | + audioPlayers.value.forEach(player => { | ||
| 350 | + if (player.id !== currentPost.id && player.pause) { | ||
| 351 | + player.pause() | ||
| 352 | + } | ||
| 353 | + }) | ||
| 354 | + } | ||
| 355 | + checkinDataList.value.forEach(post => { | ||
| 356 | + if (post.id !== currentPost.id) { | ||
| 357 | + post.isPlaying = false | ||
| 358 | + } | ||
| 359 | + }) | ||
| 360 | + stopAllVideos() | ||
| 361 | +} | ||
| 362 | + | ||
| 363 | +/** | ||
| 364 | + * 停止所有音频 | ||
| 365 | + */ | ||
| 366 | +function stopAllAudio() { | ||
| 367 | + if (!audioPlayers.value) return | ||
| 368 | + audioPlayers.value?.forEach(player => { | ||
| 369 | + if (typeof player.pause === 'function') { | ||
| 370 | + player?.pause() | ||
| 371 | + } | ||
| 372 | + }) | ||
| 373 | + checkinDataList.value.forEach(post => { | ||
| 374 | + if (post.audio.length) { | ||
| 375 | + post.isPlaying = false | ||
| 376 | + } | ||
| 377 | + }) | ||
| 378 | +} | ||
| 379 | + | ||
| 380 | +/** | ||
| 381 | + * 停止所有视频 | ||
| 382 | + */ | ||
| 383 | +function stopAllVideos() { | ||
| 384 | + if (!videoPlayers.value) return | ||
| 385 | + checkinDataList.value.forEach(p => { | ||
| 386 | + p.videoList.forEach(v => { | ||
| 387 | + v.isPlaying = false | ||
| 388 | + }) | ||
| 389 | + }) | ||
| 390 | +} | ||
| 391 | + | ||
| 392 | +/** | ||
| 393 | + * 拉取作业记录分页数据 | ||
| 394 | + * @returns {Promise<void>} | ||
| 395 | + */ | ||
| 396 | +async function onLoad() { | ||
| 397 | + const nextPage = page.value | ||
| 398 | + const res = await getStudentUploadListAPI({ | ||
| 399 | + limit: limit.value, | ||
| 400 | + page: nextPage, | ||
| 401 | + user_id: fixedUserId, | ||
| 402 | + group_id: fixedGroupId, | ||
| 403 | + }) | ||
| 404 | + if (res.code) { | ||
| 405 | + checkinDataList.value = [...checkinDataList.value, ...formatData(res.data)] | ||
| 406 | + finished.value = res.data.length < limit.value | ||
| 407 | + page.value = nextPage + 1 | ||
| 408 | + } | ||
| 409 | + loading.value = false | ||
| 410 | +} | ||
| 411 | + | ||
| 412 | +/** | ||
| 413 | + * 规范化接口数据为页面所需结构 | ||
| 414 | + * @param {Array} data - 原始接口数据 | ||
| 415 | + * @returns {Array} 规范化后的列表 | ||
| 416 | + */ | ||
| 417 | +function formatData(data) { | ||
| 418 | + let formattedData = [] | ||
| 419 | + formattedData = data?.map(item => { | ||
| 420 | + let images = [] | ||
| 421 | + let audio = [] | ||
| 422 | + let videoList = [] | ||
| 423 | + if (item.file_type === 'image') { | ||
| 424 | + images = item.files.map(file => file.value) | ||
| 425 | + } else if (item.file_type === 'video') { | ||
| 426 | + videoList = item.files.map(file => ({ | ||
| 427 | + id: file.meta_id, | ||
| 428 | + video: file.value, | ||
| 429 | + videoCover: file.cover, | ||
| 430 | + isPlaying: false, | ||
| 431 | + })) | ||
| 432 | + } else if (item.file_type === 'audio') { | ||
| 433 | + audio = item.files.map(file => ({ | ||
| 434 | + title: file.name ? file.name : '打卡音频', | ||
| 435 | + artist: file.artist ? file.artist : '', | ||
| 436 | + url: file.value, | ||
| 437 | + cover: file.cover ? file.cover : '', | ||
| 438 | + })) | ||
| 439 | + } | ||
| 440 | + return { | ||
| 441 | + id: item.id, | ||
| 442 | + task_id: item.task_id, | ||
| 443 | + user: { | ||
| 444 | + name: item.username, | ||
| 445 | + avatar: item.avatar, | ||
| 446 | + time: item.created_time_desc, | ||
| 447 | + }, | ||
| 448 | + content: item.note, | ||
| 449 | + images, | ||
| 450 | + videoList, | ||
| 451 | + audio, | ||
| 452 | + isPlaying: false, | ||
| 453 | + likes: item.like_count, | ||
| 454 | + is_liked: item.is_like, | ||
| 455 | + is_my: item.is_my, | ||
| 456 | + file_type: item.file_type, | ||
| 457 | + is_feedback: item.feedback_id || false, | ||
| 458 | + feedback: item.feedback || '', | ||
| 459 | + feedback_id: item.feedback_id || '', | ||
| 460 | + feedback_score: item.feedback_score || 0, | ||
| 461 | + comment: item.comment || null, | ||
| 462 | + } | ||
| 463 | + }) | ||
| 464 | + return formattedData | ||
| 465 | +} | ||
| 466 | + | ||
| 467 | +// 生命周期:卸载时清理播放器引用 | ||
| 468 | +onMounted(() => {}) | ||
| 469 | +onBeforeUnmount(() => { | ||
| 470 | + if (videoPlayers.value) { | ||
| 471 | + videoPlayers.value.forEach(player => { | ||
| 472 | + if (player && typeof player?.pause === 'function') { | ||
| 473 | + player?.pause() | ||
| 474 | + } | ||
| 475 | + }) | ||
| 476 | + } | ||
| 477 | + stopAllAudio() | ||
| 478 | + if (videoPlayers.value) videoPlayers.value = [] | ||
| 479 | + if (audioPlayers.value) audioPlayers.value = [] | ||
| 480 | +}) | ||
| 481 | +</script> | ||
| 482 | + | ||
| 483 | +<style lang="less"> | ||
| 484 | +.post-card { | ||
| 485 | + padding: 1rem; | ||
| 486 | + background-color: #FFF; | ||
| 487 | + border-radius: 5px; | ||
| 488 | + | ||
| 489 | + .post-header { | ||
| 490 | + margin-bottom: 1rem; | ||
| 491 | + } | ||
| 492 | + | ||
| 493 | + .user-info { | ||
| 494 | + margin-left: 0.5rem; | ||
| 495 | + | ||
| 496 | + .username { | ||
| 497 | + font-weight: 500; | ||
| 498 | + } | ||
| 499 | + | ||
| 500 | + .post-time { | ||
| 501 | + color: gray; | ||
| 502 | + font-size: 0.8rem; | ||
| 503 | + } | ||
| 504 | + } | ||
| 505 | + | ||
| 506 | + .post-content { | ||
| 507 | + .post-text { | ||
| 508 | + color: #666; | ||
| 509 | + margin-bottom: 1rem; | ||
| 510 | + white-space: pre-wrap; | ||
| 511 | + word-wrap: break-word; | ||
| 512 | + } | ||
| 513 | + | ||
| 514 | + .post-media { | ||
| 515 | + .post-images { | ||
| 516 | + display: flex; | ||
| 517 | + flex-wrap: wrap; | ||
| 518 | + gap: 0.5rem; | ||
| 519 | + } | ||
| 520 | + | ||
| 521 | + .post-video { | ||
| 522 | + margin: 1rem 0; | ||
| 523 | + width: 100%; | ||
| 524 | + border-radius: 8px; | ||
| 525 | + overflow: hidden; | ||
| 526 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 527 | + } | ||
| 528 | + | ||
| 529 | + .post-audio { | ||
| 530 | + margin: 1rem 0; | ||
| 531 | + } | ||
| 532 | + } | ||
| 533 | + } | ||
| 534 | + | ||
| 535 | + .post-footer { | ||
| 536 | + margin-top: 1rem; | ||
| 537 | + color: #666; | ||
| 538 | + | ||
| 539 | + .like-icon { | ||
| 540 | + margin-right: 0.25rem; | ||
| 541 | + } | ||
| 542 | + | ||
| 543 | + .like-count { | ||
| 544 | + font-size: 0.9rem; | ||
| 545 | + } | ||
| 546 | + } | ||
| 547 | +} | ||
| 548 | + | ||
| 549 | +.comment-popup { | ||
| 550 | + .van-popup { | ||
| 551 | + max-width: 90vw; | ||
| 552 | + } | ||
| 553 | + | ||
| 554 | + .van-rate { | ||
| 555 | + display: flex; | ||
| 556 | + justify-content: center; | ||
| 557 | + } | ||
| 558 | + | ||
| 559 | + .van-field { | ||
| 560 | + padding: 12px; | ||
| 561 | + border-radius: 8px; | ||
| 562 | + } | ||
| 563 | + | ||
| 564 | + .van-button { | ||
| 565 | + height: 44px; | ||
| 566 | + border-radius: 8px; | ||
| 567 | + } | ||
| 568 | +} | ||
| 569 | +</style> |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-11-19 21:00:00 | 2 | * @Date: 2025-11-19 21:00:00 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-11-19 21:43:15 | 4 | + * @LastEditTime: 2025-11-19 22:10:07 |
| 5 | * @FilePath: /mlaj/src/views/teacher/taskHomePage.vue | 5 | * @FilePath: /mlaj/src/views/teacher/taskHomePage.vue |
| 6 | * @Description: 教师端作业主页(头部介绍、统计、日历与学生完成情况;数据Mock) | 6 | * @Description: 教师端作业主页(头部介绍、统计、日历与学生完成情况;数据Mock) |
| 7 | --> | 7 | --> |
| ... | @@ -66,7 +66,8 @@ | ... | @@ -66,7 +66,8 @@ |
| 66 | <div class="grid grid-cols-5 gap-3 StudentsGrid"> | 66 | <div class="grid grid-cols-5 gap-3 StudentsGrid"> |
| 67 | <div v-for="(stu, idx) in students_status" :key="stu.id" | 67 | <div v-for="(stu, idx) in students_status" :key="stu.id" |
| 68 | class="studentItem relative rounded-md h-16 flex flex-col items-center justify-center text-center border overflow-hidden" | 68 | class="studentItem relative rounded-md h-16 flex flex-col items-center justify-center text-center border overflow-hidden" |
| 69 | - :class="stu.completed ? 'bg-white border-green-500 text-green-600' : 'bg-gray-100 border-gray-300 text-gray-500'"> | 69 | + :class="stu.completed ? 'bg-white border-green-500 text-green-600' : 'bg-gray-100 border-gray-300 text-gray-500'" |
| 70 | + @click="go_student_record(stu)"> | ||
| 70 | <div class="text-sm font-semibold">{{ idx + 1 }}</div> | 71 | <div class="text-sm font-semibold">{{ idx + 1 }}</div> |
| 71 | <div class="text-sm mt-1">{{ stu.name }}</div> | 72 | <div class="text-sm mt-1">{{ stu.name }}</div> |
| 72 | <img v-if="stu.completed" :src="checkCorner" alt="checked" class="cornerIcon" /> | 73 | <img v-if="stu.completed" :src="checkCorner" alt="checked" class="cornerIcon" /> |
| ... | @@ -78,12 +79,13 @@ | ... | @@ -78,12 +79,13 @@ |
| 78 | 79 | ||
| 79 | <script setup> | 80 | <script setup> |
| 80 | import { ref, computed } from 'vue' | 81 | import { ref, computed } from 'vue' |
| 81 | -import { useRoute } from 'vue-router' | 82 | +import { useRoute, useRouter } from 'vue-router' |
| 82 | import { useTitle } from '@vueuse/core' | 83 | import { useTitle } from '@vueuse/core' |
| 83 | import TaskCalendar from '@/components/ui/TaskCalendar.vue' | 84 | import TaskCalendar from '@/components/ui/TaskCalendar.vue' |
| 84 | import checkCorner from '@/assets/check_corner.svg' | 85 | import checkCorner from '@/assets/check_corner.svg' |
| 85 | 86 | ||
| 86 | const $route = useRoute() | 87 | const $route = useRoute() |
| 88 | +const $router = useRouter() | ||
| 87 | useTitle('作业主页') | 89 | useTitle('作业主页') |
| 88 | 90 | ||
| 89 | // | 91 | // |
| ... | @@ -193,6 +195,16 @@ const completed_count = computed(() => students_status.value.filter(s => s.compl | ... | @@ -193,6 +195,16 @@ const completed_count = computed(() => students_status.value.filter(s => s.compl |
| 193 | * @returns {string} 文本 | 195 | * @returns {string} 文本 |
| 194 | */ | 196 | */ |
| 195 | const current_date_text = computed(() => selected_date.value) | 197 | const current_date_text = computed(() => selected_date.value) |
| 198 | + | ||
| 199 | +/** | ||
| 200 | + * 跳转至学员作业记录页面(固定ID的示例页面) | ||
| 201 | + * @param {{id:string,name:string,completed:boolean}} stu - 学员对象 | ||
| 202 | + * @returns {void} | ||
| 203 | + */ | ||
| 204 | +function go_student_record(stu) { | ||
| 205 | + // 跳转到固定ID的作业记录页面,当前版本不使用传入ID | ||
| 206 | + $router.push({ name: 'StudentRecord' }) | ||
| 207 | +} | ||
| 196 | </script> | 208 | </script> |
| 197 | 209 | ||
| 198 | <style lang="less" scoped> | 210 | <style lang="less" scoped> | ... | ... |
-
Please register or login to post a comment