xlx_teacher_app/src/pages/student/detail.vue

248 lines
8.6 KiB
Vue

<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query'
import { computed, onMounted, ref } from 'vue'
import AdvantageAnalysisChart from '@/components/student/AdvantageAnalysisChart.vue'
import AverageComparisonChart from '@/components/student/AverageComparisonChart.vue'
import ScoreDistributionChart from '@/components/student/ScoreDistributionChart.vue'
import ScoreTrendChart from '@/components/student/ScoreTrendChart.vue'
import { teacherAnalysisPersonalReportUsingPost } from '@/service/laoshichengjifenxi'
import { useHomeStore } from '@/store/home'
const homeStore = useHomeStore()
const studentNumber = ref('')
const studentName = ref('')
const selectedSubjectId = ref(0)
const { data: reportData, isLoading } = useQuery({
queryKey: computed(() => [
'personal-report',
homeStore.selectedClassKey,
homeStore.selectedGradeKey,
homeStore.selectedExamId,
studentNumber.value,
selectedSubjectId.value,
]),
queryFn: async () => {
const response = await teacherAnalysisPersonalReportUsingPost({
body: {
class_key: homeStore.selectedClassKey,
grade_key: homeStore.selectedGradeKey,
exam_id: homeStore.selectedExamId,
student_number: studentNumber.value,
subject_id: selectedSubjectId.value || 0,
},
})
return response
},
enabled: computed(() =>
!!homeStore.selectedClassKey
&& !!homeStore.selectedGradeKey
&& !!homeStore.selectedExamId
&& !!studentNumber.value,
),
staleTime: 30000,
})
const examSummary = computed(() => reportData.value?.exam_summary)
const studentInfo = computed(() => {
const summary = examSummary.value
if (!summary)
return null
return {
totalScore: summary.all_score || 0,
classRank: summary.class_rank || 0,
classTotal: summary.class_member_count || 0,
gradeRank: summary.grade_rank || 0,
gradeTotal: summary.grade_member_count || 0,
gradeAverage: summary.grade_average_score || 0,
classAverage: summary.class_average_score || 0,
fullScore: summary.full_score || 0,
}
})
const subjectTabs = computed(() => {
const tabs = [{ name: 'all', title: '全科', subjectId: 0 }]
homeStore.subjectOptions.forEach((subject) => {
tabs.push({
name: `subject-${subject.subjectId}`,
title: subject.label,
subjectId: subject.subjectId,
})
})
return tabs
})
const activeTab = computed({
get: () => selectedSubjectId.value === 0 ? 'all' : `subject-${selectedSubjectId.value}`,
set: (value) => {
selectedSubjectId.value = value === 'all' ? 0 : Number.parseInt(value.replace('subject-', ''))
},
})
function goBack() {
uni.navigateBack()
}
function handleTabClick({ name }: { name: string }) {
activeTab.value = name
}
onMounted(async () => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = (currentPage as any).options || {}
studentNumber.value = options.student_number || ''
studentName.value = options.student_name || ''
selectedSubjectId.value = options.subject_id || 0
if (homeStore.examOptions.length === 0) {
await homeStore.fetchOptions()
}
})
</script>
<template>
<view class="min-h-screen bg-gray-100 pb-8">
<wd-navbar placeholder fixed custom-class="bg-white">
<template #left>
<view class="h-8 w-8 flex items-center justify-center" @click="goBack">
<wd-icon name="arrow-left" size="20px" color="#333" />
</view>
</template>
<template #title>
<text class="text-base text-gray-800 font-bold">{{ studentName }} 成绩报告</text>
</template>
</wd-navbar>
<wd-tabs
v-model="activeTab"
sticky
line-width="20"
line-height="3"
color="#2563EB"
inactive-color="#64748B"
custom-class="bg-white shadow-sm mb-3"
@click="handleTabClick"
>
<wd-tab
v-for="tab in subjectTabs"
:key="tab.name"
:name="tab.name"
:title="tab.title"
/>
</wd-tabs>
<view v-if="isLoading" class="h-64 flex items-center justify-center">
<wd-loading size="32" color="#2563EB" />
</view>
<view v-else-if="studentInfo" class="px-3">
<view class="relative mb-3 overflow-hidden rounded-xl bg-blue-600 shadow-lg">
<view class="absolute right-0 top-0 h-32 w-32 translate-x-8 rounded-full bg-white opacity-10 -translate-y-8" />
<view class="absolute bottom-0 left-0 h-24 w-24 translate-y-8 rounded-full bg-white opacity-10 -translate-x-8" />
<view class="relative z-10 p-5">
<view class="flex items-start justify-between">
<view>
<text class="mb-1 block text-sm text-blue-100">学生姓名</text>
<text class="text-xl text-white font-bold">{{ studentName }}</text>
</view>
<view class="text-right">
<text class="mb-1 block text-sm text-blue-100">卷面总分</text>
<view class="flex items-baseline justify-end">
<text class="mr-1 text-3xl text-white font-bold">{{ studentInfo.totalScore }}</text>
<text class="text-sm text-blue-200">/ {{ studentInfo.fullScore }}</text>
</view>
</view>
</view>
</view>
</view>
<view class="grid grid-cols-2 mb-4 gap-3">
<view class="flex flex-col justify-center rounded-xl bg-white p-4 shadow-sm">
<view class="mb-1 flex items-baseline">
<text class="mr-1 text-xl text-gray-800 font-bold">{{ studentInfo.classRank }}</text>
<text class="text-xs text-gray-400">/ {{ studentInfo.classTotal }}</text>
</view>
<text class="text-xs text-gray-500">班级排名</text>
</view>
<view class="flex flex-col justify-center rounded-xl bg-white p-4 shadow-sm">
<view class="mb-1 flex items-baseline">
<text class="mr-1 text-xl text-gray-800 font-bold">{{ studentInfo.gradeRank }}</text>
<text class="text-xs text-gray-400">/ {{ studentInfo.gradeTotal }}</text>
</view>
<text class="text-xs text-gray-500">年级排名</text>
</view>
<view class="flex flex-col justify-center rounded-xl bg-white p-4 shadow-sm">
<text class="mb-1 text-xl text-gray-800 font-bold">{{ studentInfo.classAverage.toFixed(1) }}</text>
<text class="text-xs text-gray-500">班级平均分</text>
</view>
<view class="flex flex-col justify-center rounded-xl bg-white p-4 shadow-sm">
<text class="mb-1 text-xl text-gray-800 font-bold">{{ studentInfo.gradeAverage.toFixed(1) }}</text>
<text class="text-xs text-gray-500">年级平均分</text>
</view>
</view>
<view class="space-y-3">
<view class="rounded-xl bg-white p-3 shadow-sm">
<view class="mb-2 border-b border-gray-100 pb-2">
<text class="border-l-4 border-blue-500 pl-2 text-sm text-gray-700 font-bold">成绩分布</text>
</view>
<ScoreDistributionChart :data="reportData?.learning_situation_analysis" />
</view>
<template v-if="selectedSubjectId === 0">
<view class="rounded-xl bg-white p-3 shadow-sm">
<view class="mb-2 border-b border-gray-100 pb-2">
<text class="border-l-4 border-blue-500 pl-2 text-sm text-gray-700 font-bold">优劣势分析</text>
</view>
<AdvantageAnalysisChart :data="reportData?.advantage_analysis" />
</view>
<view class="rounded-xl bg-white p-3 shadow-sm">
<view class="mb-2 border-b border-gray-100 pb-2">
<text class="border-l-4 border-blue-500 pl-2 text-sm text-gray-700 font-bold">均分对比</text>
</view>
<AverageComparisonChart
:data="reportData?.average_comparison_analysis"
:exam-summary="reportData?.exam_summary"
/>
</view>
</template>
<template v-else>
<view class="rounded-xl bg-white p-3 shadow-sm">
<view class="mb-2 border-b border-gray-100 pb-2">
<text class="border-l-4 border-blue-500 pl-2 text-sm text-gray-700 font-bold">历史趋势</text>
</view>
<ScoreTrendChart :data="reportData?.score_trend" />
</view>
</template>
</view>
</view>
<view v-else class="mt-20 flex flex-col items-center justify-center">
<wd-img
width="160"
height="160"
src="https://img.yzcdn.cn/vant/empty-image-default.png"
/>
<text class="mt-4 text-sm text-gray-400">暂无数据</text>
</view>
</view>
</template>
<route lang="jsonc">
{
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "学生详情"
}
}
</route>