studentRecordPage.vue 11.3 KB
<!--
 * @Date: 2025-11-19 22:05:00
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2026-01-26 09:43:18
 * @FilePath: /mlaj/src/views/teacher/studentRecordPage.vue
 * @Description: 学生作业记录页面(仅作业记录与点评功能),固定 user_id 与 group_id
-->
<template>
    <div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen">
        <!-- 作业记录列表 -->
        <van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad" class="space-y-4 p-4">
            <CheckinCard
                v-for="post in checkinDataList"
                :key="post.id"
                :post="post"
                :use-cdn-optimization="true"
                :ref="(el) => setCheckinCardRef(el, post.id)"
                @like="handLike(post)"
                @video-play="handleVideoPlay"
                @audio-play="handleAudioPlay"
            >
                <template #content-top>
                    <div class="text-gray-500 font-bold text-sm mb-4">{{ post.subtask_title }}</div>
                </template>
                <template #footer-right>
                    <div class="flex items-center cursor-pointer" @click="openCommentPopup(post)">
                        <van-icon name="comment-o" :color="post?.is_feedback ? '#10b981' : '#999'" size="19" class="mr-1" style="margin-top: 0.2rem;" />
                        <span class="text-sm" :class="post?.is_feedback ? 'text-green-600' : 'text-gray-500'">
                            {{ post?.is_feedback ? '已点评' : '待点评' }}
                        </span>
                    </div>
                </template>
            </CheckinCard>
        </van-list>
        <van-empty v-show="!checkinDataList.length" description="暂无数据" />

        <!-- 点评弹窗 -->
        <van-popup v-model:show="showCommentPopup" position="bottom" round class="comment-popup">
            <div class="p-6 w-100">
                <div class="text-center text-lg font-bold mb-4">作业点评</div>

                <!-- 评分 -->
                <div class="mb-4">
                    <div class="text-sm text-gray-600 mb-2">评分</div>
                    <van-rate v-model="commentForm.score" :size="24" color="#ffd21e" void-color="#eee" :readonly="currentCommentPost?.is_feedback" />
                </div>

                <!-- 点评内容 -->
                <div class="mb-6">
                    <div class="text-sm text-gray-600 mb-2">点评内容</div>
                    <van-field v-model="commentForm.note" type="textarea" placeholder="请输入点评内容..." rows="4" :border="false" class="bg-gray-50 rounded-lg" :readonly="currentCommentPost?.is_feedback" />
                </div>

                <!-- 操作按钮 -->
                <div v-if="!currentCommentPost?.is_feedback" class="flex gap-3">
                    <van-button block type="default" @click="closeCommentPopup" class="flex-1">取消</van-button>
                    <van-button block type="primary" @click="submitComment" class="flex-1">提交</van-button>
                </div>
                <div v-else class="flex gap-3">
                    <van-button block type="default" @click="closeCommentPopup" class="flex-1">关闭</van-button>
                </div>
            </div>
        </van-popup>
    </div>
    <van-back-top right="5vw" bottom="25vh" offset="600" />
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTitle } from '@vueuse/core'
import { showSuccessToast, showFailToast, showLoadingToast } from 'vant'
import CheckinCard from '@/components/checkin/CheckinCard.vue'
import PostCountModel from '@/components/count/postCountModel.vue'
import { addCheckinFeedbackAPI } from '@/api/teacher'
import { likeUploadTaskInfoAPI, dislikeUploadTaskInfoAPI, getCheckinTeacherListAPI } from '@/api/checkin'

const $route = useRoute()
const $router = useRouter()
useTitle($route.meta.title)

// 从url获取固定的用户与班级ID
const fixedUserId = Number($route.query.created_by)
const fixedDate = $route.query.date

// 列表相关状态
const checkinDataList = ref([])
const loading = ref(false)
const finished = ref(false)
const limit = ref(10)
const page = ref(0)

// 存储 CheckinCard 组件引用的 Map
const checkinCardRefs = ref(new Map());

// 设置 CheckinCard 引用
const setCheckinCardRef = (el, id) => {
  if (el) {
    checkinCardRefs.value.set(id, el);
  } else {
    checkinCardRefs.value.delete(id);
  }
};

// 点评相关状态
const showCommentPopup = ref(false)
const currentCommentPost = ref(null)
const commentForm = ref({
    checkin_id: '',
    score: 0,
    note: ''
})

/**
 * 点赞/取消点赞
 * @param {Object} post - 帖子对象
 * @returns {Promise<void>}
 */
async function handLike(post) {
    if (!post.is_liked) {
        const { code } = await likeUploadTaskInfoAPI({ checkin_id: post.id })
        if (code === 1) {
            showSuccessToast('点赞成功')
            post.likes++
            post.is_liked = true
        }
    } else {
        const { code } = await dislikeUploadTaskInfoAPI({ checkin_id: post.id })
        if (code === 1) {
            showSuccessToast('取消点赞成功')
            post.likes--
            post.is_liked = false
        }
    }
}

/**
 * 打开点评弹窗
 * @param {Object} post - 帖子对象
 */
function openCommentPopup(post) {
    currentCommentPost.value = post
    commentForm.value.checkin_id = post.id
    if (post.feedback_id) {
        commentForm.value.score = post.feedback_score || 0
        commentForm.value.note = post.feedback || ''
    } else {
        commentForm.value.score = 0
        commentForm.value.note = ''
    }
    showCommentPopup.value = true
}

