refactor: 成绩报告
continuous-integration/drone/push Build is passing Details

This commit is contained in:
张哲铜 2025-10-10 00:12:59 +08:00
parent 9fc79d7b7e
commit 967d14efa2
8 changed files with 908 additions and 142 deletions

View File

@ -0,0 +1,198 @@
<script setup lang="ts">
import type * as API from '@/service/types'
import UniEcharts from 'uni-echarts'
import { computed, ref } from 'vue'
const props = defineProps<{
data: API.AdvantageAnalysis | undefined
}>()
//
type AnalysisMode = 'score_range' | 'class' | 'grade'
const analysisMode = ref<AnalysisMode>('class')
//
const chartTitle = computed(() => {
if (analysisMode.value === 'score_range') {
return '同分数段优劣势分析'
}
else if (analysisMode.value === 'class') {
return '班级优劣势分析'
}
else {
return '年级优劣势分析'
}
})
//
const currentData = computed(() => {
if (!props.data)
return null
if (analysisMode.value === 'score_range') {
return props.data.score_range_analysis
}
else if (analysisMode.value === 'class') {
return props.data.class_analysis
}
else {
return props.data.grade_analysis
}
})
//
const chartOption = computed(() => {
const data = currentData.value
if (!data || !('subjects' in data) || !data.subjects || data.subjects.length === 0) {
return {}
}
const categories = data.subjects.map((item: any) => item.subject_name || '')
const myScores = data.subjects.map((item: any) => item.my_score || 0)
const avgScores = data.subjects.map((item: any) => item.average_score || 0)
return {
tooltip: {
trigger: 'axis',
confine: true,
axisPointer: {
type: 'shadow',
},
formatter: (params: any) => {
if (!Array.isArray(params))
return ''
let result = `${params[0].axisValue}`
params.forEach((param: any) => {
result += `\n${param.marker} ${param.seriesName}: ${param.value.toFixed(2)}`
})
return result
},
},
legend: {
data: ['我的成绩', '平均分'],
top: 5,
textStyle: {
fontSize: 12,
},
},
grid: {
left: 45,
right: 15,
bottom: 40,
top: 40,
containLabel: false,
},
xAxis: {
type: 'category',
data: categories,
axisLabel: {
rotate: 30,
interval: 0,
fontSize: 10,
},
axisLine: {
lineStyle: {
color: '#cccccc',
},
},
},
yAxis: {
type: 'value',
name: '分数',
nameTextStyle: {
fontSize: 11,
},
axisLabel: {
fontSize: 10,
},
splitLine: {
lineStyle: {
type: 'dashed',
color: '#eeeeee',
},
},
},
series: [
{
name: '我的成绩',
type: 'bar',
data: myScores,
itemStyle: {
color: '#3b82f6',
},
barMaxWidth: 35,
},
{
name: '平均分',
type: 'bar',
data: avgScores,
itemStyle: {
color: '#94a3b8',
},
barMaxWidth: 35,
},
],
}
})
const showChart = computed(() => {
const data = currentData.value
return data && 'subjects' in data && data.subjects && data.subjects.length > 0
})
//
function setAnalysisMode(mode: AnalysisMode) {
analysisMode.value = mode
}
</script>
<template>
<view class="rounded-xl bg-white p-4 shadow-sm">
<!-- 标题 -->
<view class="mb-4">
<text class="text-lg text-slate-800 font-semibold">{{ chartTitle }}</text>
</view>
<!-- 模式切换按钮 -->
<view class="mb-4 flex justify-center gap-2">
<wd-button
size="small"
:type="analysisMode === 'score_range' ? 'primary' : 'default'"
@click="setAnalysisMode('score_range')"
>
同分数段
</wd-button>
<wd-button
size="small"
:type="analysisMode === 'class' ? 'primary' : 'default'"
@click="setAnalysisMode('class')"
>
班级
</wd-button>
<wd-button
size="small"
:type="analysisMode === 'grade' ? 'primary' : 'default'"
@click="setAnalysisMode('grade')"
>
年级
</wd-button>
</view>
<!-- 图表 -->
<view v-if="showChart" class="chart-container">
<uni-echarts :option="chartOption" custom-class="chart" />
</view>
<!-- 暂无数据 -->
<view v-else class="h-60 flex items-center justify-center rounded-lg bg-gray-50">
<text class="text-sm text-gray-400">暂无数据</text>
</view>
</view>
</template>
<style scoped>
.chart {
width: 100%;
height: 280px;
}
</style>

View File

@ -0,0 +1,235 @@
<script setup lang="ts">
import type * as API from '@/service/types'
import UniEcharts from 'uni-echarts'
import { computed, ref } from 'vue'
const props = defineProps<{
data: API.AverageComparisonAnalysis | undefined
examSummary: API.ExamSummary | undefined
}>()
// N
type ComparisonMode = 'class' | 'grade' | 'top_n'
const comparisonMode = ref<ComparisonMode>('class')
// N
const topN = ref(50)
const showTopNDialog = ref(false)
const tempTopN = ref(topN.value)
//
const chartTitle = computed(() => {
if (comparisonMode.value === 'class') {
return '班级平均分对比'
}
else if (comparisonMode.value === 'grade') {
return '年级平均分对比'
}
else {
return `年级前${topN.value}名对比`
}
})
//
const chartOption = computed(() => {
if (!props.data?.subjects || props.data.subjects.length === 0) {
return {}
}
const categories = props.data.subjects.map(item => item.subject_name || '')
const myScores = props.data.subjects.map(item => item.my_score || 0)
const avgScores = props.data.subjects.map(item => item.average_score || 0)
let comparisonLabel = ''
if (comparisonMode.value === 'class') {
comparisonLabel = '班级平均分'
}
else if (comparisonMode.value === 'grade') {
comparisonLabel = '年级平均分'
}
else {
comparisonLabel = `年级前${topN.value}名平均分`
}
return {
tooltip: {
trigger: 'axis',
confine: true,
axisPointer: {
type: 'shadow',
},
formatter: (params: any) => {
if (!Array.isArray(params))
return ''
let result = `${params[0].axisValue}`
params.forEach((param: any) => {
result += `\n${param.marker} ${param.seriesName}: ${param.value.toFixed(2)}`
})
return result
},
},
legend: {
data: ['我的成绩', comparisonLabel],
top: 5,
textStyle: {
fontSize: 12,
},
},
grid: {
left: 45,
right: 15,
bottom: 40,
top: 40,
containLabel: false,
},
xAxis: {
type: 'category',
data: categories,
axisLabel: {
rotate: 30,
interval: 0,
fontSize: 10,
},
axisLine: {
lineStyle: {
color: '#cccccc',
},
},
},
yAxis: {
type: 'value',
name: '分数',
nameTextStyle: {
fontSize: 11,
},
axisLabel: {
fontSize: 10,
},
splitLine: {
lineStyle: {
type: 'dashed',
color: '#eeeeee',
},
},
},
series: [
{
name: '我的成绩',
type: 'bar',
data: myScores,
itemStyle: {
color: '#10b981',
},
barMaxWidth: 35,
},
{
name: comparisonLabel,
type: 'bar',
data: avgScores,
itemStyle: {
color: '#94a3b8',
},
barMaxWidth: 35,
},
],
}
})
const showChart = computed(() => {
return props.data?.subjects && props.data.subjects.length > 0
})
//
function setComparisonMode(mode: ComparisonMode) {
if (mode === 'top_n') {
showTopNDialog.value = true
}
else {
comparisonMode.value = mode
}
}
// N
function confirmTopN() {
topN.value = tempTopN.value
comparisonMode.value = 'top_n'
showTopNDialog.value = false
}
//
function cancelTopN() {
showTopNDialog.value = false
tempTopN.value = topN.value
}
</script>
<template>
<view class="rounded-xl bg-white p-4 shadow-sm">
<!-- 标题 -->
<view class="mb-4">
<text class="text-lg text-slate-800 font-semibold">{{ chartTitle }}</text>
</view>
<!-- 模式切换按钮 -->
<view class="mb-4 flex justify-center gap-2">
<wd-button
size="small"
:type="comparisonMode === 'class' ? 'primary' : 'default'"
@click="setComparisonMode('class')"
>
班级平均分
</wd-button>
<wd-button
size="small"
:type="comparisonMode === 'grade' ? 'primary' : 'default'"
@click="setComparisonMode('grade')"
>
年级平均分
</wd-button>
<wd-button
size="small"
:type="comparisonMode === 'top_n' ? 'primary' : 'default'"
@click="setComparisonMode('top_n')"
>
年级前{{ topN }}
</wd-button>
</view>
<!-- 图表 -->
<view v-if="showChart" class="chart-container">
<uni-echarts :option="chartOption" custom-class="chart" />
</view>
<!-- 暂无数据 -->
<view v-else class="h-60 flex items-center justify-center rounded-lg bg-gray-50">
<text class="text-sm text-gray-400">暂无数据</text>
</view>
<!-- 设置年级前N名弹窗 -->
<wd-popup v-model="showTopNDialog" position="center" :close-on-click-modal="false">
<view class="w-70 rounded-2 bg-white p-6">
<view class="mb-4 text-center">
<text class="text-base text-slate-800 font-semibold">设置年级前N名</text>
</view>
<view class="mb-6 flex justify-center p-4">
<wd-input-number v-model="tempTopN" :min="1" :max="200" :step="1" />
</view>
<view class="flex gap-3">
<wd-button block type="default" @click="cancelTopN">
取消
</wd-button>
<wd-button block type="primary" @click="confirmTopN">
确定
</wd-button>
</view>
</view>
</wd-popup>
</view>
</template>
<style scoped>
.chart {
width: 100%;
height: 280px;
}
</style>