/**
 * 关闭点评弹窗
 */
function closeCommentPopup() {
    if (currentCommentPost.value && currentCommentPost.value.is_feedback) {
        showCommentPopup.value = false
        return
    }
    showCommentPopup.value = false
    currentCommentPost.value = null
    commentForm.value.score = 0
    commentForm.value.note = ''
}

/**
 * 提交点评
 * @returns {Promise<void>}
 */
async function submitComment() {
    if (!commentForm.value.note.trim()) {
        showFailToast('请输入点评内容')
        return
    }
    if (commentForm.value.score === 0) {
        showFailToast('请选择评分')
        return
    }
    try {
        showLoadingToast('提交中...')
        const { code, data } = await addCheckinFeedbackAPI(commentForm.value)
        if (code === 1) {
            commentForm.value.feedback_id = data.id
            currentCommentPost.value.is_feedback = true
            checkinDataList.value.forEach(item => {
                if (item.id === currentCommentPost.value.id) {
                    item.feedback_id = commentForm.value.feedback_id
                    item.feedback_score = commentForm.value.score
                    item.feedback = commentForm.value.note
                }
            })
            showSuccessToast('点评提交成功')
            closeCommentPopup()
        }
    } catch (err) {
        showFailToast('提交失败,请重试')
    }
}

/**
 * 开始播放视频
 * @param {Object} post - 视频条目(含video、isPlaying)
 */
function startPlay(post) {
    if (checkinDataList.value) {
        checkinDataList.value.forEach(p => {
            p.videoList.forEach(v => {
                if (v.id !== post.id) {
                    v.isPlaying = false
                }
            })
        })
    }
    post.isPlaying = true
}

/**
 * 视频播放事件:同时停止音频
 */
function handleVideoPlay(id) {
    // 暂停其他所有卡片的媒体播放
    checkinCardRefs.value.forEach((card, key) => {
        if (key !== id && card) {
            card.stopAllMedia();
        }
    });
}

/**
 * 音频播放事件:停止其他音频
 */
function handleAudioPlay(id) {
    // 暂停其他所有卡片的媒体播放
    checkinCardRefs.value.forEach((card, key) => {
        if (key !== id && card) {
            card.stopAllMedia();
        }
    });
}

/**
 * 拉取作业记录分页数据
 * @returns {Promise<void>}
 */
async function onLoad() {
    const nextPage = page.value
    const res = await getCheckinTeacherListAPI({
        task_id: $route.query.task_id,
        subtask_id: $route.query.subtask_id,
        limit: limit.value,
        page: nextPage,
        created_by: $route.query.created_by,
        date: $route.query.date,
    })
    if (res.code === 1) {
        checkinDataList.value = [...checkinDataList.value, ...formatData(res.data.checkin_list)]
        finished.value = res.data.checkin_list.length < limit.value
        page.value = nextPage + 1
    }
    loading.value = false
}

/**
 * 规范化接口数据为页面所需结构
 * @param {Array} data - 原始接口数据
 * @returns {Array} 规范化后的列表
 */
function formatData(data) {
    let formattedData = []
    formattedData = data?.map(item => {
        let images = []
        let audio = []
        let videoList = []
        if (item.file_type === 'image') {
            images = item.files.map(file => file.value)
        } else if (item.file_type === 'video') {
            videoList = item.files.map(file => ({
                id: file.meta_id,
                video: file.value,
                videoCover: file.cover,
                isPlaying: false,
            }))
        } else if (item.file_type === 'audio') {
            audio = item.files.map(file => ({
                title: file.name ? file.name : '打卡音频',
                artist: file.artist ? file.artist : '',
                url: file.value,
                cover: file.cover ? file.cover : '',
            }))
        }
        return {
            id: item.id,
            task_id: item.task_id,
            user: {
                name: item.username,
                avatar: item.avatar,
                time: item.created_time_desc,
                is_makeup: item.is_makeup,
            },
            content: item.note,
            images,
            videoList,
            audio,
            isPlaying: false,
            likes: item.like_count,
            is_liked: item.is_like,
            is_my: item.is_my,
            file_type: item.file_type,
            is_feedback: item.feedback_id || false,
            feedback: item.feedback || '',
            feedback_id: item.feedback_id || '',
            feedback_score: item.feedback_score || 0,
            comment: item.comment || null,
            task_title: item.task_title || '',
            subtask_title: item.subtask_title || '',
            subtask_id: item.subtask_id || '',
            gratitude_count: item.gratitude_count || 0,
            gratitude_form_list: item.gratitude_form_list || [],
        }
    })
    return formattedData
}

// 生命周期:卸载时清理播放器引用
onMounted(() => {})
onBeforeUnmount(() => {
    checkinCardRefs.value.forEach(card => {
        if (card) {
            card.stopAllMedia();
        }
    });
    checkinCardRefs.value.clear();
})
</script>

<style lang="less">
.van-back-top {
    background-color: #4caf50;
}
.comment-popup {
    .van-popup {
        max-width: 90vw;
    }

    .van-rate {
        display: flex;
        justify-content: center;
    }

    .van-field {
        padding: 12px;
        border-radius: 8px;
    }

    .van-button {
        height: 44px;
        border-radius: 8px;
    }
}
</style>