View File

@ -0,0 +1,191 @@
<script setup lang="ts">
import type * as API from '@/service/types'
import UniEcharts from 'uni-echarts'
import { computed, ref } from 'vue'
const props = defineProps<{
data: API.LearningSituationAnalysis | undefined
}>()
//
const chartOption = computed(() => {
if (!props.data?.data || props.data.data.length === 0) {
return {}
}
const categories = props.data.data.map(item => item.score_scope || '')
const values = props.data.data.map(item => item.student_num || 0)
//
const studentIndex = props.data.data.findIndex((item) => {
const allScore = props.data?.all_score || 0
const [min, max] = (item.score_scope || '').split('~').map(Number)
return allScore >= min && allScore <= max
})
return {
tooltip: {
trigger: 'axis',
confine: true,
axisPointer: {
type: 'cross',
},
formatter: (params: any) => {
if (!Array.isArray(params) || params.length === 0)
return ''
const param = params[0]
return `${param.axisValue}\n${param.marker} 学生数:${param.value}`
},
},
grid: {
left: 45,
right: 15,
bottom: 40,
top: 20,
containLabel: false,
},
xAxis: {
type: 'category',
data: categories,
axisLabel: {
rotate: 30,
interval: 0,
fontSize: 10,
},
axisLine: {
lineStyle: {
color: '#cccccc',
},
},
},
yAxis: {
type: 'value',
name: '人数',
nameTextStyle: {
fontSize: 11,
},
axisLabel: {
fontSize: 10,
},
splitLine: {
lineStyle: {
type: 'dashed',
color: '#eeeeee',
},
},
},
series: [
{
type: 'line',
data: values,
smooth: true,
lineStyle: {
color: '#3b82f6',
width: 2,
},
itemStyle: {
color: '#3b82f6',
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(59, 130, 246, 0.3)' },
{ offset: 1, color: 'rgba(59, 130, 246, 0.05)' },
],
},
},
markPoint: studentIndex >= 0
? {
symbol: 'pin',
symbolSize: 50,
itemStyle: {
color: '#f59e0b',
},
label: {
formatter: '我',
fontSize: 12,
color: '#fff',
},
data: [
{
coord: [studentIndex, values[studentIndex]],
},
],
}
: undefined,
},
],
}
})
const showChart = computed(() => {
return props.data?.data && props.data.data.length > 0
})
//
const scoreRangeInfo = computed(() => {
if (!props.data?.data || !props.data?.all_score)
return null
const allScore = props.data.all_score
const item = props.data.data.find((d) => {
const [min, max] = (d.score_scope || '').split('~').map(Number)
return allScore >= min && allScore <= max
})
if (!item)
return null
return {
range: item.score_scope,
gradeCount: item.student_num || 0,
classCount: 0, // TODO:
}
})
</script>
<template>
<view class="rounded-xl bg-white p-4 shadow-sm">
<view class="mb-4">
<text class="text-lg text-slate-800 font-semibold">成绩分布图</text>
</view>
<!-- 图表 -->
<view v-if="showChart" class="chart-container">
<uni-echarts :option="chartOption" custom-class="chart" />
</view>
<!-- 暂无数据 -->
<view v-else class="h-60 flex items-center justify-center rounded-lg bg-gray-50">
<text class="text-sm text-gray-400">暂无数据</text>
</view>
<!-- 分数段信息 -->
<view v-if="scoreRangeInfo" class="mt-4 rounded-lg from-blue-50 to-indigo-50 bg-gradient-to-r p-4">
<view class="flex items-center gap-2">
<view class="h-1 w-1 rounded-full bg-blue-500" />
<text class="text-sm text-slate-700">
本人所在分数段<text class="text-blue-600 font-semibold">{{ scoreRangeInfo.range }}</text>
</text>
</view>
<view class="mt-2 flex items-center gap-2">
<view class="h-1 w-1 rounded-full bg-blue-500" />
<text class="text-sm text-slate-700">
本分数段年级学生数<text class="text-blue-600 font-semibold">{{ scoreRangeInfo.gradeCount }}</text>其中本班占<text class="text-blue-600 font-semibold">{{ scoreRangeInfo.classCount }}</text>
</text>
</view>
</view>
</view>
</template>
<style scoped>
.chart {
width: 100%;
height: 250px;
}
</style>

View File

@ -0,0 +1,122 @@
<script setup lang="ts">
import type * as API from '@/service/types'
import UniEcharts from 'uni-echarts'
import { computed } from 'vue'
const props = defineProps<{
data: API.ScoreTrendItem[] | undefined
}>()
//
const chartOption = computed(() => {
if (!props.data || props.data.length === 0) {
return {}
}
const categories = props.data.map(item => item.exam_name || '')
const values = props.data.map(item => item.total_score || 0)
return {
tooltip: {
trigger: 'axis',
confine: true,
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985',
},
},
formatter: (params: any) => {
if (!Array.isArray(params) || params.length === 0)
return ''
const param = params[0]
const item = props.data?.[param.dataIndex]
return `${param.axisValue}\n${item?.exam_date || ''}\n${param.marker} 成绩:${param.value}`
},
},
grid: {
left: 45,
right: 15,
bottom: 40,
top: 20,
containLabel: false,
},
xAxis: {
type: 'category',
data: categories,
axisLabel: {
rotate: 30,
interval: 0,
fontSize: 10,
},
axisLine: {
lineStyle: {
color: '#cccccc',
},
},
},
yAxis: {
type: 'value',
name: '分数',
nameTextStyle: {
fontSize: 11,
},
axisLabel: {
fontSize: 10,
},
splitLine: {
lineStyle: {
type: 'dashed',
color: '#eeeeee',
},
},
},
series: [
{
type: 'line',
data: values,
smooth: true,
lineStyle: {
color: '#10b981',
width: 3,
},
itemStyle: {
color: '#10b981',
borderWidth: 2,
borderColor: '#fff',
},
symbolSize: 8,
},
],
}
})
const showChart = computed(() => {
return props.data && props.data.length > 0
})
</script>
<template>
<view class="rounded-xl bg-white p-4 shadow-sm">
<view class="mb-4">
<text class="text-lg text-slate-800 font-semibold">成绩趋势</text>
</view>
<!-- 图表 -->
<view v-if="showChart" class="chart-container">
<uni-echarts :option="chartOption" custom-class="chart" />
</view>
<!-- 暂无数据 -->
<view v-else class="h-60 flex items-center justify-center rounded-lg bg-gray-50">
<text class="text-sm text-gray-400">暂无数据</text>
</view>
</view>
</template>
<style scoped>
.chart {
width: 100%;
height: 250px;
}
</style>

View File

@ -108,18 +108,13 @@ async function fetchScoreList() {
//
function viewStudentReport(student: ScoreSheetItemExtended) {
// TODO:
console.log('查看学生成绩报告:', student)
uni.showToast({
title: '功能开发中',
icon: 'none',
uni.navigateTo({
url: `/pages/student/detail?student_number=${student.student_exam_number}&student_name=${student.student_name}`,
})
}
//
function viewSubjectAnswerSheet(student: ScoreSheetItemExtended, subjectId: number) {
// TODO:
console.log('查看科目答题卡:', student, subjectId)
function viewSubjectAnswerSheet(student: ScoreSheetItemExtended, subjectId: number = 0) {
uni.showToast({
title: '功能开发中',
icon: 'none',

View File

@ -1,25 +1,75 @@
<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()
// ID
const studentId = ref('')
//
const studentNumber = ref('')
const studentName = ref('')
// ID0
const selectedSubjectId = ref(0)
// 使 TanStack Query
const {
data: reportData,
isLoading,
} = useQuery({
queryKey: computed(() => [
'personal-report',
homeStore.selectedClassId,
homeStore.selectedGradeKey,
homeStore.selectedExamId,
studentNumber.value,
selectedSubjectId.value,
]),
queryFn: async () => {
const response = await teacherAnalysisPersonalReportUsingPost({
body: {
class_key: homeStore.selectedClassId,
grade_key: homeStore.selectedGradeKey,
exam_id: homeStore.selectedExamId,
student_number: studentNumber.value,
subject_id: selectedSubjectId.value || 0,
},
})
return response
},
enabled: computed(() =>
!!homeStore.selectedClassId
&& !!homeStore.selectedGradeKey
&& !!homeStore.selectedExamId
&& !!studentNumber.value,
),
staleTime: 30000,
})
//
const examSummary = computed(() => reportData.value?.exam_summary)
//
const studentInfo = ref({
name: '李星云',
totalScore: 570,
classRank: 122,
classTotal: 234,
gradeRank: 11,
gradeTotal: 22,
gradeAverage: 470,
classAverage: 510,
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,
}
})
//
@ -30,9 +80,9 @@ const subjectTabs = computed(() => {
homeStore.subjectOptions.forEach((subject) => {
tabs.push({
name: `subject-${subject.value}`,
name: `subject-${subject.subjectId}`,
title: subject.label,
subjectId: subject.value,
subjectId: subject.subjectId,
})
})
@ -69,20 +119,19 @@ onMounted(async () => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = (currentPage as any).options || {}
studentId.value = options.id || ''
studentNumber.value = options.student_number || ''
studentName.value = options.student_name || ''
selectedSubjectId.value = options.subject_id || 0
// store
if (homeStore.examOptions.length === 0) {
await homeStore.fetchOptions()
}
// TODO: studentId
console.log('学生ID:', studentId.value)
})
</script>
<template>
<view class="min-h-screen bg-gray-50">
<view class="min-h-screen from-blue-50 to-gray-50 bg-gradient-to-b">
<!-- 导航栏 -->
<wd-navbar placeholder fixed>
<template #left>
@ -91,121 +140,110 @@ onMounted(async () => {
</view>
</template>
<template #title>
<text class="text-lg font-semibold">{{ studentInfo.name }}成绩报告</text>
<text class="text-lg font-semibold">{{ studentName }}成绩报告</text>
</template>
</wd-navbar>
<!-- 科目选择标签栏 -->
<wd-tabs
v-model="activeTab"
sticky
class="bg-white"
@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-screen flex items-center justify-center">
<wd-loading size="32" />
</view>
<!-- 主要内容区域 -->
<view class="p-4 space-y-4">
<!-- 个人成绩卡片 -->
<view class="rounded-xl bg-white p-4 shadow-sm">
<!-- 学生姓名和总分 -->
<view class="mb-4 flex items-center justify-between">
<view class="rounded-full bg-blue-100 px-3 py-1">
<text class="text-sm text-blue-600 font-medium">{{ studentInfo.name }}</text>
</view>
<text class="text-2xl text-orange-500 font-bold">{{ studentInfo.totalScore }}</text>
</view>
<!-- 内容区域 -->
<view v-else-if="studentInfo" class="pb-4">
<!-- 科目选择标签栏 -->
<wd-tabs
v-model="activeTab"
sticky
class="bg-white shadow-sm"
@click="handleTabClick"
>
<wd-tab
v-for="tab in subjectTabs"
:key="tab.name"
:name="tab.name"
:title="tab.title"
/>
</wd-tabs>
<!-- 成绩统计网格 - 2行2列 -->
<view class="grid grid-cols-2 gap-3">
<!-- 年级排名 -->
<view class="rounded-lg bg-orange-50 p-3">
<view class="mb-1 flex items-center justify-between">
<text class="text-2xl text-gray-800 font-bold">{{ studentInfo.classRank }}</text>
<text class="text-xs text-gray-500">/{{ studentInfo.classTotal }}</text>
</view>
<text class="text-sm text-gray-600">年级排名</text>
</view>
<!-- 班级排名 -->
<view class="rounded-lg bg-orange-50 p-3">
<view class="mb-1 flex items-center justify-between">
<text class="text-2xl text-gray-800 font-bold">{{ studentInfo.gradeRank }}</text>
<text class="text-xs text-gray-500">/{{ studentInfo.gradeTotal }}</text>
</view>
<text class="text-sm text-gray-600">班级排名</text>
</view>
<!-- 年级平均分 -->
<view class="flex flex-col rounded-lg bg-orange-50 p-3">
<text class="text-2xl text-gray-800 font-bold">{{ studentInfo.gradeAverage }}</text>
<text class="text-sm text-gray-600">年级平均分</text>
</view>
<!-- 班级平均分 -->
<view class="flex flex-col rounded-lg bg-blue-50 p-3">
<text class="text-2xl text-gray-800 font-bold">{{ studentInfo.classAverage }}</text>
<text class="text-sm text-gray-600">班级平均分</text>
</view>
</view>
</view>
<!-- 成绩分布图 -->
<view class="rounded-xl bg-white p-4 shadow-sm">
<view class="mb-4 flex items-center justify-between">
<text class="text-lg text-gray-800 font-semibold">成绩分布图</text>
</view>
<!-- TODO: 实现成绩分布图表 -->
<view class="h-48 flex items-center justify-center rounded-lg bg-gray-50">
<text class="text-gray-500">成绩分布图表待实现</text>
</view>
</view>
<!-- 全科专属内容 -->
<template v-if="selectedSubjectId === 0">
<!-- 优劣势分析 -->
<view class="rounded-xl bg-white p-4 shadow-sm">
<view class="mb-4 flex items-center justify-between">
<text class="text-lg text-gray-800 font-semibold">优劣势分析</text>
</view>
<!-- TODO: 实现优劣势分析内容 -->
<view class="space-y-3">
<view class="rounded-lg bg-green-50 p-3">
<text class="text-sm text-green-600 font-medium">优势学科</text>
<view class="mt-2">
<text class="text-gray-600">数学物理等学科表现优秀...</text>
<!-- 主要内容区域 -->
<view class="p-4 space-y-4">
<!-- 个人成绩卡片 -->
<view class="rounded-2xl from-blue-500 to-blue-600 bg-gradient-to-br p-5 shadow-lg">
<!-- 总分展示 -->
<view class="mb-4 flex items-end justify-between">
<view>
<text class="text-sm text-blue-100">总分</text>
<view class="mt-1 flex items-baseline gap-1">
<text class="text-4xl text-white font-bold">{{ studentInfo.totalScore }}</text>
<text class="text-lg text-blue-100">/{{ studentInfo.fullScore }}</text>
</view>
</view>
<view class="rounded-full bg-white/20 px-4 py-2 backdrop-blur">
<text class="text-sm text-white">学号: {{ studentNumber }}</text>
</view>
</view>
<view class="rounded-lg bg-red-50 p-3">
<text class="text-sm text-red-600 font-medium">待提升学科</text>
<view class="mt-2">
<text class="text-gray-600">语文英语等学科有提升空间...</text>
<!-- 成绩统计网格 -->
<view class="grid grid-cols-2 gap-3">
<!-- 班级排名 -->
<view class="flex flex-col rounded-xl bg-white/90 p-3 backdrop-blur">
<view class="mb-1 flex items-baseline gap-1">
<text class="text-2xl text-slate-800 font-bold">{{ studentInfo.classRank }}</text>
<text class="text-xs text-slate-500">/{{ studentInfo.classTotal }}</text>
</view>
<text class="text-sm text-slate-600">班级排名</text>
</view>
<!-- 年级排名 -->
<view class="flex flex-col rounded-xl bg-white/90 p-3 backdrop-blur">
<view class="mb-1 flex items-baseline gap-1">
<text class="text-2xl text-slate-800 font-bold">{{ studentInfo.gradeRank }}</text>
<text class="text-xs text-slate-500">/{{ studentInfo.gradeTotal }}</text>
</view>
<text class="text-sm text-slate-600">年级排名</text>
</view>
<!-- 班级平均分 -->
<view class="flex flex-col rounded-xl bg-white/90 p-3 backdrop-blur">
<text class="text-2xl text-slate-800 font-bold">{{ studentInfo.classAverage.toFixed(1) }}</text>
<text class="text-sm text-slate-600">班级平均分</text>
</view>
<!-- 年级平均分 -->
<view class="flex flex-col rounded-xl bg-white/90 p-3 backdrop-blur">
<text class="text-2xl text-slate-800 font-bold">{{ studentInfo.gradeAverage.toFixed(1) }}</text>
<text class="text-sm text-slate-600">年级平均分</text>
</view>
</view>
</view>
<!-- 均分对比 -->
<view class="rounded-xl bg-white p-4 shadow-sm">
<view class="mb-4 flex items-center justify-between">
<text class="text-lg text-gray-800 font-semibold">各科均分对比</text>
</view>
<!-- 成绩分布图 -->
<ScoreDistributionChart :data="reportData?.learning_situation_analysis" />
<!-- TODO: 实现各科均分对比图表 -->
<view class="h-48 flex items-center justify-center rounded-lg bg-gray-50">
<text class="text-gray-500">各科均分对比图表待实现</text>
</view>
</view>
</template>
<!-- 全科专属内容 -->
<template v-if="selectedSubjectId === 0">
<!-- 优劣势分析 -->
<AdvantageAnalysisChart :data="reportData?.advantage_analysis" />
<!-- 均分对比 -->
<AverageComparisonChart
:data="reportData?.average_comparison_analysis"
:exam-summary="reportData?.exam_summary"
/>
</template>
<!-- 单科专属内容 -->
<template v-else>
<!-- 成绩趋势 -->
<ScoreTrendChart :data="reportData?.score_trend" />
</template>
</view>
</view>
<!-- 暂无数据 -->
<view v-else class="h-screen flex flex-col items-center justify-center">
<text class="text-gray-400">暂无数据</text>
</view>
</view>
</template>

View File

@ -54,7 +54,7 @@ const otherStudents = computed(() => studentsData.value?.other_students || [])
//
function goToStudentDetail(student: StudentInfo) {
uni.navigateTo({
url: `/pages/student/detail?info_id=${student.info_id}`,
url: `/pages/student/detail?info_id=${student.info_id}&student_number=${student.student_number}&student_name=${student.student_name}`,
})
}

View File

@ -3407,19 +3407,6 @@ export type TotalScoreInfo = {
score_line?: number;
};
export type TrendDataItem = {
/** 班级key */
class_key?: number;
/** 班级名称 */
class_name?: string;
/** 班级平均分 */
class_avg_score?: number;
/** 班级排名 */
class_rank?: number;
/** 班级前N名人数 */
class_top_count?: number;
};
export type TrendInfo = {
/** 考试日期 */
exam_date?: string;
@ -3428,7 +3415,7 @@ export type TrendInfo = {
/** 考试名称 */
exam_name?: string;
/** 年级走势数据 */
trend_data?: TrendDataItem[];
trend_data?: unknown[];
};
export type UnifiedConfigData = {